From bc2b6f5da569ac23fe7996f7d589569810852d53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8F=AD=E6=89=AC?= Date: Fri, 25 Sep 2026 01:36:01 +0800 Subject: [PATCH 1/4] add LeapRobot and refactor the hcp --- pyproject.toml | 13 + src/leapflow/cli/commands/interactive.py | 36 +- src/leapflow/cli/commands/registry.py | 2 +- src/leapflow/cli/commands/slash_handlers.py | 146 ++- src/leapflow/daemon/_service_helpers.py | 27 + src/leapflow/daemon/service.py | 101 +- src/leapflow/hardware/__init__.py | 22 + src/leapflow/hardware/capability_router.py | 601 +++++++++++ src/leapflow/hardware/context.py | 292 +++++- .../hardware/control_binding_resolver.py | 341 +++++++ src/leapflow/hardware/environment_source.py | 592 +++++++++++ src/leapflow/hardware/evidence.py | 672 ++++++++++++ .../hardware/examples/generic_sensor.yaml | 31 + .../hardware/examples/realsense_d435.yaml | 59 ++ .../hardware/examples/so100_follower.yaml | 161 +++ src/leapflow/hardware/providers/__init__.py | 5 +- src/leapflow/hardware/providers/robot_arm.py | 316 ++++++ .../hardware/providers/yaml_provider.py | 60 +- src/leapflow/hardware/registry.py | 73 +- src/leapflow/hardware/stream.py | 278 +++++ src/leapflow/hardware/tools.py | 452 ++++++++- src/leapflow/hardware/transport.py | 80 ++ src/leapflow/hardware/transports/__init__.py | 3 + src/leapflow/hardware/transports/robot_arm.py | 757 ++++++++++++++ .../hardware/transports/robot_channels.py | 449 ++++++++ src/leapflow/hardware/transports/ros2.py | 742 ++++++++++++++ .../hardware/transports/vla_inference.py | 666 ++++++++++++ src/leapflow/hardware/verification.py | 536 ++++++++++ src/leapflow/learning/physical_episode.py | 615 +++++++++++ .../plugins/physical_capability_resolver.py | 505 +++++++++ src/leapflow/plugins/tool_plugins/__init__.py | 9 +- .../tool_plugins/_physical_skill_helpers.py | 265 +++++ .../plugins/tool_plugins/physical_skill.py | 739 ++++++++++++++ .../plugins/tool_plugins/self_awareness.py | 529 +++++----- .../plugins/tool_plugins/self_management.py | 170 +--- src/leapflow/prompts/templates.py | 9 +- src/leapflow/robot/__init__.py | 12 + src/leapflow/robot/base.py | 166 +++ src/leapflow/robot/cameras.py | 372 +++++++ src/leapflow/robot/drivers/__init__.py | 10 + src/leapflow/robot/drivers/feetech.py | 636 ++++++++++++ src/leapflow/robot/episode_export.py | 384 +++++++ src/leapflow/robot/inference/__init__.py | 32 + .../robot/inference/position_control.py | 129 +++ src/leapflow/robot/inference/registry.py | 227 +++++ src/leapflow/robot/inference/strategy.py | 127 +++ src/leapflow/robot/inference/vla_local.py | 137 +++ src/leapflow/robot/inference/vla_remote.py | 163 +++ src/leapflow/robot/motors.py | 637 ++++++++++++ src/leapflow/robot/orchestrator.py | 516 ++++++++++ src/leapflow/robot/policy.py | 315 ++++++ src/leapflow/robot/processor.py | 263 +++++ src/leapflow/robot/teleop.py | 470 +++++++++ src/leapflow/robot/trajectory.py | 198 ++++ src/leapflow/robot/types.py | 71 ++ src/leapflow/skills/discovery.py | 125 ++- src/leapflow/skills/index.py | 67 +- ...sette-model-1fa2f8d8bcfe7801.cassette.json | 79 ++ ...sette-model-22b0d11ed24a7f8d.cassette.json | 63 ++ ...sette-model-6b1f4760e34b3ba7.cassette.json | 67 -- ...sette-model-809a7a1875c59886.cassette.json | 67 ++ ...sette-model-b37c5a15793b65ba.cassette.json | 67 -- ...sette-model-d8f8fe8c20d702ae.cassette.json | 63 -- ...sette-model-edd842ba40e1cf99.cassette.json | 79 -- ...sette-model-f8a8a8ec2f575965.cassette.json | 67 ++ ...sette-model-203cbc712bed9109.cassette.json | 67 ++ ...sette-model-30e1383d84511916.cassette.json | 63 -- ...sette-model-381119793b34c8d1.cassette.json | 67 -- ...sette-model-49cfe7e29183008c.cassette.json | 63 ++ ...sette-model-81fbf16923453579.cassette.json | 67 ++ ...sette-model-a6c1581f67fa97e9.cassette.json | 63 ++ ...sette-model-cc8c5e1f035ddae3.cassette.json | 67 -- ...sette-model-d1b363e9073f14ac.cassette.json | 63 -- ...sette-model-3454f9cdf57276a1.cassette.json | 63 -- ...sette-model-7adcad6b98235ca1.cassette.json | 63 ++ ...sette-model-04a0e495d61a1558.cassette.json | 68 ++ ...sette-model-3e15bacc2b11d537.cassette.json | 33 - ...sette-model-40d35bee3d5c7435.cassette.json | 72 ++ ...sette-model-575a245bc30282d0.cassette.json | 72 -- ...sette-model-6b24f7969238dc45.cassette.json | 72 -- ...sette-model-a35ff02ba7c81b17.cassette.json | 72 -- ...sette-model-a82111141f8af2ea.cassette.json | 68 -- ...sette-model-bbbc6c7348e3fd72.cassette.json | 72 ++ ...sette-model-bee84f304abdb716.cassette.json | 72 ++ ...sette-model-c55e75b7af1dfc19.cassette.json | 33 + ...sette-model-2431a179bda357c1.cassette.json | 63 ++ ...sette-model-4be3dad592bb7457.cassette.json | 67 -- ...sette-model-4c8af80689cd92d0.cassette.json | 67 ++ ...sette-model-4f0b806f2fac7ad3.cassette.json | 67 -- ...sette-model-6fd8d1817ad97f81.cassette.json | 75 ++ ...sette-model-7128a97f73c51fa9.cassette.json | 63 -- ...sette-model-97b715bc47cd3386.cassette.json | 67 ++ ...sette-model-f0f9b74778416194.cassette.json | 75 -- ...sette-model-00d40eb3a49fcc37.cassette.json | 80 -- ...sette-model-1453ebd57d44e98b.cassette.json | 80 ++ ...sette-model-150ecbf4f218a6f3.cassette.json | 76 -- ...sette-model-1a13aff243894a1b.cassette.json | 68 -- ...sette-model-41234a1a103ae958.cassette.json | 63 ++ ...sette-model-762a64c11fccb54a.cassette.json | 64 ++ ...sette-model-8cd0e76f98de22bb.cassette.json | 76 ++ ...sette-model-9636485fee9d86e6.cassette.json | 80 -- ...sette-model-9d4d96f5b3e803b7.cassette.json | 68 ++ ...sette-model-b3aa805eb9999a33.cassette.json | 75 ++ ...sette-model-b71caaf94fa09b73.cassette.json | 64 -- ...sette-model-c6f993b9ef27fc12.cassette.json | 68 ++ ...sette-model-ca3f68468ce7ee23.cassette.json | 75 -- ...sette-model-cd919c33f2f9d638.cassette.json | 68 -- ...sette-model-e69606f5ea8a656b.cassette.json | 63 -- ...sette-model-fe57e4e3e4d734cb.cassette.json | 80 ++ ...sette-model-002e2be7b234dab4.cassette.json | 67 -- ...sette-model-045af568ab039454.cassette.json | 79 -- ...sette-model-0c54665421c11f8a.cassette.json | 67 ++ ...sette-model-1f2daf894a253f48.cassette.json | 68 ++ ...sette-model-3a06db86fba7f34f.cassette.json | 79 -- ...sette-model-4b7eca2c226e3ad7.cassette.json | 68 -- ...sette-model-4fbfcb945a4ef6a0.cassette.json | 140 +++ ...sette-model-55a7650db1502566.cassette.json | 67 -- ...sette-model-5e0f084389286456.cassette.json | 63 -- ...sette-model-87b870c95fc18e67.cassette.json | 67 ++ ...sette-model-9518e5ff224e4b45.cassette.json | 63 ++ ...sette-model-99947561cfa4cffa.cassette.json | 138 --- ...sette-model-b19d1c99be3d0e2b.cassette.json | 79 ++ ...sette-model-b2f5d9ad1acae44e.cassette.json | 80 ++ ...sette-model-b7be149ed0142af8.cassette.json | 79 ++ ...sette-model-bf150d79488eaa8e.cassette.json | 80 -- ...sette-model-00600853bb84f29e.cassette.json | 79 ++ ...sette-model-0c0469d68ba791cb.cassette.json | 87 -- ...sette-model-2c150449ffad6f0c.cassette.json | 76 ++ ...sette-model-34622e77d4a39a56.cassette.json | 92 ++ ...sette-model-352db33d2794588e.cassette.json | 80 ++ ...sette-model-383884acaf1063e3.cassette.json | 80 ++ ...sette-model-3c2ec50a177f5fc1.cassette.json | 75 -- ...sette-model-476d996d8812c2c2.cassette.json | 91 ++ ...sette-model-4948dea531cf18fc.cassette.json | 91 -- ...sette-model-601b0e5ebe77c051.cassette.json | 91 -- ...sette-model-68fb63fed8d61ee1.cassette.json | 79 -- ...sette-model-6fac1290a0a8773b.cassette.json | 92 ++ ...sette-model-7c33a815e1b4488c.cassette.json | 67 -- ...sette-model-8fe5c07eca8abc41.cassette.json | 91 -- ...sette-model-9e7bcefd3da7080c.cassette.json | 91 -- ...sette-model-9fee9f953ce214d5.cassette.json | 79 -- ...sette-model-a5687558f18fe171.cassette.json | 92 ++ ...sette-model-aedf2373abf08185.cassette.json | 79 -- ...sette-model-b65a352522b4ce5c.cassette.json | 88 ++ ...sette-model-c39c9ee78d87ec90.cassette.json | 79 -- ...sette-model-d96419d4e6a98990.cassette.json | 67 ++ ...sette-model-f4eba7c0d916788b.cassette.json | 80 ++ ...sette-model-f837185b141289a5.cassette.json | 79 -- ...sette-model-ff9056562a13326a.cassette.json | 80 ++ tests/test_architecture_contracts.py | 4 + tests/test_hardware_governance.py | 3 +- tests/test_hardware_transport_contract.py | 9 + tests/test_orchestrator.py | 518 ++++++++++ tests/test_physical_capability_resolver.py | 342 +++++++ tests/test_physical_episode.py | 576 +++++++++++ tests/test_physical_skill_e2e.py | 955 ++++++++++++++++++ tests/test_physical_teleop.py | 502 +++++++++ tests/test_robot_arm_transport.py | 612 +++++++++++ tests/test_self_awareness.py | 448 ++++---- tests/test_self_management.py | 18 +- tests/test_skill_curator.py | 40 +- tests/test_tool_handler_invocation.py | 2 +- 162 files changed, 22058 insertions(+), 4175 deletions(-) create mode 100644 src/leapflow/hardware/capability_router.py create mode 100644 src/leapflow/hardware/control_binding_resolver.py create mode 100644 src/leapflow/hardware/environment_source.py create mode 100644 src/leapflow/hardware/evidence.py create mode 100644 src/leapflow/hardware/examples/generic_sensor.yaml create mode 100644 src/leapflow/hardware/examples/realsense_d435.yaml create mode 100644 src/leapflow/hardware/examples/so100_follower.yaml create mode 100644 src/leapflow/hardware/providers/robot_arm.py create mode 100644 src/leapflow/hardware/transports/robot_arm.py create mode 100644 src/leapflow/hardware/transports/robot_channels.py create mode 100644 src/leapflow/hardware/transports/ros2.py create mode 100644 src/leapflow/hardware/transports/vla_inference.py create mode 100644 src/leapflow/hardware/verification.py create mode 100644 src/leapflow/learning/physical_episode.py create mode 100644 src/leapflow/plugins/physical_capability_resolver.py create mode 100644 src/leapflow/plugins/tool_plugins/_physical_skill_helpers.py create mode 100644 src/leapflow/plugins/tool_plugins/physical_skill.py create mode 100644 src/leapflow/robot/__init__.py create mode 100644 src/leapflow/robot/base.py create mode 100644 src/leapflow/robot/cameras.py create mode 100644 src/leapflow/robot/drivers/__init__.py create mode 100644 src/leapflow/robot/drivers/feetech.py create mode 100644 src/leapflow/robot/episode_export.py create mode 100644 src/leapflow/robot/inference/__init__.py create mode 100644 src/leapflow/robot/inference/position_control.py create mode 100644 src/leapflow/robot/inference/registry.py create mode 100644 src/leapflow/robot/inference/strategy.py create mode 100644 src/leapflow/robot/inference/vla_local.py create mode 100644 src/leapflow/robot/inference/vla_remote.py create mode 100644 src/leapflow/robot/motors.py create mode 100644 src/leapflow/robot/orchestrator.py create mode 100644 src/leapflow/robot/policy.py create mode 100644 src/leapflow/robot/processor.py create mode 100644 src/leapflow/robot/teleop.py create mode 100644 src/leapflow/robot/trajectory.py create mode 100644 src/leapflow/robot/types.py create mode 100644 tests/_fixtures/cassettes/r1_conversation/cassette-model-1fa2f8d8bcfe7801.cassette.json create mode 100644 tests/_fixtures/cassettes/r1_conversation/cassette-model-22b0d11ed24a7f8d.cassette.json delete mode 100644 tests/_fixtures/cassettes/r1_conversation/cassette-model-6b1f4760e34b3ba7.cassette.json create mode 100644 tests/_fixtures/cassettes/r1_conversation/cassette-model-809a7a1875c59886.cassette.json delete mode 100644 tests/_fixtures/cassettes/r1_conversation/cassette-model-b37c5a15793b65ba.cassette.json delete mode 100644 tests/_fixtures/cassettes/r1_conversation/cassette-model-d8f8fe8c20d702ae.cassette.json delete mode 100644 tests/_fixtures/cassettes/r1_conversation/cassette-model-edd842ba40e1cf99.cassette.json create mode 100644 tests/_fixtures/cassettes/r1_conversation/cassette-model-f8a8a8ec2f575965.cassette.json create mode 100644 tests/_fixtures/cassettes/r2_isolation/cassette-model-203cbc712bed9109.cassette.json delete mode 100644 tests/_fixtures/cassettes/r2_isolation/cassette-model-30e1383d84511916.cassette.json delete mode 100644 tests/_fixtures/cassettes/r2_isolation/cassette-model-381119793b34c8d1.cassette.json create mode 100644 tests/_fixtures/cassettes/r2_isolation/cassette-model-49cfe7e29183008c.cassette.json create mode 100644 tests/_fixtures/cassettes/r2_isolation/cassette-model-81fbf16923453579.cassette.json create mode 100644 tests/_fixtures/cassettes/r2_isolation/cassette-model-a6c1581f67fa97e9.cassette.json delete mode 100644 tests/_fixtures/cassettes/r2_isolation/cassette-model-cc8c5e1f035ddae3.cassette.json delete mode 100644 tests/_fixtures/cassettes/r2_isolation/cassette-model-d1b363e9073f14ac.cassette.json delete mode 100644 tests/_fixtures/cassettes/r3_control_plane/cassette-model-3454f9cdf57276a1.cassette.json create mode 100644 tests/_fixtures/cassettes/r3_control_plane/cassette-model-7adcad6b98235ca1.cassette.json create mode 100644 tests/_fixtures/cassettes/r4_recovery/cassette-model-04a0e495d61a1558.cassette.json delete mode 100644 tests/_fixtures/cassettes/r4_recovery/cassette-model-3e15bacc2b11d537.cassette.json create mode 100644 tests/_fixtures/cassettes/r4_recovery/cassette-model-40d35bee3d5c7435.cassette.json delete mode 100644 tests/_fixtures/cassettes/r4_recovery/cassette-model-575a245bc30282d0.cassette.json delete mode 100644 tests/_fixtures/cassettes/r4_recovery/cassette-model-6b24f7969238dc45.cassette.json delete mode 100644 tests/_fixtures/cassettes/r4_recovery/cassette-model-a35ff02ba7c81b17.cassette.json delete mode 100644 tests/_fixtures/cassettes/r4_recovery/cassette-model-a82111141f8af2ea.cassette.json create mode 100644 tests/_fixtures/cassettes/r4_recovery/cassette-model-bbbc6c7348e3fd72.cassette.json create mode 100644 tests/_fixtures/cassettes/r4_recovery/cassette-model-bee84f304abdb716.cassette.json create mode 100644 tests/_fixtures/cassettes/r4_recovery/cassette-model-c55e75b7af1dfc19.cassette.json create mode 100644 tests/_fixtures/cassettes/r5_learning/cassette-model-2431a179bda357c1.cassette.json delete mode 100644 tests/_fixtures/cassettes/r5_learning/cassette-model-4be3dad592bb7457.cassette.json create mode 100644 tests/_fixtures/cassettes/r5_learning/cassette-model-4c8af80689cd92d0.cassette.json delete mode 100644 tests/_fixtures/cassettes/r5_learning/cassette-model-4f0b806f2fac7ad3.cassette.json create mode 100644 tests/_fixtures/cassettes/r5_learning/cassette-model-6fd8d1817ad97f81.cassette.json delete mode 100644 tests/_fixtures/cassettes/r5_learning/cassette-model-7128a97f73c51fa9.cassette.json create mode 100644 tests/_fixtures/cassettes/r5_learning/cassette-model-97b715bc47cd3386.cassette.json delete mode 100644 tests/_fixtures/cassettes/r5_learning/cassette-model-f0f9b74778416194.cassette.json delete mode 100644 tests/_fixtures/cassettes/r6_lifecycle/cassette-model-00d40eb3a49fcc37.cassette.json create mode 100644 tests/_fixtures/cassettes/r6_lifecycle/cassette-model-1453ebd57d44e98b.cassette.json delete mode 100644 tests/_fixtures/cassettes/r6_lifecycle/cassette-model-150ecbf4f218a6f3.cassette.json delete mode 100644 tests/_fixtures/cassettes/r6_lifecycle/cassette-model-1a13aff243894a1b.cassette.json create mode 100644 tests/_fixtures/cassettes/r6_lifecycle/cassette-model-41234a1a103ae958.cassette.json create mode 100644 tests/_fixtures/cassettes/r6_lifecycle/cassette-model-762a64c11fccb54a.cassette.json create mode 100644 tests/_fixtures/cassettes/r6_lifecycle/cassette-model-8cd0e76f98de22bb.cassette.json delete mode 100644 tests/_fixtures/cassettes/r6_lifecycle/cassette-model-9636485fee9d86e6.cassette.json create mode 100644 tests/_fixtures/cassettes/r6_lifecycle/cassette-model-9d4d96f5b3e803b7.cassette.json create mode 100644 tests/_fixtures/cassettes/r6_lifecycle/cassette-model-b3aa805eb9999a33.cassette.json delete mode 100644 tests/_fixtures/cassettes/r6_lifecycle/cassette-model-b71caaf94fa09b73.cassette.json create mode 100644 tests/_fixtures/cassettes/r6_lifecycle/cassette-model-c6f993b9ef27fc12.cassette.json delete mode 100644 tests/_fixtures/cassettes/r6_lifecycle/cassette-model-ca3f68468ce7ee23.cassette.json delete mode 100644 tests/_fixtures/cassettes/r6_lifecycle/cassette-model-cd919c33f2f9d638.cassette.json delete mode 100644 tests/_fixtures/cassettes/r6_lifecycle/cassette-model-e69606f5ea8a656b.cassette.json create mode 100644 tests/_fixtures/cassettes/r6_lifecycle/cassette-model-fe57e4e3e4d734cb.cassette.json delete mode 100644 tests/_fixtures/cassettes/r7_adaptive_plugin_loop/cassette-model-002e2be7b234dab4.cassette.json delete mode 100644 tests/_fixtures/cassettes/r7_adaptive_plugin_loop/cassette-model-045af568ab039454.cassette.json create mode 100644 tests/_fixtures/cassettes/r7_adaptive_plugin_loop/cassette-model-0c54665421c11f8a.cassette.json create mode 100644 tests/_fixtures/cassettes/r7_adaptive_plugin_loop/cassette-model-1f2daf894a253f48.cassette.json delete mode 100644 tests/_fixtures/cassettes/r7_adaptive_plugin_loop/cassette-model-3a06db86fba7f34f.cassette.json delete mode 100644 tests/_fixtures/cassettes/r7_adaptive_plugin_loop/cassette-model-4b7eca2c226e3ad7.cassette.json create mode 100644 tests/_fixtures/cassettes/r7_adaptive_plugin_loop/cassette-model-4fbfcb945a4ef6a0.cassette.json delete mode 100644 tests/_fixtures/cassettes/r7_adaptive_plugin_loop/cassette-model-55a7650db1502566.cassette.json delete mode 100644 tests/_fixtures/cassettes/r7_adaptive_plugin_loop/cassette-model-5e0f084389286456.cassette.json create mode 100644 tests/_fixtures/cassettes/r7_adaptive_plugin_loop/cassette-model-87b870c95fc18e67.cassette.json create mode 100644 tests/_fixtures/cassettes/r7_adaptive_plugin_loop/cassette-model-9518e5ff224e4b45.cassette.json delete mode 100644 tests/_fixtures/cassettes/r7_adaptive_plugin_loop/cassette-model-99947561cfa4cffa.cassette.json create mode 100644 tests/_fixtures/cassettes/r7_adaptive_plugin_loop/cassette-model-b19d1c99be3d0e2b.cassette.json create mode 100644 tests/_fixtures/cassettes/r7_adaptive_plugin_loop/cassette-model-b2f5d9ad1acae44e.cassette.json create mode 100644 tests/_fixtures/cassettes/r7_adaptive_plugin_loop/cassette-model-b7be149ed0142af8.cassette.json delete mode 100644 tests/_fixtures/cassettes/r7_adaptive_plugin_loop/cassette-model-bf150d79488eaa8e.cassette.json create mode 100644 tests/_fixtures/cassettes/r8_hardware/cassette-model-00600853bb84f29e.cassette.json delete mode 100644 tests/_fixtures/cassettes/r8_hardware/cassette-model-0c0469d68ba791cb.cassette.json create mode 100644 tests/_fixtures/cassettes/r8_hardware/cassette-model-2c150449ffad6f0c.cassette.json create mode 100644 tests/_fixtures/cassettes/r8_hardware/cassette-model-34622e77d4a39a56.cassette.json create mode 100644 tests/_fixtures/cassettes/r8_hardware/cassette-model-352db33d2794588e.cassette.json create mode 100644 tests/_fixtures/cassettes/r8_hardware/cassette-model-383884acaf1063e3.cassette.json delete mode 100644 tests/_fixtures/cassettes/r8_hardware/cassette-model-3c2ec50a177f5fc1.cassette.json create mode 100644 tests/_fixtures/cassettes/r8_hardware/cassette-model-476d996d8812c2c2.cassette.json delete mode 100644 tests/_fixtures/cassettes/r8_hardware/cassette-model-4948dea531cf18fc.cassette.json delete mode 100644 tests/_fixtures/cassettes/r8_hardware/cassette-model-601b0e5ebe77c051.cassette.json delete mode 100644 tests/_fixtures/cassettes/r8_hardware/cassette-model-68fb63fed8d61ee1.cassette.json create mode 100644 tests/_fixtures/cassettes/r8_hardware/cassette-model-6fac1290a0a8773b.cassette.json delete mode 100644 tests/_fixtures/cassettes/r8_hardware/cassette-model-7c33a815e1b4488c.cassette.json delete mode 100644 tests/_fixtures/cassettes/r8_hardware/cassette-model-8fe5c07eca8abc41.cassette.json delete mode 100644 tests/_fixtures/cassettes/r8_hardware/cassette-model-9e7bcefd3da7080c.cassette.json delete mode 100644 tests/_fixtures/cassettes/r8_hardware/cassette-model-9fee9f953ce214d5.cassette.json create mode 100644 tests/_fixtures/cassettes/r8_hardware/cassette-model-a5687558f18fe171.cassette.json delete mode 100644 tests/_fixtures/cassettes/r8_hardware/cassette-model-aedf2373abf08185.cassette.json create mode 100644 tests/_fixtures/cassettes/r8_hardware/cassette-model-b65a352522b4ce5c.cassette.json delete mode 100644 tests/_fixtures/cassettes/r8_hardware/cassette-model-c39c9ee78d87ec90.cassette.json create mode 100644 tests/_fixtures/cassettes/r8_hardware/cassette-model-d96419d4e6a98990.cassette.json create mode 100644 tests/_fixtures/cassettes/r8_hardware/cassette-model-f4eba7c0d916788b.cassette.json delete mode 100644 tests/_fixtures/cassettes/r8_hardware/cassette-model-f837185b141289a5.cassette.json create mode 100644 tests/_fixtures/cassettes/r8_hardware/cassette-model-ff9056562a13326a.cassette.json create mode 100644 tests/test_orchestrator.py create mode 100644 tests/test_physical_capability_resolver.py create mode 100644 tests/test_physical_episode.py create mode 100644 tests/test_physical_skill_e2e.py create mode 100644 tests/test_physical_teleop.py create mode 100644 tests/test_robot_arm_transport.py diff --git a/pyproject.toml b/pyproject.toml index 8637a8c6..72da8c47 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -70,6 +70,19 @@ web = ["trafilatura>=2.2"] # default `--all-extras` syncs deliberately exclude it (`--no-extra leapspace`, # see Makefile/CI/setup.sh); `make space-sync` opts back in. leapspace = ["pyqt6>=6.7", "cua-sandbox", "pydantic>=2"] +robot = [ + "torch>=2.0", + "torchvision>=0.15", + "safetensors>=0.4", + "huggingface-hub>=0.20", + "numpy>=1.24", + "opencv-python>=4.8", + "pyserial>=3.5", + "Pillow>=10.0", +] +robot-ros2 = [ + "rclpy>=3.0; python_version>='3.11'", +] [project.scripts] leap = "leapflow.__main__:main" diff --git a/src/leapflow/cli/commands/interactive.py b/src/leapflow/cli/commands/interactive.py index c3f79c99..d7012f1c 100644 --- a/src/leapflow/cli/commands/interactive.py +++ b/src/leapflow/cli/commands/interactive.py @@ -1704,11 +1704,33 @@ async def _handle_teach( if ctx.session and ctx.session.mode == SessionMode.LEARNING: console.warning("Already in teaching mode. Say '/teach stop' to end.") return True - goal = "" + + # Parse --mode for physical demonstration modes. + raw_args = "" if line.startswith("teach start "): - goal = line[len("teach start "):] + raw_args = line[len("teach start "):] elif line.startswith("教学开始 "): - goal = line[len("教学开始 "):] + raw_args = line[len("教学开始 "):] + + from leapflow.cli.commands.slash_handlers import _parse_teach_start_args + + parsed = _parse_teach_start_args(raw_args) + mode = parsed.get("mode", "gui") + + if mode in ("teleop", "kinesthetic"): + from leapflow.cli.commands.slash_handlers import ( + _start_physical_teach, + ) + + result = await _start_physical_teach(ctx, mode, parsed) + if result.get("ok"): + console.success(result["message"]) + else: + console.error(result.get("message", "Failed to start physical teach.")) + return True + + # Default GUI path — unchanged. + goal = parsed.get("goal", "") try: session = await ctx.session.enter_learning(goal=goal) console.success(f"Teaching started — session {session.session_id}") @@ -1723,6 +1745,14 @@ async def _handle_teach( return True if line == "teach stop": + # Check for an active physical session first. + from leapflow.cli.commands.slash_handlers import _stop_physical_teach + + physical_result = await _stop_physical_teach(ctx) + if physical_result is not None: + console.success(physical_result["message"]) + return True + if not ctx.session or ctx.session.mode != SessionMode.LEARNING: if learning: ctx.imitation.end_control_input() diff --git a/src/leapflow/cli/commands/registry.py b/src/leapflow/cli/commands/registry.py index f700da2a..dc8f029e 100644 --- a/src/leapflow/cli/commands/registry.py +++ b/src/leapflow/cli/commands/registry.py @@ -96,7 +96,7 @@ def supports_runtime(self, runtime: CommandRuntime) -> bool: CommandDef("orient", "Show the agent's unified orientation (layered) and pending re-entries", "Chat", effect=CommandEffect.READ_ONLY), # Teaching - CommandDef("teach start", "Start teaching mode", "Teaching", aliases=("teach",), args_hint="[goal]"), + CommandDef("teach start", "Start teaching mode", "Teaching", aliases=("teach",), args_hint="[goal] [--mode=gui|teleop|kinesthetic] [--leader= --follower=] [--device=]"), CommandDef("teach stop", "Stop and distill skill", "Teaching"), CommandDef("teach status", "Show distillation progress", "Teaching"), CommandDef("teach pause", "Pause recording", "Teaching"), diff --git a/src/leapflow/cli/commands/slash_handlers.py b/src/leapflow/cli/commands/slash_handlers.py index 286ebb7e..fcc5b1dc 100644 --- a/src/leapflow/cli/commands/slash_handlers.py +++ b/src/leapflow/cli/commands/slash_handlers.py @@ -3317,11 +3317,141 @@ async def _distill_background(session) -> None: _log.warning("background_distill failed", exc_info=True) +# --------------------------------------------------------------------------- +# Physical teach helpers (teleop / kinesthetic) +# --------------------------------------------------------------------------- + +def _parse_teach_start_args(raw_args: str) -> dict[str, Any]: + """Parse ``--mode``, ``--leader``, ``--follower``, ``--device`` from teach start args. + + Unrecognized tokens (i.e. those not prefixed with ``--``) are treated as + the goal string, preserving backward compatibility with the existing + ``/teach start `` form. + """ + result: dict[str, Any] = {"mode": "gui", "goal": ""} + goal_parts: list[str] = [] + tokens = raw_args.split() + i = 0 + while i < len(tokens): + token = tokens[i] + if "=" in token and token.startswith("--"): + key, _, val = token.partition("=") + result[key.lstrip("-")] = val + elif token.startswith("--") and i + 1 < len(tokens): + result[token.lstrip("-")] = tokens[i + 1] + i += 1 + else: + goal_parts.append(token) + i += 1 + if goal_parts: + result["goal"] = " ".join(goal_parts) + return result + + +async def _start_physical_teach( + ctx: "Context", mode: str, parsed: dict[str, Any] +) -> dict[str, Any]: + """Start a physical teach session (teleop or kinesthetic).""" + from leapflow.robot.teleop import ( + KinestheticRecorder, + LeaderFollowerBridge, + get_physical_session, + register_physical_session, + ) + + session_key = _physical_session_key(ctx) + if get_physical_session(session_key) is not None: + return { + "ok": False, + "message": "A physical teach session is already active. Use '/teach stop' first.", + "session_mode": "learning", + } + + registry = getattr(ctx, "hardware_registry", None) + if registry is None: + return {"ok": False, "message": "Hardware subsystem is not available."} + + goal = parsed.get("goal", "") + + if mode == "teleop": + leader = parsed.get("leader", "") + follower = parsed.get("follower", "") + if not leader or not follower: + return { + "ok": False, + "message": "Teleop mode requires --leader= --follower=.", + } + bridge = LeaderFollowerBridge( + registry, + leader_device_id=leader, + follower_device_id=follower, + goal=goal, + ) + else: # kinesthetic + device = parsed.get("device", "") + if not device: + return { + "ok": False, + "message": "Kinesthetic mode requires --device=.", + } + bridge = KinestheticRecorder(registry, device_id=device, goal=goal) + + await bridge.start() + register_physical_session(session_key, bridge) + + label = "Teleop" if mode == "teleop" else "Kinesthetic" + msg = f"{label} teach started — session {bridge.session_id}" + if goal: + msg += f"\nGoal: {goal}" + msg += "\nSay '/teach stop' to stop and save the trajectory." + return {"ok": True, "message": msg, "session_mode": "learning"} + + +async def _stop_physical_teach(ctx: "Context") -> dict[str, Any] | None: + """Stop an active physical teach session if one exists. Returns None otherwise.""" + from leapflow.robot.teleop import get_physical_session, remove_physical_session + + session_key = _physical_session_key(ctx) + bridge = get_physical_session(session_key) + if bridge is None: + return None + + trajectory = await bridge.stop() + remove_physical_session(session_key) + + msg = ( + f"Physical recording stopped — {trajectory.sample_count} samples, " + f"{trajectory.duration_s:.1f}s" + ) + return { + "ok": True, + "message": msg, + "sample_count": trajectory.sample_count, + "duration": trajectory.duration_s, + "trajectory_id": trajectory.trajectory_id, + "session_mode": "idle", + } + + +def _physical_session_key(ctx: "Context") -> str: + """Derive a stable key for tracking physical sessions on *ctx*.""" + session = getattr(ctx, "session", None) + if session is not None: + sid = getattr(session, "session_id", "") + if sid: + return f"physical:{sid}" + return "physical:default" + + async def _execute_teach(ctx: "Context", name: str, args: str) -> dict[str, Any]: """Execute teach commands. Returns ``session_mode`` in the payload so the TUI client can track whether it should route subsequent inputs as annotations. + + Physical modes (``--mode=teleop`` / ``--mode=kinesthetic``) start a + hardware bridge instead of the desktop GUI recorder. The existing + GUI path (``--mode=gui`` or no ``--mode``) is completely unchanged. """ from leapflow.engine.session.session import SessionMode @@ -3331,7 +3461,16 @@ async def _execute_teach(ctx: "Context", name: str, args: str) -> dict[str, Any] return {"ok": False, "message": "No active session.", "session_mode": "idle"} if ctx.session.mode == SessionMode.LEARNING: return {"ok": False, "message": "Already in teaching mode. Say '/teach stop' to end.", "session_mode": "learning"} - goal = args if name == "teach start" else "" + + # Parse --mode and physical-mode arguments from the raw args. + parsed = _parse_teach_start_args(args if name == "teach start" else "") + mode = parsed.get("mode", "gui") + + if mode in ("teleop", "kinesthetic"): + return await _start_physical_teach(ctx, mode, parsed) + + # Default GUI path — unchanged. + goal = parsed.get("goal", "") try: session = await ctx.session.enter_learning(goal=goal) msg = f"Teaching started — session {session.session_id}" @@ -3343,6 +3482,11 @@ async def _execute_teach(ctx: "Context", name: str, args: str) -> dict[str, Any] return {"ok": False, "message": str(e)} if name == "teach stop": + # Check for an active physical session first. + physical_result = await _stop_physical_teach(ctx) + if physical_result is not None: + return physical_result + if not ctx.session or ctx.session.mode != SessionMode.LEARNING: return {"ok": False, "message": "Not in teaching mode.", "session_mode": "idle"} try: diff --git a/src/leapflow/daemon/_service_helpers.py b/src/leapflow/daemon/_service_helpers.py index a03d80bb..19645544 100644 --- a/src/leapflow/daemon/_service_helpers.py +++ b/src/leapflow/daemon/_service_helpers.py @@ -96,6 +96,33 @@ def engine_context_metadata(engine: Any | None, settings: Any) -> dict[str, Any] return metadata +def engine_runtime_extras(engine: Any | None) -> dict[str, Any]: + """Return session turn count and cache hit rate for status introspection. + + These live on the engine, not the budget snapshot, and are needed by the + self-awareness runtime facet. Kept separate from ``engine_context_metadata`` + so they ride only on the (rare) status call, never on every stream chunk. + The cache hit rate is normalised from the tracker's 0..1 fraction to a + human-facing percentage. + """ + extras: dict[str, Any] = {} + if engine is None: + return extras + turn_count = getattr(engine, "_session_turn_count", None) + if isinstance(turn_count, int) and turn_count >= 0: + extras["session_turn_count"] = turn_count + tracker = getattr(engine, "_usage_tracker", None) + summary_fn = getattr(tracker, "summary", None) if tracker is not None else None + if callable(summary_fn): + try: + rate = getattr(summary_fn(), "cache_hit_rate", None) + except Exception: # noqa: BLE001 - telemetry must never fail status + rate = None + if isinstance(rate, (int, float)) and rate >= 0: + extras["cache_hit_rate"] = round(float(rate) * 100, 1) + return extras + + def host_backend_status(ctx: Any | None) -> dict[str, Any]: """Inspect daemon host-backend state for status reporting.""" if ctx is None: diff --git a/src/leapflow/daemon/service.py b/src/leapflow/daemon/service.py index 5febb992..7c7332c0 100644 --- a/src/leapflow/daemon/service.py +++ b/src/leapflow/daemon/service.py @@ -23,6 +23,7 @@ ProducerServices as _ProducerServices, checkpoint_open_connection, engine_context_metadata, + engine_runtime_extras, host_backend_status, install_learn_notifications, memory_entry_to_dict, @@ -478,6 +479,17 @@ async def start(self) -> None: self._approval_coordinator.install_gate(ctx, self) install_learn_notifications(ctx, self.notification_bus) self._ctx = ctx + # Wire the session-scoped status snapshot into the self-awareness surface + # so self_describe/runtime_snapshot answer from live evidence rather than + # source code. bind_runtime reaches only plugins declaring + # 'runtime_status_provider' (self_awareness); status(session_id) resolves + # the caller's own session engine, so concurrent clients never cross-report. + try: + from leapflow.plugins import get_registry as _get_tool_registry + + _get_tool_registry().bind_runtime(runtime_status_provider=self.status) + except Exception: # noqa: BLE001 - introspection wiring must never block boot + logger.debug("self_awareness status provider wiring failed", exc_info=True) await self._start_environment_sources(ctx) if self._auto_start_deferred: self.start_deferred_init() @@ -507,34 +519,70 @@ async def start(self) -> None: self._observation = None async def _start_environment_sources(self, ctx: Any) -> None: - """Start explicitly enabled experiment sources inside the daemon lifecycle.""" - settings = getattr(ctx, "settings", self._settings) - if str(getattr(settings, "environment_mode", "production")) != "experiment": - return - if not bool(getattr(settings, "environment_leapspace_enabled", False)): - return - state_root = str(getattr(settings, "environment_leapspace_state_root", "") or "") - session_id = str(getattr(settings, "environment_leapspace_session_id", "") or "") - if not state_root or not session_id: - logger.warning( - "LeapSpace environment source requires both state_root and session_id" - ) - return - from leapflow.layout import workspace_id_for_path + """Start environment sources inside the daemon lifecycle. + + Two independent sources are considered: + - LeapSpaceEnvironmentSource: experiment-only, requires explicit config. + - PhysicalEnvironmentSource: available whenever hardware is admitted, + regardless of environment_mode. Observes device topology, health, + and affordance changes through HCP. + """ from leapflow.perception.environment_source import EnvironmentSourceManager - from leapflow.perception.leapspace_source import LeapSpaceEnvironmentSource + settings = getattr(ctx, "settings", self._settings) manager = EnvironmentSourceManager(ctx.record_environment_observation) - manager.register( - LeapSpaceEnvironmentSource( - state_root, - workspace_id=workspace_id_for_path(self._workspace_root()), - session_id=session_id, - poll_interval_s=float( - getattr(settings, "environment_leapspace_poll_interval_s", 0.5) - ), - ) - ) + registered_any = False + + # ── LeapSpace source (experiment-only) ── + if ( + str(getattr(settings, "environment_mode", "production")) == "experiment" + and bool(getattr(settings, "environment_leapspace_enabled", False)) + ): + state_root = str(getattr(settings, "environment_leapspace_state_root", "") or "") + session_id = str(getattr(settings, "environment_leapspace_session_id", "") or "") + if state_root and session_id: + try: + from leapflow.layout import workspace_id_for_path + from leapflow.perception.leapspace_source import LeapSpaceEnvironmentSource + + manager.register( + LeapSpaceEnvironmentSource( + state_root, + workspace_id=workspace_id_for_path(self._workspace_root()), + session_id=session_id, + poll_interval_s=float( + getattr(settings, "environment_leapspace_poll_interval_s", 0.5) + ), + ) + ) + registered_any = True + except Exception as exc: # noqa: BLE001 + logger.warning("Could not register LeapSpaceEnvironmentSource: %s", exc, exc_info=True) + else: + logger.warning( + "LeapSpace environment source requires both state_root and session_id" + ) + + # ── Physical hardware source (available whenever hardware is admitted) ── + hardware_registry = getattr(ctx, "_hardware_registry", None) + if hardware_registry is not None: + try: + from leapflow.hardware.environment_source import PhysicalEnvironmentSource + + physical_source = PhysicalEnvironmentSource( + hardware_registry, + evidence_store=None, + capability_index=None, + ) + manager.register(physical_source) + registered_any = True + logger.info("Registered PhysicalEnvironmentSource for hardware observation") + except Exception as exc: # noqa: BLE001 + logger.warning("Could not register PhysicalEnvironmentSource: %s", exc, exc_info=True) + + if not registered_any: + return + await manager.start() self._environment_source_manager = manager @@ -1732,6 +1780,9 @@ async def status(self, session_id: str = "") -> dict[str, Any]: else 0, }, "evolution_performance": self._evolution_performance_status(ctx), + # Session turn count + cache hit rate for the self-awareness runtime + # facet (empty when no session engine is resolved for this caller). + **engine_runtime_extras(engine), # Whether *this* daemon process still matches the source tree on # disk (None when outside a git checkout, e.g. a packaged install). "build": {**self._build_info.to_dict(), "stale": build_stale}, diff --git a/src/leapflow/hardware/__init__.py b/src/leapflow/hardware/__init__.py index da8b5ec2..78962bfe 100644 --- a/src/leapflow/hardware/__init__.py +++ b/src/leapflow/hardware/__init__.py @@ -60,6 +60,19 @@ TransportStatus, WriteOutcome, ) +from leapflow.hardware.evidence import ( + EvidenceStore, +) +from leapflow.hardware.verification import ( + EvidenceBundle, + GraspVerifier, + OperationVerdict, + OperationVerifier, + PositionVerifier, + VerdictStatus, + VerificationMode, + collect_evidence, +) __all__ = [ "HC_VERSION", @@ -74,8 +87,11 @@ "ContextSource", "Direction", "Envelope", + "EvidenceBundle", + "EvidenceStore", "FrameReading", "FrameTransport", + "GraspVerifier", "HardwareContext", "HardwareEffect", "HardwareRegistry", @@ -83,6 +99,9 @@ "HardwareTransport", "Interlock", "LoadReport", + "OperationVerdict", + "OperationVerifier", + "PositionVerifier", "PrivacyTier", "Quality", "Reading", @@ -91,5 +110,8 @@ "TransportRef", "TransportStatus", "UnverifiedContextPolicy", + "VerdictStatus", + "VerificationMode", "WriteOutcome", + "collect_evidence", ] diff --git a/src/leapflow/hardware/capability_router.py b/src/leapflow/hardware/capability_router.py new file mode 100644 index 00000000..6596904e --- /dev/null +++ b/src/leapflow/hardware/capability_router.py @@ -0,0 +1,601 @@ +# Copyright (c) Alibaba, Inc. and its affiliates. +"""Capability router: maps declared affordances to callable tools. + +When a device declares ``capabilities.affordances = ("grasp", "place", "push")``, +the router generates high-level semantic tools that the LLM agent can invoke +directly. Each generated tool delegates to the appropriate device, channels, +and inference strategy through the existing HCP execution pipeline. + +This is the bridge between *what a device can do* (declared in YAML) and +*what an agent can ask for* (tools in the LLM context). Without it, the +agent must reason about raw channels and joint positions; with it, the +agent works at the task level. + +The router is deliberately a thin routing layer. It never implements +manipulation logic: an affordance command resolves to one of the existing +execution paths (policy inference or a batch actuate transaction), so every +physical write still flows through the same approval chain, trust ledger, and +audit record that a hand-written ``hw_*`` tool would. +""" + +from __future__ import annotations + +import logging +import threading +from dataclasses import dataclass +from typing import Any + +from leapflow.hardware.context import HardwareEffect +from leapflow.plugins.protocol import ToolMetadata + +logger = logging.getLogger(__name__) + +# Metadata shared by every generated affordance tool. An affordance always +# commands the physical world, so it is declared high-risk, state-mutating, and +# non-replayable: re-issuing "grasp" from an unknown pose is not a safe retry. +# ``effect_scope="external"`` is what makes the execution policy classify a +# failed affordance as an external side effect rather than an idempotent +# mutation, which is what stops the loop from replaying it blindly. +_AFFORDANCE_METADATA: dict[str, Any] = { + "category": "hardware", + "risk_level": "high", + "requires_approval": True, + "effect_scope": "external", + "idempotency_scope": "session", + "mutates_state": True, + "execution_policy": "serial", + "schema_cost": "low", +} + + +@dataclass(frozen=True) +class CapabilityEntry: + """One device's ability to perform an affordance. + + Immutable snapshot built from a device's ``CapabilityDeclaration`` and its + channel set. ``actuate_channels`` are the writable ACTUATE endpoints a + command can move; ``sensor_channels`` are the readable scalar/state + endpoints used to observe current state and verify the result; and + ``frame_channels`` are the camera endpoints an observation-driven policy + consumes. + """ + + device_id: str + affordance: str + device_class: str # "robot_arm", "mobile_robot", etc. + kinematics: Any # KinematicsDeclaration or None + reach_m: float + payload_kg: float + actuate_channels: tuple[str, ...] # writable ACTUATE channels + sensor_channels: tuple[str, ...] # readable channels for verification + frame_channels: tuple[str, ...] # camera channels + + def to_dict(self) -> dict[str, Any]: + return { + "device_id": self.device_id, + "affordance": self.affordance, + "device_class": self.device_class, + "reach_m": self.reach_m, + "payload_kg": self.payload_kg, + "actuate_channels": list(self.actuate_channels), + "sensor_channels": list(self.sensor_channels), + "frame_channels": list(self.frame_channels), + } + + +class CapabilityIndex: + """Index of all affordances across all registered devices. + + Built from ``HardwareRegistry.contexts()`` -- scans every device's + ``CapabilityDeclaration`` and builds a lookup from affordance name + to the devices that can provide it. + + Thread-safe: rebuilt on demand when the registry changes. Construction is + lazy -- the first ``resolve``/``all_affordances`` call triggers a scan, and + callers may force a re-scan with ``rebuild`` after the registry reloads. + """ + + def __init__(self, registry: Any) -> None: + self._registry = registry + self._index: dict[str, list[CapabilityEntry]] = {} + self._built = False + self._lock = threading.RLock() + + def rebuild(self) -> None: + """Scan all devices and rebuild the affordance index. + + Fail-soft: a registry that cannot enumerate its contexts yields an + empty index rather than propagating, because a router that raises on + access would take down tool assembly for every other plugin. + """ + with self._lock: + index: dict[str, list[CapabilityEntry]] = {} + for context in self._iter_contexts(): + capabilities = getattr(context, "capabilities", None) + if capabilities is None: + continue + affordances = tuple(getattr(capabilities, "affordances", ()) or ()) + if not affordances: + continue + actuate, sensors, frames = self._classify_channels(context) + for affordance in affordances: + name = str(affordance).strip() + if not name: + continue + entry = CapabilityEntry( + device_id=context.device_id, + affordance=name, + device_class=str(getattr(context, "device_class", "") or ""), + kinematics=getattr(context, "kinematics", None), + reach_m=float(getattr(capabilities, "reach_m", 0.0) or 0.0), + payload_kg=float(getattr(capabilities, "payload_kg", 0.0) or 0.0), + actuate_channels=actuate, + sensor_channels=sensors, + frame_channels=frames, + ) + index.setdefault(name, []).append(entry) + self._index = index + self._built = True + + def resolve(self, affordance: str) -> list[CapabilityEntry]: + """Return all devices that declare the given affordance.""" + self._ensure_built() + with self._lock: + return list(self._index.get(str(affordance).strip(), ())) + + def all_affordances(self) -> frozenset[str]: + """Return the union of all declared affordances.""" + self._ensure_built() + with self._lock: + return frozenset(self._index.keys()) + + def _ensure_built(self) -> None: + with self._lock: + already = self._built + if not already: + self.rebuild() + + def _iter_contexts(self) -> tuple[Any, ...]: + if self._registry is None: + return () + try: + return tuple(self._registry.contexts()) + except Exception as exc: # noqa: BLE001 - a scan failure must not raise + logger.warning("Capability index scan failed: %s", exc, exc_info=True) + return () + + @staticmethod + def _classify_channels(context: Any) -> tuple[tuple[str, ...], tuple[str, ...], tuple[str, ...]]: + """Partition a device's channels into actuate / sensor / frame sets. + + A channel is an actuator when it is writable and declares + ``effect=actuate``; a sensor when it is readable and carries a value + (not bytes); a frame when it carries media. The partition is what lets + the executor read state, drive motion, and feed a vision policy without + re-deriving channel roles at every call. + """ + actuate: list[str] = [] + sensors: list[str] = [] + frames: list[str] = [] + for channel in getattr(context, "channels", ()) or (): + if getattr(channel, "is_media", False): + frames.append(channel.channel_id) + continue + if channel.is_writable and channel.effect == HardwareEffect.ACTUATE.value: + actuate.append(channel.channel_id) + if channel.is_readable: + sensors.append(channel.channel_id) + return tuple(actuate), tuple(sensors), tuple(frames) + + +class CapabilityExecutor: + """Executes a high-level affordance command on a device. + + This is the runtime component that receives a tool call like + ``hw_grasp(device_id="robot.arm", target="red cup")`` and translates it + into the HCP execution pipeline: + + 1. Resolve device and capability entry from ``CapabilityIndex``. + 2. Read current state via ``registry.read_batch()``. + 3. Determine execution strategy: + a. If ``PhysicalSkillPlugin`` has a loaded policy -> delegate to + ``hw_policy_infer``. + b. If the caller supplied an explicit joint-space target -> use + ``hw_batch_actuate``. + c. Otherwise -> return an error with a suggestion to load a policy. + 4. Execute through the approval chain (owned by the delegated tool). + 5. Optionally verify via the delegated tool's verification path. + + The executor does NOT implement manipulation logic itself -- it routes to + the appropriate execution path. + """ + + def __init__( + self, + index: CapabilityIndex, + registry: Any, + skill_plugin: Any = None, # PhysicalSkillPlugin reference + gate: Any = None, # approval gate + ) -> None: + self._index = index + self._registry = registry + self._skill_plugin = skill_plugin + self._gate = gate + self._hw_tools: Any = None + + async def execute(self, affordance: str, params: dict) -> dict: + """Execute an affordance command. Returns a structured result.""" + params = dict(params or {}) + entries = self._index.resolve(affordance) + if not entries: + return { + "ok": False, + "error": "unsupported_affordance", + "affordance": affordance, + "hint": ( + "No admitted device declares this affordance. Declared " + f"affordances: {sorted(self._index.all_affordances()) or '(none)'}." + ), + } + + entry = self._select(entries, str(params.get("device_id") or "")) + if entry is None: + candidates = [e.device_id for e in entries] + return { + "ok": False, + "error": "ambiguous_device", + "affordance": affordance, + "candidates": candidates, + "hint": ( + "Multiple devices provide this affordance; pass device_id to " + f"choose one of: {', '.join(candidates)}." + ), + } + + # Read current state (best effort): it is context for the operator and + # the starting point a policy or verification compares against. A read + # failure must not block the command -- it is diagnostic, not gating. + state = await self._read_state(entry) + verify = bool(params.get("verify")) + + # Strategy (a): a loaded policy owns the manipulation logic. + policy = self._selected_policy() + if policy is not None: + result = await self._execute_via_policy(entry, affordance, params, policy) + return self._decorate(result, entry, state, strategy="policy") + + # Strategy (b): an explicit joint-space target maps directly to a batch + # actuate transaction; no inverse kinematics is performed here. + commands = self._commands_from_position(entry, params.get("position")) + if commands: + result = await self._execute_via_batch(entry, commands, verify=verify) + return self._decorate(result, entry, state, strategy="batch_actuate") + + # Strategy (c): nothing can turn the target into motion. + return { + "ok": False, + "error": "no_execution_strategy", + "affordance": affordance, + "device_id": entry.device_id, + "current_state": state, + "suggestion": ( + "No policy is loaded and no explicit joint-space target was given. " + "Load a manipulation policy (hw_policy_infer with a policy path), or " + "issue joint positions directly via hw_batch_actuate. A natural-language " + "target alone cannot be turned into motion without a policy." + ), + } + + # -- Strategy routing -------------------------------------------------- + + async def _execute_via_policy( + self, entry: CapabilityEntry, affordance: str, params: dict, policy: str + ) -> dict: + """Delegate to the physical skill plugin's policy inference tool.""" + task = str(params.get("target") or affordance) + infer = getattr(self._skill_plugin, "policy_infer", None) + if infer is None: + return {"ok": False, "error": "skill_plugin_unavailable"} + try: + return await infer( + { + "device_id": entry.device_id, + "policy": policy, + "task": task, + "execute": True, + "verify": bool(params.get("verify")), + } + ) + except Exception as exc: # noqa: BLE001 - surface as a structured failure + logger.warning( + "Affordance %s policy inference failed on %s: %s", + affordance, + entry.device_id, + exc, + exc_info=True, + ) + return {"ok": False, "error": "policy_inference_failed", "detail": str(exc)} + + async def _execute_via_batch( + self, entry: CapabilityEntry, commands: list[dict], *, verify: bool + ) -> dict: + """Route a joint-space target through the batch actuate tool. + + Builds a ``HardwareTools`` instance lazily so the full approval chain, + envelope checks, and audit record apply exactly as they would for a + hand-issued ``hw_batch_actuate`` call. + """ + tools = self._hardware_tools() + if tools is None: + return {"ok": False, "error": "hardware_tools_unavailable"} + try: + return await tools.batch_actuate( + {"device_id": entry.device_id, "commands": commands, "verify": verify} + ) + except Exception as exc: # noqa: BLE001 - surface as a structured failure + logger.warning( + "Affordance batch actuate failed on %s: %s", + entry.device_id, + exc, + exc_info=True, + ) + return {"ok": False, "error": "batch_actuate_failed", "detail": str(exc)} + + def _hardware_tools(self) -> Any: + if self._hw_tools is None and self._registry is not None: + from leapflow.hardware.tools import HardwareTools + + self._hw_tools = HardwareTools(self._registry, gate=self._gate) + return self._hw_tools + + async def _read_state(self, entry: CapabilityEntry) -> dict[str, Any]: + """Read the device's sensor and actuator channels, best effort.""" + channels = tuple(dict.fromkeys((*entry.sensor_channels, *entry.actuate_channels))) + if not channels or self._registry is None: + return {"available": False, "reason": "no readable channels"} + try: + batch = await self._registry.read_batch(entry.device_id, channels) + except Exception as exc: # noqa: BLE001 - state read is diagnostic, not gating + logger.debug("State read failed for %s: %s", entry.device_id, exc, exc_info=True) + return {"available": False, "reason": str(exc)} + readings = getattr(batch, "readings", ()) or () + values = { + getattr(r, "channel_id", ""): getattr(r, "value", None) + for r in readings + } + return {"available": True, "values": values} + + # -- Resolution helpers ------------------------------------------------ + + @staticmethod + def _select(entries: list[CapabilityEntry], device_id: str) -> CapabilityEntry | None: + """Pick the entry to act on: the named device, or the sole candidate.""" + if device_id: + return next((e for e in entries if e.device_id == device_id), None) + if len(entries) == 1: + return entries[0] + return None + + def _selected_policy(self) -> str | None: + """Return a loaded policy key from the skill plugin, if any. + + Reads the plugin's policy cache directly: without a cached policy there + is no path to hand ``hw_policy_infer``, so the executor falls through to + the batch or error strategy instead of guessing one. + """ + if self._skill_plugin is None: + return None + policies = getattr(self._skill_plugin, "_policies", None) + if not policies: + return None + # The most recently loaded policy is the most likely intended one. + try: + return next(reversed(list(policies.keys()))) + except StopIteration: + return None + + @staticmethod + def _commands_from_position(entry: CapabilityEntry, position: Any) -> list[dict]: + """Turn an explicit joint-space ``position`` mapping into batch commands. + + Only a mapping of ``channel_id -> value`` naming this device's actuate + channels is accepted. A cartesian ``{x, y, z}`` target is deliberately + rejected here: converting it to joint angles is inverse kinematics, and + the executor refuses to guess it. + """ + if not isinstance(position, dict): + return [] + actuate = set(entry.actuate_channels) + commands: list[dict] = [] + for channel_id, value in position.items(): + if channel_id in actuate and isinstance(value, (int, float)): + commands.append({"channel_id": channel_id, "value": float(value)}) + return commands + + @staticmethod + def _decorate( + result: dict, entry: CapabilityEntry, state: dict, *, strategy: str + ) -> dict: + """Attach routing provenance to a delegated tool result.""" + if not isinstance(result, dict): + result = {"ok": False, "error": "invalid_result"} + result.setdefault("device_id", entry.device_id) + result["affordance"] = entry.affordance + result["strategy"] = strategy + result.setdefault("current_state", state) + return result + + +class CapabilityToolGenerator: + """Generates ``ToolMetadata`` for each declared affordance. + + For each affordance in the ``CapabilityIndex``, generates one tool: + - name: ``hw_{affordance}`` (e.g. ``hw_grasp``, ``hw_place``) + - parameters: device_id (auto-selected if only one), target description, + optional position and verify flag + - x_leapflow: category=hardware, risk_level=high, mutates_state=True + - handler: delegates to ``CapabilityExecutor`` + + Generated tools are registered through the plugin assembly pipeline, so + they appear in the PCD tool catalog and go through the full approval chain. + """ + + def __init__(self, index: CapabilityIndex, executor: CapabilityExecutor) -> None: + self._index = index + self._executor = executor + + def generate(self) -> list[Any]: # list[ToolMetadata] + """Generate one ToolMetadata per unique affordance.""" + tools: list[ToolMetadata] = [] + for affordance in sorted(self._index.all_affordances()): + entries = self._index.resolve(affordance) + if not entries: + continue + tools.append(self._tool_for_affordance(affordance, entries)) + return tools + + def _tool_for_affordance( + self, affordance: str, entries: list[CapabilityEntry] + ) -> Any: + """Build ToolMetadata for one affordance.""" + devices = ", ".join(sorted(e.device_id for e in entries)) + description = ( + f"Perform the '{affordance}' affordance on a robot device. This commands " + "physical motion at the task level: describe the target in natural language " + "and the router selects the device, reads current state, and routes to a " + "loaded manipulation policy (or an explicit joint-space command). Physical " + "and irreversible -- a repeat after a failure is not a safe retry. " + f"Available on: {devices}." + ) + schema = { + "type": "object", + "properties": { + "device_id": { + "type": "string", + "description": ( + "Target device from hw_list (optional if only one device " + f"provides '{affordance}')." + ), + }, + "target": { + "type": "string", + "description": ( + f"What to {affordance} (natural language, e.g. 'the red cup')." + ), + }, + "position": { + "type": "object", + "description": ( + "Optional explicit joint-space target: a mapping of actuate " + "channel_id to value. Used only when no manipulation policy is " + "loaded; a cartesian {x, y, z} target is not accepted here." + ), + }, + "verify": { + "type": "boolean", + "description": "Run post-operation verification (default false).", + }, + }, + "required": [], + } + return ToolMetadata( + name=f"hw_{affordance}", + description=description, + parameters_schema=schema, + handler=self._make_handler(affordance), + x_leapflow=dict(_AFFORDANCE_METADATA), + mutates_state=True, + execution_policy="serial", + provides_capabilities=(f"hw.affordance.{affordance}",), + requires_capabilities=("hardware_control",), + ) + + def _make_handler(self, affordance: str) -> Any: + """Return an async handler bound to one affordance. + + Accepts keyword arguments so ``invoke_tool_handler`` dispatches the + LLM-provided arguments straight through to the executor. + """ + executor = self._executor + + async def handler(**kwargs: Any) -> dict: + return await executor.execute(affordance, kwargs) + + return handler + + +class CapabilityRouterPlugin: + """ToolPlugin that exposes auto-generated affordance tools. + + Dependencies: + - hardware_registry: to scan device capabilities + - hardware_approval_gate: for generated tools' approval chain + - physical_skill_plugin: optional, for policy-based execution + + Tool generation is lazy: tools are generated on first access to + ``self.tools`` and regenerated when the registry changes. + """ + + plugin_id = "capability_router" + category = "hardware" + dependencies = ["hardware_registry", "hardware_approval_gate"] + + def __init__(self) -> None: + self._registry: Any = None + self._gate: Any = None + self._skill_plugin: Any = None + self._tools: list[ToolMetadata] | None = None + + def bind_runtime(self, **deps: Any) -> None: + """Receive the registry, the approval gate, and an optional skill plugin. + + The registry is optional on purpose: with hardware disabled nothing + binds it, ``tools`` stays empty, and the tool index is byte-identical to + a build without this plugin. Any dependency change invalidates the + generated tool cache so a registry reload is reflected on next access. + """ + changed = False + if "hardware_registry" in deps: + self._registry = deps.get("hardware_registry") + changed = True + if "hardware_approval_gate" in deps: + self._gate = deps.get("hardware_approval_gate") + changed = True + # Optional: the physical skill plugin enables the policy execution path. + # Not in ``dependencies`` because it is an enhancement, not a requirement; + # accepted opportunistically when the assembler injects it. + if "physical_skill_plugin" in deps: + self._skill_plugin = deps.get("physical_skill_plugin") + changed = True + if changed: + self._tools = None + + @property + def tools(self) -> list[ToolMetadata]: + """Return generated affordance tools, empty if no device has capabilities.""" + if self._registry is None: + return [] + if self._tools is None: + index = CapabilityIndex(self._registry) + executor = CapabilityExecutor( + index, + self._registry, + skill_plugin=self._skill_plugin, + gate=self._gate, + ) + generator = CapabilityToolGenerator(index, executor) + self._tools = generator.generate() + return list(self._tools) + + +plugin = CapabilityRouterPlugin() + + +__all__ = [ + "CapabilityEntry", + "CapabilityExecutor", + "CapabilityIndex", + "CapabilityRouterPlugin", + "CapabilityToolGenerator", + "plugin", +] diff --git a/src/leapflow/hardware/context.py b/src/leapflow/hardware/context.py index 96b98c9e..e382c0da 100644 --- a/src/leapflow/hardware/context.py +++ b/src/leapflow/hardware/context.py @@ -27,7 +27,7 @@ worse than rejecting it with a reason. """ -SUPPORTED_HC_VERSIONS: frozenset[str] = frozenset({HC_VERSION}) +SUPPORTED_HC_VERSIONS: frozenset[str] = frozenset({HC_VERSION, "hc.v1"}) _DEFAULT_HYSTERESIS_SPAN_FRACTION = 0.01 """Settle band used when a channel declares no quantization, as a share of span.""" @@ -619,6 +619,214 @@ def from_mapping(cls, data: Mapping[str, Any] | None) -> "TransportRef": ) +@dataclass(frozen=True) +class KinematicsDeclaration: + """Robot kinematics declaration for motion planning and safety. + + Describes the physical structure of a robot manipulator: degrees of + freedom, joint limits, and workspace bounds. This information is + consumed by safety checks (workspace violation detection) and by + the LLM context assembler (task feasibility assessment). + """ + + chain_type: str = "" # "serial", "parallel", "mobile", "humanoid" + dof: int = 0 + joint_limits: tuple[tuple[float, float], ...] = () # per-joint (min, max) in rad + workspace_bounds: tuple[float, ...] = () # (x_min, x_max, y_min, y_max, z_min, z_max) in meters + base_frame: str = "" # reference frame name + tool_frame: str = "" # end-effector frame name + notes: str = "" + + def to_dict(self) -> dict[str, Any]: + return { + "chain_type": self.chain_type, + "dof": self.dof, + "joint_limits": [list(pair) for pair in self.joint_limits], + "workspace_bounds": list(self.workspace_bounds), + "base_frame": self.base_frame, + "tool_frame": self.tool_frame, + "notes": self.notes, + } + + @classmethod + def from_mapping(cls, data: Mapping[str, Any] | None) -> "KinematicsDeclaration": + data = data or {} + raw_limits = data.get("joint_limits") or () + joint_limits = tuple( + (float(pair[0]), float(pair[1])) + for pair in raw_limits + if isinstance(pair, (list, tuple)) and len(pair) >= 2 + ) + raw_bounds = data.get("workspace_bounds") or () + workspace_bounds = tuple(float(v) for v in raw_bounds) + return cls( + chain_type=str(data.get("chain_type") or ""), + dof=int(data.get("dof") or 0), + joint_limits=joint_limits, + workspace_bounds=workspace_bounds, + base_frame=str(data.get("base_frame") or ""), + tool_frame=str(data.get("tool_frame") or ""), + notes=str(data.get("notes") or ""), + ) + + +@dataclass(frozen=True) +class CapabilityDeclaration: + """Declared capabilities and affordances of a device. + + Affordances are verbs that describe what the device can do in the + physical world: 'grasp', 'push', 'pour', 'inspect'. They are the + bridge between LLM task decomposition and hardware capability + resolution. + """ + + affordances: tuple[str, ...] = () # ("grasp", "push", "place", ...) + tool_slots: int = 0 # number of tool change positions + payload_kg: float = 0.0 # maximum payload in kilograms + reach_m: float = 0.0 # maximum reach in meters + notes: str = "" + + def to_dict(self) -> dict[str, Any]: + return { + "affordances": list(self.affordances), + "tool_slots": self.tool_slots, + "payload_kg": self.payload_kg, + "reach_m": self.reach_m, + "notes": self.notes, + } + + @classmethod + def from_mapping(cls, data: Mapping[str, Any] | None) -> "CapabilityDeclaration": + data = data or {} + raw_affordances = data.get("affordances") or () + return cls( + affordances=tuple(str(a) for a in raw_affordances), + tool_slots=int(data.get("tool_slots") or 0), + payload_kg=_as_float(data.get("payload_kg"), default=0.0) or 0.0, + reach_m=_as_float(data.get("reach_m"), default=0.0) or 0.0, + notes=str(data.get("notes") or ""), + ) + + +@dataclass(frozen=True) +class SafetyPolicy: + """Declared safety constraints for a device. + + These constraints are enforced at runtime by the registry before + any write command reaches the transport. They complement the + per-channel Envelope (which constrains individual values) with + device-level spatial and force limits. + """ + + max_velocity_rad_s: float = 0.0 # global joint velocity limit + max_force_n: float = 0.0 # global force limit + collision_zones: tuple[tuple[float, ...], ...] = () # axis-aligned boxes (x_min,x_max,y_min,y_max,z_min,z_max) + emergency_decel_s: float = 0.0 # time to reach zero velocity on e-stop + require_safety_interlock: bool = False + notes: str = "" + + def to_dict(self) -> dict[str, Any]: + return { + "max_velocity_rad_s": self.max_velocity_rad_s, + "max_force_n": self.max_force_n, + "collision_zones": [list(zone) for zone in self.collision_zones], + "emergency_decel_s": self.emergency_decel_s, + "require_safety_interlock": self.require_safety_interlock, + "notes": self.notes, + } + + @classmethod + def from_mapping(cls, data: Mapping[str, Any] | None) -> "SafetyPolicy": + data = data or {} + raw_zones = data.get("collision_zones") or () + collision_zones = tuple( + tuple(float(v) for v in zone) + for zone in raw_zones + if isinstance(zone, (list, tuple)) + ) + return cls( + max_velocity_rad_s=_as_float(data.get("max_velocity_rad_s"), default=0.0) or 0.0, + max_force_n=_as_float(data.get("max_force_n"), default=0.0) or 0.0, + collision_zones=collision_zones, + emergency_decel_s=_as_float(data.get("emergency_decel_s"), default=0.0) or 0.0, + require_safety_interlock=bool(data.get("require_safety_interlock", False)), + notes=str(data.get("notes") or ""), + ) + + +@dataclass(frozen=True) +class TrustConfig: + """Per-device trust progression configuration. + + Overrides the global HardwareTrustGate defaults for this specific + device. A high-risk industrial arm may require more successes to + promote, while a simple sensor may start at a higher level. + """ + + initial_level: str = "" # "UNTRUSTED", "CANDIDATE", "VERIFIED", "PRODUCTION" + promotion_thresholds: tuple[int, ...] = () # successes needed per level + demotion_threshold: int = 0 # consecutive failures to demote + approval_override: str = "" # "always", "never", "" (use default) + notes: str = "" + + def to_dict(self) -> dict[str, Any]: + return { + "initial_level": self.initial_level, + "promotion_thresholds": list(self.promotion_thresholds), + "demotion_threshold": self.demotion_threshold, + "approval_override": self.approval_override, + "notes": self.notes, + } + + @classmethod + def from_mapping(cls, data: Mapping[str, Any] | None) -> "TrustConfig": + data = data or {} + raw_thresholds = data.get("promotion_thresholds") or () + return cls( + initial_level=str(data.get("initial_level") or ""), + promotion_thresholds=tuple(int(t) for t in raw_thresholds), + demotion_threshold=int(data.get("demotion_threshold") or 0), + approval_override=str(data.get("approval_override") or ""), + notes=str(data.get("notes") or ""), + ) + + +@dataclass(frozen=True) +class DegradationPolicy: + """Declared behavior when components degrade or fail. + + Tells the runtime what to do when a sensor stops reporting, when + communication is lost, or when the device cannot maintain its + declared performance. + """ + + sensor_loss_policy: str = "halt" # "halt", "continue_blind", "switch_sensor" + comm_loss_policy: str = "halt" # "halt", "hold_position", "safe_return" + fallback_mode: str = "" # device-specific fallback configuration + max_comm_loss_s: float = 1.0 # max seconds of communication loss before policy triggers + notes: str = "" + + def to_dict(self) -> dict[str, Any]: + return { + "sensor_loss_policy": self.sensor_loss_policy, + "comm_loss_policy": self.comm_loss_policy, + "fallback_mode": self.fallback_mode, + "max_comm_loss_s": self.max_comm_loss_s, + "notes": self.notes, + } + + @classmethod + def from_mapping(cls, data: Mapping[str, Any] | None) -> "DegradationPolicy": + data = data or {} + return cls( + sensor_loss_policy=str(data.get("sensor_loss_policy") or "halt"), + comm_loss_policy=str(data.get("comm_loss_policy") or "halt"), + fallback_mode=str(data.get("fallback_mode") or ""), + max_comm_loss_s=_as_float(data.get("max_comm_loss_s"), default=1.0) or 1.0, + notes=str(data.get("notes") or ""), + ) + + @dataclass(frozen=True) class HardwareContext: """Everything an agent must know about one device. The SSOT of the protocol. @@ -650,6 +858,20 @@ class HardwareContext: camera, storage and sensor sections instead of one flat list. """ + # hc.v1 extensions — optional, None for hc.v0 declarations + kinematics: KinematicsDeclaration | None = None + capabilities: CapabilityDeclaration | None = None + safety: SafetyPolicy | None = None + trust_config: TrustConfig | None = None + degradation: DegradationPolicy | None = None + control_bindings: Mapping[str, Any] | None = None + """Raw control_bindings section from the YAML declaration. + + Parsed by :class:`~leapflow.hardware.control_binding_resolver.ControlBindingResolver` + into executable transport configurations. Stored as-is so the resolver + can operate after ``from_mapping`` without a second YAML parse pass. + """ + def channel(self, channel_id: str) -> Channel | None: return next((c for c in self.channels if c.channel_id == channel_id), None) @@ -695,6 +917,12 @@ def with_channels(self, channels: tuple[Channel, ...]) -> "HardwareContext": notes=self.notes, provenance=self.provenance, device_class=self.device_class, + kinematics=self.kinematics, + capabilities=self.capabilities, + safety=self.safety, + trust_config=self.trust_config, + degradation=self.degradation, + control_bindings=self.control_bindings, ) def read_only(self) -> "HardwareContext": @@ -702,7 +930,7 @@ def read_only(self) -> "HardwareContext": return self.with_channels(tuple(c.without_write() for c in self.channels)) def to_dict(self) -> dict[str, Any]: - return { + result: dict[str, Any] = { "hc_version": self.hc_version, "device_id": self.device_id, "display_name": self.display_name, @@ -717,6 +945,20 @@ def to_dict(self) -> dict[str, Any]: "channels": [c.to_dict() for c in self.channels], "interlocks": [i.to_dict() for i in self.interlocks], } + # hc.v1 extensions — omit when absent for backward-compatible output + if self.kinematics is not None: + result["kinematics"] = self.kinematics.to_dict() + if self.capabilities is not None: + result["capabilities"] = self.capabilities.to_dict() + if self.safety is not None: + result["safety"] = self.safety.to_dict() + if self.trust_config is not None: + result["trust_config"] = self.trust_config.to_dict() + if self.degradation is not None: + result["degradation"] = self.degradation.to_dict() + if self.control_bindings is not None: + result["control_bindings"] = dict(self.control_bindings) + return result @classmethod def from_mapping(cls, data: Mapping[str, Any]) -> "HardwareContext": @@ -736,6 +978,41 @@ def from_mapping(cls, data: Mapping[str, Any]) -> "HardwareContext": for item in data.get("interlocks") or () if isinstance(item, Mapping) ) + # hc.v1 extensions — parse when present, leave None otherwise + raw_kin = data.get("kinematics") + kinematics = ( + KinematicsDeclaration.from_mapping(raw_kin) + if isinstance(raw_kin, Mapping) + else None + ) + raw_cap = data.get("capabilities") + capabilities = ( + CapabilityDeclaration.from_mapping(raw_cap) + if isinstance(raw_cap, Mapping) + else None + ) + raw_safety = data.get("safety") + safety = ( + SafetyPolicy.from_mapping(raw_safety) + if isinstance(raw_safety, Mapping) + else None + ) + raw_trust = data.get("trust_config") + trust_config = ( + TrustConfig.from_mapping(raw_trust) + if isinstance(raw_trust, Mapping) + else None + ) + raw_degrad = data.get("degradation") + degradation = ( + DegradationPolicy.from_mapping(raw_degrad) + if isinstance(raw_degrad, Mapping) + else None + ) + raw_bindings = data.get("control_bindings") + control_bindings = ( + dict(raw_bindings) if isinstance(raw_bindings, Mapping) else None + ) return cls( device_id=str(data.get("device_id") or ""), hc_version=str(data.get("hc_version") or ""), @@ -750,6 +1027,12 @@ def from_mapping(cls, data: Mapping[str, Any]) -> "HardwareContext": notes=str(data.get("notes") or ""), provenance=ContextProvenance.from_mapping(data.get("provenance")), device_class=str(data.get("device_class") or ""), + kinematics=kinematics, + capabilities=capabilities, + safety=safety, + trust_config=trust_config, + degradation=degradation, + control_bindings=control_bindings, ) @@ -808,17 +1091,22 @@ def _declared_span(envelope: "Envelope") -> float: __all__ = [ "HC_VERSION", "SUPPORTED_HC_VERSIONS", + "CapabilityDeclaration", "Channel", "ContextProvenance", "ContextSource", + "DegradationPolicy", "Direction", "Envelope", "HardwareContext", "HardwareEffect", "Interlock", + "KinematicsDeclaration", "PrivacyTier", "Quality", "Representation", + "SafetyPolicy", "TransportRef", + "TrustConfig", "as_numeric", ] diff --git a/src/leapflow/hardware/control_binding_resolver.py b/src/leapflow/hardware/control_binding_resolver.py new file mode 100644 index 00000000..f832d04d --- /dev/null +++ b/src/leapflow/hardware/control_binding_resolver.py @@ -0,0 +1,341 @@ +# Copyright (c) Alibaba, Inc. and its affiliates. +"""Control binding resolver: translates declarative interface bindings +into executable transport configurations. + +When a hardware YAML declares ``control_bindings``, this module resolves +them to concrete ``TransportRef`` instances that the registry can use to +instantiate the appropriate transport. The binding declaration is +transport-neutral — it says *what* to connect to, not *how* — and this +resolver maps the *what* to the *how*. + +Supported binding types: + +- ``local_bus``: serial port, CAN bus, or I2C — maps to ``robot_arm`` transport +- ``ros2_topics``: ROS2 topic/service/action — maps to ``ros2`` transport (when available) +- ``mcp_server``: MCP server endpoint — maps to ``mcp`` transport +- ``network``: TCP/UDP endpoint — maps to future network transport + +When multiple bindings are declared, the resolver picks the highest-priority +available one (local_bus > mcp_server > ros2_topics > network), ensuring +the lowest-latency path is used when available. +""" + +from __future__ import annotations + +import logging +from dataclasses import dataclass +from typing import Any, Mapping + +logger = logging.getLogger(__name__) + +# --------------------------------------------------------------------------- +# Default priority — lower-latency transports are preferred. +# --------------------------------------------------------------------------- + +_DEFAULT_PRIORITIES: Mapping[str, int] = { + "local_bus": 100, + "mcp_server": 80, + "ros2_topics": 60, + "network": 40, +} + +# Binding type → transport kind that fulfils it. +_BINDING_TO_TRANSPORT: Mapping[str, str] = { + "local_bus": "robot_arm", + "ros2_topics": "ros2", + "mcp_server": "mcp", + "network": "network", +} + + +# --------------------------------------------------------------------------- +# Data types +# --------------------------------------------------------------------------- + + +@dataclass(frozen=True) +class ControlBinding: + """One declared control interface binding.""" + + binding_type: str # "local_bus", "ros2_topics", "mcp_server", "network" + config: Mapping[str, Any] # type-specific configuration + priority: int = 0 # higher = preferred + + +@dataclass(frozen=True) +class ResolvedBinding: + """A control binding resolved to a concrete transport configuration.""" + + transport_ref: Any # TransportRef + binding: ControlBinding + available: bool = True # whether the transport kind is registered + reason: str = "" # why unavailable, if not available + + +# --------------------------------------------------------------------------- +# Resolver +# --------------------------------------------------------------------------- + + +class ControlBindingResolver: + """Resolves control_bindings declarations to TransportRef instances. + + Usage:: + + resolver = ControlBindingResolver() + bindings = resolver.parse(yaml_data.get("control_bindings", {})) + resolved = resolver.resolve(bindings) + if resolved and resolved.available: + context = replace(context, transport=resolved.transport_ref) + """ + + # -- public API --------------------------------------------------------- + + def parse(self, raw: Mapping[str, Any]) -> tuple[ControlBinding, ...]: + """Parse a control_bindings YAML section into typed bindings. + + Example YAML:: + + control_bindings: + local_bus: + type: serial + port: /dev/ttyUSB0 + baudrate: 1000000 + ros2_topics: + joint_states: /robot/joint_states + joint_commands: /robot/joint_commands + mcp_server: + url: "http://localhost:8080" + + Each top-level key is a binding type. The value is a mapping of + type-specific configuration. An explicit ``priority`` key inside + the config overrides the default ordering. + """ + if not isinstance(raw, Mapping): + return () + + bindings: list[ControlBinding] = [] + for key, value in raw.items(): + key_str = str(key).strip() + if not key_str or key_str not in _BINDING_TO_TRANSPORT: + logger.debug( + "control_bindings: ignoring unknown binding type %r", key_str + ) + continue + if not isinstance(value, Mapping): + logger.warning( + "control_bindings: expected a mapping for %r, got %s; skipping", + key_str, + type(value).__name__, + ) + continue + + config = dict(value) + # Allow an explicit priority override. + explicit_priority = config.pop("priority", None) + priority = ( + int(explicit_priority) + if explicit_priority is not None + else _DEFAULT_PRIORITIES.get(key_str, 0) + ) + bindings.append( + ControlBinding( + binding_type=key_str, + config=config, + priority=priority, + ) + ) + + # Sort descending by priority so the first match is best. + bindings.sort(key=lambda b: b.priority, reverse=True) + return tuple(bindings) + + def resolve( + self, + bindings: tuple[ControlBinding, ...], + *, + available_transports: frozenset[str] | None = None, + ) -> ResolvedBinding | None: + """Resolve bindings to the best available transport. + + Tries bindings in priority order (local_bus > mcp_server > ros2 > network). + Returns the first one whose transport kind is available, or the first + unavailable one (with ``available=False``) if none are available, or + ``None`` if *bindings* is empty. + """ + if not bindings: + return None + + if available_transports is None: + available_transports = self._discover_available_transports() + + first_unavailable: ResolvedBinding | None = None + + for binding in bindings: + transport_kind = _BINDING_TO_TRANSPORT.get(binding.binding_type) + if transport_kind is None: + continue + + resolver = self._resolver_for(binding.binding_type) + if resolver is None: + continue + + transport_ref = resolver(binding.config) + is_available = transport_kind in available_transports + + resolved = ResolvedBinding( + transport_ref=transport_ref, + binding=binding, + available=is_available, + reason="" if is_available else ( + f"transport kind {transport_kind!r} is not registered" + ), + ) + + if is_available: + logger.debug( + "control_bindings: resolved %s -> transport %s", + binding.binding_type, + transport_kind, + ) + return resolved + + if first_unavailable is None: + first_unavailable = resolved + + # None available — return best-priority unavailable as advisory. + return first_unavailable + + # -- private resolvers -------------------------------------------------- + + def _resolver_for(self, binding_type: str) -> Any: + """Return the resolver method for the given binding type, or None.""" + dispatch: dict[str, Any] = { + "local_bus": self._resolve_local_bus, + "ros2_topics": self._resolve_ros2, + "mcp_server": self._resolve_mcp, + "network": self._resolve_network, + } + return dispatch.get(binding_type) + + def _resolve_local_bus(self, config: Mapping[str, Any]) -> Any: + """Map local_bus binding to robot_arm TransportRef. + + Supported sub-types: + - ``serial`` → serial_port in robot_arm transport config + - ``can`` → can_bus in robot_arm transport config + - ``i2c`` → i2c in robot_arm transport config + """ + from leapflow.hardware.context import TransportRef + + bus_type = str(config.get("type") or "serial").lower() + transport_config: dict[str, Any] = {} + + if bus_type == "serial": + transport_config["robot_config"] = { + "serial_port": str(config.get("port") or ""), + } + baudrate = config.get("baudrate") + if baudrate is not None: + transport_config["robot_config"]["baudrate"] = int(baudrate) + elif bus_type == "can": + transport_config["robot_config"] = { + "can_bus": str(config.get("interface") or ""), + "can_channel": str(config.get("channel") or ""), + } + bitrate = config.get("bitrate") + if bitrate is not None: + transport_config["robot_config"]["can_bitrate"] = int(bitrate) + elif bus_type == "i2c": + transport_config["robot_config"] = { + "i2c_bus": str(config.get("bus") or ""), + "i2c_address": config.get("address", 0), + } + else: + # Unknown sub-type — pass config through verbatim. + transport_config["robot_config"] = dict(config) + + # Propagate robot_type if declared alongside. + robot_type = config.get("robot_type") + if robot_type: + transport_config["robot_type"] = str(robot_type) + + return TransportRef(kind="robot_arm", config=transport_config) + + def _resolve_ros2(self, config: Mapping[str, Any]) -> Any: + """Map ros2_topics binding to ros2 TransportRef. + + The config maps logical names to ROS2 topic paths:: + + joint_states: /robot/joint_states + joint_commands: /robot/joint_commands + """ + from leapflow.hardware.context import TransportRef + + transport_config: dict[str, Any] = {"topics": dict(config)} + + # Hoist well-known keys that are transport-level rather than topic-level. + for hoist_key in ("node_name", "namespace", "qos_depth"): + value = config.get(hoist_key) + if value is not None: + transport_config[hoist_key] = value + + return TransportRef(kind="ros2", config=transport_config) + + def _resolve_mcp(self, config: Mapping[str, Any]) -> Any: + """Map mcp_server binding to mcp TransportRef. + + Expected keys: ``url`` (required), plus optional auth/timeout. + """ + from leapflow.hardware.context import TransportRef + + transport_config: dict[str, Any] = {} + url = config.get("url") + if url: + transport_config["url"] = str(url) + + # Forward optional keys. + for key in ("auth_token", "timeout_s", "headers"): + value = config.get(key) + if value is not None: + transport_config[key] = value + + return TransportRef(kind="mcp", config=transport_config) + + def _resolve_network(self, config: Mapping[str, Any]) -> Any: + """Map network binding to an appropriate transport. + + Produces a ``network`` transport kind. The transport implementation + is not yet available — the resolver emits a well-formed TransportRef + so that ``available=False`` carries a complete description of what + *would* be instantiated. + """ + from leapflow.hardware.context import TransportRef + + transport_config: dict[str, Any] = {} + for key in ("host", "port", "protocol", "timeout_s"): + value = config.get(key) + if value is not None: + transport_config[key] = value + + return TransportRef(kind="network", config=transport_config) + + # -- helpers ------------------------------------------------------------ + + @staticmethod + def _discover_available_transports() -> frozenset[str]: + """Query the transport registry for currently available kinds. + + Uses a function-local import to avoid a module-level dependency on the + transport sub-package (which may pull optional C extensions). + """ + from leapflow.hardware.transports import available_transports + + return frozenset(available_transports()) + + +__all__ = [ + "ControlBinding", + "ControlBindingResolver", + "ResolvedBinding", +] diff --git a/src/leapflow/hardware/environment_source.py b/src/leapflow/hardware/environment_source.py new file mode 100644 index 00000000..62823d7f --- /dev/null +++ b/src/leapflow/hardware/environment_source.py @@ -0,0 +1,592 @@ +# Copyright (c) Alibaba, Inc. and its affiliates. +"""Physical environment source: observes the robot workspace through HCP. + +Produces ``EnvironmentObservation`` events that feed the evolution engine's +capability gap detector. Three observation types: + +- **snapshot**: complete workspace state — all devices, their health, + capabilities, current sensor readings summary, and camera frame metadata. + Emitted periodically (configurable interval, default 30s) and on demand. + +- **delta**: structural changes since last snapshot — devices that appeared + or disappeared, affordances gained or lost, channels that changed direction + or effect, health transitions (OK→STALE, connected→disconnected). + +- **outcome**: operation result feedback — sourced from EvidenceStore's + recent verdicts, aggregated as success/failure rates per device/affordance. + +Unlike ``LeapSpaceEnvironmentSource`` which polls an external agent runner, +this source observes LeapFlow's own hardware stack directly, making it +zero-latency and always-available when hardware is enabled. +""" + +from __future__ import annotations + +import asyncio +import logging +import time +from typing import Any + +logger = logging.getLogger(__name__) + +_SOURCE_ID = "physical_hardware" +_DEFAULT_SNAPSHOT_INTERVAL_S = 30.0 +_DEFAULT_POLL_INTERVAL_S = 5.0 +_OUTCOME_WINDOW_S = 300.0 # 5-minute lookback for operation verdicts + + +class PhysicalEnvironmentSource: + """EnvironmentSource that observes the physical robot workspace. + + Satisfies the EnvironmentSource Protocol: + - source_id: "physical_hardware" + - start(emit): begin periodic observation loop + - stop(): cancel observation loop + + Dependencies (injected via constructor): + - registry: HardwareRegistry — device enumeration and health + - evidence_store: EvidenceStore (optional) — operation success rates + - capability_index: CapabilityIndex (optional) — affordance inventory + """ + + def __init__( + self, + registry: Any, + *, + evidence_store: Any = None, + capability_index: Any = None, + snapshot_interval_s: float = _DEFAULT_SNAPSHOT_INTERVAL_S, + poll_interval_s: float = _DEFAULT_POLL_INTERVAL_S, + ) -> None: + self._registry = registry + self._evidence_store = evidence_store + self._capability_index = capability_index + self._snapshot_interval_s = max(1.0, float(snapshot_interval_s)) + self._poll_interval_s = max(0.1, float(poll_interval_s)) + self._task: asyncio.Task[None] | None = None + self._stopped = asyncio.Event() + self._last_snapshot: dict[str, Any] | None = None + + # ── Protocol surface ── + + @property + def source_id(self) -> str: + return _SOURCE_ID + + async def start(self, emit: Any) -> None: + """Begin periodic observation. Returns promptly; loop runs as internal task.""" + if self._task is not None: + return + self._stopped.clear() + self._task = asyncio.create_task( + self._run(emit), name="physical-environment-source" + ) + + async def stop(self) -> None: + """Stop observation. Idempotent.""" + self._stopped.set() + task = self._task + self._task = None + if task is None: + return + task.cancel() + try: + await asyncio.wait_for(task, timeout=3.0) + except (asyncio.CancelledError, asyncio.TimeoutError): + pass + except Exception: # noqa: BLE001 – teardown must not propagate + logger.warning("Physical environment source stop raised", exc_info=True) + + # ── Core observation loop ── + + async def _run(self, emit: Any) -> None: + """Core observation loop. + + Every poll_interval_s: + 1. Scan registry for device changes → emit delta if changed + 2. Check stream health for degradation events + + Every snapshot_interval_s: + 3. Build full workspace snapshot → emit snapshot + 4. Query evidence_store for recent verdicts → emit outcome + """ + # Function-local imports: hardware must not create a top-level dependency + # on domain types so the import graph stays clean. + from leapflow.domain.environment_signal import EnvironmentObservation + + start_mono = time.monotonic() + next_poll_at = start_mono + next_snapshot_at = start_mono # first cycle always takes a full snapshot + + while not self._stopped.is_set(): + now_mono = time.monotonic() + + try: + current = await self.take_snapshot() + except Exception: # noqa: BLE001 – one failed scan must not kill the loop + logger.warning("Physical environment snapshot failed", exc_info=True) + await self._sleep(self._poll_interval_s) + next_poll_at = time.monotonic() + self._poll_interval_s + continue + + wall = current.get("timestamp", time.time()) + + if now_mono >= next_snapshot_at: + # ── Full snapshot cycle ── + iface = self._to_interface_snapshot(current, wall) + await self._emit_safe(emit, EnvironmentObservation.snapshot(iface)) + await self._emit_outcomes(emit, wall) + next_snapshot_at = now_mono + self._snapshot_interval_s + else: + # ── Delta-only cycle ── + if self._last_snapshot is not None: + delta_dict = await self.compute_delta(self._last_snapshot, current) + if delta_dict is not None: + prev_wall = self._last_snapshot.get("timestamp", wall) + prev_iface = self._to_interface_snapshot( + self._last_snapshot, prev_wall + ) + curr_iface = self._to_interface_snapshot(current, wall) + delta_obs = EnvironmentObservation.between(prev_iface, curr_iface) + if delta_obs is not None: + await self._emit_safe(emit, delta_obs) + + self._last_snapshot = current + + # Deadline-based scheduling (matches HardwareStreamSource pattern). + next_poll_at += self._poll_interval_s + delay = next_poll_at - time.monotonic() + if delay < 0: + missed = int(-delay // self._poll_interval_s) + 1 + next_poll_at += missed * self._poll_interval_s + delay = max(0.0, next_poll_at - time.monotonic()) + await self._sleep(delay) + + # ── Public observation builders ── + + async def take_snapshot(self) -> dict[str, Any]: + """Build a complete workspace snapshot (also used on demand). + + Returns a dict suitable for EnvironmentObservation(kind="snapshot"): + { + "source": "physical_hardware", + "devices": [ + { + "device_id": "robot.arm", + "device_class": "robot_arm", + "connected": true, + "health": "ok", + "affordances": ["grasp", "place", "push"], + "channels": {"joint.shoulder.position": {"value": 0.5, ...}}, + "kinematics": {"dof": 6, "chain_type": "serial"}, + }, + ... + ], + "capability_summary": { + "total_affordances": 4, + "available_affordances": ["grasp", "place", "push", "pour"], + "device_count": 2, + }, + "timestamp": 1695000000.0, + } + """ + devices: list[dict[str, Any]] = [] + all_affordances: set[str] = set() + + for context in self._registry.contexts(): + device_info = self._device_to_dict(context) + devices.append(device_info) + all_affordances.update(device_info.get("affordances", ())) + + # Merge capability_index affordances when available. + if self._capability_index is not None: + try: + all_affordances.update(self._capability_index.all_affordances()) + except Exception: # noqa: BLE001 – observation must not fail + pass + + return { + "source": _SOURCE_ID, + "devices": devices, + "capability_summary": { + "total_affordances": len(all_affordances), + "available_affordances": sorted(all_affordances), + "device_count": len(devices), + }, + "timestamp": time.time(), + } + + async def compute_delta( + self, previous: dict[str, Any], current: dict[str, Any] + ) -> dict[str, Any] | None: + """Compare two snapshots and return structural delta, or None if unchanged. + + Detects: + - device_added / device_removed + - affordance_gained / affordance_lost + - health_changed (ok→stale, connected→disconnected) + - channel_changed (direction, effect, or representation changed) + + Returns None when nothing structurally changed (value changes are + not structural — they're handled by the streaming/event system). + """ + prev_devs = {d["device_id"]: d for d in previous.get("devices", ())} + curr_devs = {d["device_id"]: d for d in current.get("devices", ())} + + prev_fps = {did: self._fingerprint_from_dict(d) for did, d in prev_devs.items()} + curr_fps = {did: self._fingerprint_from_dict(d) for did, d in curr_devs.items()} + + added = sorted(set(curr_fps) - set(prev_fps)) + removed = sorted(set(prev_fps) - set(curr_fps)) + + health_changed: list[dict[str, str]] = [] + affordance_gained: list[str] = [] + affordance_lost: list[str] = [] + channel_changed: list[dict[str, Any]] = [] + + for did in sorted(set(prev_fps) & set(curr_fps)): + pf, cf = prev_fps[did], curr_fps[did] + + # Health transitions. + if pf.get("health") != cf.get("health"): + health_changed.append( + {"device_id": did, "before": pf["health"], "after": cf["health"]} + ) + if pf.get("connected") != cf.get("connected"): + health_changed.append( + { + "device_id": did, + "before": "connected" if pf["connected"] else "disconnected", + "after": "connected" if cf["connected"] else "disconnected", + } + ) + + # Affordance changes. + prev_aff = set(pf.get("affordances", ())) + curr_aff = set(cf.get("affordances", ())) + for a in sorted(curr_aff - prev_aff): + affordance_gained.append(a) + for a in sorted(prev_aff - curr_aff): + affordance_lost.append(a) + + # Channel structural changes (added/removed channels). + prev_ch = set(pf.get("channels", ())) + curr_ch = set(cf.get("channels", ())) + if prev_ch != curr_ch: + channel_changed.append( + { + "device_id": did, + "added": sorted(curr_ch - prev_ch), + "removed": sorted(prev_ch - curr_ch), + } + ) + + if not any( + (added, removed, health_changed, affordance_gained, + affordance_lost, channel_changed) + ): + return None + + return { + "source": _SOURCE_ID, + "device_added": added, + "device_removed": removed, + "health_changed": health_changed, + "affordance_gained": affordance_gained, + "affordance_lost": affordance_lost, + "channel_changed": channel_changed, + "timestamp": time.time(), + } + + async def collect_outcomes(self) -> dict[str, Any] | None: + """Query recent operation verdicts from EvidenceStore. + + Returns None if no evidence_store or no recent operations. + + Returns: + { + "source": "physical_hardware", + "period_s": 300, + "devices": { + "robot.arm": { + "total_operations": 15, + "success_rate": 0.87, + "recent_failures": ["position_deviation > tolerance on joint.elbow"], + }, + }, + } + """ + if self._evidence_store is None: + return None + + device_outcomes: dict[str, dict[str, Any]] = {} + window_days = _OUTCOME_WINDOW_S / 86400.0 + + for context in self._registry.contexts(): + did = context.device_id + try: + rate = await self._evidence_store.success_rate( + did, window_days=window_days + ) + except Exception: # noqa: BLE001 – a query failure is diagnostic, not fatal + logger.debug("Evidence query failed for %s", did, exc_info=True) + continue + + total = rate.get("total", 0) + if total == 0: + continue + + # Best-effort query for recent failure details. + failures: list[str] = [] + try: + records = await self._evidence_store.query( + device_id=did, + verdict_status="failure", + since=time.time() - _OUTCOME_WINDOW_S, + limit=5, + ) + for rec in records: + detail = rec.get("verdict_detail") or "" + if detail: + failures.append(detail) + except Exception: # noqa: BLE001 + pass + + device_outcomes[did] = { + "total_operations": total, + "success_rate": rate.get("success_rate", 0.0), + "recent_failures": failures, + } + + if not device_outcomes: + return None + + return { + "source": _SOURCE_ID, + "period_s": _OUTCOME_WINDOW_S, + "devices": device_outcomes, + } + + def _device_fingerprint(self, context: Any) -> dict[str, Any]: + """Extract structural fingerprint of a device for delta comparison. + + Only structural attributes (not values): device_id, channels list, + affordances, kinematics, transport kind, halt_supported. + """ + channels: list[str] = [ch.channel_id for ch in context.channels] + + affordances: list[str] = [] + cap = getattr(context, "capabilities", None) + if cap is not None: + affordances = list(getattr(cap, "affordances", ()) or ()) + + kin = getattr(context, "kinematics", None) + kin_key = "" + if kin is not None: + kin_key = f"{getattr(kin, 'chain_type', '')}:{getattr(kin, 'dof', 0)}" + + return { + "device_id": context.device_id, + "device_class": getattr(context, "device_class", ""), + "channels": sorted(channels), + "affordances": sorted(affordances), + "kinematics": kin_key, + "transport_kind": getattr(context.transport, "kind", ""), + "halt_supported": bool(context.halt_supported), + } + + # ── Internal helpers ── + + def _device_to_dict(self, context: Any) -> dict[str, Any]: + """Build a device dict for the snapshot payload.""" + channels: dict[str, dict[str, Any]] = {} + for ch in context.channels: + summary = self._channel_summary(context.device_id, ch.channel_id) + entry: dict[str, Any] = { + "direction": ch.direction, + "effect": ch.effect, + "unit": ch.unit, + "quality": "unknown", + } + if summary and summary.get("samples", 0) > 0: + entry["value"] = summary.get("latest") + entry["quality"] = summary.get("quality", "ok") + channels[ch.channel_id] = entry + + affordances: list[str] = [] + cap = getattr(context, "capabilities", None) + if cap is not None: + affordances = list(getattr(cap, "affordances", ()) or ()) + + kin_dict: dict[str, Any] | None = None + kin = getattr(context, "kinematics", None) + if kin is not None: + kin_dict = { + "dof": getattr(kin, "dof", 0), + "chain_type": getattr(kin, "chain_type", ""), + } + + health = self._device_health(context.device_id) + + return { + "device_id": context.device_id, + "device_class": getattr(context, "device_class", ""), + "connected": True, # admitted by the registry ⇒ structurally present + "health": health, + "affordances": sorted(affordances), + "channels": channels, + "kinematics": kin_dict, + } + + def _device_health(self, device_id: str) -> str: + """Derive device health from recent hardware events. + + Returns "ok", "stale", "degraded", or "unreachable". + """ + try: + events = self._registry.recent_events(device_id=device_id, limit=5) + except Exception: # noqa: BLE001 + return "ok" + if not events: + return "ok" + # Walk newest-first (events are stored oldest-first, so reverse). + for event in reversed(events): + kind = getattr(event, "kind", "") + if kind == "unreachable": + return "unreachable" + if kind == "stale": + return "stale" + if kind == "quality_degraded": + return "degraded" + if kind == "settled": + return "ok" + return "ok" + + def _channel_summary( + self, device_id: str, channel_id: str + ) -> dict[str, Any] | None: + """Get channel reading summary from the registry's stream sources.""" + try: + return self._registry.channel_summary(device_id, channel_id) + except Exception: # noqa: BLE001 – never fail an observation read + return None + + @staticmethod + def _fingerprint_from_dict(device: dict[str, Any]) -> dict[str, Any]: + """Extract structural fingerprint from a snapshot device dict.""" + raw_channels = device.get("channels") + channels = sorted(raw_channels.keys()) if isinstance(raw_channels, dict) else [] + return { + "device_id": device.get("device_id", ""), + "device_class": device.get("device_class", ""), + "channels": channels, + "affordances": sorted(device.get("affordances", ())), + "connected": device.get("connected", True), + "health": device.get("health", "ok"), + } + + def _to_interface_snapshot( + self, snapshot: dict[str, Any], observed_at: float + ) -> Any: + """Convert a snapshot dict to an InterfaceSnapshot for domain observation. + + Each device becomes an InterfaceElement (name=device_id, role=device_class, + enabled=connected). Affordances map directly to the snapshot's affordance + tuple. The version encodes the structural composition so that + ``EnvironmentObservation.between()`` detects changes correctly. + """ + from leapflow.domain.environment_signal import InterfaceElement, InterfaceSnapshot + + devices = snapshot.get("devices", ()) + elements: list[InterfaceElement] = [] + affordances: set[str] = set() + + for dev in devices: + elements.append( + InterfaceElement( + name=dev.get("device_id", ""), + role=dev.get("device_class", "") or "device", + enabled=bool(dev.get("connected", True)), + ) + ) + affordances.update(dev.get("affordances", ())) + + cap_summary = snapshot.get("capability_summary", {}) + affordances.update(cap_summary.get("available_affordances", ())) + + # Encode the structural shape as a version so the domain delta catches + # topology changes (device count, total channel count). + total_channels = sum( + len(d.get("channels", {})) if isinstance(d.get("channels"), dict) else 0 + for d in devices + ) + version = f"{len(devices)}d.{total_channels}ch" + + return InterfaceSnapshot.create( + source_id=_SOURCE_ID, + app_id="physical_workspace", + version=version, + affordances=tuple(sorted(affordances)), + elements=tuple(elements), + data=cap_summary, + observed_at=observed_at, + provenance={ + "kind": "hardware_registry", + "device_count": str(len(devices)), + }, + ) + + async def _emit_outcomes(self, emit: Any, wall: float) -> None: + """Collect and emit per-device outcome observations.""" + from leapflow.domain.environment_signal import EnvironmentObservation + + outcomes = await self.collect_outcomes() + if outcomes is None: + return + + for did, stats in outcomes.get("devices", {}).items(): + total = stats.get("total_operations", 0) + sr = stats.get("success_rate", 0.0) + if total == 0: + outcome_str = "UNKNOWN" + elif sr >= 0.5: + outcome_str = "PASS" + else: + outcome_str = "FAIL" + + obs = EnvironmentObservation.task_outcome( + source_id=_SOURCE_ID, + task_id=did, + outcome=outcome_str, + capability=f"hardware.{did}", + observed_at=wall, + provenance={ + "kind": "hardware_evidence", + "total_operations": str(total), + "success_rate": f"{sr:.2f}", + "period_s": str(outcomes.get("period_s", _OUTCOME_WINDOW_S)), + }, + ) + await self._emit_safe(emit, obs) + + @staticmethod + async def _emit_safe(emit: Any, observation: Any) -> None: + """Emit an observation, logging but never propagating errors.""" + if emit is None: + return + try: + await emit(observation) + except Exception: # noqa: BLE001 – a sink failure must not kill the loop + logger.warning( + "Physical environment observation emit failed", exc_info=True + ) + + async def _sleep(self, seconds: float) -> None: + """Sleep respecting the stop event, matching HardwareStreamSource pattern.""" + try: + await asyncio.wait_for(self._stopped.wait(), timeout=seconds) + except asyncio.TimeoutError: + pass + + +__all__ = ["PhysicalEnvironmentSource"] diff --git a/src/leapflow/hardware/evidence.py b/src/leapflow/hardware/evidence.py new file mode 100644 index 00000000..ca0c6fba --- /dev/null +++ b/src/leapflow/hardware/evidence.py @@ -0,0 +1,672 @@ +# Copyright (c) Alibaba, Inc. and its affiliates. +"""Evidence store: persists operation evidence bundles for audit and learning. + +Evidence bundles capture the post-operation state of a device -- what was +commanded, what the transport reported, and what the sensors observed after +the physical system settled. This store persists that evidence so it can +be queried for audit trails, fed into the learning pipeline, and used to +diagnose failures long after the operation occurred. + +Storage layout: +- DuckDB table ``hardware_evidence`` for structured metadata (operation_id, + device_id, channel_id, timestamp, verdict, deviation, etc.) +- File system directory for binary artifacts (camera frames) referenced + by operation_id. +- Retention policy: configurable days, default 30, with periodic cleanup. + +The store is append-only by design: evidence is immutable once recorded. +""" + +from __future__ import annotations + +import asyncio +import json +import logging +import shutil +import time +from pathlib import Path +from typing import Any + +from leapflow.storage.connection import ConnectionHolder, LocalConnectionHolder + +logger = logging.getLogger(__name__) + +EVIDENCE_CATEGORY = "hardware_evidence" +"""Cache category for the evidence tier of ``instrument.duckdb``.""" + +EVIDENCE_SCHEMA_VERSION = 1 +"""Row format version. Recorded on every row and read as an exact-match +filter, so an incompatible format bump excludes rows written under any other +layout -- both older and newer -- rather than silently mixing them.""" + +DEFAULT_RETENTION_DAYS = 30.0 +"""How long evidence rows and associated frames survive before cleanup.""" + +DEFAULT_MAX_FRAMES_MB = 500.0 +"""Maximum total size of stored frame artifacts on disk.""" + +_PRUNE_INTERVAL_S = 3600.0 +"""Floor on how often retention runs, matching reading_store.""" + + +class EvidenceStore: + """Persists EvidenceBundle and OperationVerdict for audit and learning. + + Thread-safe: all DuckDB writes go through a dedicated writer thread + via ``asyncio.to_thread``, matching the pattern in ``reading_store.py``. + """ + + def __init__( + self, + db_path: str | Path, + *, + frames_dir: str | Path | None = None, + retention_days: float = DEFAULT_RETENTION_DAYS, + max_frames_mb: float = DEFAULT_MAX_FRAMES_MB, + connection_holder: ConnectionHolder | None = None, + ) -> None: + """Initialize the evidence store. + + Args: + db_path: Path to the DuckDB database file. + frames_dir: Directory for frame artifact storage. + If None, frames are stored alongside the database. + retention_days: How long to keep evidence (default 30 days). + max_frames_mb: Maximum total size of stored frames. + connection_holder: Shared DuckDB connection holder. When + provided the store uses it; otherwise it creates and owns + a ``LocalConnectionHolder`` from *db_path*. + """ + self._db_path = Path(db_path) + self._frames_dir = Path(frames_dir) if frames_dir is not None else self._db_path.parent / "evidence_frames" + self._retention_days = max(0.0, float(retention_days)) + self._max_frames_mb = max(0.0, float(max_frames_mb)) + + # Connection management -- same pattern as CalibrationStore. + self._holder: ConnectionHolder | None = connection_holder + self._owns_holder = False + if self._holder is None: + self._holder = LocalConnectionHolder(self._db_path) + self._owns_holder = True + + self._db_ready = False + self._records_written = 0 + self._write_failures = 0 + self._last_prune_at = 0.0 + + # ── Schema ── + + def _ensure_schema(self) -> None: + """Create the evidence table if it doesn't exist. + + The index comes after the table, matching the calibration/reading + store ordering: it is a maintenance aid, not a correctness + requirement. + """ + if self._holder is None: + return + connection = self._holder.connection + connection.execute(_SCHEMA) + try: + connection.execute(_INDEX_DEVICE_TIME) + except Exception: # noqa: BLE001 - an unindexed table still answers + logger.debug("hardware_evidence index unavailable", exc_info=True) + try: + connection.execute(_INDEX_VERDICT) + except Exception: # noqa: BLE001 + logger.debug("hardware_evidence verdict index unavailable", exc_info=True) + + def _ensure_ready(self) -> Any: + """Ensure schema exists and return the connection. + + Returns the DuckDB connection, or raises if the holder is absent. + """ + if self._holder is None: + raise RuntimeError("EvidenceStore has no connection holder") + connection = self._holder.connection + if not self._db_ready: + self._ensure_schema() + self._db_ready = True + return connection + + # ── Record ── + + async def record( + self, + bundle: Any, # EvidenceBundle from verification.py + verdict: Any | None = None, # OperationVerdict from verification.py + ) -> str: + """Persist an evidence bundle and optional verdict. + + Returns the operation_id. + + 1. Serialize bundle metadata to DuckDB row. + 2. If bundle has frames, write them to ``frames_dir/{operation_id}/``. + 3. If verdict is provided, include the verdict columns. + """ + operation_id = getattr(bundle, "operation_id", "") or "" + if not operation_id: + raise ValueError("EvidenceBundle must have a non-empty operation_id") + + # Prepare the row values from the bundle. + outcome = getattr(bundle, "actual_outcome", None) + outcome_ok: bool | None = None + outcome_side_effect: str | None = None + if outcome is not None: + outcome_ok = getattr(outcome, "ok", None) + outcome_side_effect = getattr(outcome, "side_effect", None) + + # Verdict columns -- filled when provided. + verdict_status: str | None = None + verdict_confidence: float | None = None + verdict_deviation: float | None = None + verdict_detail: str | None = None + verifier_id: str | None = None + if verdict is not None: + verdict_status = getattr(verdict, "status", None) + verdict_confidence = getattr(verdict, "confidence", None) + verdict_deviation = getattr(verdict, "deviation", None) + verdict_detail = getattr(verdict, "detail", None) or None + verifier_id = getattr(verdict, "verifier_id", None) + + # Serialize readings and metadata. + readings_json = _serialize_readings(getattr(bundle, "post_settle_readings", ())) + metadata_json = _serialize_value(dict(getattr(bundle, "metadata", {}))) + + # Write frames to the file system before the DB row so the row + # never references frames that do not exist yet. + frame_paths = await self._write_frames(bundle) + frame_paths_json = json.dumps(frame_paths, ensure_ascii=False) if frame_paths else None + + row = ( + operation_id, + getattr(bundle, "device_id", ""), + getattr(bundle, "channel_id", ""), + float(getattr(bundle, "timestamp", 0.0)), + _serialize_value(getattr(bundle, "intended_value", None)), + outcome_ok, + outcome_side_effect, + float(getattr(bundle, "settle_delay_s", 0.0)), + readings_json, + verdict_status, + verdict_confidence, + verdict_deviation, + verdict_detail, + verifier_id, + frame_paths_json, + metadata_json, + EVIDENCE_SCHEMA_VERSION, + ) + + await asyncio.to_thread(self._write_row, row) + return operation_id + + def _write_row(self, row: tuple[Any, ...]) -> None: + """Blocking DuckDB insert, safe to call from a worker thread.""" + try: + connection = self._ensure_ready() + connection.execute(_INSERT, row) + self._records_written += 1 + except Exception as exc: # noqa: BLE001 - storage must not fail the operation + self._write_failures += 1 + logger.warning("Could not persist evidence record: %s", exc) + + # ── Verdict update ── + + async def record_verdict( + self, + operation_id: str, + verdict: Any, # OperationVerdict from verification.py + ) -> None: + """Update verdict for an existing evidence record. + + Used when verification runs asynchronously after the initial record. + """ + verdict_status = getattr(verdict, "status", None) + verdict_confidence = getattr(verdict, "confidence", None) + verdict_deviation = getattr(verdict, "deviation", None) + verdict_detail = getattr(verdict, "detail", None) or None + verifier_id = getattr(verdict, "verifier_id", None) + + params = ( + verdict_status, + verdict_confidence, + verdict_deviation, + verdict_detail, + verifier_id, + operation_id, + ) + await asyncio.to_thread(self._update_verdict_row, params) + + def _update_verdict_row(self, params: tuple[Any, ...]) -> None: + """Blocking DuckDB update for verdict columns.""" + try: + connection = self._ensure_ready() + connection.execute(_UPDATE_VERDICT, params) + except Exception as exc: # noqa: BLE001 + logger.warning("Could not update evidence verdict: %s", exc) + + # ── Frame persistence ── + + async def _write_frames(self, bundle: Any) -> list[str]: + """Write frame artifacts to the file system. + + Returns a list of relative paths (relative to ``frames_dir``) for + storage in the DuckDB row. Frame files are named + ``{operation_id}/{sequence}.{ext}``. + """ + frames = getattr(bundle, "frames", ()) + if not frames: + return [] + operation_id = getattr(bundle, "operation_id", "unknown") + op_dir = self._frames_dir / operation_id + return await asyncio.to_thread(self._write_frames_sync, frames, op_dir, operation_id) + + @staticmethod + def _write_frames_sync( + frames: tuple[Any, ...], + op_dir: Path, + operation_id: str, + ) -> list[str]: + """Blocking frame write, safe for a worker thread.""" + paths: list[str] = [] + try: + op_dir.mkdir(parents=True, exist_ok=True) + except OSError as exc: + logger.warning("Could not create frame directory %s: %s", op_dir, exc) + return paths + + for seq, frame in enumerate(frames): + # FrameReading carries ``data`` (bytes) and ``format`` (str). + data = getattr(frame, "data", None) + fmt = getattr(frame, "format", "bin") or "bin" + if data is None: + continue + filename = f"{seq:04d}.{fmt}" + file_path = op_dir / filename + try: + file_path.write_bytes(data if isinstance(data, bytes) else bytes(data)) + paths.append(f"{operation_id}/{filename}") + except OSError as exc: + logger.warning("Could not write frame %s: %s", file_path, exc) + return paths + + # ── Query ── + + async def query( + self, + *, + device_id: str | None = None, + channel_id: str | None = None, + verdict_status: str | None = None, + since: float | None = None, + limit: int = 100, + ) -> list[dict[str, Any]]: + """Query evidence records with optional filters. + + Returns dicts with all columns, suitable for the learning pipeline + and audit display. + """ + return await asyncio.to_thread( + self._query_sync, device_id, channel_id, verdict_status, since, limit + ) + + def _query_sync( + self, + device_id: str | None, + channel_id: str | None, + verdict_status: str | None, + since: float | None, + limit: int, + ) -> list[dict[str, Any]]: + """Blocking query implementation.""" + if self._holder is None: + return [] + if self._db_path is not None and not self._db_path.exists(): + return [] + try: + connection = self._ensure_ready() + except Exception as exc: # noqa: BLE001 + logger.debug("Could not open evidence store for query: %s", exc) + return [] + + # Build dynamic WHERE clause. + conditions: list[str] = [f"schema_version = {EVIDENCE_SCHEMA_VERSION}"] + params: list[Any] = [] + if device_id is not None: + conditions.append("device_id = ?") + params.append(device_id) + if channel_id is not None: + conditions.append("channel_id = ?") + params.append(channel_id) + if verdict_status is not None: + conditions.append("verdict_status = ?") + params.append(verdict_status) + if since is not None: + conditions.append("timestamp >= ?") + params.append(float(since)) + + where = " AND ".join(conditions) + sql = f"SELECT {', '.join(_COLUMNS)} FROM hardware_evidence WHERE {where} ORDER BY timestamp DESC LIMIT ?" + params.append(int(limit)) + + try: + rows = connection.execute(sql, params).fetchall() + except Exception as exc: # noqa: BLE001 + logger.debug("Evidence query failed: %s", exc) + return [] + return [dict(zip(_COLUMNS, row)) for row in rows] + + async def get_frame_paths(self, operation_id: str) -> list[Path]: + """Return file paths for frames associated with an operation.""" + return await asyncio.to_thread(self._get_frame_paths_sync, operation_id) + + def _get_frame_paths_sync(self, operation_id: str) -> list[Path]: + """Blocking frame path lookup.""" + if self._holder is None: + return [] + try: + connection = self._ensure_ready() + rows = connection.execute( + "SELECT frame_paths FROM hardware_evidence WHERE operation_id = ?", + (operation_id,), + ).fetchall() + except Exception as exc: # noqa: BLE001 + logger.debug("Could not query frame paths: %s", exc) + return [] + if not rows or rows[0][0] is None: + return [] + try: + relative_paths = json.loads(rows[0][0]) + except (TypeError, ValueError): + return [] + return [self._frames_dir / rp for rp in relative_paths] + + # ── Analytics ── + + async def success_rate( + self, + device_id: str, + *, + channel_id: str | None = None, + window_days: float = 7.0, + ) -> dict[str, Any]: + """Compute success/failure/inconclusive rate for a device or channel. + + Returns a dict with total, success, failure, inconclusive counts + and a success_rate in [0.0, 1.0]. + """ + return await asyncio.to_thread( + self._success_rate_sync, device_id, channel_id, window_days + ) + + def _success_rate_sync( + self, + device_id: str, + channel_id: str | None, + window_days: float, + ) -> dict[str, Any]: + """Blocking success rate computation.""" + result: dict[str, Any] = { + "total": 0, + "success": 0, + "failure": 0, + "inconclusive": 0, + "success_rate": 0.0, + "window_days": window_days, + } + if self._holder is None: + return result + if self._db_path is not None and not self._db_path.exists(): + return result + try: + connection = self._ensure_ready() + except Exception: # noqa: BLE001 + return result + + cutoff = time.time() - window_days * 86400.0 + conditions = ["device_id = ?", "timestamp >= ?", f"schema_version = {EVIDENCE_SCHEMA_VERSION}"] + params: list[Any] = [device_id, cutoff] + if channel_id is not None: + conditions.append("channel_id = ?") + params.append(channel_id) + where = " AND ".join(conditions) + sql = ( + f"SELECT verdict_status, COUNT(*) FROM hardware_evidence " + f"WHERE {where} GROUP BY verdict_status" + ) + try: + rows = connection.execute(sql, params).fetchall() + except Exception as exc: # noqa: BLE001 + logger.debug("Evidence success_rate query failed: %s", exc) + return result + + total = 0 + for status_val, count in rows: + total += count + if status_val == "success": + result["success"] = count + elif status_val == "failure": + result["failure"] = count + else: + result["inconclusive"] += count + result["total"] = total + if total > 0: + result["success_rate"] = result["success"] / total + return result + + # ── Cleanup ── + + async def cleanup(self) -> dict[str, int]: + """Remove evidence older than ``retention_days``. + + Returns ``{"rows_deleted": N, "frames_deleted": M, "bytes_freed": B}``. + """ + return await asyncio.to_thread(self._cleanup_sync) + + def _cleanup_sync(self) -> dict[str, int]: + """Blocking cleanup of expired evidence rows and frame artifacts.""" + stats: dict[str, int] = {"rows_deleted": 0, "frames_deleted": 0, "bytes_freed": 0} + if self._holder is None or self._retention_days <= 0: + return stats + + cutoff = time.time() - self._retention_days * 86400.0 + + try: + connection = self._ensure_ready() + except Exception as exc: # noqa: BLE001 + logger.debug("Could not open evidence store for cleanup: %s", exc) + return stats + + # First retrieve frame_paths for rows that will be deleted so we + # can clean up the file system artifacts. + try: + expired_rows = connection.execute( + "SELECT operation_id, frame_paths FROM hardware_evidence WHERE timestamp < ?", + (cutoff,), + ).fetchall() + except Exception as exc: # noqa: BLE001 + logger.debug("Could not query expired evidence: %s", exc) + return stats + + # Delete the DuckDB rows. + try: + deleted = connection.execute( + "DELETE FROM hardware_evidence WHERE timestamp < ? RETURNING 1", + (cutoff,), + ).fetchall() + stats["rows_deleted"] = len(deleted) + except Exception as exc: # noqa: BLE001 + logger.warning("Could not prune expired evidence rows: %s", exc) + return stats + + # Clean up frame directories. + for operation_id, frame_paths_json in expired_rows: + op_dir = self._frames_dir / str(operation_id) + if not op_dir.exists(): + continue + try: + dir_size = sum(f.stat().st_size for f in op_dir.rglob("*") if f.is_file()) + file_count = sum(1 for f in op_dir.rglob("*") if f.is_file()) + shutil.rmtree(op_dir) + stats["frames_deleted"] += file_count + stats["bytes_freed"] += dir_size + except OSError as exc: + logger.warning("Could not remove frame directory %s: %s", op_dir, exc) + + self._last_prune_at = time.time() + if stats["rows_deleted"]: + logger.info( + "hardware evidence: pruned %d row(s), %d frame(s), %d bytes freed", + stats["rows_deleted"], + stats["frames_deleted"], + stats["bytes_freed"], + ) + return stats + + # ── Introspection ── + + @property + def records_written(self) -> int: + """Total evidence records successfully persisted.""" + return self._records_written + + @property + def write_failures(self) -> int: + """Evidence records that could not be persisted.""" + return self._write_failures + + def close(self) -> None: + """Close the database connection. + + Only closes the holder it created itself: when the registry injected + the shared holder, closing it here would pull the connection out from + under sibling stores. + """ + if self._owns_holder and self._holder is not None: + try: + self._holder.close() + except Exception: # noqa: BLE001 - teardown must not propagate + logger.debug("evidence holder close failed", exc_info=True) + + +# --------------------------------------------------------------------------- +# Serialisation helpers +# --------------------------------------------------------------------------- + + +def _serialize_value(value: Any) -> str: + """JSON-serialize a value for DuckDB storage. + + Falls back to ``str(value)`` wrapped in quotes when json.dumps cannot + handle the type, so a row is never lost to a serialisation failure. + """ + try: + return json.dumps(value, ensure_ascii=False, default=str) + except (TypeError, ValueError): + return json.dumps(str(value)) + + +def _serialize_readings(readings: tuple[Any, ...]) -> str: + """Serialize a tuple of Reading objects to JSON. + + Each reading is converted via its ``to_dict()`` method when available, + falling back to ``str()`` for opaque objects. + """ + items: list[Any] = [] + for r in readings: + to_dict = getattr(r, "to_dict", None) + if callable(to_dict): + items.append(to_dict()) + else: + items.append(str(r)) + return json.dumps(items, ensure_ascii=False, default=str) + + +# --------------------------------------------------------------------------- +# SQL constants +# --------------------------------------------------------------------------- + +_COLUMNS = ( + "operation_id", + "device_id", + "channel_id", + "timestamp", + "intended_value", + "outcome_ok", + "outcome_side_effect", + "settle_delay_s", + "readings_json", + "verdict_status", + "verdict_confidence", + "verdict_deviation", + "verdict_detail", + "verifier_id", + "frame_paths", + "metadata", + "created_at", +) + +_SCHEMA = """ +CREATE TABLE IF NOT EXISTS hardware_evidence ( + operation_id VARCHAR PRIMARY KEY, + device_id VARCHAR NOT NULL, + channel_id VARCHAR NOT NULL, + timestamp DOUBLE NOT NULL, + intended_value VARCHAR, + outcome_ok BOOLEAN, + outcome_side_effect VARCHAR, + settle_delay_s DOUBLE, + readings_json VARCHAR, + verdict_status VARCHAR, + verdict_confidence DOUBLE, + verdict_deviation DOUBLE, + verdict_detail VARCHAR, + verifier_id VARCHAR, + frame_paths VARCHAR, + metadata VARCHAR, + schema_version INTEGER DEFAULT 1, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +) +""" + +_INSERT = """ +INSERT OR REPLACE INTO hardware_evidence ( + operation_id, device_id, channel_id, timestamp, + intended_value, outcome_ok, outcome_side_effect, settle_delay_s, + readings_json, + verdict_status, verdict_confidence, verdict_deviation, verdict_detail, + verifier_id, frame_paths, metadata, schema_version +) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) +""" + +_UPDATE_VERDICT = """ +UPDATE hardware_evidence +SET verdict_status = ?, + verdict_confidence = ?, + verdict_deviation = ?, + verdict_detail = ?, + verifier_id = ? +WHERE operation_id = ? +""" + +_INDEX_DEVICE_TIME = """ +CREATE INDEX IF NOT EXISTS idx_evidence_device_time +ON hardware_evidence (device_id, channel_id, timestamp) +""" +"""Serves per-device/channel queries ordered by recency.""" + +_INDEX_VERDICT = """ +CREATE INDEX IF NOT EXISTS idx_evidence_verdict +ON hardware_evidence (verdict_status, timestamp) +""" +"""Serves verdict-filtered analytics queries.""" + + +__all__ = [ + "DEFAULT_MAX_FRAMES_MB", + "DEFAULT_RETENTION_DAYS", + "EVIDENCE_CATEGORY", + "EVIDENCE_SCHEMA_VERSION", + "EvidenceStore", +] diff --git a/src/leapflow/hardware/examples/generic_sensor.yaml b/src/leapflow/hardware/examples/generic_sensor.yaml new file mode 100644 index 00000000..ac10265b --- /dev/null +++ b/src/leapflow/hardware/examples/generic_sensor.yaml @@ -0,0 +1,31 @@ +# Generic sensor — the absolute minimal valid hc.v0 declaration. +# +# Demonstrates backward compatibility: just device_id, transport, and one +# read channel. No hc.v1 sections are present; the provider must load +# this without error and leave all v1 fields as None. + +hc_version: "hc.v0" +device_id: "sensor.generic_temp" +display_name: "Generic Temperature Sensor" +device_class: "sensor" + +transport: + kind: "modbus" + config: + address: 1 + port: "/dev/ttyUSB1" + baudrate: 9600 + +channels: + - channel_id: "temperature" + direction: "read" + quantity: "temperature" + unit: "degC" + representation: "scalar" + effect: "read" + sample_rate_hz: 1.0 + description: "Ambient temperature reading" + envelope: + declared: true + min_value: -40.0 + max_value: 125.0 diff --git a/src/leapflow/hardware/examples/realsense_d435.yaml b/src/leapflow/hardware/examples/realsense_d435.yaml new file mode 100644 index 00000000..8369bad2 --- /dev/null +++ b/src/leapflow/hardware/examples/realsense_d435.yaml @@ -0,0 +1,59 @@ +# Intel RealSense D435 — stereo depth camera (pure sensor, no actuators). +# +# Demonstrates an hc.v1 declaration for a device that only reads: no +# kinematics, no safety policy, no trust overrides. The capabilities +# section declares what the sensor can observe, not what it can do. + +hc_version: "hc.v1" +device_id: "sensor.realsense_d435" +display_name: "RealSense D435 Depth Camera" +device_class: "camera" +vendor: "Intel" +model: "D435" +location: "workbench_1" +halt_supported: false + +capabilities: + affordances: ["observe_depth", "observe_rgb"] + notes: "Passive sensor — no actuation capability" + +degradation: + sensor_loss_policy: "halt" + comm_loss_policy: "halt" + max_comm_loss_s: 2.0 + +transport: + kind: "realsense" + config: + serial_number: "0" + depth_resolution: [640, 480] + color_resolution: [640, 480] + fps: 30 + +channels: + - channel_id: "camera.color" + direction: "read" + quantity: "image" + representation: "frame" + effect: "read" + sample_rate_hz: 30.0 + description: "RGB color stream" + privacy_tier: "environment" + + - channel_id: "camera.depth" + direction: "read" + quantity: "depth_image" + representation: "frame" + effect: "read" + sample_rate_hz: 30.0 + description: "Aligned depth stream (16-bit millimeters)" + privacy_tier: "environment" + + - channel_id: "camera.infrared" + direction: "read" + quantity: "image" + representation: "frame" + effect: "read" + sample_rate_hz: 30.0 + description: "Left infrared stream for stereo" + privacy_tier: "environment" diff --git a/src/leapflow/hardware/examples/so100_follower.yaml b/src/leapflow/hardware/examples/so100_follower.yaml new file mode 100644 index 00000000..3ffcdf15 --- /dev/null +++ b/src/leapflow/hardware/examples/so100_follower.yaml @@ -0,0 +1,161 @@ +# SO-100 Follower Arm — 6-DOF desktop manipulator with top camera and gripper. +# +# This is an hc.v1 declaration that exercises every new section: +# kinematics, capabilities, safety, trust_config, degradation. + +hc_version: "hc.v1" +device_id: "robot.so100_arm" +display_name: "SO-100 Follower Arm" +device_class: "robot_arm" +vendor: "Feetech" +model: "SO-100" +location: "workbench_1" +halt_supported: true + +kinematics: + chain_type: "serial" + dof: 6 + joint_limits: + - [-3.14, 3.14] # shoulder_pan + - [-1.57, 1.57] # shoulder_lift + - [-3.14, 3.14] # elbow + - [-3.14, 3.14] # wrist_1 + - [-3.14, 3.14] # wrist_2 + - [-3.14, 3.14] # wrist_3 + workspace_bounds: [-0.5, 0.5, -0.5, 0.5, 0.0, 0.6] + +capabilities: + affordances: ["grasp", "place", "push", "pour"] + payload_kg: 0.5 + reach_m: 0.28 + tool_slots: 1 + +safety: + max_velocity_rad_s: 3.14 + max_force_n: 10.0 + emergency_decel_s: 0.1 + +trust_config: + initial_level: "UNTRUSTED" + promotion_thresholds: [3, 8, 20] + demotion_threshold: 3 + +degradation: + sensor_loss_policy: "halt" + comm_loss_policy: "hold_position" + max_comm_loss_s: 0.5 + +transport: + kind: "robot_arm" + config: + robot_type: "so100_follower" + robot_config: + serial_port: "/dev/ttyUSB0" + cameras: + top: {index: 0, width: 640, height: 480, fps: 30} + +channels: + - channel_id: "joint.shoulder_pan.position" + direction: "readwrite" + quantity: "angular_position" + unit: "rad" + representation: "scalar" + effect: "actuate" + sample_rate_hz: 50.0 + description: "Shoulder pan joint position" + envelope: + declared: true + min_value: -3.14 + max_value: 3.14 + max_rate: 6.28 + settling_time_s: 0.1 + reversible: true + + - channel_id: "joint.shoulder_lift.position" + direction: "readwrite" + quantity: "angular_position" + unit: "rad" + representation: "scalar" + effect: "actuate" + sample_rate_hz: 50.0 + description: "Shoulder lift joint position" + envelope: + declared: true + min_value: -1.57 + max_value: 1.57 + max_rate: 6.28 + settling_time_s: 0.1 + reversible: true + + - channel_id: "joint.elbow.position" + direction: "readwrite" + quantity: "angular_position" + unit: "rad" + representation: "scalar" + effect: "actuate" + sample_rate_hz: 50.0 + description: "Elbow joint position" + envelope: + declared: true + min_value: -3.14 + max_value: 3.14 + max_rate: 6.28 + settling_time_s: 0.1 + reversible: true + + - channel_id: "joint.wrist_1.position" + direction: "readwrite" + quantity: "angular_position" + unit: "rad" + representation: "scalar" + effect: "actuate" + sample_rate_hz: 50.0 + description: "Wrist 1 joint position" + envelope: + declared: true + min_value: -3.14 + max_value: 3.14 + max_rate: 6.28 + settling_time_s: 0.1 + reversible: true + + - channel_id: "joint.wrist_2.position" + direction: "readwrite" + quantity: "angular_position" + unit: "rad" + representation: "scalar" + effect: "actuate" + sample_rate_hz: 50.0 + description: "Wrist 2 joint position" + envelope: + declared: true + min_value: -3.14 + max_value: 3.14 + max_rate: 6.28 + settling_time_s: 0.1 + reversible: true + + - channel_id: "joint.wrist_3.position" + direction: "readwrite" + quantity: "angular_position" + unit: "rad" + representation: "scalar" + effect: "actuate" + sample_rate_hz: 50.0 + description: "Wrist 3 joint position" + envelope: + declared: true + min_value: -3.14 + max_value: 3.14 + max_rate: 6.28 + settling_time_s: 0.1 + reversible: true + + - channel_id: "camera.top" + direction: "read" + quantity: "image" + representation: "frame" + effect: "read" + sample_rate_hz: 30.0 + description: "Top-view RGB camera for workspace observation" + privacy_tier: "environment" diff --git a/src/leapflow/hardware/providers/__init__.py b/src/leapflow/hardware/providers/__init__.py index e1b9aafe..ec48b5cd 100644 --- a/src/leapflow/hardware/providers/__init__.py +++ b/src/leapflow/hardware/providers/__init__.py @@ -54,6 +54,7 @@ def discover(self) -> tuple[HardwareContext, ...]: "yaml": "leapflow.hardware.providers.yaml_provider:build_provider", "host": "leapflow.hardware.providers.host_provider:build_provider", "media": "leapflow.hardware.providers.media_provider:build_provider", + "robot_arm": "leapflow.hardware.providers.robot_arm:build_provider", } _EP_GROUP = "leapflow.hardware.providers" @@ -64,8 +65,8 @@ def _discover_entry_points() -> None: """Merge entry-point declared providers into ``_PROVIDERS``, once. Idempotent, and built-ins win: an installed package must not be able to - hijack ``yaml``, ``host`` or ``media`` and change where every profile's device - knowledge comes from. + hijack ``yaml``, ``host``, ``media`` or ``robot_arm`` and change where every + profile's device knowledge comes from. """ global _ep_scanned # noqa: PLW0603 if _ep_scanned: diff --git a/src/leapflow/hardware/providers/robot_arm.py b/src/leapflow/hardware/providers/robot_arm.py new file mode 100644 index 00000000..22b46a84 --- /dev/null +++ b/src/leapflow/hardware/providers/robot_arm.py @@ -0,0 +1,316 @@ +# Copyright (c) Alibaba, Inc. and its affiliates. +# Adapted from LeRobot (https://github.com/huggingface/lerobot). +"""Robot arm context provider for LeapRobot: discovers robot arms from configuration. + +A robot arm is described by its type (e.g. ``koch_v1_1``, +``so100_follower``) and its physical connections (serial ports, camera +indices). This provider reads that configuration from the hardware +providers YAML block and produces a fully declared ``HardwareContext`` +with channel declarations derived from the robot's feature description. + +The robot is not instantiated by the provider -- only described. The +transport factory in ``transports/robot_arm.py`` handles instantiation +during ``open()``. This separation means ``discover()`` can run without +hardware attached, which is what makes offline ``hw_describe`` and test +assembly work. +""" + +from __future__ import annotations + +import logging +import re +from typing import Any, Mapping + +from leapflow.hardware.context import ( + ContextProvenance, + ContextSource, + HardwareContext, + TransportRef, +) +from leapflow.hardware.transports.robot_channels import RobotChannelMapper + +logger = logging.getLogger(__name__) + +# --------------------------------------------------------------------------- +# Device-class inference patterns +# --------------------------------------------------------------------------- + +_ARM_PATTERNS: tuple[re.Pattern[str], ...] = ( + re.compile(r"follower", re.IGNORECASE), + re.compile(r"leader", re.IGNORECASE), + re.compile(r"koch", re.IGNORECASE), + re.compile(r"so10[01]", re.IGNORECASE), + re.compile(r"aloha", re.IGNORECASE), + re.compile(r"widow", re.IGNORECASE), + re.compile(r"viper", re.IGNORECASE), + re.compile(r"panda", re.IGNORECASE), +) + +_MOBILE_PATTERNS: tuple[re.Pattern[str], ...] = ( + re.compile(r"kiwi", re.IGNORECASE), + re.compile(r"rover", re.IGNORECASE), + re.compile(r"mobile", re.IGNORECASE), +) + +_HUMANOID_PATTERNS: tuple[re.Pattern[str], ...] = ( + re.compile(r"\bg[1-9]\b", re.IGNORECASE), + re.compile(r"\bh[1-9]\b", re.IGNORECASE), + re.compile(r"reachy", re.IGNORECASE), + re.compile(r"humanoid", re.IGNORECASE), +) + + +# --------------------------------------------------------------------------- +# Provider implementation +# --------------------------------------------------------------------------- + + +class RobotArmContextProvider: + """HardwareContextProvider that discovers robot arms from configuration. + + Configuration example (in profile hardware YAML):: + + hardware: + providers: + - type: robot_arm + config: + robot_type: so100_follower + device_name: my_robot_arm + display_name: "SO-100 Follower Arm" + serial_port: /dev/ttyUSB0 + cameras: + top: + index: 0 + width: 640 + height: 480 + fps: 30 + motor_limits: + shoulder_pan: [-3.14, 3.14] + shoulder_lift: [-1.57, 1.57] + elbow: [-3.14, 3.14] + wrist_1: [-3.14, 3.14] + wrist_2: [-3.14, 3.14] + wrist_3: [-3.14, 3.14] + sample_rate_hz: 50.0 + """ + + kind: str = "robot_arm" + + def __init__(self, config: Mapping[str, Any]) -> None: + self._config = dict(config) + + def discover(self) -> tuple[HardwareContext, ...]: + """Return one HardwareContext per configured robot arm. + + The context contains: + + - device_id: ``robot.{device_name}`` or ``robot.{robot_type}`` + - display_name: from config or robot_type + - vendor: from config ``robot_type`` (generic) + - model: robot_type + - device_class: ``"robot_arm"`` (or inferred from type) + - channels: from :meth:`RobotChannelMapper.map_features` + (using motor_limits and camera configs from config) + - transport_ref: ``TransportRef(kind="robot_arm", config={...})`` + - provenance: :attr:`ContextSource.IMPORTED`, notes about robot arm + - halt_supported: ``True`` (zero-velocity stop) + """ + robot_type = str(self._config.get("robot_type") or "").strip() + if not robot_type: + logger.warning("robot_arm provider: 'robot_type' is required; skipping") + return () + + device_name = str( + self._config.get("device_name") or robot_type + ).strip() + device_id = f"robot.{device_name}" + display_name = str( + self._config.get("display_name") or robot_type + ).strip() + + sample_rate_hz = float(self._config.get("sample_rate_hz") or 0.0) + + # Build synthetic feature dicts from configuration so the channel + # mapper can produce declarations without a live robot. + observation_features = _build_observation_features(self._config) + action_features = _build_action_features(self._config) + + # Parse motor limits from config. + motor_limits = _parse_motor_limits(self._config.get("motor_limits")) + + # Parse camera configs. + camera_configs = _parse_camera_configs(self._config.get("cameras")) + + mapper = RobotChannelMapper( + default_sample_rate_hz=sample_rate_hz, + default_camera_fps=30.0, + ) + channels = mapper.map_features( + observation_features, + action_features, + motor_limits=motor_limits, + camera_configs=camera_configs, + ) + + if not channels: + logger.warning( + "robot_arm provider: no channels derived for robot_type=%r; " + "check motor_limits and cameras config", + robot_type, + ) + return () + + # Build the transport config carrying everything the transport + # factory needs to instantiate the robot at open() time. + transport_config: dict[str, Any] = { + "robot_type": robot_type, + "robot_config": {}, + } + if self._config.get("serial_port"): + transport_config["robot_config"]["serial_port"] = str(self._config["serial_port"]) + if self._config.get("cameras"): + transport_config["robot_config"]["cameras"] = dict(self._config["cameras"]) + if self._config.get("sample_rate_hz"): + transport_config["default_sample_rate_hz"] = sample_rate_hz + + context = HardwareContext( + device_id=device_id, + display_name=display_name, + device_class=_infer_device_class(robot_type), + vendor=robot_type, + model=robot_type, + transport=TransportRef(kind="robot_arm", config=transport_config), + channels=channels, + halt_supported=True, + notes=( + f"Robot arm '{robot_type}' discovered from provider " + f"configuration. Transport instantiates the robot on open()." + ), + provenance=ContextProvenance( + source=ContextSource.IMPORTED.value, + notes=( + "Mapped from robot type descriptor. Motor limits " + "and camera parameters are declared in the provider config, " + "not probed from hardware." + ), + ), + ) + return (context,) + + +# --------------------------------------------------------------------------- +# Feature synthesis helpers +# --------------------------------------------------------------------------- + + +def _build_observation_features(config: Mapping[str, Any]) -> dict[str, Any]: + """Synthesize observation features from provider config. + + When a real robot instance is not available (offline discovery), + build a feature dictionary from the configured motor names and camera + entries so the channel mapper can still produce declarations. + + Observation features include all motor positions (read) and all cameras. + """ + features: dict[str, Any] = {} + + # Motor positions from motor_limits keys. + motor_limits = config.get("motor_limits") + if isinstance(motor_limits, Mapping): + for motor_name in motor_limits: + features[f"{motor_name}.position"] = (1,) + + # Camera observations from camera config. + cameras = config.get("cameras") + if isinstance(cameras, Mapping): + for cam_name, cam_cfg in cameras.items(): + if isinstance(cam_cfg, Mapping): + w = int(cam_cfg.get("width", 640)) + h = int(cam_cfg.get("height", 480)) + features[f"{cam_name}.image"] = (h, w, 3) + else: + features[f"{cam_name}.image"] = (480, 640, 3) + + return features + + +def _build_action_features(config: Mapping[str, Any]) -> dict[str, Any]: + """Synthesize action features from provider config. + + Action features include all motor positions (write) -- the same joints + that appear in observations but marked as actionable so the channel + mapper promotes them to READWRITE. + """ + features: dict[str, Any] = {} + + motor_limits = config.get("motor_limits") + if isinstance(motor_limits, Mapping): + for motor_name in motor_limits: + features[f"{motor_name}.position"] = (1,) + + return features + + +def _parse_motor_limits( + raw: Any, +) -> dict[str, tuple[float, float]] | None: + """Parse motor limits from config into the format expected by the mapper.""" + if not isinstance(raw, Mapping): + return None + limits: dict[str, tuple[float, float]] = {} + for name, bounds in raw.items(): + if isinstance(bounds, (list, tuple)) and len(bounds) >= 2: + try: + limits[str(name)] = (float(bounds[0]), float(bounds[1])) + except (ValueError, TypeError): + logger.warning( + "robot_arm provider: invalid motor limits for %r, skipping", + name, + ) + return limits if limits else None + + +def _parse_camera_configs( + raw: Any, +) -> dict[str, dict[str, Any]] | None: + """Parse camera configs from the provider config block.""" + if not isinstance(raw, Mapping): + return None + configs: dict[str, dict[str, Any]] = {} + for name, cfg in raw.items(): + if isinstance(cfg, Mapping): + configs[str(name)] = dict(cfg) + return configs if configs else None + + +def _infer_device_class(robot_type: str) -> str: + """Infer the device class from the robot type name. + + Known patterns: + + - ``*follower*``, ``*leader*``, ``*koch*``, ``*so100*``, ``*so101*`` + -> ``"robot_arm"`` + - ``*kiwi*``, ``*rover*`` -> ``"mobile_robot"`` + - ``*g1*``, ``*h1*``, ``*reachy*`` -> ``"humanoid"`` + - otherwise -> ``"robot"`` + """ + if any(p.search(robot_type) for p in _ARM_PATTERNS): + return "robot_arm" + if any(p.search(robot_type) for p in _MOBILE_PATTERNS): + return "mobile_robot" + if any(p.search(robot_type) for p in _HUMANOID_PATTERNS): + return "humanoid" + return "robot" + + +# --------------------------------------------------------------------------- +# Factory function +# --------------------------------------------------------------------------- + + +def build_provider(config: Mapping[str, Any]) -> RobotArmContextProvider: + """Factory function for the provider registry.""" + return RobotArmContextProvider(config) + + +__all__ = ["RobotArmContextProvider", "build_provider"] diff --git a/src/leapflow/hardware/providers/yaml_provider.py b/src/leapflow/hardware/providers/yaml_provider.py index 4a5589b0..7bbad059 100644 --- a/src/leapflow/hardware/providers/yaml_provider.py +++ b/src/leapflow/hardware/providers/yaml_provider.py @@ -58,7 +58,9 @@ def discover(self) -> tuple[HardwareContext, ...]: if payload is None: continue context = HardwareContext.from_mapping(payload) - contexts.append(self._apply_verification(context)) + context = self._apply_verification(context) + context = _apply_control_bindings(context) + contexts.append(context) return tuple(contexts) def _apply_verification(self, context: HardwareContext) -> HardwareContext: @@ -120,4 +122,60 @@ def build_provider(config: Mapping[str, Any] | None = None) -> YamlContextProvid return YamlContextProvider(config) +def _apply_control_bindings(context: HardwareContext) -> HardwareContext: + """Resolve control_bindings into a TransportRef when the YAML declares them. + + Rules: + - If no ``control_bindings`` section, return *context* unchanged. + - If the YAML already declares an explicit ``transport.kind``, the bindings + are kept as supplementary metadata but do **not** override the transport. + - Otherwise the resolver picks the best available binding and sets it as + the context's transport. + """ + if not context.control_bindings: + return context + + # Explicit transport already declared — nothing to override. + if context.transport.kind: + logger.debug( + "control_bindings: device %s already has transport.kind=%r; " + "bindings kept as metadata only", + context.device_id, + context.transport.kind, + ) + return context + + # Function-local import to avoid a module-level circular dependency. + from leapflow.hardware.control_binding_resolver import ControlBindingResolver + + resolver = ControlBindingResolver() + bindings = resolver.parse(context.control_bindings) + if not bindings: + return context + + resolved = resolver.resolve(bindings) + if resolved is None: + return context + + if not resolved.available: + logger.info( + "control_bindings: best binding %s for device %s resolved to " + "unavailable transport (%s); transport left empty", + resolved.binding.binding_type, + context.device_id, + resolved.reason, + ) + return context + + from dataclasses import replace + + logger.debug( + "control_bindings: device %s transport set to %s from %s binding", + context.device_id, + resolved.transport_ref.kind, + resolved.binding.binding_type, + ) + return replace(context, transport=resolved.transport_ref) + + __all__ = ["YamlContextProvider", "build_provider"] diff --git a/src/leapflow/hardware/registry.py b/src/leapflow/hardware/registry.py index cf3cb000..b70ffe1a 100644 --- a/src/leapflow/hardware/registry.py +++ b/src/leapflow/hardware/registry.py @@ -724,6 +724,25 @@ def channel_is_writable(self, device_id: str, channel_id: str) -> bool: channel = context.channel(channel_id) if context is not None else None return bool(channel is not None and channel.is_writable) + async def read(self, device_id: str, channel_id: str) -> Any: + """Read a single channel value. Convenience wrapper around transport.read().""" + async with self.device_io(device_id): + transport = await self.transport(device_id) + return await transport.read(channel_id) + + async def read_frame(self, device_id: str, channel_id: str, **kwargs: Any) -> Any: + """Read a frame from a camera channel. Requires FrameTransport.""" + async with self.device_io(device_id): + transport = await self.transport(device_id) + from leapflow.hardware.transport import FrameTransport + + if not isinstance(transport, FrameTransport): + raise TransportError( + f"Transport for {device_id!r} does not support frame capture", + failure_code="frame_transport_missing", + ) + return await transport.read_frame(channel_id, **kwargs) + async def transport(self, device_id: str) -> HardwareTransport: """Return the open transport for *device_id*, opening it on first use. @@ -776,14 +795,25 @@ def stream_sources(self) -> tuple[Any, ...]: if not self._settings.stream_enabled: return () if self._stream_sources is None: - from leapflow.hardware.stream import build_stream_sources + from leapflow.hardware.stream import build_batch_coordinators, build_stream_sources - self._stream_sources = build_stream_sources( + batch_coordinators = build_batch_coordinators( self, ring_capacity=self._settings.stream_ring_capacity, event_sink=self.record_event, reading_store=self.reading_store, ) + batch_device_ids = frozenset( + c._context.device_id for c in batch_coordinators + ) + individual_sources = build_stream_sources( + self, + ring_capacity=self._settings.stream_ring_capacity, + event_sink=self.record_event, + reading_store=self.reading_store, + batch_device_ids=batch_device_ids, + ) + self._stream_sources = (*batch_coordinators, *individual_sources) return self._stream_sources @property @@ -1119,6 +1149,45 @@ def device_io(self, device_id: str) -> Any: self._io_locks[key] = lock return lock + def device_io_batch(self, device_id: str) -> Any: + """Acquire the device I/O lock for a batch operation. + + Identical to ``device_io`` but named explicitly so batch vs single + operations are distinguishable in logs and metrics. The lock is the + same per-device asyncio.Lock -- batch does not bypass it, it simply + acquires it once for multiple channels instead of once per channel. + """ + key = str(device_id) + lock = self._io_locks.get(key) + if lock is None: + lock = asyncio.Lock() + self._io_locks[key] = lock + return lock + + async def read_batch(self, device_id: str, channel_ids: tuple[str, ...]) -> Any: + """Read multiple channels in one atomic operation if the transport supports it. + + Falls back to sequential reads if ``BatchTransport`` is not satisfied. + Returns a ``BatchReading`` or a synthesized equivalent from sequential reads. + """ + from leapflow.hardware.transport import BatchReading, BatchTransport, Reading + + transport = await self.transport(device_id) + if isinstance(transport, BatchTransport): + async with self.device_io_batch(device_id): + return await transport.read_batch(channel_ids) + else: + # Fallback: sequential reads under one lock acquisition. + async with self.device_io(device_id): + readings: list[Reading] = [] + for ch_id in channel_ids: + reading = await transport.read(ch_id) + readings.append(reading) + return BatchReading( + device_id=device_id, + readings=tuple(readings), + ) + # ── Rate-limit baseline ── def record_command(self, device_id: str, channel_id: str, value: float) -> None: diff --git a/src/leapflow/hardware/stream.py b/src/leapflow/hardware/stream.py index f8be6e25..f3484062 100644 --- a/src/leapflow/hardware/stream.py +++ b/src/leapflow/hardware/stream.py @@ -625,6 +625,275 @@ def health(self) -> dict[str, Any]: } +class BatchStreamCoordinator: + """Coordinates batch sampling for all streaming channels of one device. + + When the transport satisfies ``BatchTransport``, one coordinator replaces + N individual ``HardwareStreamSource`` tasks with a single sampling loop + that calls ``read_batch`` once per period. The readings are then + distributed to the per-channel rings and detectors, preserving the + per-channel event model while eliminating per-read lock contention. + + Falls back to individual sources when the transport does not support + batch -- the coordinator is an optimisation, not a requirement. + """ + + def __init__( + self, + registry: Any, + context: HardwareContext, + channels: tuple[Channel, ...], + *, + ring_capacity: int = DEFAULT_RING_CAPACITY, + event_sink: EventSink | None = None, + reading_store: Any = None, + alert_policy: Any = None, + ) -> None: + self._registry = registry + self._context = context + self._channels = channels + self._channel_ids = tuple(ch.channel_id for ch in channels) + self._rings: dict[str, ReadingRing] = { + ch.channel_id: ReadingRing(ring_capacity) for ch in channels + } + self._detectors: dict[str, HardwareEventDetector] = { + ch.channel_id: HardwareEventDetector(context, ch) for ch in channels + } + self._event_sink = event_sink + self._store = reading_store + self._alert_policy = alert_policy + self._task: asyncio.Task[None] | None = None + self._stopping = asyncio.Event() + self._last_emitted: dict[str, float] = {} + self._paced_out = 0 + self._samples = 0 + self._skipped_slots = 0 + self._started_monotonic: float | None = None + + @property + def source_id(self) -> str: + return f"hw.batch:{self._context.device_id}" + + async def start(self, emit: Any) -> None: + """Begin batch sampling. Returns promptly; the loop runs as an internal task.""" + if self._task is not None: + return + self._stopping.clear() + self._task = asyncio.create_task(self._run(emit), name=self.source_id) + + async def stop(self) -> None: + """Stop batch sampling. Idempotent.""" + self._stopping.set() + task = self._task + self._task = None + if task is None: + return + task.cancel() + try: + await asyncio.wait_for(task, timeout=2.0) + except (asyncio.CancelledError, asyncio.TimeoutError): + pass + except Exception as exc: # noqa: BLE001 - teardown must not propagate + logger.warning("Batch stream %s stop raised: %s", self.source_id, exc) + + async def _run(self, emit: Any) -> None: + """Core batch sampling loop. + + Uses deadline-based scheduling identical to HardwareStreamSource._run(), + but calls read_batch() once to get all channels simultaneously. + The batch readings are then split and fed to per-channel rings, + detectors, persistence and event dispatch. + + The sampling rate is the maximum declared rate across all channels. + """ + max_rate = max((ch.sample_rate_hz for ch in self._channels if ch.sample_rate_hz > 0), default=0.0) + interval = 1.0 / max_rate if max_rate > 0 else 1.0 + consecutive_failures = 0 + self._started_monotonic = time.monotonic() + next_at = self._started_monotonic + while not self._stopping.is_set(): + try: + batch = await self._registry.read_batch( + self._context.device_id, self._channel_ids + ) + except asyncio.CancelledError: + raise + except Exception as exc: # noqa: BLE001 - one device must not stop the rest + consecutive_failures += 1 + if consecutive_failures == 1: + logger.warning( + "Batch stream %s read failed: %s", self.source_id, exc, exc_info=True + ) + # Check staleness on all channels during failures. + for detector in self._detectors.values(): + self._dispatch(detector.check_stale(), emit) + await self._sleep(min(interval * (2 ** consecutive_failures), 30.0)) + next_at = time.monotonic() + continue + + consecutive_failures = 0 + self._samples += 1 + + # Distribute batch readings to per-channel rings and detectors. + for reading in batch.readings: + ch_id = reading.channel_id + ring = self._rings.get(ch_id) + if ring is None: + continue + lost = ring.record(reading) + await self._persist(reading, lost=lost) + detector = self._detectors.get(ch_id) + if detector is not None: + self._dispatch(detector.observe(reading, lost=lost), emit) + + next_at += interval + delay = next_at - time.monotonic() + if delay < 0: + missed = int(-delay // interval) + 1 + self._skipped_slots += missed + next_at += missed * interval + delay = max(0.0, next_at - time.monotonic()) + await self._sleep(delay) + + async def _persist(self, reading: Reading, *, lost: int) -> None: + """Buffer one sample for persistence, mirroring HardwareStreamSource._persist.""" + store = self._store + if store is None: + return + try: + store.record(reading, dropped=lost) + if not store.due_for_flush(): + return + batches = store.drain() + if batches: + await asyncio.to_thread(store.write_batches, batches) + except Exception as exc: # noqa: BLE001 - persistence must not stop sampling + logger.warning( + "Hardware reading persistence failed for %s: %s", + self.source_id, + exc, + exc_info=True, + ) + + async def _sleep(self, seconds: float) -> None: + try: + await asyncio.wait_for(self._stopping.wait(), timeout=seconds) + except asyncio.TimeoutError: + return + + def _dispatch(self, events: Iterable[HardwareEvent], emit: Any) -> None: + """Hand events to the sink, signal pipeline, and alert policy, paced per kind.""" + for event in events: + if not self._admit(event): + continue + if self._event_sink is not None: + try: + self._event_sink(event) + except Exception as exc: # noqa: BLE001 - a sink must not stop sampling + logger.warning("Hardware event sink raised: %s", exc, exc_info=True) + if emit is not None: + try: + emit(event) + except Exception as exc: # noqa: BLE001 - as above + logger.warning("Hardware event emit raised: %s", exc, exc_info=True) + if self._alert_policy is not None: + try: + self._alert_policy.evaluate(event) + if event.kind == EventKind.SETTLED: + self._alert_policy.reset_channel( + event.device_id, event.channel_id + ) + except Exception as exc: # noqa: BLE001 - policy must not stop sampling + logger.warning("Hardware alert policy raised: %s", exc, exc_info=True) + + def _admit(self, event: HardwareEvent) -> bool: + """Return whether this event clears the per-kind-per-channel rate floor.""" + now = time.monotonic() + key = f"{event.kind}:{event.device_id}.{event.channel_id}" + previous = self._last_emitted.get(key) + if previous is not None and now - previous < MIN_EVENT_INTERVAL_S: + self._paced_out += 1 + return False + self._last_emitted[key] = now + return True + + def ring(self, channel_id: str) -> ReadingRing | None: + """Return the per-channel ring, or None for an unknown channel.""" + return self._rings.get(channel_id) + + @property + def health(self) -> dict[str, Any]: + """Return batch sampling health metrics.""" + max_rate = max((ch.sample_rate_hz for ch in self._channels if ch.sample_rate_hz > 0), default=0.0) + started = self._started_monotonic + elapsed = (time.monotonic() - started) if started is not None else 0.0 + observed = (self._samples / elapsed) if elapsed > 0 else 0.0 + total_dropped = sum(r.dropped for r in self._rings.values()) + return { + "source_id": self.source_id, + "device_id": self._context.device_id, + "channels": len(self._channels), + "declared_hz": float(max_rate), + "observed_hz": observed, + "rate_ratio": (observed / max_rate) if max_rate > 0 else 0.0, + "samples": self._samples, + "skipped_slots": self._skipped_slots, + "dropped": total_dropped, + "events_paced_out": self._paced_out, + } + + +def build_batch_coordinators( + registry: Any, + *, + ring_capacity: int = DEFAULT_RING_CAPACITY, + event_sink: EventSink | None = None, + reading_store: Any = None, + alert_policy: Any = None, +) -> tuple[BatchStreamCoordinator, ...]: + """Return one coordinator per device whose transport supports BatchTransport. + + Devices whose transport does not satisfy ``BatchTransport`` are skipped; + their channels are handled by individual ``HardwareStreamSource`` instances + from ``build_stream_sources``. + """ + from leapflow.hardware.transport import BatchTransport + + coordinators: list[BatchStreamCoordinator] = [] + for context in registry.contexts(): + channels = tuple( + ch for ch in context.streaming_channels + if ch.is_readable and not ch.is_media + ) + if not channels: + continue + # Check whether the transport declaration supports batch. + # We inspect the transport kind; the actual isinstance check against the + # live transport is deferred to read_batch at runtime, but the coordinator + # is only created for devices whose transport advertises BatchTransport. + transport_kind = context.transport.kind + try: + from leapflow.hardware.transports import build_transport + + proto = build_transport(transport_kind, context.transport.config) + if not isinstance(proto, BatchTransport): + continue + except Exception: # noqa: BLE001 - skip devices we cannot probe + continue + coordinators.append( + BatchStreamCoordinator( + registry, + context, + channels, + ring_capacity=ring_capacity, + event_sink=event_sink, + reading_store=reading_store, + alert_policy=alert_policy, + ) + ) + return tuple(coordinators) + + def build_stream_sources( registry: Any, *, @@ -632,6 +901,7 @@ def build_stream_sources( event_sink: EventSink | None = None, reading_store: Any = None, alert_policy: Any = None, + batch_device_ids: frozenset[str] = frozenset(), ) -> tuple[HardwareStreamSource, ...]: """Return one source per streaming channel across all admitted devices. @@ -643,9 +913,15 @@ def build_stream_sources( because this is the loop that would otherwise push a few hundred kilobytes per sample into the raw NDJSON segment and try to average frames into a downsample window. Previews are pulled on demand by whoever is watching, never sampled. + + Channels belonging to devices in *batch_device_ids* are skipped: they are + handled by a ``BatchStreamCoordinator`` instead, and building individual + sources for them would double-sample the device. """ sources: list[HardwareStreamSource] = [] for context in registry.contexts(): + if context.device_id in batch_device_ids: + continue for channel in context.streaming_channels: if not channel.is_readable or channel.is_media: continue @@ -688,6 +964,7 @@ def _bounds(envelope: Any) -> str: __all__ = [ + "BatchStreamCoordinator", "DEFAULT_RING_CAPACITY", "MIN_EVENT_INTERVAL_S", "EventKind", @@ -695,5 +972,6 @@ def _bounds(envelope: Any) -> str: "HardwareEventDetector", "HardwareStreamSource", "ReadingRing", + "build_batch_coordinators", "build_stream_sources", ] diff --git a/src/leapflow/hardware/tools.py b/src/leapflow/hardware/tools.py index 6a348a63..8953c7fc 100644 --- a/src/leapflow/hardware/tools.py +++ b/src/leapflow/hardware/tools.py @@ -391,6 +391,410 @@ async def hw_estop(self, device_id: str = "", **_: Any) -> dict[str, Any]: "status": status.to_dict(), } + # ── Batch write ── + + async def batch_actuate(self, params: dict) -> dict: + """Write multiple ACTUATE channels in one atomic bus transaction. + + Parameters: + - device_id (str, required): Target device identifier + - commands (list[dict], required): List of {channel_id: str, value: number} + - dry_run (bool, optional): Preview mode, default false + - verify (bool, optional): Run post-write verification, default false + + This tool is the primary interface for policy-driven control: a VLA + model produces a full joint-space action vector, and this tool writes + all joints atomically rather than one at a time. + + Uses BatchTransport.write_batch() when available, falling back to + sequential writes otherwise. Each channel in the command list must + be an ACTUATE-effect channel on the target device. + + Approval: follows the same approval chain as hw_actuate -- each + channel's trust level and reversibility are checked. The batch + is approved as a unit: if any channel requires approval, the + entire batch requires approval. + """ + from leapflow.hardware.transport import BatchTransport, BatchWriteOutcome + + device_id = str(params.get("device_id") or "") + commands = params.get("commands") or [] + dry_run = bool(params.get("dry_run")) + verify = bool(params.get("verify")) + + context = self._registry.context(device_id) + if context is None: + return self._unknown_device(device_id) + + if self._requires_describe(device_id): + return self._refusal( + device_id, + "", + "describe_required", + f"Call hw_describe(device_id={device_id!r}) before commanding it. Writing to " + "channels whose envelope you have not read risks out-of-range commands; the " + "describe result carries the allowed range, rate, and reversibility.", + ) + + if not commands: + return self._refusal( + device_id, "", "empty_commands", "No commands provided in the batch." + ) + + # ── Validate every channel ── + resolved: list[tuple[Channel, Any]] = [] # (channel, value) + for idx, cmd in enumerate(commands): + channel_id = str(cmd.get("channel_id") or "") + value = cmd.get("value") + channel = context.channel(channel_id) + if channel is None: + known = ", ".join(c.channel_id for c in context.channels) or "(none)" + return self._refusal( + device_id, + channel_id, + "unknown_channel", + f"Command [{idx}]: device {device_id!r} has no channel " + f"{channel_id!r}. Declared: {known}.", + ) + if not channel.is_writable: + return self._refusal( + device_id, + channel_id, + "channel_not_writable", + f"Command [{idx}]: channel {channel_id!r} on {device_id!r} is read-only " + "in the admitted declaration.", + ) + if channel.effect != HardwareEffect.ACTUATE.value: + return self._refusal( + device_id, + channel_id, + "effect_class_mismatch", + f"Command [{idx}]: channel {channel_id!r} declares effect " + f"{channel.effect!r}; hw_batch_actuate requires effect=actuate.", + ) + + envelope = channel.envelope + in_envelope = envelope.contains(value) + if not in_envelope: + return self._refusal( + device_id, + channel_id, + "value_out_of_envelope", + f"Command [{idx}]: value {value!r} lies outside the declared " + f"envelope for {device_id}.{channel_id}. Call hw_describe to " + "read the allowed range.", + ) + + wait_s = self._rate_wait_s(device_id, channel, value) + if wait_s > 0.0: + return { + "ok": False, + "device_id": device_id, + "channel_id": channel_id, + "error": ( + f"Command [{idx}]: commanding {device_id}.{channel_id} to {value} now " + f"would exceed its declared maximum rate of " + f"{envelope.max_rate:g} {channel.unit or 'units'}/s. Wait about " + f"{wait_s:.2f}s and issue the same command again, or command a " + "smaller step." + ), + "failure_code": "rate_limited", + "retry_after_s": round(wait_s, 3), + "side_effect_state": SIDE_EFFECT_NONE, + } + resolved.append((channel, value)) + + # ── Reachability (once for the device) ── + unreachable = await self._unreachable(device_id, resolved[0][0]) + if unreachable is not None: + return unreachable + + # ── Interlocks (union of all channels) ── + all_interlocks_failed: list[str] = [] + for channel, _value in resolved: + failed = await self._failed_interlocks(context, channel) + for name in failed: + if name not in all_interlocks_failed: + all_interlocks_failed.append(name) + + # ── Build batch approval descriptor ── + # Use the first channel for the primary descriptor; metadata carries + # the full command list so the approval UI can render all of them. + any_irreversible = any( + not ch.envelope.reversible for ch, _ in resolved + ) + channel_summaries = [ + { + "channel_id": ch.channel_id, + "quantity": ch.quantity, + "value": v, + "unit": ch.unit, + "envelope_band": self._grant_band(ch.envelope, v), + "reversible": ch.envelope.reversible, + } + for ch, v in resolved + ] + descriptor = ActionDescriptor.device( + kind=ActionKind.DEVICE_ACTUATE.value, + device_id=device_id, + channel_id=resolved[0][0].channel_id, + quantity="batch", + value=f"{len(resolved)} channels", + unit="", + envelope_band="batch", + location=context.location, + reversible=not any_irreversible, + metadata={ + "batch": True, + "commands": channel_summaries, + "interlocks_satisfied": not all_interlocks_failed, + "interlocks_failed": list(all_interlocks_failed), + "session_id": self._session_id, + }, + ) + + if dry_run: + ok = not all_interlocks_failed + return { + "ok": ok, + "device_id": device_id, + "preview": True, + "side_effect_state": SIDE_EFFECT_NONE, + "plan": { + "summary": descriptor.summary, + "detail": descriptor.detail, + "resource": descriptor.resource, + "batch_size": len(resolved), + "commands": channel_summaries, + "any_irreversible": any_irreversible, + "requires_approval": True, + "interlocks_satisfied": not all_interlocks_failed, + "interlocks_failed": list(all_interlocks_failed), + }, + **({ + "failure_code": "interlocks_unsatisfied", + "error": ( + f"Interlocks {all_interlocks_failed} are not satisfied; " + "the real command would be refused." + ), + } if all_interlocks_failed else { + "next_step": ( + "Dry run only: nothing was commanded. Re-issue the same call " + "without dry_run to execute it, which will require approval." + ), + }), + } + + if all_interlocks_failed: + # Use the first channel for the readiness failure, listing all failed interlocks. + return self._not_ready(context, resolved[0][0], tuple(all_interlocks_failed)) + + allowed, denial = await self._evaluate(descriptor) + if not allowed: + return self._refusal(device_id, "", "approval_denied", denial) + + # ── Execute ── + try: + async with self._registry.device_io(device_id): + transport = await self._registry.transport(device_id) + if isinstance(transport, BatchTransport): + command_tuples = tuple( + (ch.channel_id, v) for ch, v in resolved + ) + batch_outcome = await transport.write_batch(command_tuples) + else: + logger.warning( + "Transport for %s does not support BatchTransport; " + "falling back to sequential writes (non-atomic)", + device_id, + ) + outcomes: list[WriteOutcome] = [] + for ch, v in resolved: + outcome = await transport.write(ch.channel_id, v) + outcomes.append(outcome) + # Build a synthetic BatchWriteOutcome from sequential results. + all_ok = all(o.ok for o in outcomes) + worst_effect = self._worst_side_effect( + [o.side_effect_state for o in outcomes] + ) + batch_outcome = BatchWriteOutcome( + ok=all_ok, + outcomes=tuple(outcomes), + side_effect_state=worst_effect, + ) + except TransportError as exc: + if self._trust_gate is not None: + for ch, _ in resolved: + self._trust_gate.record_failure(device_id, ch.channel_id) + return { + "ok": False, + "device_id": device_id, + "error": str(exc), + "failure_code": exc.failure_code, + "side_effect_state": SIDE_EFFECT_NONE, + } + except Exception as exc: # noqa: BLE001 - see _write for rationale + logger.error( + "Hardware transport raised a non-contract exception on batch write " + "to %s: %s", + device_id, + exc, + exc_info=True, + ) + if self._trust_gate is not None: + for ch, _ in resolved: + self._trust_gate.record_failure(device_id, ch.channel_id, hard=True) + return { + "ok": False, + "device_id": device_id, + "error": ( + f"The device driver failed unexpectedly ({type(exc).__name__}). " + "Whether the commands reached the device is unknown; verify each " + "channel before attempting anything similar." + ), + "failure_code": "driver_contract_violation", + "side_effect_state": SIDE_EFFECT_UNKNOWN, + "effect_uncertain": True, + } + + # ── Post-write bookkeeping per channel ── + per_channel_results: list[dict] = [] + for i, (channel, value) in enumerate(resolved): + outcome = batch_outcome.outcomes[i] if i < len(batch_outcome.outcomes) else None + ch_ok = outcome.ok if outcome else False + + self._audit.record( + action="batch_write", + device=device_id, + channel=channel.channel_id, + value=value, + outcome="ok" if ch_ok else (outcome.failure_code if outcome else "error"), + identity=self._session_id, + ) + + if self._trust_gate is not None: + if ch_ok: + self._trust_gate.record_success(device_id, channel.channel_id) + else: + self._trust_gate.record_failure(device_id, channel.channel_id) + + if ch_ok: + numeric = as_numeric(value) + if numeric is not None: + self._registry.record_command(device_id, channel.channel_id, numeric) + if outcome: + self._learn_from_write(device_id, channel, value, "", outcome) + else: + recorder = self._registry.outcome_recorder + if recorder is not None: + recorder.drop_pending(device_id, channel.channel_id) + + ch_result: dict = { + "channel_id": channel.channel_id, + "ok": ch_ok, + } + if outcome: + ch_result["side_effect_state"] = outcome.side_effect_state + if outcome.readback is not None: + ch_result["readback"] = outcome.readback.to_dict() + if not ch_ok and outcome.error: + ch_result["error"] = outcome.error + per_channel_results.append(ch_result) + + # ── Optional verification ── + if verify and batch_outcome.ok: + verification_results = await self._batch_verify(device_id, resolved) + if verification_results: + for i, vr in enumerate(verification_results): + if i < len(per_channel_results) and vr is not None: + per_channel_results[i]["verification"] = vr + + payload: dict = { + "ok": batch_outcome.ok, + "device_id": device_id, + "side_effect_state": batch_outcome.side_effect_state, + "batch_size": len(resolved), + "channels": per_channel_results, + } + if not batch_outcome.ok: + failed_channels = [ + r["channel_id"] for r in per_channel_results if not r.get("ok") + ] + payload["failed_channels"] = failed_channels + if any(not ch.envelope.reversible for ch, _ in resolved): + payload["effect_uncertain"] = True + payload["next_step"] = ( + "Some commands in the batch failed but may have reached the device. " + "Read back each channel before attempting a retry; the batch contains " + "irreversible channels." + ) + return payload + + async def _batch_verify( + self, + device_id: str, + resolved: list[tuple[Channel, Any]], + ) -> list[dict | None]: + """Run post-write verification for each channel in a batch. + + Returns a list parallel to *resolved*, with verification result dicts + or None when verification is not applicable or not available. + Uses tolerance-based comparison for floating-point values to avoid + false negatives on robot joint positions. + """ + from leapflow.hardware.context import as_numeric + + results: list[dict | None] = [] + for channel, value in resolved: + try: + transport = await self._registry.transport(device_id) + reading = await transport.read(channel.channel_id) + commanded_num = as_numeric(value) + readback_num = as_numeric(reading.value) + if commanded_num is not None and readback_num is not None: + deviation = abs(readback_num - commanded_num) + # Use envelope tolerance or default 1e-3. + tol = getattr(channel.envelope, "tolerance", 0.0) or 1e-3 + match = deviation <= tol + else: + deviation = None + match = reading.value == value + results.append({ + "channel_id": channel.channel_id, + "commanded": value, + "readback": reading.value, + "match": match, + "deviation": deviation, + }) + except Exception as exc: # noqa: BLE001 - verification must not fail the batch + logger.warning( + "Post-write verification failed for %s.%s: %s", + device_id, + channel.channel_id, + exc, + exc_info=True, + ) + results.append(None) + return results + + @staticmethod + def _worst_side_effect(states: list[str]) -> str: + """Return the worst-case side-effect state across a list of outcomes. + + Precedence: UNKNOWN > PARTIAL > COMMITTED > NONE. + """ + _PRECEDENCE = { + SIDE_EFFECT_NONE: 0, + SIDE_EFFECT_COMMITTED: 1, + SIDE_EFFECT_PARTIAL: 2, + SIDE_EFFECT_UNKNOWN: 3, + } + worst = SIDE_EFFECT_NONE + for s in states: + if _PRECEDENCE.get(s, 3) > _PRECEDENCE.get(worst, 0): + worst = s + return worst + # ── Write path ── async def _write(self, tool_name: str, params: Mapping[str, Any]) -> dict[str, Any]: @@ -1194,7 +1598,7 @@ def _tool_for_effect(effect: str) -> str: def build_hardware_tools(tools: HardwareTools) -> list[ToolMetadata]: - """Return the eight tool definitions bound to *tools*.""" + """Return the nine tool definitions bound to *tools*.""" return [ ToolMetadata( name="hw_list", @@ -1312,6 +1716,52 @@ def build_hardware_tools(tools: HardwareTools) -> list[ToolMetadata]: }, provides_capabilities=("hw.estop",), ), + ToolMetadata( + name="hw_batch_actuate", + description=( + "Write multiple actuator channels in one atomic transaction. " + "Used for coordinated multi-joint control where all positions " + "must be commanded simultaneously (e.g. robot arm joint-space control)." + ), + parameters_schema={ + "type": "object", + "properties": { + "device_id": { + "type": "string", + "description": "Target device identifier", + }, + "commands": { + "type": "array", + "items": { + "type": "object", + "properties": { + "channel_id": {"type": "string"}, + "value": {"type": "number"}, + }, + "required": ["channel_id", "value"], + }, + "description": "List of channel commands to write atomically", + }, + "dry_run": { + "type": "boolean", + "description": "Preview mode: validate without executing", + }, + "verify": { + "type": "boolean", + "description": "Run post-write verification", + }, + }, + "required": ["device_id", "commands"], + }, + handler=tools.batch_actuate, + x_leapflow={ + "category": "hardware", + "risk_level": "high", + "mutates_state": True, + "execution_policy": "serial", + "provides_capabilities": ["hardware_batch_control"], + }, + ), ] diff --git a/src/leapflow/hardware/transport.py b/src/leapflow/hardware/transport.py index 4be045ae..50a0d681 100644 --- a/src/leapflow/hardware/transport.py +++ b/src/leapflow/hardware/transport.py @@ -332,7 +332,87 @@ async def read_frame( ... +@dataclass(frozen=True) +class BatchReading: + """Atomic vector read of multiple channels at one physical instant. + + A single bus transaction that samples every requested channel at one + coordinated instant, rather than N sequential ``read()`` calls whose + timestamps drift by the inter-read latency. The contract is that all + ``readings`` share the same physical sampling moment, so the pair + (``observed_at``, ``monotonic_at``) is per-batch, not per-reading. + + Returned by ``BatchTransport.read_batch``. Each ``Reading`` inside + carries the per-channel quantity, unit and quality; the batch adds + only the device scope and the shared timebase. + """ + + device_id: str + readings: tuple[Reading, ...] + observed_at: float = field(default_factory=time.time) + monotonic_at: float = field(default_factory=time.monotonic) + + +@dataclass(frozen=True) +class BatchWriteOutcome: + """Result of an atomic vector write to multiple channels. + + Mirrors ``WriteOutcome`` for the batch case. ``ok`` is True only when + every individual outcome succeeded. ``side_effect_state`` is the + worst-case across all outcomes: if any channel reports ``UNKNOWN``, + the batch reports ``UNKNOWN``; if any reports ``PARTIAL``, the batch + reports ``PARTIAL``. A caller must never assume that a partially + successful batch can be meaningfully retried as a whole. + """ + + ok: bool + outcomes: tuple[WriteOutcome, ...] + side_effect_state: str = SIDE_EFFECT_UNKNOWN + + +@runtime_checkable +class BatchTransport(Protocol): + """Optional side-protocol for vectorized read/write. + + Discovered via ``isinstance(transport, BatchTransport)``, exactly like + ``FrameTransport``. A transport that does not satisfy this Protocol + falls back to sequential single-channel ``read``/``write`` calls + automatically -- the registry and the streaming engine both check + before attempting batch operations. + + The motivation is physical: a six-joint robot arm reads all joint + positions in one bus transaction (``sync_read`` on Dynamixel/Feetech, + ``read_input_registers`` on Modbus, one ``get_observation()`` in + LeapRobot). Expressing that as six sequential ``read()`` calls pays + six lock acquisitions and produces timestamps that drift by the + inter-read latency, which at 100 Hz is the period itself. + + ``read_batch`` must return readings whose ``observed_at`` / + ``monotonic_at`` reflect the *physical* sampling instant, not the + moment each value was unpacked. ``write_batch`` must be atomic at + the bus level: either all channels receive their commanded values in + one transaction, or the outcome reports which ones landed. + """ + + async def read_batch(self, channel_ids: tuple[str, ...]) -> BatchReading: + """Read multiple channels in one atomic bus transaction.""" + ... + + async def write_batch( + self, commands: tuple[tuple[str, Any], ...] + ) -> BatchWriteOutcome: + """Write multiple channels in one atomic bus transaction. + + ``commands`` is a tuple of ``(channel_id, value)`` pairs. Order + is preserved but the transport may reorder for bus efficiency. + """ + ... + + __all__ = [ + "BatchReading", + "BatchTransport", + "BatchWriteOutcome", "SIDE_EFFECT_COMMITTED", "SIDE_EFFECT_NONE", "SIDE_EFFECT_PARTIAL", diff --git a/src/leapflow/hardware/transports/__init__.py b/src/leapflow/hardware/transports/__init__.py index c9cbfd9c..a2e1574c 100644 --- a/src/leapflow/hardware/transports/__init__.py +++ b/src/leapflow/hardware/transports/__init__.py @@ -34,6 +34,9 @@ "mcp": "leapflow.hardware.transports.mcp:build_transport", "host": "leapflow.hardware.transports.host:build_transport", "media": "leapflow.hardware.transports.media:build_transport", + "robot_arm": "leapflow.hardware.transports.robot_arm:build_transport", + "ros2": "leapflow.hardware.transports.ros2:build_transport", + "vla_inference": "leapflow.hardware.transports.vla_inference:build_transport", } _TRANSPORTS: dict[str, str] = dict(_BUILTIN_TRANSPORTS) diff --git a/src/leapflow/hardware/transports/robot_arm.py b/src/leapflow/hardware/transports/robot_arm.py new file mode 100644 index 00000000..1298565f --- /dev/null +++ b/src/leapflow/hardware/transports/robot_arm.py @@ -0,0 +1,757 @@ +# Copyright (c) Alibaba, Inc. and its affiliates. +# Adapted from LeRobot (https://github.com/huggingface/lerobot). +"""Robot arm transport for LeapRobot: bridges a robot arm instance to the HCP six-method contract. + +Robot arms expose a synchronous Python API -- ``connect()``, ``disconnect()``, +``get_observation()``, ``send_action()`` -- designed for direct control loops. +This transport wraps that API behind the asynchronous HCP contract so the rest +of the hardware stack (registry, streaming, tools, trust, audit) works without +knowing anything about the robot arm implementation. + +The synchronous calls are dispatched to a thread via ``asyncio.to_thread`` so +the event loop is never blocked. ``read_batch`` and ``write_batch`` exploit +the fact that the robot's native interface is already vectorized: one +``get_observation()`` returns all joint positions and camera frames at once, +and one ``send_action()`` commands all actuators atomically. + +Implements three protocols: +- ``HardwareTransport`` (core six methods) +- ``BatchTransport`` (vectorized read/write for high-frequency control) +- ``FrameTransport`` (camera frame capture) +""" + +from __future__ import annotations + +import asyncio +import io +import logging +import time +from typing import Any, Callable, Mapping + +from leapflow.hardware.context import ( + HardwareContext, + HardwareEffect, + Quality, + Representation, +) +from leapflow.hardware.transport import ( + BatchReading, + BatchWriteOutcome, + FrameReading, + Reading, + SIDE_EFFECT_COMMITTED, + SIDE_EFFECT_UNKNOWN, + TransportError, + TransportStatus, + WriteOutcome, +) +from leapflow.hardware.transports.robot_channels import RobotChannelMapper + +logger = logging.getLogger(__name__) + +# Observation cache TTL in seconds. Multiple reads within one control step +# reuse the same hardware observation rather than each triggering a bus poll. +_OBS_CACHE_TTL_S = 0.005 # 5 ms + + +class RobotArmTransport: + """Wraps a robot arm instance as an HCP transport. + + Construction receives a factory callable rather than a live robot: + ``open()`` calls the factory, ``close()`` calls ``disconnect()``. + This ensures the transport owns the robot lifecycle and can recover + from a disconnection by re-creating the instance. + """ + + kind: str = "robot_arm" + + def __init__( + self, + robot_factory: Callable[[], Any], + channel_mapper: RobotChannelMapper | None = None, + ) -> None: + self._factory = robot_factory + self._mapper = channel_mapper or RobotChannelMapper() + self._robot: Any = None + self._channels: dict[str, Any] = {} # channel_id -> Channel + self._connected = False + self._context: HardwareContext | None = None + self._sequence: dict[str, int] = {} + + # Observation cache: avoids redundant hardware polls within one step. + self._obs_cache: dict[str, Any] | None = None + self._obs_cache_mono: float = 0.0 + + # Snapshot of last known observation for hold-position during writes. + self._last_obs: dict[str, Any] = {} + + # ------------------------------------------------------------------ + # HardwareTransport (6 methods) + # ------------------------------------------------------------------ + + async def open(self, context: HardwareContext) -> TransportStatus: + """Create the robot via factory, call connect(), populate channel map.""" + try: + self._robot = self._factory() + await asyncio.to_thread(self._robot.connect) + except Exception as exc: + self._connected = False + raise TransportError( + f"failed to connect robot arm: {exc}", + failure_code="robot_arm_connect_failed", + ) from exc + + self._context = context + self._connected = True + # Build channel lookup table from the declared context. + self._channels = {ch.channel_id: ch for ch in context.channels} + self._obs_cache = None + self._obs_cache_mono = 0.0 + logger.info( + "Robot arm transport opened: %d channels declared", + len(self._channels), + ) + return await self.probe() + + async def close(self) -> TransportStatus: + """Disconnect the robot. Idempotent, never raises.""" + if self._robot is not None: + try: + await asyncio.to_thread(self._robot.disconnect) + except Exception: + logger.debug("Robot arm disconnect raised; suppressed", exc_info=True) + self._connected = False + self._robot = None + self._obs_cache = None + self._last_obs.clear() + return TransportStatus( + connected=False, + halt_supported=True, + detail="robot arm transport closed", + ) + + async def read(self, channel_id: str) -> Reading: + """Read one channel by calling get_observation() and extracting the value. + + For motor channels: extract position/velocity from observation dict. + For camera channels: raise TransportError (use read_frame instead). + """ + self._require_open(channel_id) + ch = self._channels.get(channel_id) + if ch is None: + raise TransportError( + f"unknown channel {channel_id!r}", + failure_code="unknown_channel", + ) + if ch.representation == Representation.FRAME.value: + raise TransportError( + f"channel {channel_id!r} is a frame channel; use read_frame()", + failure_code="frame_channel_not_readable", + ) + obs = await self._get_observation() + value = self._extract_value(channel_id, obs) + seq = self._next_seq(channel_id) + return Reading( + device_id=self._context.device_id if self._context else "", + channel_id=channel_id, + value=value, + quantity=ch.quantity, + unit=ch.unit, + sequence=seq, + quality=Quality.OK.value, + ) + + async def write(self, channel_id: str, value: Any) -> WriteOutcome: + """Write one channel by building an action vector and calling send_action(). + + Only ACTUATE channels (joint positions, gripper) are writable. + The action vector is constructed with the current observation for + non-targeted channels (hold position) and the commanded value for + the target channel. + """ + self._require_open(channel_id) + ch = self._channels.get(channel_id) + if ch is None: + raise TransportError( + f"unknown channel {channel_id!r}", + failure_code="unknown_channel", + ) + if not ch.is_writable: + raise TransportError( + f"channel {channel_id!r} is read-only", + failure_code="channel_read_only", + ) + # Build command dict for a single channel write. + commands = {channel_id: value} + # Refresh observation so hold-position uses current joint values, + # not stale or empty data. + try: + await self._get_observation() + except Exception: + logger.debug( + "Observation refresh before write failed; " + "hold-position may use stale data", + exc_info=True, + ) + try: + action = self._build_action(commands) + await asyncio.to_thread(self._robot.send_action, action) + except TransportError: + raise + except Exception as exc: + # The action may have partially reached the device. + return WriteOutcome( + ok=False, + side_effect_state=SIDE_EFFECT_UNKNOWN, + error=f"send_action failed: {exc}", + failure_code="robot_arm_send_action_failed", + ) + + # Invalidate observation cache so the next read picks up the new state. + self._obs_cache = None + + # Readback if the channel requests verification. + readback: Reading | None = None + if ch.verify_after_write: + try: + readback = await self.read(channel_id) + except Exception: + logger.debug("Readback after write failed for %s", channel_id, exc_info=True) + + return WriteOutcome( + ok=True, + side_effect_state=SIDE_EFFECT_COMMITTED, + readback=readback, + settled=ch.envelope.settling_time_s <= 0.0, + ) + + async def probe(self) -> TransportStatus: + """Check robot connection health. Non-blocking, returns from cached state.""" + connected = self._connected and self._robot is not None + return TransportStatus( + connected=connected, + halt_supported=True, + detail="robot arm transport" if connected else "robot arm disconnected", + metadata={ + "channels": len(self._channels), + "robot_type": type(self._robot).__name__ if self._robot else "none", + }, + ) + + async def halt(self) -> TransportStatus: + """Emergency stop: send zero velocity to all actuators. + + Lock-free (as required by HCP). Constructs a zero-velocity action + and sends it immediately without acquiring any lock. + """ + if not self._connected or self._robot is None: + return TransportStatus( + connected=False, + halt_supported=True, + detail="halt: not connected", + ) + try: + zero_commands: dict[str, Any] = {} + for cid, ch in self._channels.items(): + if ch.effect in HardwareEffect.writable(): + # Send zero / current position to stop all motion. + zero_commands[cid] = 0.0 + if zero_commands: + action = self._build_action(zero_commands, hold_position=False) + # Dispatch to thread but do NOT hold any lock. + await asyncio.to_thread(self._robot.send_action, action) + except Exception as exc: + logger.warning("Robot arm halt encountered error: %s", exc, exc_info=True) + return TransportStatus( + connected=self._connected, + halt_supported=True, + detail=f"halt attempted with error: {exc}", + ) + return TransportStatus( + connected=self._connected, + halt_supported=True, + detail="halted", + ) + + # ------------------------------------------------------------------ + # BatchTransport + # ------------------------------------------------------------------ + + async def read_batch(self, channel_ids: tuple[str, ...]) -> BatchReading: + """Read multiple channels in one get_observation() call. + + This is the natural mode for robot arms: one call returns all values. + Much more efficient than N sequential read() calls. + """ + self._require_open("batch_read") + obs = await self._get_observation() + now_wall = time.time() + now_mono = time.monotonic() + readings: list[Reading] = [] + device_id = self._context.device_id if self._context else "" + for cid in channel_ids: + ch = self._channels.get(cid) + if ch is None: + raise TransportError( + f"unknown channel {cid!r} in batch read", + failure_code="unknown_channel", + ) + if ch.representation == Representation.FRAME.value: + raise TransportError( + f"frame channel {cid!r} cannot appear in read_batch; " + "use read_frame() instead", + failure_code="frame_channel_not_readable", + ) + value = self._extract_value(cid, obs) + seq = self._next_seq(cid) + readings.append( + Reading( + device_id=device_id, + channel_id=cid, + value=value, + quantity=ch.quantity, + unit=ch.unit, + observed_at=now_wall, + monotonic_at=now_mono, + sequence=seq, + quality=Quality.OK.value, + ) + ) + return BatchReading( + device_id=device_id, + readings=tuple(readings), + observed_at=now_wall, + monotonic_at=now_mono, + ) + + async def write_batch( + self, commands: tuple[tuple[str, Any], ...] + ) -> BatchWriteOutcome: + """Write multiple channels in one send_action() call. + + Builds the full action vector from commands, filling non-commanded + channels with their current values (hold position). + """ + self._require_open("batch_write") + # Validate all channels first. + cmd_dict: dict[str, Any] = {} + for cid, val in commands: + ch = self._channels.get(cid) + if ch is None: + raise TransportError( + f"unknown channel {cid!r} in batch write", + failure_code="unknown_channel", + ) + if not ch.is_writable: + raise TransportError( + f"channel {cid!r} is read-only", + failure_code="channel_read_only", + ) + cmd_dict[cid] = val + + # Refresh observation so hold-position uses current joint values, + # not stale or empty data. + try: + await self._get_observation() + except Exception: + logger.debug( + "Observation refresh before batch write failed; " + "hold-position may use stale data", + exc_info=True, + ) + try: + action = self._build_action(cmd_dict) + await asyncio.to_thread(self._robot.send_action, action) + except TransportError: + raise + except Exception as exc: + outcomes = tuple( + WriteOutcome( + ok=False, + side_effect_state=SIDE_EFFECT_UNKNOWN, + error=f"send_action failed: {exc}", + failure_code="robot_arm_send_action_failed", + ) + for _ in commands + ) + return BatchWriteOutcome( + ok=False, + outcomes=outcomes, + side_effect_state=SIDE_EFFECT_UNKNOWN, + ) + + # Invalidate cache. + self._obs_cache = None + + outcomes = tuple( + WriteOutcome( + ok=True, + side_effect_state=SIDE_EFFECT_COMMITTED, + settled=True, + ) + for _ in commands + ) + return BatchWriteOutcome( + ok=True, + outcomes=outcomes, + side_effect_state=SIDE_EFFECT_COMMITTED, + ) + + # ------------------------------------------------------------------ + # FrameTransport + # ------------------------------------------------------------------ + + async def read_frame( + self, + channel_id: str, + *, + max_width: int = 0, + quality: int = 0, + fps: float = 0.0, + ) -> FrameReading: + """Capture one frame from a camera channel. + + Calls get_observation() and extracts the image for the requested + camera, encoding it as JPEG. + """ + self._require_open(channel_id) + ch = self._channels.get(channel_id) + if ch is None: + raise TransportError( + f"unknown channel {channel_id!r}", + failure_code="unknown_channel", + ) + if ch.representation != Representation.FRAME.value: + raise TransportError( + f"channel {channel_id!r} is not a frame channel", + failure_code="not_frame_channel", + ) + obs = await self._get_observation() + image_data = self._extract_frame(channel_id, obs) + jpeg_bytes, width, height = self._encode_jpeg( + image_data, max_width=max_width, quality=quality, + ) + seq = self._next_seq(channel_id) + return FrameReading( + device_id=self._context.device_id if self._context else "", + channel_id=channel_id, + data=jpeg_bytes, + media_type="image/jpeg", + width=width, + height=height, + sequence=seq, + quality=Quality.OK.value, + ) + + # ------------------------------------------------------------------ + # Internal helpers + # ------------------------------------------------------------------ + + async def _get_observation(self) -> dict[str, Any]: + """Thread-safe observation retrieval with caching. + + Cache the observation for a short window (5 ms) so that multiple + reads within one control step don't each trigger a hardware poll. + """ + now = time.monotonic() + if self._obs_cache is not None and (now - self._obs_cache_mono) < _OBS_CACHE_TTL_S: + return self._obs_cache + + if self._robot is None: + raise TransportError( + "robot instance is None", + failure_code="transport_not_open", + ) + obs = await asyncio.to_thread(self._robot.get_observation) + if not isinstance(obs, dict): + # Robot returns a dict; convert if needed. + obs = dict(obs) if hasattr(obs, "items") else {} + self._obs_cache = obs + self._obs_cache_mono = time.monotonic() + # Keep a snapshot for hold-position writes. + self._last_obs.update(obs) + return obs + + def _build_action( + self, + commands: dict[str, Any], + *, + hold_position: bool = True, + ) -> Any: + """Build a robot action tensor from channel commands. + + Non-commanded channels hold their current observed position when + *hold_position* is True. When False (halt), zeros are used. + """ + try: + import torch + except ImportError: + torch = None # type: ignore[assignment] + + # Collect all writable channels that map to action features. + action_values: list[float] = [] + action_keys: list[str] = [] + + for cid, ch in self._channels.items(): + if not ch.is_writable: + continue + # Only scalar/state channels participate in the action vector; + # frame channels are never written. + if ch.representation == Representation.FRAME.value: + continue + action_keys.append(cid) + if cid in commands: + action_values.append(float(commands[cid])) + elif hold_position: + # Use the feature key that maps to this channel. + obs_val = self._resolve_obs_value(cid, self._last_obs) if self._last_obs else None + if obs_val is not None: + action_values.append(float(obs_val)) + else: + raise TransportError( + f"Cannot hold position for channel {cid!r}: no observation " + "available. Read the device at least once before writing.", + failure_code="hold_position_no_observation", + ) + else: + action_values.append(0.0) + + # Return as a torch tensor if available (expected format for many + # robot SDKs), otherwise as a plain list. + if torch is not None: + return torch.tensor(action_values, dtype=torch.float32) + return action_values + + def _extract_value(self, channel_id: str, obs: dict[str, Any]) -> Any: + """Extract a scalar value for *channel_id* from the observation dict. + + Tries multiple key strategies: + 1. The channel_id itself (if the user named observation keys directly). + 2. The feature name derived from the channel_id (e.g. "joint.X.position" -> "X.position"). + 3. Partial matching against observation keys. + """ + # Strategy 1: direct key. + if channel_id in obs: + return _scalar(obs[channel_id]) + + # Strategy 2: derive feature name from HCP channel_id. + feature_key = self._channel_to_feature_key(channel_id) + if feature_key and feature_key in obs: + return _scalar(obs[feature_key]) + + # Strategy 3: partial match -- find any obs key that contains the + # base name of this channel. + base = _channel_base_name(channel_id) + suffix = _channel_suffix(channel_id) + for key in obs: + if base in key and (not suffix or suffix in key): + return _scalar(obs[key]) + + # No match found -- return None rather than fabricating a value. + logger.debug( + "No observation key found for channel %r (obs keys: %s)", + channel_id, list(obs.keys()), + ) + return None + + def _extract_frame(self, channel_id: str, obs: dict[str, Any]) -> Any: + """Extract the raw image tensor/array for a camera channel.""" + # Derive the camera feature name. + # channel_id is "camera.{name}", feature key is typically "{name}" or + # "observation.images.{name}" in some robot SDKs. + camera_name = channel_id.removeprefix("camera.") + for candidate in ( + channel_id, + camera_name, + f"observation.images.{camera_name}", + f"images.{camera_name}", + ): + if candidate in obs: + return obs[candidate] + # Fallback: search for any key containing the camera name and "image"/"cam". + for key, val in obs.items(): + if camera_name in key: + return val + raise TransportError( + f"no image data found for camera channel {channel_id!r}", + failure_code="camera_data_missing", + ) + + def _resolve_obs_value(self, channel_id: str, obs: dict[str, Any]) -> Any: + """Resolve a channel's value from the observation, for hold-position.""" + return self._extract_value(channel_id, obs) + + @staticmethod + def _channel_to_feature_key(channel_id: str) -> str: + """Convert an HCP channel_id to a probable robot feature key. + + ``"joint.shoulder_pan.position"`` -> ``"shoulder_pan.position"`` + ``"gripper.left"`` -> ``"left"`` + ``"camera.top"`` -> ``"top"`` + """ + parts = channel_id.split(".", 1) + if len(parts) == 2 and parts[0] in ("joint", "gripper", "camera"): + return parts[1] + return channel_id + + @staticmethod + def _encode_jpeg( + image_data: Any, + *, + max_width: int = 0, + quality: int = 0, + ) -> tuple[bytes, int, int]: + """Encode an image tensor/array as JPEG bytes. + + Returns (jpeg_bytes, width, height). + """ + try: + import numpy as np + from PIL import Image + except ImportError as exc: + raise TransportError( + f"PIL/numpy required for frame encoding: {exc}", + failure_code="frame_encoding_unavailable", + ) from exc + + # Convert torch tensor to numpy if needed. + if hasattr(image_data, "cpu"): + arr = image_data.cpu().numpy() + elif isinstance(image_data, np.ndarray): + arr = image_data + else: + arr = np.asarray(image_data) + + # Handle CHW -> HWC conversion (common for torch tensors). + if arr.ndim == 3 and arr.shape[0] in (1, 3, 4) and arr.shape[2] not in (1, 3, 4): + arr = np.transpose(arr, (1, 2, 0)) + + # Normalize float [0, 1] to uint8 [0, 255]. + if arr.dtype in (np.float32, np.float64): + arr = (np.clip(arr, 0.0, 1.0) * 255).astype(np.uint8) + + img = Image.fromarray(arr) + + # Resize if requested. + if max_width > 0 and img.width > max_width: + ratio = max_width / img.width + new_height = max(1, int(img.height * ratio)) + img = img.resize((max_width, new_height), Image.LANCZOS) + + width, height = img.size + jpeg_quality = quality if quality > 0 else 85 + buf = io.BytesIO() + img.save(buf, format="JPEG", quality=jpeg_quality) + return buf.getvalue(), width, height + + def _require_open(self, channel_id: str) -> None: + """Raise TransportError when the transport is not connected.""" + if not self._connected or self._robot is None: + raise TransportError( + f"transport for {channel_id!r} is not open", + failure_code="transport_not_open", + ) + + def _next_seq(self, channel_id: str) -> int: + """Advance and return the sequence counter for a channel.""" + seq = self._sequence.get(channel_id, 0) + 1 + self._sequence[channel_id] = seq + return seq + + +# ---------------------------------------------------------------------- +# Module-level helpers +# ---------------------------------------------------------------------- + + +def _scalar(value: Any) -> Any: + """Extract a Python scalar from a tensor/array if possible.""" + if hasattr(value, "item"): + try: + return value.item() + except (ValueError, RuntimeError): + pass + return value + + +def _channel_base_name(channel_id: str) -> str: + """Extract the base joint/device name from a channel_id. + + ``"joint.shoulder_pan.position"`` -> ``"shoulder_pan"`` + ``"gripper.left"`` -> ``"left"`` + """ + parts = channel_id.split(".") + if len(parts) >= 3 and parts[0] == "joint": + return parts[1] + if len(parts) >= 2 and parts[0] in ("gripper", "camera"): + return parts[1] + return channel_id + + +def _channel_suffix(channel_id: str) -> str: + """Extract the suffix (position/velocity) from a channel_id, or empty.""" + parts = channel_id.split(".") + if len(parts) >= 3: + return parts[-1] + return "" + + +# ---------------------------------------------------------------------- +# Factory +# ---------------------------------------------------------------------- + + +def build_transport(config: Mapping[str, Any]) -> RobotArmTransport: + """Factory function for the transport registry. + + Config keys: + - robot_type (str): Robot type name (e.g. "koch_v1_1", "so100_follower") + - robot_factory (callable): Optional factory callable for robot instantiation + - robot_config (dict): Additional robot configuration passed to the robot constructor + - default_sample_rate_hz (float): Default sampling rate for channel mapper + - default_camera_fps (float): Default camera FPS for channel mapper + + The robot is NOT instantiated here -- only a factory callable is prepared. + Actual instantiation happens in open(). + """ + robot_type = str(config.get("robot_type", "")) + robot_config = dict(config.get("robot_config", {})) if isinstance(config.get("robot_config"), Mapping) else {} + + # Compat: aggregate top-level serial_port/cameras into robot_config so + # configs written before the provider placed them inside robot_config + # still reach the robot constructor. + serial_port = config.get("serial_port") + if serial_port is not None: + robot_config.setdefault("serial_port", str(serial_port)) + cameras = config.get("cameras") + if isinstance(cameras, Mapping): + robot_config.setdefault("cameras", dict(cameras)) + + default_sample_rate = float( + config.get("default_sample_rate_hz", config.get("sample_rate_hz", 0.0)) or 0.0 + ) + default_camera_fps = float(config.get("default_camera_fps", 30.0) or 30.0) + + def _factory() -> Any: + """Lazy-import robot SDK and construct the robot.""" + try: + from leapflow.robot.base import make_robot # type: ignore[import-untyped] + except ImportError as exc: + raise TransportError( + "robot arm SDK is not installed; " + "install it with: pip install leapflow[robot]", + failure_code="robot_arm_not_installed", + ) from exc + kwargs: dict[str, Any] = {"robot_type": robot_type} + if robot_config: + kwargs.update(robot_config) + return make_robot(**kwargs) + + mapper = RobotChannelMapper( + default_sample_rate_hz=default_sample_rate, + default_camera_fps=default_camera_fps, + ) + return RobotArmTransport(robot_factory=_factory, channel_mapper=mapper) + + +__all__ = ["RobotArmTransport", "build_transport"] diff --git a/src/leapflow/hardware/transports/robot_channels.py b/src/leapflow/hardware/transports/robot_channels.py new file mode 100644 index 00000000..98338003 --- /dev/null +++ b/src/leapflow/hardware/transports/robot_channels.py @@ -0,0 +1,449 @@ +# Copyright (c) Alibaba, Inc. and its affiliates. +# Adapted from LeRobot (https://github.com/huggingface/lerobot). +"""Maps robot features to HCP channel declarations. + +Robot arms describe their observation and action spaces as typed feature +dictionaries. This module translates those declarations into HCP ``Channel`` +instances so the rest of the hardware stack -- registry admission, streaming, +tools, trust, audit -- works without knowing anything about the specific +robot SDK. + +This module has no hard dependencies on any specific robot SDK. Nothing in +this file imports external robot packages at module level; the ``map_robot`` +convenience method inspects a live robot instance through duck-typed attribute +access so that the mapper remains usable in environments where no robot SDK +is installed. +""" + +from __future__ import annotations + +import logging +import re +from typing import Any + +from leapflow.hardware.context import ( + Channel, + Direction, + Envelope, + HardwareEffect, + PrivacyTier, + Representation, +) + +logger = logging.getLogger(__name__) + +# --------------------------------------------------------------------------- +# Heuristic patterns for feature classification +# --------------------------------------------------------------------------- + +_CAMERA_PATTERNS: tuple[re.Pattern[str], ...] = ( + re.compile(r"(?:^|[._])cam(?:era)?(?:[._]|$)", re.IGNORECASE), + re.compile(r"(?:^|[._])image(?:[._]|$)", re.IGNORECASE), + re.compile(r"(?:^|[._])rgb(?:[._]|$)", re.IGNORECASE), + re.compile(r"(?:^|[._])depth(?:[._]|$)", re.IGNORECASE), +) + +_GRIPPER_PATTERNS: tuple[re.Pattern[str], ...] = ( + re.compile(r"(?:^|[._])grip(?:per)?(?:[._]|$)", re.IGNORECASE), + re.compile(r"(?:^|[._])jaw(?:s)?(?:[._]|$)", re.IGNORECASE), + re.compile(r"(?:^|[._])tool(?:_?center)?(?:[._]|$)", re.IGNORECASE), +) + +_VELOCITY_SUFFIX = re.compile(r"[._]velocity$", re.IGNORECASE) +_POSITION_SUFFIX = re.compile(r"[._]position$", re.IGNORECASE) + +# Feature shapes with 3+ dimensions are likely image tensors. +_MIN_IMAGE_DIMS = 3 + + +# --------------------------------------------------------------------------- +# Public helpers -- one channel factory per feature type +# --------------------------------------------------------------------------- + + +def infer_channel_type(feature_name: str, feature_shape: Any) -> str: + """Infer whether a feature is motor/camera/gripper/other. + + Classification relies on the feature name and, when available, the + dimensionality of its shape. A shape with three or more dimensions + (height x width x channels) is classified as camera regardless of name + so that unnamed image observations are not lost. + + Returns one of ``"motor"``, ``"camera"``, ``"gripper"``, or ``"other"``. + """ + name_lower = feature_name.lower() + + # Camera: name match or high-dimensional shape (image tensor). + if any(p.search(feature_name) for p in _CAMERA_PATTERNS): + return "camera" + if _is_image_shape(feature_shape): + return "camera" + + # Gripper / binary state. + if any(p.search(feature_name) for p in _GRIPPER_PATTERNS): + return "gripper" + + # Motor: anything with a position/velocity suffix, or a 1-D scalar + # that is neither camera nor gripper. + if _VELOCITY_SUFFIX.search(name_lower) or _POSITION_SUFFIX.search(name_lower): + return "motor" + + # Fallback: scalar features are assumed to be motor-like joints when + # they appear in the action space. The caller may override this. + if _is_scalar_shape(feature_shape): + return "motor" + + return "other" + + +def motor_channel_pair( + name: str, + *, + limits: tuple[float, float] | None = None, + sample_rate_hz: float = 0.0, +) -> tuple[Channel, Channel]: + """Return (position_channel, velocity_channel) for one motor. + + *name* is the bare joint name (e.g. ``"shoulder_pan"``), not the full + feature key. The returned channels carry LLM-readable descriptions so + the agent can reason about the joint without external documentation. + """ + pos_envelope = Envelope( + declared=True, + min_value=limits[0] if limits else None, + max_value=limits[1] if limits else None, + settling_time_s=0.1, + reversible=True, + notes=f"Angular position limits for joint '{name}'.", + ) + pos_channel = Channel( + channel_id=f"joint.{name}.position", + direction=Direction.READWRITE.value, + quantity="angular_position", + unit="rad", + effect=HardwareEffect.ACTUATE.value, + representation=Representation.SCALAR.value, + envelope=pos_envelope, + sample_rate_hz=sample_rate_hz, + verify_after_write=True, + description=( + f"Commanded angular position of the '{name}' joint in radians. " + f"Writing to this channel actuates the motor to the target angle." + ), + ) + vel_channel = Channel( + channel_id=f"joint.{name}.velocity", + direction=Direction.READ.value, + quantity="angular_velocity", + unit="rad/s", + effect=HardwareEffect.READ.value, + representation=Representation.SCALAR.value, + envelope=Envelope(declared=True, reversible=True), + sample_rate_hz=sample_rate_hz, + description=( + f"Current angular velocity of the '{name}' joint in radians per " + f"second. Read-only feedback from the motor encoder." + ), + ) + return pos_channel, vel_channel + + +def camera_channel( + name: str, + *, + fps: float = 30.0, + width: int = 0, + height: int = 0, +) -> Channel: + """Return one frame channel for a camera. + + The ``sample_rate_hz`` field carries the capture ceiling (frames per + second) so that downstream consumers respect the hardware limit. + """ + res_note = f" Resolution {width}x{height}." if width > 0 and height > 0 else "" + return Channel( + channel_id=f"camera.{name}", + direction=Direction.READ.value, + quantity="image", + unit="frame", + effect=HardwareEffect.READ.value, + representation=Representation.FRAME.value, + privacy=PrivacyTier.ENVIRONMENT.value, + envelope=Envelope(declared=True), + sample_rate_hz=fps, + media_type="image/jpeg", + description=( + f"RGB image stream from the '{name}' camera at up to {fps:.0f} " + f"fps.{res_note} Reading this channel observes the physical " + f"environment." + ), + ) + + +def gripper_channel( + name: str, + *, + limits: tuple[float, float] | None = None, +) -> Channel: + """Return one state channel for a gripper. + + When *limits* are not provided the envelope defaults to a binary + open/close model with allowed values (0, 1). + """ + if limits is not None: + envelope = Envelope( + declared=True, + min_value=limits[0], + max_value=limits[1], + reversible=True, + notes=f"Gripper '{name}' continuous range.", + ) + else: + envelope = Envelope( + declared=True, + allowed_values=(0, 1), + reversible=True, + notes=f"Gripper '{name}' binary open/close.", + ) + return Channel( + channel_id=f"gripper.{name}", + direction=Direction.READWRITE.value, + quantity="gripper_state", + unit="", + effect=HardwareEffect.ACTUATE.value, + representation=Representation.STATE.value, + envelope=envelope, + verify_after_write=True, + description=( + f"State of the '{name}' gripper. Writing actuates the gripper; " + f"reading returns its current position or open/close state." + ), + ) + + +# --------------------------------------------------------------------------- +# Main mapper class +# --------------------------------------------------------------------------- + + +class RobotChannelMapper: + """Extracts HCP channel declarations from robot arm configuration. + + Mapping rules + ------------- + - Motor position feature -> 1 READWRITE channel (SCALAR, ACTUATE) + ``channel_id = "joint.{name}.position"`` + quantity = ``"angular_position"``, unit = ``"rad"`` + Envelope: declared=True, min/max from motor limits if available, + settling_time_s=0.1, reversible=True + + - Motor velocity feature -> 1 READ channel (SCALAR, READ) + ``channel_id = "joint.{name}.velocity"`` + quantity = ``"angular_velocity"``, unit = ``"rad/s"`` + + - Camera feature -> 1 READ channel (FRAME, READ) + ``channel_id = "camera.{name}"`` + representation = FRAME, privacy_tier = ENVIRONMENT + sample_rate_hz from camera config fps, or default 30 + + - Gripper / binary state -> 1 READWRITE channel (STATE, ACTUATE) + ``channel_id = "gripper.{name}"`` + Envelope: declared=True, allowed_values=(0, 1) or min/max + reversible=True + """ + + def __init__( + self, + *, + default_sample_rate_hz: float = 0.0, + default_camera_fps: float = 30.0, + ) -> None: + self._default_sample_rate = default_sample_rate_hz + self._default_camera_fps = default_camera_fps + + # ------------------------------------------------------------------ + # Public API + # ------------------------------------------------------------------ + + def map_features( + self, + observation_features: dict[str, Any], + action_features: dict[str, Any], + *, + motor_limits: dict[str, tuple[float, float]] | None = None, + camera_configs: dict[str, dict[str, Any]] | None = None, + ) -> tuple[Channel, ...]: + """Return HCP channels derived from robot feature dictionaries. + + Observation and action features may overlap (e.g. the same joint + appears in both). Duplicate channel ids are deduplicated, with + action features taking precedence because they carry write intent. + """ + limits = motor_limits or {} + cam_cfgs = camera_configs or {} + channels: dict[str, Channel] = {} + + # Pass 1: observation features (read-only by default). + for feat_name, feat_shape in observation_features.items(): + for ch in self._channels_for_feature( + feat_name, feat_shape, is_action=False, + motor_limits=limits, camera_configs=cam_cfgs, + ): + channels[ch.channel_id] = ch + + # Pass 2: action features override observations (promote to READWRITE). + for feat_name, feat_shape in action_features.items(): + for ch in self._channels_for_feature( + feat_name, feat_shape, is_action=True, + motor_limits=limits, camera_configs=cam_cfgs, + ): + channels[ch.channel_id] = ch + + return tuple(channels.values()) + + def map_robot(self, robot: Any) -> tuple[Channel, ...]: + """Convenience: extract features from a connected Robot instance. + + Works by duck-typed attribute access so no specific robot SDK need + be installed. Raises ``TypeError`` when the object does not look like + a robot with observation and action features. + """ + obs = getattr(robot, "observation_features", None) + act = getattr(robot, "action_features", None) + if obs is None or act is None: + raise TypeError( + f"{type(robot).__name__} does not expose observation_features " + "and action_features; expected a Robot instance" + ) + if not isinstance(obs, dict) or not isinstance(act, dict): + raise TypeError( + "observation_features and action_features must be dicts, " + f"got {type(obs).__name__} and {type(act).__name__}" + ) + + # Attempt to extract motor limits from the robot if available. + motor_limits: dict[str, tuple[float, float]] | None = None + raw_limits = getattr(robot, "motor_limits", None) + if isinstance(raw_limits, dict): + motor_limits = { + str(k): (float(v[0]), float(v[1])) + for k, v in raw_limits.items() + if isinstance(v, (list, tuple)) and len(v) >= 2 + } + + # Attempt to extract camera configs. + camera_configs: dict[str, dict[str, Any]] | None = None + raw_cams = getattr(robot, "camera_configs", None) + if isinstance(raw_cams, dict): + camera_configs = {str(k): dict(v) for k, v in raw_cams.items() if isinstance(v, dict)} + + return self.map_features( + obs, act, + motor_limits=motor_limits, + camera_configs=camera_configs, + ) + + # ------------------------------------------------------------------ + # Internal + # ------------------------------------------------------------------ + + def _channels_for_feature( + self, + feature_name: str, + feature_shape: Any, + *, + is_action: bool, + motor_limits: dict[str, tuple[float, float]], + camera_configs: dict[str, dict[str, Any]], + ) -> list[Channel]: + """Map a single feature to zero or more HCP channels.""" + kind = infer_channel_type(feature_name, feature_shape) + base_name = _strip_suffixes(feature_name) + + if kind == "camera": + cfg = camera_configs.get(base_name, {}) + fps = float(cfg.get("fps", self._default_camera_fps)) + width = int(cfg.get("width", 0)) + height = int(cfg.get("height", 0)) + return [camera_channel(base_name, fps=fps, width=width, height=height)] + + if kind == "gripper": + lim = motor_limits.get(base_name) + return [gripper_channel(base_name, limits=lim)] + + if kind == "motor": + lim = motor_limits.get(base_name) + pos_ch, vel_ch = motor_channel_pair( + base_name, + limits=lim, + sample_rate_hz=self._default_sample_rate, + ) + # Observation-only motors are demoted to read-only position. + if not is_action: + pos_ch = Channel( + channel_id=pos_ch.channel_id, + direction=Direction.READ.value, + quantity=pos_ch.quantity, + unit=pos_ch.unit, + effect=HardwareEffect.READ.value, + representation=pos_ch.representation, + envelope=pos_ch.envelope, + sample_rate_hz=pos_ch.sample_rate_hz, + description=pos_ch.description, + ) + return [pos_ch, vel_ch] + + # Unknown feature type -- log and skip rather than crash. + logger.debug( + "Skipping unrecognised robot feature %r (shape=%r)", + feature_name, feature_shape, + ) + return [] + + +# --------------------------------------------------------------------------- +# Private helpers +# --------------------------------------------------------------------------- + + +def _is_image_shape(shape: Any) -> bool: + """Return True when *shape* looks like an image tensor (>= 3 dims).""" + if isinstance(shape, (list, tuple)) and len(shape) >= _MIN_IMAGE_DIMS: + return True + # numpy / torch shapes expose __len__. + try: + return len(shape) >= _MIN_IMAGE_DIMS + except TypeError: + return False + + +def _is_scalar_shape(shape: Any) -> bool: + """Return True when *shape* describes a single scalar value.""" + if isinstance(shape, (list, tuple)): + return len(shape) == 0 or shape == (1,) or shape == [1] + try: + return len(shape) <= 1 + except TypeError: + # Bare int (e.g. shape=1) is scalar. + return isinstance(shape, int) and shape <= 1 + + +def _strip_suffixes(name: str) -> str: + """Remove common trailing qualifiers to derive a base joint/device name. + + ``"shoulder_pan.position"`` -> ``"shoulder_pan"`` + ``"left_gripper_velocity"`` -> ``"left_gripper"`` + """ + for suffix in (".position", ".velocity", "_position", "_velocity"): + if name.lower().endswith(suffix): + return name[: -len(suffix)] + return name + + +__all__ = [ + "RobotChannelMapper", + "camera_channel", + "gripper_channel", + "infer_channel_type", + "motor_channel_pair", +] diff --git a/src/leapflow/hardware/transports/ros2.py b/src/leapflow/hardware/transports/ros2.py new file mode 100644 index 00000000..9c41479c --- /dev/null +++ b/src/leapflow/hardware/transports/ros2.py @@ -0,0 +1,742 @@ +# Copyright (c) Alibaba, Inc. and its affiliates. +"""ROS2 transport: bridges ROS2 topics, services and actions to HCP. + +Maps the Robot Operating System 2 communication primitives to the +six-method HardwareTransport contract. Topic subscriptions become +read channels, topic publishers and service clients become write +channels, and action clients become long-running write operations. + +rclpy is an optional dependency. When unavailable, ``open()`` returns +``TransportStatus(connected=False)`` and no other method is callable. +The transport is discovered through the standard entry-point mechanism +and is available when ``pip install leapflow[robot-ros2]`` is installed. + +Channel-to-ROS2-primitive mapping is declared in the YAML config produced +by ``control_binding_resolver._resolve_ros2``:: + + topics: + joint_states: "/robot/joint_states" + joint_commands: "/robot/joint_commands" + services: + set_mode: "/robot/set_mode" + actions: + follow_trajectory: "/robot/follow_joint_trajectory" + node_name: "leapflow_bridge" + namespace: "" + qos_depth: 10 + spin_period_s: 0.01 + +There is deliberately no top-level ``import rclpy``. Every use is +function-local with a ``try/except ImportError``, so the module can be +imported, inspected, and rejected cleanly when rclpy is absent. +""" + +from __future__ import annotations + +import asyncio +import logging +import time +from typing import Any, Mapping + +from leapflow.hardware.context import HardwareContext, Quality +from leapflow.hardware.transport import ( + SIDE_EFFECT_COMMITTED, + SIDE_EFFECT_NONE, + SIDE_EFFECT_UNKNOWN, + Reading, + TransportError, + TransportStatus, + WriteOutcome, +) + +logger = logging.getLogger(__name__) + +# Staleness threshold: if the last message on a subscribed topic is older +# than this many seconds, ``read()`` reports Quality.STALE. +_STALE_THRESHOLD_S = 2.0 + + +class ROS2Transport: + """Six-method transport over ROS2 topics, services and actions. + + All rclpy interaction is deferred to ``open()`` and runs inside + ``asyncio.to_thread`` to avoid blocking the event loop. A background + spin task pumps callbacks so that subscriber messages arrive. + """ + + kind: str = "ros2" + + def __init__( + self, + topics: Mapping[str, str] | None = None, + services: Mapping[str, str] | None = None, + actions: Mapping[str, str] | None = None, + node_name: str = "leapflow_bridge", + namespace: str = "", + qos_depth: int = 10, + spin_period_s: float = 0.01, + ) -> None: + self._topics: dict[str, str] = dict(topics or {}) + self._services: dict[str, str] = dict(services or {}) + self._actions: dict[str, str] = dict(actions or {}) + self._node_name = node_name + self._namespace = namespace + self._qos_depth = qos_depth + self._spin_period_s = spin_period_s + + # ROS2 runtime state -- populated by open(). + self._node: Any = None + self._subscribers: dict[str, Any] = {} # channel_id -> Subscription + self._publishers: dict[str, Any] = {} # channel_id -> Publisher + self._service_clients: dict[str, Any] = {} # channel_id -> Client + self._latest_msgs: dict[str, Any] = {} # channel_id -> latest message + self._msg_timestamps: dict[str, float] = {} # channel_id -> monotonic + self._spin_task: asyncio.Task[None] | None = None + self._connected = False + self._context: HardwareContext | None = None + self._sequence: dict[str, int] = {} + # Set of channel_ids that map to command topics (for halt). + self._cmd_channels: set[str] = set() + + # ── Lifecycle ───────────────────────────────────────────────────── + + async def open(self, context: HardwareContext) -> TransportStatus: + """Initialize rclpy, create Node, set up subscriptions and publishers. + + Uses ``asyncio.to_thread`` for rclpy.init() and node creation. + Starts a background spin task for receiving messages. + + Channel mapping from *context*: + + - READ / READWRITE channels with a matching topic config → + subscriber (populates ``_latest_msgs`` for ``read()``). + - WRITE / READWRITE channels with a matching topic config → + publisher (used by ``write()`` and ``halt()``). + - Channels matching a service config → service client. + """ + self._context = context + + try: + rclpy = _require_rclpy() + except TransportError: + self._connected = False + return TransportStatus( + connected=False, + halt_supported=False, + detail="rclpy is not installed; install with: pip install leapflow[robot-ros2]", + ) + + # Init rclpy (idempotent -- rclpy.ok() checks first). + def _init_node() -> Any: + if not rclpy.ok(): + rclpy.init() + node = rclpy.create_node(self._node_name, namespace=self._namespace or None) + return node + + try: + self._node = await asyncio.to_thread(_init_node) + except Exception as exc: + self._connected = False + raise TransportError( + f"failed to create ROS2 node: {exc}", + failure_code="ros2_node_create_failed", + ) from exc + + # Wire up subscriptions, publishers and service clients. + self._setup_channels(context) + self._connected = True + + # Start background spin loop so subscriber callbacks fire. + loop = asyncio.get_running_loop() + self._spin_task = loop.create_task(self._spin_loop(), name="ros2-spin") + + logger.info( + "ROS2 transport opened: node=%s, %d subscribers, %d publishers, %d service clients", + self._node_name, + len(self._subscribers), + len(self._publishers), + len(self._service_clients), + ) + return await self.probe() + + async def close(self) -> TransportStatus: + """Destroy Node and cancel spin task. Idempotent, never raises.""" + # Cancel the spin task first so no callbacks fire during teardown. + if self._spin_task is not None: + self._spin_task.cancel() + try: + await self._spin_task + except (asyncio.CancelledError, Exception): + pass + self._spin_task = None + + node = self._node + if node is not None: + try: + await asyncio.to_thread(node.destroy_node) + except Exception: + logger.debug("ROS2 node destroy raised; suppressed", exc_info=True) + + self._node = None + self._subscribers.clear() + self._publishers.clear() + self._service_clients.clear() + self._latest_msgs.clear() + self._msg_timestamps.clear() + self._cmd_channels.clear() + self._connected = False + + return TransportStatus( + connected=False, + halt_supported=False, + detail="ros2 transport closed", + ) + + # ── Data plane ──────────────────────────────────────────────────── + + async def read(self, channel_id: str) -> Reading: + """Return the latest cached message for the subscribed topic. + + Messages are received by the background spin task and cached in + ``_latest_msgs``. ``read()`` returns the most recent value + without blocking. Quality is STALE if no message has been + received within ``_STALE_THRESHOLD_S``. + """ + self._require_open(channel_id) + if channel_id not in self._subscribers: + raise TransportError( + f"channel {channel_id!r} has no ROS2 subscription", + failure_code="ros2_no_subscription", + ) + + raw_msg = self._latest_msgs.get(channel_id) + if raw_msg is None: + # No message received yet -- report as stale with None value. + return Reading( + device_id=self._device_id, + channel_id=channel_id, + value=None, + sequence=self._next_seq(channel_id), + quality=Quality.STALE.value, + ) + + # Determine staleness from monotonic timestamp. + ts = self._msg_timestamps.get(channel_id, 0.0) + elapsed = time.monotonic() - ts + quality = Quality.OK.value if elapsed < _STALE_THRESHOLD_S else Quality.STALE.value + + value = self._parse_msg(channel_id, raw_msg) + ch = self._context.channel(channel_id) if self._context else None + + return Reading( + device_id=self._device_id, + channel_id=channel_id, + value=value, + quantity=ch.quantity if ch else "", + unit=ch.unit if ch else "", + sequence=self._next_seq(channel_id), + quality=quality, + ) + + async def write(self, channel_id: str, value: Any) -> WriteOutcome: + """Publish to a topic or call a service. + + For topics: publish message, ``side_effect=COMMITTED`` + (fire-and-forget, ROS2 pub is non-blocking). + + For services: call and wait for response, + ``side_effect`` based on the service response. + """ + self._require_open(channel_id) + + # Service call path. + if channel_id in self._service_clients: + return await self._call_service(channel_id, value) + + # Topic publish path. + pub = self._publishers.get(channel_id) + if pub is None: + raise TransportError( + f"channel {channel_id!r} has no ROS2 publisher or service client", + failure_code="ros2_no_publisher", + ) + + try: + msg = self._build_write_msg(channel_id, value, pub) + await asyncio.to_thread(pub.publish, msg) + except TransportError: + raise + except Exception as exc: + return WriteOutcome( + ok=False, + side_effect_state=SIDE_EFFECT_UNKNOWN, + error=f"ROS2 publish failed: {exc}", + failure_code="ros2_publish_failed", + ) + + return WriteOutcome( + ok=True, + side_effect_state=SIDE_EFFECT_COMMITTED, + settled=True, + ) + + async def probe(self) -> TransportStatus: + """Check Node health and topic availability.""" + if not self._connected or self._node is None: + return TransportStatus( + connected=False, + halt_supported=False, + detail="ros2 transport not connected", + ) + + # Check that the node is still alive. + try: + # rclpy nodes expose a handle that is None after destruction. + alive = self._node.handle is not None + except Exception: + alive = self._connected + + has_cmd = len(self._cmd_channels) > 0 + return TransportStatus( + connected=alive, + halt_supported=has_cmd, + detail=f"ros2 node {self._node_name!r}", + latency_ms=self._spin_period_s * 1000.0, + metadata={ + "node_name": self._node_name, + "namespace": self._namespace, + "subscribers": len(self._subscribers), + "publishers": len(self._publishers), + "service_clients": len(self._service_clients), + }, + ) + + async def halt(self) -> TransportStatus: + """Publish zero-velocity commands to all command topics. + + Lock-free (HCP requirement). Iterates ``_cmd_channels`` and + publishes a zeroed message to each without acquiring any lock. + """ + if not self._connected or self._node is None: + return TransportStatus( + connected=False, + halt_supported=False, + detail="halt: not connected", + ) + + if not self._cmd_channels: + return TransportStatus( + connected=True, + halt_supported=False, + detail="halt: no command topics registered", + ) + + errors: list[str] = [] + for cid in self._cmd_channels: + pub = self._publishers.get(cid) + if pub is None: + continue + try: + msg = self._build_zero_msg(pub) + # Publish in thread -- lock-free, no shared state modified. + await asyncio.to_thread(pub.publish, msg) + except Exception as exc: + errors.append(f"{cid}: {exc}") + + detail = "halted" if not errors else f"halt partial: {'; '.join(errors)}" + return TransportStatus( + connected=self._connected, + halt_supported=True, + detail=detail, + ) + + # ── Internal: spin loop ─────────────────────────────────────────── + + async def _spin_loop(self) -> None: + """Background loop: ``rclpy.spin_once`` in a thread. + + Runs until cancelled. Each iteration spins the node once with a + bounded timeout so that cancellation is responsive. + """ + rclpy = _require_rclpy() + timeout = self._spin_period_s + while self._connected and self._node is not None: + try: + await asyncio.to_thread(rclpy.spin_once, self._node, timeout_sec=timeout) + except asyncio.CancelledError: + return + except Exception: + # spin_once may raise if the node was destroyed concurrently. + logger.debug("ROS2 spin_once raised; will retry", exc_info=True) + await asyncio.sleep(timeout) + + # ── Internal: channel setup ─────────────────────────────────────── + + def _setup_channels(self, context: HardwareContext) -> None: + """Create ROS2 subscriptions, publishers and service clients. + + Matches declared channels against the topic/service/action config + maps using channel_id as the logical key. + """ + from functools import partial + + rclpy_qos = self._make_qos() + + for ch in context.channels: + cid = ch.channel_id + + # Topic subscription for readable channels. + topic = self._topics.get(cid) + if topic and ch.is_readable: + msg_type = self._resolve_topic_type(topic) + if msg_type is not None: + sub = self._node.create_subscription( + msg_type, + topic, + partial(self._msg_callback, cid), + qos_profile=rclpy_qos, + ) + self._subscribers[cid] = sub + logger.debug("ROS2 subscriber: %s -> %s", cid, topic) + + # Topic publisher for writable channels. + if topic and ch.is_writable: + msg_type = self._resolve_topic_type(topic) + if msg_type is not None: + pub = self._node.create_publisher(msg_type, topic, qos_profile=rclpy_qos) + self._publishers[cid] = pub + self._cmd_channels.add(cid) + logger.debug("ROS2 publisher: %s -> %s", cid, topic) + + # Service client. + svc_name = self._services.get(cid) + if svc_name and ch.is_writable: + srv_type = self._resolve_service_type(svc_name) + if srv_type is not None: + cli = self._node.create_client(srv_type, svc_name) + self._service_clients[cid] = cli + logger.debug("ROS2 service client: %s -> %s", cid, svc_name) + + def _make_qos(self) -> Any: + """Build a QoS profile from the configured depth.""" + try: + from rclpy.qos import QoSProfile, ReliabilityPolicy + return QoSProfile( + depth=self._qos_depth, + reliability=ReliabilityPolicy.BEST_EFFORT, + ) + except ImportError: + return self._qos_depth # fallback: an int works as depth-only + + def _resolve_topic_type(self, topic_name: str) -> Any: + """Discover the message type for a topic at runtime. + + Uses ``rclpy`` utilities to query the ROS2 graph for the topic's + type. Falls back to ``std_msgs/msg/String`` when the topic is + not yet advertised (the subscriber will still receive messages + once a publisher appears). + """ + node = self._node + if node is None: + return None + try: + # get_topic_names_and_types returns [(name, [type_strings])]. + topic_types = node.get_topic_names_and_types() + for name, types in topic_types: + if name == topic_name and types: + return _import_msg_type(types[0]) + except Exception: + logger.debug( + "Could not discover type for topic %r; using String", + topic_name, exc_info=True, + ) + + # Fallback: use std_msgs/msg/String as a generic carrier. + return _import_msg_type("std_msgs/msg/String") + + def _resolve_service_type(self, service_name: str) -> Any: + """Discover the service type for a service at runtime. + + Uses ``rclpy`` utilities to query the ROS2 graph. Returns None + when the service type cannot be determined. + """ + node = self._node + if node is None: + return None + try: + svc_types = node.get_service_names_and_types() + for name, types in svc_types: + if name == service_name and types: + return _import_msg_type(types[0]) + except Exception: + logger.debug( + "Could not discover type for service %r", + service_name, exc_info=True, + ) + return None + + # ── Internal: message handling ──────────────────────────────────── + + def _msg_callback(self, channel_id: str, msg: Any) -> None: + """Cache incoming message for ``read()``.""" + self._latest_msgs[channel_id] = msg + self._msg_timestamps[channel_id] = time.monotonic() + + def _parse_msg(self, channel_id: str, msg: Any) -> Any: + """Extract a Python-friendly value from a ROS2 message. + + Tries several strategies: + 1. ``sensor_msgs/JointState`` → dict of joint positions. + 2. Messages with a ``data`` attribute → the data value. + 3. Fall back to converting the message to a dict via ``get_fields_and_field_types``. + """ + # Strategy 1: JointState-like messages. + if hasattr(msg, "position") and hasattr(msg, "name"): + return self._parse_joint_state_msg(msg) + + # Strategy 2: simple data field (Float64, String, Bool, etc.). + if hasattr(msg, "data"): + return msg.data + + # Strategy 3: generic conversion to dict. + return _msg_to_dict(msg) + + def _parse_joint_state_msg(self, msg: Any) -> dict[str, float]: + """Extract joint positions/velocities from a JointState message.""" + result: dict[str, float] = {} + names = list(msg.name) if hasattr(msg, "name") else [] + positions = list(msg.position) if hasattr(msg, "position") else [] + velocities = list(msg.velocity) if hasattr(msg, "velocity") else [] + + for i, name in enumerate(names): + if i < len(positions): + result[f"{name}.position"] = float(positions[i]) + if i < len(velocities): + result[f"{name}.velocity"] = float(velocities[i]) + + return result + + def _build_write_msg(self, channel_id: str, value: Any, publisher: Any) -> Any: + """Build a ROS2 message from a channel value for publishing. + + Uses the publisher's message type. For simple types (float, int, + str, bool) wraps the value in a ``data`` field. For dicts, + attempts to set message fields from dict keys. + """ + msg_type = publisher.msg_type + msg = msg_type() + + if isinstance(value, Mapping): + # Set fields from dict. + for k, v in value.items(): + if hasattr(msg, k): + setattr(msg, k, v) + elif hasattr(msg, "data"): + msg.data = value + else: + # Attempt to set the first field. + fields = _msg_fields(msg) + if fields: + setattr(msg, fields[0], value) + + return msg + + def _build_zero_msg(self, publisher: Any) -> Any: + """Build a zero-valued message for halt. + + Creates a default-constructed message, which for numeric types + (Float64, Twist, JointTrajectory) produces zeros. + """ + return publisher.msg_type() + + def _build_joint_command_msg(self, value: Any) -> Any: + """Build a JointTrajectoryPoint or similar from a channel value. + + Used for trajectory-style command topics. *value* may be a list + of joint positions or a dict mapping joint names to positions. + """ + try: + from trajectory_msgs.msg import JointTrajectoryPoint + except ImportError: + return value # Cannot build typed message without ROS2 msg package. + + point = JointTrajectoryPoint() + if isinstance(value, (list, tuple)): + point.positions = [float(v) for v in value] + elif isinstance(value, Mapping): + point.positions = [float(v) for v in value.values()] + return point + + # ── Internal: service calls ─────────────────────────────────────── + + async def _call_service(self, channel_id: str, value: Any) -> WriteOutcome: + """Call a ROS2 service and return a WriteOutcome.""" + cli = self._service_clients[channel_id] + + # Wait for service availability (bounded). + try: + ready = await asyncio.to_thread(cli.wait_for_service, timeout_sec=5.0) + if not ready: + return WriteOutcome( + ok=False, + side_effect_state=SIDE_EFFECT_NONE, + error=f"service for {channel_id!r} not available after 5s", + failure_code="ros2_service_unavailable", + ) + except Exception as exc: + return WriteOutcome( + ok=False, + side_effect_state=SIDE_EFFECT_NONE, + error=f"service wait failed: {exc}", + failure_code="ros2_service_wait_failed", + ) + + # Build request. + req = cli.srv_type.Request() + if isinstance(value, Mapping): + for k, v in value.items(): + if hasattr(req, k): + setattr(req, k, v) + elif hasattr(req, "data"): + req.data = value + + # Call the service. + try: + future = cli.call_async(req) + response = await asyncio.to_thread( + lambda: future.result(timeout_sec=10.0) if hasattr(future, "result") else None, + ) + except Exception as exc: + # The request may have reached the server. + return WriteOutcome( + ok=False, + side_effect_state=SIDE_EFFECT_UNKNOWN, + error=f"ROS2 service call failed: {exc}", + failure_code="ros2_service_call_failed", + ) + + # Parse response. + success = True + if hasattr(response, "success"): + success = bool(response.success) + elif hasattr(response, "result"): + success = bool(response.result) + + return WriteOutcome( + ok=success, + side_effect_state=SIDE_EFFECT_COMMITTED if success else SIDE_EFFECT_UNKNOWN, + settled=True, + raw=_msg_to_dict(response) if response else {}, + ) + + # ── Internal: helpers ───────────────────────────────────────────── + + @property + def _device_id(self) -> str: + return self._context.device_id if self._context else "" + + def _require_open(self, channel_id: str) -> None: + """Raise when the transport is not connected.""" + if not self._connected or self._node is None: + raise TransportError( + f"ros2 transport for {channel_id!r} is not open", + failure_code="transport_not_open", + ) + + def _next_seq(self, channel_id: str) -> int: + """Advance and return the sequence counter for a channel.""" + seq = self._sequence.get(channel_id, 0) + 1 + self._sequence[channel_id] = seq + return seq + + +# ── Module-level helpers ────────────────────────────────────────────── + + +def _require_rclpy() -> Any: + """Return the ``rclpy`` module or raise TransportError.""" + try: + import rclpy # type: ignore[import-untyped] + return rclpy + except ImportError as exc: + raise TransportError( + "rclpy is not installed; install with: pip install leapflow[robot-ros2]", + failure_code="rclpy_not_installed", + ) from exc + + +def _import_msg_type(type_string: str) -> Any: + """Import a ROS2 message type from its type string. + + Type strings look like ``"sensor_msgs/msg/JointState"`` or + ``"std_srvs/srv/SetBool"``. This maps to + ``sensor_msgs.msg.JointState`` / ``std_srvs.srv.SetBool``. + """ + import importlib + + parts = type_string.replace("/", ".") + module_path, _, class_name = parts.rpartition(".") + if not module_path: + return None + try: + mod = importlib.import_module(module_path) + return getattr(mod, class_name, None) + except ImportError: + logger.debug("Cannot import ROS2 type %r", type_string, exc_info=True) + return None + + +def _msg_to_dict(msg: Any) -> dict[str, Any]: + """Convert a ROS2 message to a plain dict, best-effort.""" + if msg is None: + return {} + # rclpy messages expose get_fields_and_field_types(). + if hasattr(msg, "get_fields_and_field_types"): + fields = msg.get_fields_and_field_types() + return {k: getattr(msg, k, None) for k in fields} + if hasattr(msg, "__slots__"): + return {s: getattr(msg, s, None) for s in msg.__slots__} + return {} + + +def _msg_fields(msg: Any) -> list[str]: + """Return the field names of a ROS2 message.""" + if hasattr(msg, "get_fields_and_field_types"): + return list(msg.get_fields_and_field_types().keys()) + if hasattr(msg, "__slots__"): + return list(msg.__slots__) + return [] + + +# ── Factory ─────────────────────────────────────────────────────────── + + +def build_transport(config: Mapping[str, Any]) -> ROS2Transport: + """Factory for the transport registry. + + Config keys match what ``control_binding_resolver._resolve_ros2`` + produces: + + - ``topics``: dict[str, str] — channel_id → topic name + - ``services``: dict[str, str] — channel_id → service name + - ``actions``: dict[str, str] — channel_id → action name + - ``node_name``: str — ROS2 node name (default ``leapflow_bridge``) + - ``namespace``: str — ROS2 namespace (default ``""``) + - ``qos_depth``: int — QoS history depth (default 10) + - ``spin_period_s``: float — spin_once timeout (default 0.01) + """ + return ROS2Transport( + topics=dict(config.get("topics", {})) if isinstance(config.get("topics"), Mapping) else {}, + services=dict(config.get("services", {})) if isinstance(config.get("services"), Mapping) else {}, + actions=dict(config.get("actions", {})) if isinstance(config.get("actions"), Mapping) else {}, + node_name=str(config.get("node_name", "leapflow_bridge")), + namespace=str(config.get("namespace", "")), + qos_depth=int(config.get("qos_depth", 10)), + spin_period_s=float(config.get("spin_period_s", 0.01)), + ) + + +__all__ = [ + "ROS2Transport", + "build_transport", +] diff --git a/src/leapflow/hardware/transports/vla_inference.py b/src/leapflow/hardware/transports/vla_inference.py new file mode 100644 index 00000000..aec7ebd6 --- /dev/null +++ b/src/leapflow/hardware/transports/vla_inference.py @@ -0,0 +1,666 @@ +# Copyright (c) Alibaba, Inc. and its affiliates. +"""VLA inference transport: remote policy server as an HCP virtual device. + +A VLA (Vision-Language-Action) model running on a GPU server is presented +to LeapFlow as a hardware device with three channels: ``model_info`` +(read-only metadata), ``inference`` (write to submit observation, returns +action), and ``latency`` (read-only timing). + +The transport communicates with a ``PolicyServer`` via gRPC, or +with any inference endpoint that accepts the same protobuf contract. +The transport speaks HTTP/JSON when ``server_address`` starts with ``http`` +and gRPC otherwise; this is a construction-time configuration choice, not an +automatic gRPC-to-HTTP fallback. + +This is deliberately a CONFIGURE effect, not ACTUATE: inference produces +a computation result, not a physical motion. The physical effect happens +when the caller writes the returned action to an actuator transport. +""" + +from __future__ import annotations + +import asyncio +import logging +import time +from typing import Any, Mapping + +from leapflow.hardware.context import HardwareContext, Quality +from leapflow.hardware.transport import ( + SIDE_EFFECT_NONE, + Reading, + TransportError, + TransportStatus, + WriteOutcome, +) + +logger = logging.getLogger(__name__) + +# Channels exposed by this virtual device. +_CH_MODEL_INFO = "model_info" +_CH_INFERENCE = "inference" +_CH_LATENCY = "latency" +_READABLE_CHANNELS = frozenset({_CH_MODEL_INFO, _CH_LATENCY}) +_WRITABLE_CHANNELS = frozenset({_CH_INFERENCE}) + +# Retry back-off schedule in seconds. +_BACKOFF_SCHEDULE = (1.0, 2.0) + + +class VLAInferenceTransport: + """HardwareTransport backed by a remote VLA inference server. + + Construction: + - server_address: "host:port" for gRPC, or "http://host:port" for HTTP + - model_name: optional model identifier for multi-model servers + - timeout_s: per-request timeout (default 10.0) + - max_retries: retry count on transient failures (default 2) + """ + + kind: str = "vla_inference" + + def __init__( + self, + server_address: str, + *, + model_name: str = "", + timeout_s: float = 10.0, + max_retries: int = 2, + ) -> None: + self._address = server_address + self._model_name = model_name + self._timeout = timeout_s + self._max_retries = max_retries + self._connected = False + self._last_latency_ms: float = 0.0 + self._model_metadata: dict[str, Any] = {} + self._channel: Any = None # gRPC channel + self._stub: Any = None # gRPC stub + self._http_session: Any = None # aiohttp.ClientSession + self._use_http = server_address.startswith("http") + self._context: HardwareContext | None = None + self._sequence: dict[str, int] = {} + self._pending_rpcs: list[Any] = [] # in-flight gRPC calls for halt() + + # ------------------------------------------------------------------ + # HardwareTransport (6 methods) + # ------------------------------------------------------------------ + + async def open(self, context: HardwareContext) -> TransportStatus: + """Establish gRPC channel (or HTTP session) to the inference server. + + For gRPC: create insecure channel, create stub, call health check. + For HTTP: create aiohttp session, call /health endpoint. + If neither gRPC nor HTTP client libraries are available, returns + a disconnected status rather than raising. + """ + self._context = context + + if self._use_http: + ok = await self._open_http() + else: + ok = await self._open_grpc() + + if not ok: + return TransportStatus( + connected=False, + halt_supported=False, + detail="failed to connect to inference server", + metadata={"address": self._address}, + ) + + # Attempt initial health check and metadata fetch. + try: + healthy = await self._health_check() + except Exception as exc: + logger.warning( + "VLA inference server health check failed during open: %s", + exc, exc_info=True, + ) + healthy = False + + self._connected = healthy + if not healthy: + await self._teardown() + return TransportStatus( + connected=False, + halt_supported=True, + detail="inference server health check failed", + metadata={"address": self._address}, + ) + + logger.info( + "VLA inference transport opened: %s (model=%s, http=%s)", + self._address, self._model_name or "", self._use_http, + ) + return await self.probe() + + async def close(self) -> TransportStatus: + """Close gRPC channel / HTTP session. Idempotent, never raises.""" + await self._teardown() + self._connected = False + return TransportStatus( + connected=False, + halt_supported=True, + detail="vla_inference transport closed", + ) + + async def read(self, channel_id: str) -> Reading: + """Read model_info or latency channel. + + model_info: returns cached model metadata dict. + latency: returns last inference latency in milliseconds. + inference: raises TransportError (write-only semantics). + """ + self._require_open(channel_id) + + if channel_id == _CH_INFERENCE: + raise TransportError( + f"channel {_CH_INFERENCE!r} is write-only; submit observations " + "via write() instead", + failure_code="channel_write_only", + ) + + if channel_id not in _READABLE_CHANNELS: + raise TransportError( + f"unknown channel {channel_id!r}", + failure_code="unknown_channel", + ) + + device_id = self._context.device_id if self._context else "" + + if channel_id == _CH_MODEL_INFO: + return Reading( + device_id=device_id, + channel_id=channel_id, + value=dict(self._model_metadata), + quantity="metadata", + unit="", + sequence=self._next_seq(channel_id), + quality=Quality.OK.value, + ) + + # _CH_LATENCY + return Reading( + device_id=device_id, + channel_id=channel_id, + value=self._last_latency_ms, + quantity="latency", + unit="ms", + sequence=self._next_seq(channel_id), + quality=Quality.OK.value, + ) + + async def write(self, channel_id: str, value: Any) -> WriteOutcome: + """Submit an inference request to the server. + + Only the 'inference' channel is writable. + *value*: dict with 'observation' key containing sensor data. + Returns: WriteOutcome with readback containing the action vector. + + side_effect_state is always NONE (computation has no physical effect). + """ + self._require_open(channel_id) + + if channel_id != _CH_INFERENCE: + raise TransportError( + f"channel {channel_id!r} is read-only", + failure_code="channel_read_only", + ) + + if not isinstance(value, Mapping): + raise TransportError( + "inference write expects a dict with an 'observation' key", + failure_code="invalid_inference_input", + ) + + observation = value.get("observation") + if observation is None: + raise TransportError( + "inference write requires an 'observation' key in the value dict", + failure_code="missing_observation", + ) + + # Execute inference with timing. + t0 = time.monotonic() + try: + if self._use_http: + result = await self._infer_http(dict(value)) + else: + result = await self._infer_grpc(dict(value)) + except TransportError: + raise + except Exception as exc: + return WriteOutcome( + ok=False, + side_effect_state=SIDE_EFFECT_NONE, + error=f"inference failed: {exc}", + failure_code="vla_inference_failed", + ) + elapsed_ms = (time.monotonic() - t0) * 1000.0 + self._last_latency_ms = elapsed_ms + + device_id = self._context.device_id if self._context else "" + readback = Reading( + device_id=device_id, + channel_id=channel_id, + value=result, + quantity="action", + unit="", + sequence=self._next_seq(channel_id), + quality=Quality.OK.value, + ) + return WriteOutcome( + ok=True, + side_effect_state=SIDE_EFFECT_NONE, + readback=readback, + settled=True, + raw={"latency_ms": elapsed_ms}, + ) + + async def probe(self) -> TransportStatus: + """Health check: ping the server, report GPU availability if known.""" + if not self._connected: + return TransportStatus( + connected=False, + halt_supported=True, + detail="vla_inference transport not connected", + metadata={"address": self._address}, + ) + try: + healthy = await self._health_check() + except Exception as exc: + logger.debug("VLA probe failed: %s", exc, exc_info=True) + healthy = False + + meta: dict[str, Any] = { + "address": self._address, + "model_name": self._model_name, + "use_http": self._use_http, + "last_latency_ms": self._last_latency_ms, + } + if self._model_metadata: + meta["model_metadata"] = self._model_metadata + return TransportStatus( + connected=healthy, + halt_supported=True, + detail="vla_inference transport" if healthy else "health check failed", + latency_ms=self._last_latency_ms, + metadata=meta, + ) + + async def halt(self) -> TransportStatus: + """Cancel any in-flight inference requests. + + For gRPC: cancel pending RPCs. + Returns halt_supported=True (cancellation is always possible). + """ + cancelled = 0 + for rpc in self._pending_rpcs: + try: + rpc.cancel() + cancelled += 1 + except Exception: + pass + self._pending_rpcs.clear() + return TransportStatus( + connected=self._connected, + halt_supported=True, + detail=f"halted ({cancelled} in-flight RPCs cancelled)" + if cancelled + else "halted (no in-flight RPCs)", + ) + + # ------------------------------------------------------------------ + # Internal: connection lifecycle + # ------------------------------------------------------------------ + + async def _open_grpc(self) -> bool: + """Establish a gRPC channel. Returns False when grpc is unavailable.""" + try: + import grpc # type: ignore[import-untyped] + import grpc.aio # type: ignore[import-untyped] + except ImportError: + logger.warning( + "grpcio not installed; cannot connect to %s via gRPC. " + "Install with: pip install grpcio", + self._address, + ) + return False + + try: + self._channel = grpc.aio.insecure_channel(self._address) + # A lightweight connectivity check -- wait for the channel to + # leave IDLE. This does not guarantee the server is serving, + # but catches obviously wrong addresses early. + await asyncio.wait_for( + self._channel.channel_ready(), + timeout=self._timeout, + ) + except asyncio.TimeoutError: + logger.warning( + "gRPC channel to %s did not become ready within %.1fs", + self._address, self._timeout, + ) + await self._teardown() + return False + except Exception as exc: + logger.warning( + "gRPC channel creation failed for %s: %s", + self._address, exc, exc_info=True, + ) + await self._teardown() + return False + return True + + async def _open_http(self) -> bool: + """Create an aiohttp session. Returns False when aiohttp is unavailable.""" + try: + import aiohttp # type: ignore[import-untyped] + except ImportError: + logger.warning( + "aiohttp not installed; cannot connect to %s via HTTP. " + "Install with: pip install aiohttp", + self._address, + ) + return False + try: + timeout = aiohttp.ClientTimeout(total=self._timeout) + self._http_session = aiohttp.ClientSession(timeout=timeout) + except Exception as exc: + logger.warning( + "aiohttp session creation failed: %s", exc, exc_info=True, + ) + return False + return True + + async def _teardown(self) -> None: + """Release gRPC channel and HTTP session. Idempotent, never raises.""" + if self._channel is not None: + try: + await self._channel.close() + except Exception: + pass + self._channel = None + self._stub = None + if self._http_session is not None: + try: + await self._http_session.close() + except Exception: + pass + self._http_session = None + self._pending_rpcs.clear() + + # ------------------------------------------------------------------ + # Internal: inference execution + # ------------------------------------------------------------------ + + async def _infer_grpc(self, observation: dict) -> dict: + """Execute inference via gRPC with timeout and retry. + + Uses the generic ``UnaryUnaryMultiCallable`` interface so that the + transport works with any protobuf service that accepts a JSON-like + dict and returns one. When a proper proto stub is available (e.g. + a ``PolicyServer``), it is used directly. + """ + if self._channel is None: + raise TransportError( + "gRPC channel is not open", failure_code="transport_not_open", + ) + + last_exc: Exception | None = None + for attempt in range(1 + self._max_retries): + if attempt > 0: + backoff = _BACKOFF_SCHEDULE[min(attempt - 1, len(_BACKOFF_SCHEDULE) - 1)] + await asyncio.sleep(backoff) + + try: + result = await asyncio.wait_for( + self._grpc_call(observation), + timeout=self._timeout, + ) + return result + except asyncio.TimeoutError: + last_exc = TimeoutError( + f"gRPC inference timed out after {self._timeout}s " + f"(attempt {attempt + 1}/{1 + self._max_retries})" + ) + logger.debug("gRPC inference timeout (attempt %d)", attempt + 1) + except Exception as exc: + last_exc = exc + logger.debug( + "gRPC inference error (attempt %d): %s", + attempt + 1, exc, exc_info=True, + ) + + raise TransportError( + f"gRPC inference failed after {1 + self._max_retries} attempts: {last_exc}", + failure_code="vla_grpc_inference_failed", + ) + + async def _grpc_call(self, observation: dict) -> dict: + """Single gRPC call. Wraps the stub or falls back to a generic unary RPC. + + Subclasses or future versions can override this when the proto + contract is known. + """ + try: + import grpc # type: ignore[import-untyped] + except ImportError as exc: + raise TransportError( + "grpcio unavailable", failure_code="grpc_unavailable", + ) from exc + + # Build a generic request payload. This is compatible with servers + # that accept a JSON-serialisable dict (e.g. via grpc-json-proxy or + # the async_inference module's generic endpoint). + import json + + # Build the request payload. The full write value is forwarded so + # server-side fields (task, chunk_size, ...) are preserved rather than + # silently dropped; only ``observation`` was retained before. + payload = dict(observation) if isinstance(observation, dict) else {"observation": observation} + if self._model_name and "model_name" not in payload: + payload["model_name"] = self._model_name + request_bytes = json.dumps(payload).encode() + + # Use the channel's generic unary-unary callable. + method = self._channel.unary_unary( + "/policy.PolicyService/Infer", + request_serializer=lambda x: x, + response_deserializer=lambda x: x, + ) + call = method(request_bytes) + self._pending_rpcs.append(call) + try: + response_bytes = await call + finally: + try: + self._pending_rpcs.remove(call) + except ValueError: + pass + + # Parse the response. + try: + result = json.loads(response_bytes) + except (json.JSONDecodeError, TypeError): + result = {"raw": response_bytes.decode(errors="replace")} + + # Update model metadata if the server provides it. + if isinstance(result, dict) and "model_info" in result: + self._model_metadata.update(result["model_info"]) + + return result + + async def _infer_http(self, observation: dict) -> dict: + """Execute inference via HTTP/JSON with timeout and retry.""" + if self._http_session is None: + raise TransportError( + "HTTP session is not open", failure_code="transport_not_open", + ) + + url = f"{self._address.rstrip('/')}/infer" + # Forward the full write value so server-side fields (task, chunk_size, + # ...) are preserved rather than silently dropped. + payload = dict(observation) if isinstance(observation, dict) else {"observation": observation} + if self._model_name and "model_name" not in payload: + payload["model_name"] = self._model_name + + last_exc: Exception | None = None + for attempt in range(1 + self._max_retries): + if attempt > 0: + backoff = _BACKOFF_SCHEDULE[min(attempt - 1, len(_BACKOFF_SCHEDULE) - 1)] + await asyncio.sleep(backoff) + + try: + result = await asyncio.wait_for( + self._http_post(url, payload), + timeout=self._timeout, + ) + return result + except asyncio.TimeoutError: + last_exc = TimeoutError( + f"HTTP inference timed out after {self._timeout}s " + f"(attempt {attempt + 1}/{1 + self._max_retries})" + ) + logger.debug("HTTP inference timeout (attempt %d)", attempt + 1) + except Exception as exc: + last_exc = exc + logger.debug( + "HTTP inference error (attempt %d): %s", + attempt + 1, exc, exc_info=True, + ) + + raise TransportError( + f"HTTP inference failed after {1 + self._max_retries} attempts: {last_exc}", + failure_code="vla_http_inference_failed", + ) + + async def _http_post(self, url: str, payload: dict) -> dict: + """Single HTTP POST. Returns the parsed JSON response.""" + async with self._http_session.post(url, json=payload) as resp: + if resp.status != 200: + text = await resp.text() + raise TransportError( + f"HTTP inference returned status {resp.status}: {text[:200]}", + failure_code="vla_http_error", + ) + result = await resp.json() + + # Update model metadata if the server provides it. + if isinstance(result, dict) and "model_info" in result: + self._model_metadata.update(result["model_info"]) + + return result + + # ------------------------------------------------------------------ + # Internal: health check + # ------------------------------------------------------------------ + + async def _health_check(self) -> bool: + """Ping the server health endpoint. + + gRPC: attempt a unary call to a health-check method. + HTTP: GET /health and expect a 200. + """ + if self._use_http: + return await self._health_check_http() + return await self._health_check_grpc() + + async def _health_check_grpc(self) -> bool: + """gRPC health check via the standard health service or a lightweight ping.""" + if self._channel is None: + return False + try: + import json + + method = self._channel.unary_unary( + "/policy.PolicyService/Health", + request_serializer=lambda x: x, + response_deserializer=lambda x: x, + ) + response_bytes = await asyncio.wait_for( + method(b"{}"), timeout=min(self._timeout, 5.0), + ) + # Parse metadata if available. + try: + data = json.loads(response_bytes) + if isinstance(data, dict): + self._model_metadata.update(data.get("model_info", {})) + except (json.JSONDecodeError, TypeError): + pass + return True + except Exception as exc: + logger.debug("gRPC health check failed: %s", exc) + return False + + async def _health_check_http(self) -> bool: + """HTTP health check: GET /health.""" + if self._http_session is None: + return False + url = f"{self._address.rstrip('/')}/health" + try: + async with self._http_session.get(url) as resp: + if resp.status == 200: + try: + data = await resp.json() + if isinstance(data, dict): + self._model_metadata.update(data.get("model_info", {})) + except Exception: + pass + return True + return False + except Exception as exc: + logger.debug("HTTP health check failed: %s", exc) + return False + + # ------------------------------------------------------------------ + # Internal: helpers + # ------------------------------------------------------------------ + + def _require_open(self, channel_id: str) -> None: + """Raise TransportError when the transport is not connected.""" + if not self._connected: + raise TransportError( + f"transport for {channel_id!r} is not open", + failure_code="transport_not_open", + ) + + def _next_seq(self, channel_id: str) -> int: + """Advance and return the sequence counter for a channel.""" + seq = self._sequence.get(channel_id, 0) + 1 + self._sequence[channel_id] = seq + return seq + + +# ------------------------------------------------------------------ +# Factory +# ------------------------------------------------------------------ + + +def build_transport(config: Mapping[str, Any]) -> VLAInferenceTransport: + """Factory for the transport registry. + + Config keys: + - server_address (str, required): "host:port" or "http://host:port" + - model_name (str, optional): model identifier + - timeout_s (float, optional): per-request timeout + - max_retries (int, optional): retry count + """ + server_address = str(config.get("server_address", "")) + if not server_address: + raise TransportError( + "vla_inference transport requires 'server_address' in config", + failure_code="vla_config_missing_address", + ) + return VLAInferenceTransport( + server_address=server_address, + model_name=str(config.get("model_name", "")), + timeout_s=float(config.get("timeout_s", 10.0) or 10.0), + max_retries=int(config.get("max_retries", 2) or 2), + ) + + +__all__ = [ + "VLAInferenceTransport", + "build_transport", +] diff --git a/src/leapflow/hardware/verification.py b/src/leapflow/hardware/verification.py new file mode 100644 index 00000000..dacc9b22 --- /dev/null +++ b/src/leapflow/hardware/verification.py @@ -0,0 +1,536 @@ +# Copyright (c) Alibaba, Inc. and its affiliates. +"""Operation verification: the evidence-based answer to 'did the physical +action actually succeed?' + +Execution completion is not task success. A motor controller that returns +OK has fulfilled its electrical contract, not the manipulation goal. This +module provides the structural layer that separates *execution evidence* +(what the hardware reported) from *semantic verdict* (whether the physical +intent was achieved). + +Verification is optional and mode-gated: ``off`` skips it entirely, +``audit`` records evidence without blocking, ``enforce`` blocks on failure, +``recovery`` feeds failure into RecoveryCoordinator. +""" + +from __future__ import annotations + +import asyncio +import logging +import time +import uuid +from dataclasses import dataclass, field +from enum import Enum +from typing import Any, Mapping, Protocol, runtime_checkable + +from leapflow.hardware.transport import FrameReading, Reading + +logger = logging.getLogger(__name__) + + +# --------------------------------------------------------------------------- +# Enums +# --------------------------------------------------------------------------- + + +class VerificationMode(str, Enum): + """How verification results affect the execution pipeline.""" + + OFF = "off" + """Skip verification entirely.""" + + AUDIT = "audit" + """Record evidence, never block.""" + + ENFORCE = "enforce" + """Block pipeline on failure.""" + + RECOVERY = "recovery" + """Feed failure into RecoveryCoordinator.""" + + +class VerdictStatus(str, Enum): + """Outcome of a verification check.""" + + SUCCESS = "success" + FAILURE = "failure" + INCONCLUSIVE = "inconclusive" + SKIPPED = "skipped" + + +# --------------------------------------------------------------------------- +# Evidence and verdict data types +# --------------------------------------------------------------------------- + + +@dataclass(frozen=True) +class EvidenceBundle: + """Post-operation evidence collected for verification and audit. + + Captured after a write operation completes and the channel's declared + settling time has elapsed. Contains the commanded intent, the transport's + own outcome, and post-settle sensor readings that a verifier uses to + determine whether the physical goal was achieved. + + ``operation_id`` is a unique identifier for this operation instance, + linking the evidence to the audit trail entry. ``frames`` may be empty + when no camera channel is available or relevant. + """ + + operation_id: str + device_id: str + channel_id: str + timestamp: float + intended_value: Any + actual_outcome: Any # WriteOutcome, but typed as Any to avoid circular import + post_settle_readings: tuple[Reading, ...] + frames: tuple[FrameReading, ...] = () + settle_delay_s: float = 0.0 + metadata: Mapping[str, Any] = field(default_factory=dict) + + def to_dict(self) -> dict[str, Any]: + """Return the evidence as a plain dict for audit serialisation.""" + return { + "operation_id": self.operation_id, + "device_id": self.device_id, + "channel_id": self.channel_id, + "timestamp": self.timestamp, + "intended_value": self.intended_value, + "settle_delay_s": self.settle_delay_s, + "post_settle_readings": [r.to_dict() for r in self.post_settle_readings], + "frames": [f.to_metadata() for f in self.frames], + } + + +@dataclass(frozen=True) +class OperationVerdict: + """The semantic answer to 'did this operation achieve its goal?' + + ``confidence`` is 0.0--1.0 and reflects how much evidence the verifier + had. A position check with tight tolerance and immediate readback is + high confidence; a grasp check with only force data and no visual + confirmation is lower. + + ``deviation`` is the absolute difference between intended and actual + in the channel's native units, when meaningful (e.g. position error + in radians). None when the metric is not numeric (e.g. grasp binary). + + ``detail`` is a human-readable (and LLM-readable) explanation of the + verdict, suitable for the audit log and for feeding into + RecoveryCoordinator context. + """ + + status: str # VerdictStatus value + confidence: float + deviation: float | None = None + detail: str = "" + evidence: EvidenceBundle | None = None + + @property + def is_success(self) -> bool: + """Return True when the verification passed.""" + return self.status == VerdictStatus.SUCCESS.value + + @property + def is_failure(self) -> bool: + """Return True when the verification definitively failed.""" + return self.status == VerdictStatus.FAILURE.value + + def to_dict(self) -> dict[str, Any]: + """Return the verdict as a plain dict for audit serialisation.""" + payload: dict[str, Any] = { + "status": self.status, + "confidence": self.confidence, + } + if self.deviation is not None: + payload["deviation"] = self.deviation + if self.detail: + payload["detail"] = self.detail + if self.evidence is not None: + payload["evidence"] = self.evidence.to_dict() + return payload + + +# --------------------------------------------------------------------------- +# Protocol +# --------------------------------------------------------------------------- + + +@runtime_checkable +class OperationVerifier(Protocol): + """Judges whether a physical operation achieved its intended goal. + + Stateless by design: a verifier receives an evidence bundle and returns + a verdict. State (success/failure counts, trust progression) lives in + the trust gate, not here. + """ + + @property + def verifier_id(self) -> str: + """Unique identifier for this verifier type.""" + ... + + @property + def applicable_effects(self) -> frozenset[str]: + """HardwareEffect values this verifier can judge. + + The dispatcher matches a channel's declared ``effect`` against this + set to select the right verifier. + """ + ... + + async def verify(self, bundle: EvidenceBundle) -> OperationVerdict: + """Examine the evidence and return a verdict.""" + ... + + +# --------------------------------------------------------------------------- +# Concrete verifiers +# --------------------------------------------------------------------------- + + +class PositionVerifier: + """Verifies that a joint position command achieved its target. + + Verification logic: + + 1. Find the post-settle reading for the target channel. + 2. Compare with commanded value within the channel's declared tolerance + (from Envelope), or a configured default tolerance. + 3. If deviation < tolerance: SUCCESS with high confidence. + 4. If deviation < 2 * tolerance: INCONCLUSIVE. + 5. Otherwise: FAILURE. + + Applicable to: ACTUATE and CONFIGURE effects on SCALAR channels. + """ + + def __init__(self, *, default_tolerance: float = 0.05) -> None: + self._default_tolerance = max(0.0, default_tolerance) + + @property + def verifier_id(self) -> str: + return "position" + + @property + def applicable_effects(self) -> frozenset[str]: + return frozenset({"actuate", "configure"}) + + async def verify(self, bundle: EvidenceBundle) -> OperationVerdict: + """Check whether the post-settle reading matches the commanded value.""" + # Locate the reading for the target channel. + reading = _find_channel_reading(bundle.post_settle_readings, bundle.channel_id) + if reading is None: + return OperationVerdict( + status=VerdictStatus.INCONCLUSIVE.value, + confidence=0.1, + detail=( + f"No post-settle reading for channel '{bundle.channel_id}'; " + "cannot verify position." + ), + evidence=bundle, + ) + + intended = _as_float(bundle.intended_value) + actual = _as_float(reading.value) + if intended is None or actual is None: + return OperationVerdict( + status=VerdictStatus.INCONCLUSIVE.value, + confidence=0.2, + detail=( + f"Non-numeric values (intended={bundle.intended_value!r}, " + f"actual={reading.value!r}); position check requires numerics." + ), + evidence=bundle, + ) + + deviation = abs(actual - intended) + tolerance = self._resolve_tolerance(bundle) + + if deviation < tolerance: + return OperationVerdict( + status=VerdictStatus.SUCCESS.value, + confidence=0.95, + deviation=deviation, + detail=( + f"Position within tolerance: deviation={deviation:.6f}, " + f"tolerance={tolerance:.6f}." + ), + evidence=bundle, + ) + + if deviation < 2.0 * tolerance: + return OperationVerdict( + status=VerdictStatus.INCONCLUSIVE.value, + confidence=0.5, + deviation=deviation, + detail=( + f"Position marginally outside tolerance: deviation={deviation:.6f}, " + f"tolerance={tolerance:.6f} (within 2x)." + ), + evidence=bundle, + ) + + return OperationVerdict( + status=VerdictStatus.FAILURE.value, + confidence=0.9, + deviation=deviation, + detail=( + f"Position outside tolerance: deviation={deviation:.6f}, " + f"tolerance={tolerance:.6f}." + ), + evidence=bundle, + ) + + def _resolve_tolerance(self, bundle: EvidenceBundle) -> float: + """Return the effective tolerance from metadata or the default.""" + # The caller (collect_evidence or the tool layer) may inject the + # channel's Envelope tolerance into ``bundle.metadata``. + declared = bundle.metadata.get("tolerance") + if declared is not None: + val = _as_float(declared) + if val is not None and val > 0.0: + return val + return self._default_tolerance + + +class GraspVerifier: + """Verifies grasp success using force/torque readings. + + Verification logic: + + 1. Look for force/torque readings in ``post_settle_readings`` + (channel quantity contains 'force' or 'torque'). + 2. If force > force_threshold -> grip detected, SUCCESS. + 3. If force readings available but below threshold -> FAILURE. + 4. If no force readings available -> INCONCLUSIVE (low confidence). + 5. Optional: if frames available, note them in metadata for future + VLM-based verification (not implemented in v0). + + Applicable to: ACTUATE effects on STATE channels (gripper). + """ + + def __init__(self, *, force_threshold: float = 1.0) -> None: + self._force_threshold = max(0.0, force_threshold) + + @property + def verifier_id(self) -> str: + return "grasp" + + @property + def applicable_effects(self) -> frozenset[str]: + return frozenset({"actuate"}) + + async def verify(self, bundle: EvidenceBundle) -> OperationVerdict: + """Check whether force/torque readings indicate a successful grasp.""" + force_readings = [ + r for r in bundle.post_settle_readings + if _is_force_quantity(r.quantity) + ] + + if not force_readings: + frame_note = "" + if bundle.frames: + frame_note = ( + f" ({len(bundle.frames)} frame(s) available for future " + "VLM-based verification.)" + ) + return OperationVerdict( + status=VerdictStatus.INCONCLUSIVE.value, + confidence=0.15, + detail=( + f"No force/torque readings available for grasp verification." + f"{frame_note}" + ), + evidence=bundle, + ) + + # Use the maximum force among available readings. + max_force_reading = max(force_readings, key=lambda r: _as_float(r.value) or 0.0) + force_value = _as_float(max_force_reading.value) + + if force_value is None: + return OperationVerdict( + status=VerdictStatus.INCONCLUSIVE.value, + confidence=0.2, + detail=( + f"Force reading is non-numeric: {max_force_reading.value!r}." + ), + evidence=bundle, + ) + + if force_value >= self._force_threshold: + return OperationVerdict( + status=VerdictStatus.SUCCESS.value, + confidence=0.75, + detail=( + f"Grasp detected: force={force_value:.3f}, " + f"threshold={self._force_threshold:.3f}." + ), + evidence=bundle, + ) + + return OperationVerdict( + status=VerdictStatus.FAILURE.value, + confidence=0.7, + detail=( + f"Grasp not detected: force={force_value:.3f} below " + f"threshold={self._force_threshold:.3f}." + ), + evidence=bundle, + ) + + +# --------------------------------------------------------------------------- +# Evidence collection helper +# --------------------------------------------------------------------------- + + +async def collect_evidence( + registry: Any, # HardwareRegistry, typed as Any to avoid circular import + device_id: str, + channel_id: str, + *, + intended_value: Any, + outcome: Any, # WriteOutcome + settle_delay_s: float = 0.0, + related_channels: tuple[str, ...] = (), + capture_frame: bool = False, +) -> EvidenceBundle: + """Collect post-operation evidence from a device. + + Waits ``settle_delay_s`` (from the channel's Envelope), then reads + the target channel and any related channels. Optionally captures + a frame if the device has a ``FrameTransport``. + + The registry is typed as ``Any`` to avoid importing ``HardwareRegistry`` + from ``registry.py``, which would create a circular dependency. The + caller is expected to pass a registry instance that exposes ``read`` + and optionally ``read_frame`` for evidence collection. + """ + operation_id = uuid.uuid4().hex[:16] + + # Wait for the physical system to settle before reading back. + if settle_delay_s > 0.0: + await asyncio.sleep(settle_delay_s) + + # Read the target channel. + readings: list[Reading] = [] + target_reading = await _safe_read(registry, device_id, channel_id) + if target_reading is not None: + readings.append(target_reading) + + # Read related channels (e.g. adjacent force sensors for a grasp). + for related_ch in related_channels: + related_reading = await _safe_read(registry, device_id, related_ch) + if related_reading is not None: + readings.append(related_reading) + + # Optionally capture a frame for visual evidence. + frames: list[FrameReading] = [] + if capture_frame: + frame = await _safe_read_frame(registry, device_id, channel_id) + if frame is not None: + frames.append(frame) + + return EvidenceBundle( + operation_id=operation_id, + device_id=device_id, + channel_id=channel_id, + timestamp=time.time(), + intended_value=intended_value, + actual_outcome=outcome, + post_settle_readings=tuple(readings), + frames=tuple(frames), + settle_delay_s=settle_delay_s, + ) + + +# --------------------------------------------------------------------------- +# Internal helpers +# --------------------------------------------------------------------------- + + +def _find_channel_reading( + readings: tuple[Reading, ...], + channel_id: str, +) -> Reading | None: + """Return the first reading matching *channel_id*, or None.""" + return next((r for r in readings if r.channel_id == channel_id), None) + + +def _is_force_quantity(quantity: str) -> bool: + """Return True when the quantity string indicates force or torque.""" + q = quantity.lower() + return "force" in q or "torque" in q + + +def _as_float(value: Any) -> float | None: + """Coerce *value* to a finite float, returning None on failure. + + Mirrors ``context.as_numeric`` but kept local to avoid coupling on a + private-scope function signature. Booleans, NaN, and infinity are + rejected for the same reasons documented there. + """ + if value is None or isinstance(value, bool): + return None + try: + result = float(value) + except (TypeError, ValueError): + return None + import math + if math.isnan(result) or math.isinf(result): + return None + return result + + +async def _safe_read( + registry: Any, + device_id: str, + channel_id: str, +) -> Reading | None: + """Read a channel, returning None on any failure. + + Evidence collection must never raise: a failed readback is missing + evidence, not a pipeline-stopping error. + """ + try: + return await registry.read(device_id, channel_id) + except Exception: + logger.debug( + "Evidence collection: failed to read %s/%s", + device_id, channel_id, + exc_info=True, + ) + return None + + +async def _safe_read_frame( + registry: Any, + device_id: str, + channel_id: str, +) -> FrameReading | None: + """Read a frame, returning None on any failure or if unsupported.""" + try: + read_frame = getattr(registry, "read_frame", None) + if read_frame is None: + return None + return await read_frame(device_id, channel_id) + except Exception: + logger.debug( + "Evidence collection: failed to read frame %s/%s", + device_id, channel_id, + exc_info=True, + ) + return None + + +__all__ = [ + "EvidenceBundle", + "GraspVerifier", + "OperationVerdict", + "OperationVerifier", + "PositionVerifier", + "VerdictStatus", + "VerificationMode", + "collect_evidence", +] diff --git a/src/leapflow/learning/physical_episode.py b/src/leapflow/learning/physical_episode.py new file mode 100644 index 00000000..92e2c0b4 --- /dev/null +++ b/src/leapflow/learning/physical_episode.py @@ -0,0 +1,615 @@ +# Copyright (c) Alibaba, Inc. and its affiliates. +"""Physical episode recording for experience learning. + +An episode captures a complete physical manipulation attempt: the initial +state, the sequence of actions taken, intermediate observations, the final +state, and the verification verdict. Episodes feed the evolution engine +(so the agent learns which manipulations succeed) and can be exported for +offline policy training. + +Distinct from PhysicalTrajectory (which is raw demonstration data): an +episode adds the outcome dimension — what was attempted, what happened, +and whether it worked. + +Persistence follows the EvidenceStore pattern: DuckDB table for structured +metadata, ``asyncio.to_thread`` for non-blocking writes, and a +``ConnectionHolder`` for shared access. +""" + +from __future__ import annotations + +import asyncio +import json +import logging +import time +import uuid +from dataclasses import dataclass, field +from pathlib import Path +from typing import Any, Mapping + +from leapflow.storage.connection import ConnectionHolder, LocalConnectionHolder + +logger = logging.getLogger(__name__) + +EPISODE_SCHEMA_VERSION = 1 +"""Row format version for the physical_episodes table.""" + +DEFAULT_RETENTION_DAYS = 90.0 +"""How long episode rows survive before cleanup.""" + + +# --------------------------------------------------------------------------- +# PhysicalEpisode +# --------------------------------------------------------------------------- + + +@dataclass(frozen=True) +class PhysicalEpisode: + """A complete physical manipulation episode with outcome. + + ``trajectory`` is a :class:`PhysicalTrajectory` instance captured + during the episode. ``verdict`` is an optional + :class:`OperationVerdict` produced by the verification layer. + ``evidence_ids`` reference :class:`EvidenceBundle` records stored + in the :class:`EvidenceStore`. + """ + + episode_id: str + device_id: str + task: str + trajectory: Any # PhysicalTrajectory + initial_state: Mapping[str, Any] + final_state: Mapping[str, Any] + verdict: Any = None # OperationVerdict or None + evidence_ids: tuple[str, ...] = () + success: bool = False + started_at: float = 0.0 + ended_at: float = 0.0 + metadata: Mapping[str, Any] = field(default_factory=dict) + + @property + def duration_s(self) -> float: + """Duration in seconds.""" + if self.ended_at > self.started_at: + return self.ended_at - self.started_at + return 0.0 + + @property + def step_count(self) -> int: + """Number of trajectory steps.""" + traj = self.trajectory + if traj is not None: + steps = getattr(traj, "steps", ()) + return len(steps) + return 0 + + def to_dict(self) -> dict[str, Any]: + """Serialize to a plain dict suitable for JSON persistence.""" + traj_dict: dict[str, Any] | None = None + if self.trajectory is not None: + to_dict_fn = getattr(self.trajectory, "to_dict", None) + traj_dict = to_dict_fn() if callable(to_dict_fn) else None + + verdict_dict: dict[str, Any] | None = None + if self.verdict is not None: + to_dict_fn = getattr(self.verdict, "to_dict", None) + verdict_dict = to_dict_fn() if callable(to_dict_fn) else None + + return { + "episode_id": self.episode_id, + "device_id": self.device_id, + "task": self.task, + "trajectory": traj_dict, + "initial_state": dict(self.initial_state), + "final_state": dict(self.final_state), + "verdict": verdict_dict, + "evidence_ids": list(self.evidence_ids), + "success": self.success, + "started_at": self.started_at, + "ended_at": self.ended_at, + "duration_s": self.duration_s, + "step_count": self.step_count, + "metadata": dict(self.metadata), + } + + +def make_episode_id() -> str: + """Generate a short unique episode identifier.""" + return uuid.uuid4().hex[:16] + + +# --------------------------------------------------------------------------- +# PhysicalEpisodeRecorder +# --------------------------------------------------------------------------- + + +class PhysicalEpisodeRecorder: + """Records physical manipulation episodes and feeds the evolution engine. + + Lifecycle:: + + episode_id = await recorder.begin_episode(device_id, task) + recorder.record_step(step) # repeat for each step + recorder.record_evidence(eid) # optional, link evidence bundles + episode = await recorder.end_episode(verdict) + + Dependencies: + + - ``registry``: HardwareRegistry (for state capture via ``read_batch``) + - ``evidence_store``: :class:`EvidenceStore` (optional, for evidence linking) + - ``action_recorder``: evolution :class:`ActionRecorder` (optional, for + feeding physical episode outcomes into the evolution causal chain) + - ``db_path``: DuckDB path for episode persistence; when ``None`` episodes + are returned but not persisted. + """ + + def __init__( + self, + registry: Any, + *, + evidence_store: Any = None, + action_recorder: Any = None, + db_path: str | Path | None = None, + connection_holder: ConnectionHolder | None = None, + ) -> None: + self._registry = registry + self._evidence_store = evidence_store + self._action_recorder = action_recorder + + # DuckDB persistence (optional). + self._holder: ConnectionHolder | None = connection_holder + self._owns_holder = False + if self._holder is None and db_path is not None: + self._holder = LocalConnectionHolder(Path(db_path)) + self._owns_holder = True + self._db_ready = False + + # In-flight episode state (mutable during recording). + self._current_device_id: str | None = None + self._current_task: str | None = None + self._current_episode_id: str | None = None + self._current_started_at: float = 0.0 + self._current_initial_state: dict[str, Any] = {} + self._current_steps: list[Any] = [] + self._current_evidence_ids: list[str] = [] + + # Stats. + self._episodes_recorded = 0 + + @property + def is_recording(self) -> bool: + """Whether an episode is currently being recorded.""" + return self._current_episode_id is not None + + @property + def episodes_recorded(self) -> int: + """Total episodes successfully recorded.""" + return self._episodes_recorded + + # -- Lifecycle ------------------------------------------------------------ + + async def begin_episode(self, device_id: str, task: str) -> str: + """Start recording; capture initial state via ``read_batch``. + + Returns the episode_id. + + Raises: + RuntimeError: If an episode is already being recorded. + """ + if self._current_episode_id is not None: + raise RuntimeError( + f"Episode {self._current_episode_id} already in progress; " + "call end_episode() first." + ) + + episode_id = make_episode_id() + initial_state = await self._capture_state(device_id) + + self._current_episode_id = episode_id + self._current_device_id = device_id + self._current_task = task + self._current_started_at = time.time() + self._current_initial_state = initial_state + self._current_steps = [] + self._current_evidence_ids = [] + + logger.debug( + "Episode %s started: device=%s, task=%r", + episode_id, device_id, task, + ) + return episode_id + + def record_step(self, step: Any) -> None: + """Append a :class:`PhysicalTrajectoryStep` to the current episode. + + Raises: + RuntimeError: If no episode is in progress. + """ + if self._current_episode_id is None: + raise RuntimeError("No episode in progress; call begin_episode() first.") + self._current_steps.append(step) + + def record_evidence(self, evidence_id: str) -> None: + """Link an :class:`EvidenceBundle` id to the current episode. + + Raises: + RuntimeError: If no episode is in progress. + """ + if self._current_episode_id is None: + raise RuntimeError("No episode in progress; call begin_episode() first.") + self._current_evidence_ids.append(evidence_id) + + async def end_episode( + self, + verdict: Any = None, + *, + success: bool | None = None, + ) -> PhysicalEpisode: + """Capture final state, build episode, persist, and feed evolution. + + Args: + verdict: Optional :class:`OperationVerdict` from verification. + success: Explicit success flag. When ``None``, inferred from + *verdict* (``is_success`` property) or defaults to ``False``. + + Returns: + The finalized :class:`PhysicalEpisode`. + + Raises: + RuntimeError: If no episode is in progress. + """ + if self._current_episode_id is None: + raise RuntimeError("No episode in progress; call begin_episode() first.") + + device_id = self._current_device_id or "" + final_state = await self._capture_state(device_id) + ended_at = time.time() + + # Determine success. + if success is None: + if verdict is not None: + success = bool(getattr(verdict, "is_success", False)) + else: + success = False + + # Build PhysicalTrajectory from collected steps. + from leapflow.robot.trajectory import PhysicalTrajectory + + trajectory = PhysicalTrajectory( + trajectory_id=self._current_episode_id, + device_id=device_id, + goal=self._current_task or "", + steps=tuple(self._current_steps), + started_at=self._current_started_at, + ended_at=ended_at, + ) + + episode = PhysicalEpisode( + episode_id=self._current_episode_id, + device_id=device_id, + task=self._current_task or "", + trajectory=trajectory, + initial_state=dict(self._current_initial_state), + final_state=dict(final_state), + verdict=verdict, + evidence_ids=tuple(self._current_evidence_ids), + success=success, + started_at=self._current_started_at, + ended_at=ended_at, + ) + + # Reset in-flight state before async operations. + self._current_episode_id = None + self._current_device_id = None + self._current_task = None + self._current_started_at = 0.0 + self._current_initial_state = {} + self._current_steps = [] + self._current_evidence_ids = [] + + # Persist to DuckDB (best-effort). + await self._persist_episode(episode) + + # Feed evolution engine (best-effort). + await self._feed_evolution(episode) + + self._episodes_recorded += 1 + logger.debug( + "Episode %s completed: success=%s, steps=%d, duration=%.2fs", + episode.episode_id, episode.success, + episode.step_count, episode.duration_s, + ) + return episode + + # -- Query ---------------------------------------------------------------- + + async def query_episodes( + self, + *, + device_id: str | None = None, + task: str | None = None, + success_only: bool = False, + limit: int = 50, + ) -> list[dict[str, Any]]: + """Retrieve recorded episodes by device/task/success.""" + return await asyncio.to_thread( + self._query_sync, device_id, task, success_only, limit + ) + + def _query_sync( + self, + device_id: str | None, + task: str | None, + success_only: bool, + limit: int, + ) -> list[dict[str, Any]]: + """Blocking query implementation.""" + if self._holder is None: + return [] + try: + connection = self._ensure_ready() + except Exception: # noqa: BLE001 + logger.debug("Could not open episode store for query", exc_info=True) + return [] + + conditions = [f"schema_version = {EPISODE_SCHEMA_VERSION}"] + params: list[Any] = [] + if device_id is not None: + conditions.append("device_id = ?") + params.append(device_id) + if task is not None: + conditions.append("task = ?") + params.append(task) + if success_only: + conditions.append("success = true") + + where = " AND ".join(conditions) + sql = ( + f"SELECT {', '.join(_COLUMNS)} FROM physical_episodes " + f"WHERE {where} ORDER BY started_at DESC LIMIT ?" + ) + params.append(int(limit)) + + try: + rows = connection.execute(sql, params).fetchall() + except Exception as exc: # noqa: BLE001 + logger.debug("Episode query failed: %s", exc) + return [] + return [dict(zip(_COLUMNS, row)) for row in rows] + + # -- Internal: state capture ---------------------------------------------- + + async def _capture_state(self, device_id: str) -> dict[str, Any]: + """Capture the current state of a device via ``read_batch``. + + Returns a channel-id → value mapping. Returns an empty dict when + the registry or device is unavailable (state capture must never + fail the episode lifecycle). + """ + state: dict[str, Any] = {} + try: + context = self._registry.context(device_id) + if context is None: + return state + channels = tuple( + ch.channel_id for ch in context.channels if ch.is_readable + ) + if not channels: + return state + batch = await self._registry.read_batch(device_id, channels) + for reading in batch.readings: + state[reading.channel_id] = reading.value + except Exception as exc: # noqa: BLE001 + logger.debug( + "State capture failed for %s: %s", device_id, exc, + ) + return state + + # -- Internal: DuckDB persistence ----------------------------------------- + + def _ensure_schema(self) -> None: + """Create the physical_episodes table if it does not exist.""" + if self._holder is None: + return + connection = self._holder.connection + connection.execute(_SCHEMA) + try: + connection.execute(_INDEX_DEVICE_TASK) + except Exception: # noqa: BLE001 + logger.debug("physical_episodes index unavailable", exc_info=True) + + def _ensure_ready(self) -> Any: + """Ensure schema exists and return the connection.""" + if self._holder is None: + raise RuntimeError("PhysicalEpisodeRecorder has no connection holder") + connection = self._holder.connection + if not self._db_ready: + self._ensure_schema() + self._db_ready = True + return connection + + async def _persist_episode(self, episode: PhysicalEpisode) -> None: + """Write an episode row to DuckDB (best-effort).""" + if self._holder is None: + return + await asyncio.to_thread(self._write_row, episode) + + def _write_row(self, episode: PhysicalEpisode) -> None: + """Blocking DuckDB insert, safe to call from a worker thread.""" + try: + connection = self._ensure_ready() + verdict_status: str | None = None + verdict_confidence: float | None = None + if episode.verdict is not None: + verdict_status = getattr(episode.verdict, "status", None) + verdict_confidence = getattr(episode.verdict, "confidence", None) + + trajectory_json = json.dumps( + episode.trajectory.to_dict() if episode.trajectory is not None else {}, + ensure_ascii=False, + default=str, + ) + initial_json = json.dumps( + dict(episode.initial_state), ensure_ascii=False, default=str, + ) + final_json = json.dumps( + dict(episode.final_state), ensure_ascii=False, default=str, + ) + evidence_json = json.dumps(list(episode.evidence_ids)) + metadata_json = json.dumps( + dict(episode.metadata), ensure_ascii=False, default=str, + ) + + row = ( + episode.episode_id, + episode.device_id, + episode.task, + episode.success, + episode.started_at, + episode.ended_at, + episode.step_count, + verdict_status, + verdict_confidence, + trajectory_json, + initial_json, + final_json, + evidence_json, + metadata_json, + EPISODE_SCHEMA_VERSION, + ) + connection.execute(_INSERT, row) + except Exception as exc: # noqa: BLE001 + logger.warning("Could not persist episode: %s", exc) + + # -- Internal: evolution feed --------------------------------------------- + + async def _feed_evolution(self, episode: PhysicalEpisode) -> None: + """Best-effort: publish the episode outcome to the evolution engine. + + Uses the ActionRecorder's outbox to publish a lightweight evolution + event carrying the episode outcome as capability observation evidence. + Skipped silently when ``action_recorder`` is ``None`` or when any + step fails. + """ + if self._action_recorder is None: + return + try: + outbox = getattr(self._action_recorder, "_outbox", None) + if outbox is None or not hasattr(outbox, "publish"): + return + + # Function-local imports to avoid hard coupling. + from leapflow.domain.event_types import EvolutionEventType + from leapflow.domain.evolution_event import EvolutionContext, EvolutionEvent + + event_type = ( + EvolutionEventType.ACTION_COMPLETED + if episode.success + else EvolutionEventType.ACTION_FAILED + ) + context = EvolutionContext.create( + correlation_id=f"episode-{episode.episode_id}", + action_id=episode.episode_id, + ) + event = EvolutionEvent.create( + event_type, + context=context, + payload={ + "action_type": "physical_episode", + "action_name": episode.task, + "ok": episode.success, + "device_id": episode.device_id, + "step_count": episode.step_count, + "duration_s": episode.duration_s, + "episode_id": episode.episode_id, + }, + producer="robot.episode_recorder", + dedup_key=f"episode.{episode.episode_id}", + ) + await outbox.publish(event, critical=False) + logger.debug( + "Episode %s fed to evolution as %s", + episode.episode_id, event_type, + ) + except Exception: # noqa: BLE001 + logger.debug( + "Evolution feed for episode %s failed", + episode.episode_id, exc_info=True, + ) + + # -- Cleanup -------------------------------------------------------------- + + def close(self) -> None: + """Close the database connection if owned.""" + if self._owns_holder and self._holder is not None: + try: + self._holder.close() + except Exception: # noqa: BLE001 + logger.debug("Episode store holder close failed", exc_info=True) + + +# --------------------------------------------------------------------------- +# SQL constants +# --------------------------------------------------------------------------- + +_COLUMNS = ( + "episode_id", + "device_id", + "task", + "success", + "started_at", + "ended_at", + "step_count", + "verdict_status", + "verdict_confidence", + "trajectory_json", + "initial_state_json", + "final_state_json", + "evidence_ids_json", + "metadata_json", +) + +_SCHEMA = """ +CREATE TABLE IF NOT EXISTS physical_episodes ( + episode_id VARCHAR PRIMARY KEY, + device_id VARCHAR NOT NULL, + task VARCHAR NOT NULL, + success BOOLEAN NOT NULL DEFAULT false, + started_at DOUBLE NOT NULL, + ended_at DOUBLE NOT NULL, + step_count INTEGER NOT NULL DEFAULT 0, + verdict_status VARCHAR, + verdict_confidence DOUBLE, + trajectory_json VARCHAR, + initial_state_json VARCHAR, + final_state_json VARCHAR, + evidence_ids_json VARCHAR, + metadata_json VARCHAR, + schema_version INTEGER DEFAULT 1, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +) +""" + +_INSERT = """ +INSERT OR REPLACE INTO physical_episodes ( + episode_id, device_id, task, success, + started_at, ended_at, step_count, + verdict_status, verdict_confidence, + trajectory_json, initial_state_json, final_state_json, + evidence_ids_json, metadata_json, schema_version +) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) +""" + +_INDEX_DEVICE_TASK = """ +CREATE INDEX IF NOT EXISTS idx_episode_device_task +ON physical_episodes (device_id, task, started_at) +""" + + +__all__ = [ + "DEFAULT_RETENTION_DAYS", + "EPISODE_SCHEMA_VERSION", + "PhysicalEpisode", + "PhysicalEpisodeRecorder", + "make_episode_id", +] diff --git a/src/leapflow/plugins/physical_capability_resolver.py b/src/leapflow/plugins/physical_capability_resolver.py new file mode 100644 index 00000000..74135a58 --- /dev/null +++ b/src/leapflow/plugins/physical_capability_resolver.py @@ -0,0 +1,505 @@ +# Copyright (c) Alibaba, Inc. and its affiliates. +"""Physical capability resolver: four-value adjudication for the physical domain. + +When a physical operation fails or underperforms, the resolver decides how +the framework should adapt. It maps physical capability gaps to the same +four-value verdict space the software evolution engine uses (absorb, rebind, +acquire, escalate), grounding each verdict in evidence: verification verdicts, +success rates, and environment outcomes rather than a single failure. + +The verdict costs are ordered: absorb (adjust a threshold, no new code) is +cheapest, escalate (ask a human to teleoperate) is used only when nothing +cheaper applies. +""" + +from __future__ import annotations + +import re +from collections.abc import Callable, Mapping +from dataclasses import dataclass, field +from enum import Enum +from typing import Any + +from leapflow.domain.adaptation_verdict import AdaptationVerdict + +__all__ = [ + "PhysicalGapType", + "PhysicalCapabilityGap", + "PhysicalCapabilityResolver", +] + +# Success-rate window below the precision bar that is still considered a +# marginal, in-band deviation the verifier can safely widen tolerance for. +_DEFAULT_ABSORB_MARGIN = 0.15 + +# Health states that mean the device is connected but not operating correctly. +_DEGRADED_HEALTH = frozenset({"degraded", "stale", "unreachable"}) + +# Physical-domain risk ceiling requested by an ``acquire`` verdict: a downloaded +# policy ultimately actuates hardware. Still clamped downstream by the trusted +# caller, exactly as a software ``acquire`` is. +_ACQUIRE_RISK_LEVEL = "mutating" + +_SEGMENT_RE = re.compile(r"[^a-z0-9]+") + + +def _slug_segment(value: str, fallback: str) -> str: + """Coerce *value* into one capability-name segment ``[a-z0-9][a-z0-9_]*``. + + Returns *fallback* when nothing usable survives normalisation, so the + assembled name always satisfies ``is_capability_name``. + """ + text = _SEGMENT_RE.sub("_", str(value or "").strip().lower()).strip("_") + return text[:32] or fallback + + +class PhysicalGapType(str, Enum): + """Classification of a physical capability gap.""" + + MISSING_SKILL = "missing_skill" # no policy/strategy for the manipulation + INSUFFICIENT_PRECISION = "insufficient_precision" # policy exists but verification fails too often + MISSING_DEVICE = "missing_device" # required device type not available + HARDWARE_DEGRADED = "hardware_degraded" # device connected but probes/reads failing + + +@dataclass(frozen=True) +class PhysicalCapabilityGap: + """A detected physical capability gap with supporting evidence.""" + + gap_type: str # PhysicalGapType + device_id: str + affordance: str = "" + success_rate: float = 0.0 + sample_count: int = 0 + recent_failures: tuple[str, ...] = field(default_factory=tuple) + detail: str = "" + + def to_dict(self) -> dict[str, Any]: + return { + "gap_type": self.gap_type, + "device_id": self.device_id, + "affordance": self.affordance, + "success_rate": self.success_rate, + "sample_count": self.sample_count, + "recent_failures": list(self.recent_failures), + "detail": self.detail, + } + + +class PhysicalCapabilityResolver: + """Adjudicates physical capability gaps into four-value verdicts. + + Dependencies: + - inference_registry: InferenceStrategyRegistry (for rebind candidate lookup) + - evidence_store: EvidenceStore (for success rate evidence) + - trust_gate: HardwareTrustGate (optional, for trust-aware decisions) + - acquire_probe: optional callable overriding the HF Hub acquisition check, + so the acquire path can be exercised without a network round-trip. + """ + + def __init__( + self, + *, + inference_registry: Any = None, + evidence_store: Any = None, + trust_gate: Any = None, + precision_threshold: float = 0.7, # success rate below which precision is insufficient + min_samples: int = 5, # minimum operations before adjudicating + absorb_margin: float = _DEFAULT_ABSORB_MARGIN, + acquire_probe: Callable[["PhysicalCapabilityGap"], bool] | None = None, + ) -> None: + self._inference_registry = inference_registry + self._evidence_store = evidence_store + self._trust_gate = trust_gate + self._precision_threshold = max(0.0, min(1.0, float(precision_threshold))) + self._min_samples = max(1, int(min_samples)) + self._absorb_margin = max(0.0, float(absorb_margin)) + self._acquire_probe = acquire_probe + + # ── Detection ── + + async def detect_gaps( + self, environment_outcome: Mapping[str, Any] + ) -> tuple[PhysicalCapabilityGap, ...]: + """Detect physical gaps from a PhysicalEnvironmentSource outcome observation. + + Reads per-device success rates and failure patterns, classifies each + underperforming device/affordance into a PhysicalGapType. + """ + devices = environment_outcome.get("devices") if environment_outcome else None + if not isinstance(devices, Mapping): + return () + gaps: list[PhysicalCapabilityGap] = [] + for device_id, stats in devices.items(): + if not isinstance(stats, Mapping): + continue + gaps.extend(self._classify_device(str(device_id), stats)) + return tuple(gaps) + + def _classify_device( + self, device_id: str, stats: Mapping[str, Any] + ) -> list[PhysicalCapabilityGap]: + """Classify one device's outcome stats into zero or more gaps. + + Structural faults (a disconnected or degraded device) short-circuit: + a device that cannot be read cannot also be judged on success rate. + """ + # Structural gaps first — these are facts, not statistics. + if not bool(stats.get("connected", True)): + return [ + PhysicalCapabilityGap( + gap_type=PhysicalGapType.MISSING_DEVICE.value, + device_id=device_id, + affordance=str(stats.get("affordance", "")), + detail=f"Device '{device_id}' is not connected.", + ) + ] + health = str(stats.get("health", "ok")).strip().lower() + if health in _DEGRADED_HEALTH: + return [ + PhysicalCapabilityGap( + gap_type=PhysicalGapType.HARDWARE_DEGRADED.value, + device_id=device_id, + affordance=str(stats.get("affordance", "")), + success_rate=float(stats.get("success_rate", 0.0) or 0.0), + sample_count=int(stats.get("total_operations", 0) or 0), + recent_failures=_as_str_tuple(stats.get("recent_failures")), + detail=f"Device '{device_id}' health is '{health}'.", + ) + ] + + gaps: list[PhysicalCapabilityGap] = [] + + # Affordances that were requested but have no policy at all. + for affordance in _as_str_tuple(stats.get("unsupported_affordances")): + gaps.append( + PhysicalCapabilityGap( + gap_type=PhysicalGapType.MISSING_SKILL.value, + device_id=device_id, + affordance=affordance, + sample_count=0, + detail=f"No policy provides affordance '{affordance}'.", + ) + ) + + # Underperformance, either per-affordance or at the device level. + breakdown = stats.get("affordances") + if isinstance(breakdown, Mapping) and breakdown: + for affordance, aff_stats in breakdown.items(): + if isinstance(aff_stats, Mapping): + gap = self._classify_performance( + device_id, str(affordance), aff_stats + ) + if gap is not None: + gaps.append(gap) + else: + gap = self._classify_performance( + device_id, str(stats.get("affordance", "")), stats + ) + if gap is not None: + gaps.append(gap) + return gaps + + def _classify_performance( + self, device_id: str, affordance: str, stats: Mapping[str, Any] + ) -> PhysicalCapabilityGap | None: + """Return an INSUFFICIENT_PRECISION gap when a policy underperforms. + + Requires at least one recorded operation; a device with no evidence + yields no gap here (missing skills come from an explicit declaration). + """ + total = int(stats.get("total_operations", stats.get("sample_count", 0)) or 0) + if total <= 0: + return None + success_rate = float(stats.get("success_rate", 0.0) or 0.0) + if success_rate >= self._precision_threshold: + return None + return PhysicalCapabilityGap( + gap_type=PhysicalGapType.INSUFFICIENT_PRECISION.value, + device_id=device_id, + affordance=affordance, + success_rate=success_rate, + sample_count=total, + recent_failures=_as_str_tuple(stats.get("recent_failures")), + detail=( + f"Affordance '{affordance}' on '{device_id}' succeeds " + f"{success_rate:.0%} of {total} operation(s)." + ), + ) + + # ── Adjudication ── + + async def adjudicate(self, gap: PhysicalCapabilityGap) -> AdaptationVerdict | None: + """Return an AdaptationVerdict for the gap. + + Decision logic: + - INSUFFICIENT_PRECISION with a small deviation → absorb (widen tolerance) + - INSUFFICIENT_PRECISION with alternative strategy available → rebind + - MISSING_SKILL with a downloadable policy on HF Hub → acquire + - MISSING_DEVICE → escalate (human must connect hardware) + - HARDWARE_DEGRADED → escalate (human must inspect device) + - Anything unresolvable cheaply → escalate (request teleoperation) + + Returns ``None`` when the evidence is too thin to conclude anything — + an underperforming policy is judged on repeated samples, never a single + failure. Structural gaps (missing/degraded hardware) are facts and are + never gated on sample count. + """ + gap_type = gap.gap_type + + if gap_type == PhysicalGapType.INSUFFICIENT_PRECISION.value: + if gap.sample_count < self._min_samples: + # Not enough evidence to conclude the policy is imprecise. + return None + return self._adjudicate_precision(gap) + + if gap_type == PhysicalGapType.MISSING_SKILL.value: + return self._adjudicate_missing_skill(gap) + + if gap_type == PhysicalGapType.MISSING_DEVICE.value: + return self._escalate_missing_device(gap) + + if gap_type == PhysicalGapType.HARDWARE_DEGRADED.value: + return self._escalate_degraded(gap) + + # Unknown or otherwise unresolvable gap: fall back to teleoperation. + return self._escalate_teleop(gap) + + def _adjudicate_precision(self, gap: PhysicalCapabilityGap) -> AdaptationVerdict: + """Cheapest-first: widen tolerance, else switch strategy, else escalate.""" + deviation = self._precision_threshold - gap.success_rate + if deviation <= self._absorb_margin: + return self._absorb(gap, deviation) + candidate = self._find_rebind_candidate(gap) + if candidate: + return self._rebind(gap, candidate) + return self._escalate_teleop(gap) + + def _adjudicate_missing_skill(self, gap: PhysicalCapabilityGap) -> AdaptationVerdict: + """Rebind to an installed alternative, else acquire, else escalate.""" + candidate = self._find_rebind_candidate(gap) + if candidate: + return self._rebind(gap, candidate) + if self._can_acquire(gap): + return self._acquire(gap) + return self._escalate_teleop(gap) + + # ── Verdict builders ── + + def _absorb(self, gap: PhysicalCapabilityGap, deviation: float) -> AdaptationVerdict: + return AdaptationVerdict.create( + "absorb", + self._capability(gap), + knowledge=( + f"Operations for affordance '{gap.affordance}' on device " + f"'{gap.device_id}' succeed {gap.success_rate:.0%} of the time, " + f"marginally below the {self._precision_threshold:.0%} precision bar; " + "the deviation is within the tolerance the verifier can safely widen." + ), + rationale=( + "Widening the verifier tolerance is the cheapest correct adaptation " + "for a marginal, in-band deviation; no new code or strategy is warranted." + ), + confidence=self._evidence_confidence(gap), + expected_effect="widen the verifier tolerance for the affected channel", + target_affordance=gap.affordance, + ) + + def _rebind(self, gap: PhysicalCapabilityGap, candidate: str) -> AdaptationVerdict: + return AdaptationVerdict.create( + "rebind", + self._capability(gap), + knowledge=( + f"An alternative inference strategy '{candidate}' is registered for " + f"affordance '{gap.affordance}' and should serve device " + f"'{gap.device_id}', where the current policy underperforms " + f"({gap.success_rate:.0%} success)." + ), + rationale=( + "A registered alternative strategy already covers this affordance, " + "so switching to it is cheaper than acquiring new code." + ), + confidence=self._evidence_confidence(gap), + target=candidate, + expected_effect=f"route '{gap.affordance}' through strategy '{candidate}'", + target_affordance=gap.affordance, + ) + + def _acquire(self, gap: PhysicalCapabilityGap) -> AdaptationVerdict: + return AdaptationVerdict.create( + "acquire", + self._capability(gap), + knowledge=( + f"No installed policy performs affordance '{gap.affordance}' on device " + f"'{gap.device_id}', and a downloadable policy candidate exists that " + "could fill this gap." + ), + rationale=( + "No installed strategy covers this manipulation and a candidate policy " + "is downloadable; acquiring is the only path that fills the gap." + ), + confidence=self._evidence_confidence(gap), + max_risk_level=_ACQUIRE_RISK_LEVEL, # type: ignore[arg-type] + expected_effect=f"download and register a policy for '{gap.affordance}'", + target_affordance=gap.affordance, + ) + + def _escalate_missing_device(self, gap: PhysicalCapabilityGap) -> AdaptationVerdict: + return AdaptationVerdict.create( + "escalate", + self._capability(gap), + knowledge=( + f"No device of the type required for affordance '{gap.affordance}' is " + f"currently connected (device '{gap.device_id}' is unavailable); the " + "manipulation cannot proceed without hardware." + ), + rationale=( + "The framework cannot connect hardware on its own; a human must attach " + "the required device." + ), + confidence=1.0, + target=f"connect the hardware required for '{gap.affordance}'", + target_affordance=gap.affordance, + ) + + def _escalate_degraded(self, gap: PhysicalCapabilityGap) -> AdaptationVerdict: + return AdaptationVerdict.create( + "escalate", + self._capability(gap), + knowledge=( + f"Device '{gap.device_id}' is connected but its probes/reads are " + "failing; it needs physical inspection before automated operation " + "resumes." + ), + rationale=( + "A degraded device is a physical fault the framework cannot repair; " + "a human must inspect it." + ), + confidence=1.0, + target=f"inspect device '{gap.device_id}'", + target_affordance=gap.affordance, + ) + + def _escalate_teleop(self, gap: PhysicalCapabilityGap) -> AdaptationVerdict: + return AdaptationVerdict.create( + "escalate", + self._capability(gap), + knowledge=( + f"No cheaper adaptation resolves the '{gap.affordance}' gap on device " + f"'{gap.device_id}'; a human operator must teleoperate to complete " + "the task." + ), + rationale=( + "Every cheaper verdict (absorb, rebind, acquire) was exhausted; " + "teleoperation is the safe fallback." + ), + confidence=self._evidence_confidence(gap), + target=f"teleoperate to complete '{gap.affordance}'", + target_affordance=gap.affordance, + ) + + # ── Helpers ── + + def _find_rebind_candidate(self, gap: PhysicalCapabilityGap) -> str | None: + """Look for an alternative inference strategy for the same affordance. + + Prefers a strategy that declares the affordance explicitly; falls back + to an id substring match when strategies declare no affordances. + """ + registry = self._inference_registry + if registry is None: + return None + affordance = gap.affordance.strip().lower() + if not affordance: + return None + list_strategies = getattr(registry, "list_strategies", None) + if not callable(list_strategies): + return None + try: + summaries = list_strategies() + except Exception: # noqa: BLE001 – a lookup failure is not a resolver failure + return None + + get = getattr(registry, "get", None) + substring_match: str | None = None + for entry in summaries or (): + strategy_id = str((entry or {}).get("strategy_id") or "") + if not strategy_id: + continue + declared = self._declared_affordances(get, strategy_id) + if declared: + if affordance in declared: + return strategy_id + continue + if substring_match is None and affordance in strategy_id.lower(): + substring_match = strategy_id + return substring_match + + @staticmethod + def _declared_affordances(get: Any, strategy_id: str) -> set[str]: + """Return the affordances a strategy declares, or an empty set.""" + if not callable(get): + return set() + try: + strategy = get(strategy_id) + except Exception: # noqa: BLE001 + return set() + declared = getattr(strategy, "affordances", ()) if strategy is not None else () + return {str(a).strip().lower() for a in (declared or ()) if str(a).strip()} + + def _can_acquire(self, gap: PhysicalCapabilityGap) -> bool: + """Check whether a policy for this affordance could be downloaded. + + Best-effort: an injected ``acquire_probe`` decides when provided; + otherwise the Hugging Face Hub is queried. ``huggingface_hub`` is an + optional dependency, so an unavailable hub (import error or query + failure) yields ``False`` rather than raising. + """ + if self._acquire_probe is not None: + try: + return bool(self._acquire_probe(gap)) + except Exception: # noqa: BLE001 – a probe failure is a negative answer + return False + affordance = gap.affordance.strip() + if not affordance: + return False + try: + from huggingface_hub import HfApi # optional dependency + except ImportError: + return False + try: + api = HfApi() + results = api.list_models(search=affordance, limit=1) + return any(True for _ in results) + except Exception: # noqa: BLE001 – network/hub failure ⇒ cannot acquire + return False + + def _capability(self, gap: PhysicalCapabilityGap) -> str: + """Build a valid dotted capability name for the gap. + + Shaped ``physical.[.]`` so it satisfies + ``is_capability_name`` (2–4 lowercase dotted segments). + """ + device = _slug_segment(gap.device_id, "device") + affordance = _slug_segment(gap.affordance, "") + if affordance: + return f"physical.{device}.{affordance}" + return f"physical.{device}" + + def _evidence_confidence(self, gap: PhysicalCapabilityGap) -> float: + """Confidence in the verdict, scaled by how much evidence supports it.""" + if gap.sample_count <= 0: + return 0.0 + return round(min(1.0, gap.sample_count / (self._min_samples * 2.0)), 3) + + +def _as_str_tuple(value: Any) -> tuple[str, ...]: + """Coerce an optional sequence into a tuple of non-empty strings.""" + if not value: + return () + if isinstance(value, str): + return (value,) + try: + return tuple(str(item) for item in value if str(item)) + except TypeError: + return () diff --git a/src/leapflow/plugins/tool_plugins/__init__.py b/src/leapflow/plugins/tool_plugins/__init__.py index 7c13e0b6..a172c5ba 100644 --- a/src/leapflow/plugins/tool_plugins/__init__.py +++ b/src/leapflow/plugins/tool_plugins/__init__.py @@ -51,11 +51,12 @@ # tool_describe). Read-only CORE tools; appended after desktop_semantic so # existing tool-index ordering is preserved. "leapflow.plugins.tool_plugins.bridge", - # Hardware Context Protocol — appended last, and contributes no tools until a - # hardware registry is bound. With hardware disabled the tool index is - # byte-identical to a build without it, which is what keeps the journey - # cassette fingerprints valid. + # Hardware plugins contribute no tools until a hardware registry is bound. + # Physical skills precede capability routing so policy-backed affordances can + # resolve the skill plugin when runtime dependencies are assembled. "leapflow.hardware.plugin", + "leapflow.plugins.tool_plugins.physical_skill", + "leapflow.hardware.capability_router", ) diff --git a/src/leapflow/plugins/tool_plugins/_physical_skill_helpers.py b/src/leapflow/plugins/tool_plugins/_physical_skill_helpers.py new file mode 100644 index 00000000..35ccc5a1 --- /dev/null +++ b/src/leapflow/plugins/tool_plugins/_physical_skill_helpers.py @@ -0,0 +1,265 @@ +# Copyright (c) Alibaba, Inc. and its affiliates. +"""Module-level helpers for :mod:`physical_skill`. + +Split from the main plugin module so the plugin file stays under the +engine sub-package's 800-line ceiling. Nothing here holds state -- the +tool metadata factory, compute-budget coercion, action-to-command +mapping, and HuggingFace-cache discovery are all pure functions that +take the plugin (or plain arguments) and return derived data. +""" + +from __future__ import annotations + +import logging +from typing import TYPE_CHECKING, Any + +from leapflow.plugins.protocol import ToolMetadata +from leapflow.robot.inference.strategy import ComputeBudget + +if TYPE_CHECKING: + from leapflow.plugins.tool_plugins.physical_skill import PhysicalSkillPlugin + +logger = logging.getLogger(__name__) + +__all__ = [ + "build_physical_skill_tools", + "coerce_compute_budget", + "action_to_commands", + "discover_local_policies", +] + + +def build_physical_skill_tools( + plugin: PhysicalSkillPlugin, +) -> list[ToolMetadata]: + """Build the ``ToolMetadata`` list exposed by the physical skill plugin. + + Kept out of the plugin class so the class stays focused on the OODA+V + execution loop; adding a new tool means editing this factory, not the + hot path. + """ + return [ + ToolMetadata( + name="hw_policy_infer", + description=( + "Execute VLA policy inference on a robot device. " + "Reads observation, runs policy, optionally writes action " + "to actuators. Supports local (HuggingFace) and remote " + "(PolicyServer) policies." + ), + handler=plugin.policy_infer, + parameters_schema={ + "type": "object", + "properties": { + "device_id": {"type": "string", "description": "Target robot device"}, + "policy": {"type": "string", "description": "Policy path or 'remote:host:port'"}, + "task": {"type": "string", "description": "Natural language task description"}, + "execute": {"type": "boolean", "description": "Write action to device (default true)"}, + "chunk_size": {"type": "integer", "description": "Action steps to predict (default 1)"}, + "verify": {"type": "boolean", "description": "Run post-execution verification"}, + "compute_budget": { + "type": "object", + "description": ( + "Test-time compute budget: trades latency for quality " + "by controlling ensemble size, chunk size, and max " + "latency. Strategies ignore knobs they do not support." + ), + "properties": { + "max_latency_ms": { + "type": "number", + "description": "Maximum acceptable inference latency in milliseconds", + }, + "ensemble_size": { + "type": "integer", + "description": "Number of ensemble samples to draw and vote", + }, + "chunk_size": { + "type": "integer", + "description": ( + "Future action steps to predict in one call " + "(overrides top-level chunk_size)" + ), + }, + "max_refinement_steps": { + "type": "integer", + "description": "Iterative refinement steps to run", + }, + "confidence_threshold": { + "type": "number", + "description": "Confidence below which more compute should be spent", + }, + }, + }, + }, + "required": ["device_id", "policy"], + }, + x_leapflow={ + "category": "hardware", + "risk_level": "high", + "mutates_state": True, + "execution_policy": "serial", + "requires_capabilities": ["hardware_control"], + "provides_capabilities": ["physical_skill_inference"], + }, + mutates_state=True, + execution_policy="serial", + requires_capabilities=("hardware_control",), + provides_capabilities=("physical_skill_inference",), + ), + ToolMetadata( + name="hw_policy_episode", + description=( + "Manage policy inference episode lifecycle. Start an episode " + "to begin a task, stop to finalise and get stats." + ), + handler=plugin.policy_episode, + parameters_schema={ + "type": "object", + "properties": { + "action": {"type": "string", "enum": ["start", "stop", "status"]}, + "device_id": {"type": "string", "description": "Robot device (for start)"}, + "policy": {"type": "string", "description": "Policy path (for start)"}, + "task": {"type": "string", "description": "Task description"}, + }, + "required": ["action"], + }, + x_leapflow={ + "category": "hardware", + "risk_level": "medium", + "mutates_state": False, + }, + ), + ToolMetadata( + name="hw_policy_list", + description="List available VLA policies and their capabilities.", + handler=plugin.policy_list, + parameters_schema={ + "type": "object", + "properties": { + "source": {"type": "string", "enum": ["local", "remote", "all"]}, + }, + }, + x_leapflow={ + "category": "hardware", + "risk_level": "low", + "mutates_state": False, + }, + ), + ] + + +def coerce_compute_budget( + raw: Any, + fallback_chunk_size: int, +) -> ComputeBudget | None: + """Build a :class:`ComputeBudget` from raw tool parameters. + + Returns ``None`` when the caller supplied neither a ``compute_budget`` + dict nor a chunk override, so a strategy that does not care about + budgets is not forced to interpret defaults. Non-numeric or missing + knobs fall back to the dataclass defaults; ``chunk_size`` inherits + from the top-level ``chunk_size`` parameter when the budget dict + omits it, keeping the tool's call-site behaviour compatible with the + pre-registry contract. + """ + if raw is None and fallback_chunk_size <= 1: + return None + knobs: dict[str, Any] = {} + if isinstance(raw, dict): + for key in ( + "max_latency_ms", + "max_refinement_steps", + "ensemble_size", + "confidence_threshold", + "chunk_size", + ): + if key in raw and raw[key] is not None: + knobs[key] = raw[key] + knobs.setdefault("chunk_size", max(1, int(fallback_chunk_size or 1))) + try: + return ComputeBudget(**knobs) + except (TypeError, ValueError): + logger.warning("Invalid compute_budget %r; ignoring", raw) + return ComputeBudget(chunk_size=max(1, int(fallback_chunk_size or 1))) + + +def action_to_commands( + action: Any, + device: Any, +) -> list[tuple[str, Any]]: + """Convert an action vector to ``(channel_id, value)`` command pairs. + + A dict action is trusted verbatim (its keys are the channel ids). A + sequence is mapped positionally to the device's writable, non-frame + channels; extra values without a matching channel are dropped with a + debug log rather than silently applied to the wrong actuator. + """ + if isinstance(action, dict): + return [(str(k), v) for k, v in action.items()] + + # Resolve to a plain list. + values: list[Any] + if hasattr(action, "tolist"): + values = action.tolist() + elif hasattr(action, "cpu"): + values = action.cpu().tolist() + elif isinstance(action, (list, tuple)): + values = list(action) + else: + values = [action] + + # Flatten nested lists (e.g. [[v1, v2, ...]] from chunked inference). + if len(values) == 1 and isinstance(values[0], list): + values = values[0] + + # Map values to writable channels from the device context. + context = getattr(device, "context", None) + writable_channels: list[str] = [] + if context is not None: + for ch in context.channels: + if getattr(ch, "is_writable", False): + # Skip frame channels. + if getattr(ch, "representation", "") == "frame": + continue + writable_channels.append(ch.channel_id) + + commands: list[tuple[str, Any]] = [] + for i, val in enumerate(values): + if i < len(writable_channels): + commands.append((writable_channels[i], val)) + else: + logger.debug( + "Action value at index %d has no mapped writable channel; dropped", + i, + ) + return commands + + +def discover_local_policies() -> list[dict[str, Any]]: + """Best-effort discovery of locally cached HuggingFace policies. + + Empty when ``huggingface_hub`` is not installed or the scan raises; + the caller merges the result with already-loaded strategies, so a + discovery failure only means an incomplete list, never a wrong one. + """ + try: + from huggingface_hub import scan_cache_dir # type: ignore[import-untyped] + except ImportError: + return [] + + policies: list[dict[str, Any]] = [] + try: + cache_info = scan_cache_dir() + for repo in cache_info.repos: + # Heuristic: policy repos typically contain "policy" or "act" in the name. + repo_id = str(repo.repo_id) + if any(kw in repo_id.lower() for kw in ("policy", "act", "vla", "diffusion")): + policies.append({ + "name": repo_id, + "type": "local", + "loaded": False, + "source": "huggingface_cache", + }) + except Exception: # noqa: BLE001 - discovery must not crash the plugin + logger.debug("Local policy discovery failed", exc_info=True) + return policies diff --git a/src/leapflow/plugins/tool_plugins/physical_skill.py b/src/leapflow/plugins/tool_plugins/physical_skill.py new file mode 100644 index 00000000..0e8a1986 --- /dev/null +++ b/src/leapflow/plugins/tool_plugins/physical_skill.py @@ -0,0 +1,739 @@ +# Copyright (c) Alibaba, Inc. and its affiliates. +"""Physical skill plugin: VLA policy inference as LeapFlow tools. + +This plugin bridges the gap between learned manipulation policies and +LeapFlow's tool execution pipeline. A VLA model's ``select_action`` +becomes a tool call that the agent can invoke, with the full governance +chain (PCD disclosure, risk classification, approval, trust, audit) +applied to the physical execution that follows. + +Inference is delegated to the ``InferenceStrategyRegistry``: a policy +string is resolved to an :class:`InferenceStrategy` (``VLALocalStrategy`` +for a local checkpoint, ``VLARemoteStrategy`` for a ``remote:host:port`` +address) that is registered under a unique ``strategy_id`` and reused +across sessions. Both paths produce an :class:`InferenceResult` whose +action vector is written to the robot through ``BatchTransport.write_batch()`` +(or sequential writes as fallback). +""" + +from __future__ import annotations + +import logging +import time +from typing import Any + +from leapflow.plugins.protocol import ToolMetadata +from leapflow.plugins.tool_plugins._physical_skill_helpers import ( + action_to_commands as _action_to_commands, + build_physical_skill_tools as _build_physical_skill_tools, + coerce_compute_budget as _coerce_compute_budget, + discover_local_policies as _discover_local_policies, +) +from leapflow.robot.inference.registry import ( + InferenceStrategyRegistry, + get_default_registry, +) +from leapflow.robot.inference.strategy import ( + ComputeBudget, + InferenceResult, + InferenceStrategy, +) + +logger = logging.getLogger(__name__) + + +# --------------------------------------------------------------------------- +# Plugin +# --------------------------------------------------------------------------- + + +class PhysicalSkillPlugin: + """ToolPlugin that exposes VLA policy inference as LeapFlow tools. + + Dependencies (injected via bind_runtime): + - hardware_registry: HardwareRegistry instance for device access + - hardware_approval_gate: approval orchestrator for physical operations + - hardware_trust_gate: trust gate for progressive trust + - effect_scope: EffectScope for cleanup registration + - session_id: current session identifier + """ + + def __init__( + self, + *, + strategy_registry: InferenceStrategyRegistry | None = None, + ) -> None: + self._registry: Any = None + self._gate: Any = None + self._trust_gate: Any = None + self._scope: Any = None + self._session_id: str = "" + self._tools: list[ToolMetadata] | None = None + # Strategy cache: policy_path -> InferenceStrategy instance. + # The plugin keeps its own reference so scope teardown can close only + # the strategies this plugin instance is aware of, without unregistering + # them from the process-wide default registry (a shared local model + # loaded once must survive one session tearing down). + self._policies: dict[str, InferenceStrategy] = {} + # Default strategy registry; injected for tests, otherwise the + # module-level singleton. A shared registry lets a heavy local model + # loaded by one session serve every subsequent session in the process. + self._strategy_registry: InferenceStrategyRegistry = ( + strategy_registry if strategy_registry is not None else get_default_registry() + ) + # Episode state (per-plugin, session-level, not persisted). When an + # episode is active this holds the strategy it was started with, so + # every ``policy_infer`` inside it re-uses the same instance and its + # chunked-action queue rather than a fresh strategy per turn. + self._active_episode: dict[str, Any] | None = None + self._teardown_registered: bool = False + + @property + def plugin_id(self) -> str: + return "physical_skill" + + @property + def category(self) -> str: + return "hardware" + + @property + def dependencies(self) -> list[str]: + return [ + "hardware_registry", + "hardware_approval_gate", + "hardware_trust_gate", + "effect_scope", + "session_id", + ] + + def bind_runtime(self, **deps: Any) -> None: + """Receive dependencies. Registry is optional -- no registry means empty tools.""" + registry_changed = False + if "hardware_registry" in deps: + self._registry = deps.get("hardware_registry") + registry_changed = True + if "hardware_approval_gate" in deps: + self._gate = deps.get("hardware_approval_gate") + if "hardware_trust_gate" in deps: + self._trust_gate = deps.get("hardware_trust_gate") + if "session_id" in deps: + self._session_id = str(deps.get("session_id") or "") + if "effect_scope" in deps: + self._scope = deps.get("effect_scope") + self._teardown_registered = False + + if registry_changed: + self._tools = None + self._policies.clear() + self._active_episode = None + self._teardown_registered = False + + if self._registry is None: + return + + self._register_teardown() + + @property + def tools(self) -> list[ToolMetadata]: + """Return tool metadata, empty until registry is bound.""" + if self._registry is None: + return [] + if self._tools is None: + self._tools = _build_physical_skill_tools(self) + return list(self._tools) + + # -- Tool handlers ------------------------------------------------ + + async def policy_infer(self, params: dict[str, Any]) -> dict[str, Any]: + """Execute one inference step and optionally write the action to hardware.""" + device_id: str = params.get("device_id", "") + policy_path: str = params.get("policy", "") + execute: bool = params.get("execute", True) + chunk_size: int = max(1, int(params.get("chunk_size", 1) or 1)) + verify: bool = params.get("verify", False) + budget = _coerce_compute_budget(params.get("compute_budget"), chunk_size) + + if not device_id: + return {"ok": False, "error": "device_id is required"} + if not policy_path: + return {"ok": False, "error": "policy path is required"} + + # 1. Resolve the inference strategy for this policy path. + try: + strategy = await self._load_policy(policy_path) + except Exception as exc: + return {"ok": False, "error": f"failed to load policy: {exc}"} + + # 2. Read current observation from the robot. + try: + observation = await self._read_observation(device_id) + except Exception as exc: + return {"ok": False, "error": f"failed to read observation: {exc}"} + + # 3. Run policy inference through the strategy contract. + try: + result = await self._run_inference( + strategy, observation, budget=budget, + ) + except Exception as exc: + return {"ok": False, "error": f"inference failed: {exc}"} + action = result.action + + # 4. Optionally write the action to the robot. + execution: dict[str, Any] | None = None + if execute: + try: + execution = await self._execute_action( + device_id, action, verify=verify, + ) + except Exception as exc: + return self._format_result( + result, {"ok": False, "error": str(exc)}, None, + ) + + # 5. Verification result is embedded in execution if requested. + verification: dict[str, Any] | None = None + if execution and "verification" in execution: + verification = execution.pop("verification", None) + + # 6. Update episode stats if an episode is active. + if self._active_episode is not None: + self._active_episode["steps"] += 1 + self._active_episode["last_action_time"] = time.time() + + return self._format_result(result, execution, verification) + + async def policy_episode(self, params: dict[str, Any]) -> dict[str, Any]: + """Manage episode lifecycle: start / stop / status.""" + action: str = params.get("action", "") + if action not in ("start", "stop", "status"): + return {"ok": False, "error": "action must be 'start', 'stop', or 'status'"} + + if action == "status": + if self._active_episode is None: + return {"ok": True, "active": False, "episode": None} + elapsed = time.time() - self._active_episode["start_time"] + return { + "ok": True, + "active": True, + "episode": { + **self._active_episode, + "elapsed_s": round(elapsed, 2), + }, + } + + if action == "start": + device_id: str = params.get("device_id", "") + policy_path: str = params.get("policy", "") + task_desc: str = params.get("task", "") + if not device_id: + return {"ok": False, "error": "device_id is required for episode start"} + if not policy_path: + return {"ok": False, "error": "policy path is required for episode start"} + + if self._active_episode is not None: + return { + "ok": False, + "error": "an episode is already active; stop it before starting a new one", + } + + # Load and reset the policy. + try: + strategy = await self._load_policy(policy_path) + await strategy.reset() + except Exception as exc: + return {"ok": False, "error": f"failed to initialise episode: {exc}"} + + self._active_episode = { + "device_id": device_id, + "policy": policy_path, + "task": task_desc, + "start_time": time.time(), + "steps": 0, + "last_action_time": None, + } + return {"ok": True, "action": "started", "episode": dict(self._active_episode)} + + # action == "stop" + if self._active_episode is None: + return {"ok": False, "error": "no active episode to stop"} + + episode = dict(self._active_episode) + elapsed = time.time() - episode["start_time"] + episode["elapsed_s"] = round(elapsed, 2) + episode["avg_step_time_s"] = ( + round(elapsed / episode["steps"], 3) if episode["steps"] > 0 else None + ) + self._active_episode = None + return {"ok": True, "action": "stopped", "summary": episode} + + async def policy_list(self, params: dict[str, Any]) -> dict[str, Any]: + """List available policies and their capabilities.""" + source: str = params.get("source", "all") + policies: list[dict[str, Any]] = [] + + # List cached / loaded strategies keyed by policy path. + if source in ("local", "all"): + for path, strategy in self._policies.items(): + if path.startswith("remote:"): + continue + entry: dict[str, Any] = { + "name": path, + "type": "local", + "loaded": True, + "strategy_id": getattr(strategy, "strategy_id", ""), + } + profile = getattr(strategy, "compute_profile", None) + if profile is not None: + entry["latency_range_ms"] = list(profile.latency_range_ms) + entry["supports_chunking"] = profile.supports_chunking + policies.append(entry) + + if source in ("remote", "all"): + for path, strategy in self._policies.items(): + if not path.startswith("remote:"): + continue + entry = { + "name": path, + "type": "remote", + "loaded": True, + "address": path.removeprefix("remote:"), + "strategy_id": getattr(strategy, "strategy_id", ""), + } + policies.append(entry) + + # Discover local policies through LeapRobot if available. + if source in ("local", "all"): + discovered = _discover_local_policies() + # Deduplicate against already-loaded. + loaded_names = {p["name"] for p in policies} + for d in discovered: + if d["name"] not in loaded_names: + policies.append(d) + + return {"ok": True, "policies": policies, "count": len(policies)} + + # -- Policy loading ------------------------------------------------ + + async def _load_policy(self, policy_path: str) -> InferenceStrategy: + """Resolve a policy path to an :class:`InferenceStrategy` instance. + + The strategy is registered in the shared registry under a unique + ``strategy_id`` (``vla_local:`` or ``vla_remote:
``) + so a heavy local model loaded once is reused by every subsequent + session, while the plugin keeps a local reference in + ``self._policies`` so scope teardown can close the strategies it + touched without unregistering shared instances. + """ + cached = self._policies.get(policy_path) + if cached is not None: + return cached + + if policy_path.startswith("remote:"): + address = policy_path.removeprefix("remote:") + strategy_id = f"vla_remote:{address}" + strategy = self._strategy_registry.get(strategy_id) + if strategy is None: + from leapflow.robot.inference.vla_remote import VLARemoteStrategy + + strategy = VLARemoteStrategy(server_address=address) + # Override the class-level id so every remote endpoint gets + # its own registry slot -- otherwise the second address would + # collide with the first under the shared ``vla_remote`` key. + strategy.strategy_id = strategy_id # type: ignore[misc] + self._strategy_registry.register(strategy) + self._policies[policy_path] = strategy + logger.info("Remote VLA strategy registered for %s", address) + return strategy + + # Local policy path: build a VLALocalStrategy. The strategy loads the + # underlying model lazily on first infer, so construction is cheap and + # does not require torch to be importable here. + strategy_id = f"vla_local:{policy_path}" + strategy = self._strategy_registry.get(strategy_id) + if strategy is None: + from leapflow.robot.inference.vla_local import VLALocalStrategy + + strategy = VLALocalStrategy(policy_path=policy_path) + strategy.strategy_id = strategy_id # type: ignore[misc] + self._strategy_registry.register(strategy) + self._policies[policy_path] = strategy + logger.info("Local VLA strategy registered for %s", policy_path) + return strategy + + async def _run_inference( + self, + strategy: InferenceStrategy, + observation: dict[str, Any], + *, + budget: ComputeBudget | None = None, + ) -> InferenceResult: + """Run inference through the :class:`InferenceStrategy` contract. + + The strategy owns model loading, transport lifecycle, and any + budget interpretation (chunk size, ensemble, refinement). The + plugin passes the observation through unchanged and receives an + :class:`InferenceResult` whose ``action`` is turned into hardware + commands downstream. + """ + return await strategy.infer(observation, budget=budget) + + # -- Observation reading ------------------------------------------- + + async def _read_observation(self, device_id: str) -> dict[str, Any]: + """Read full observation via registry. Prefers read_batch for atomicity.""" + if self._registry is None: + raise RuntimeError("hardware registry is not bound") + + # Attempt batch read first. + device = await self._resolve_device(device_id) + transport = getattr(device, "transport", None) + if transport is None: + transport = device + + # Collect readable channel ids. + channels: list[str] = [] + context = getattr(device, "context", None) + if context is not None: + for ch in context.channels: + # Skip frame channels -- they are not part of observation vectors. + if getattr(ch, "representation", "") == "frame": + continue + channels.append(ch.channel_id) + + from leapflow.hardware.transport import BatchTransport + + if isinstance(transport, BatchTransport) and channels: + batch = await transport.read_batch(tuple(channels)) + return {r.channel_id: r.value for r in batch.readings} + + # Fallback: sequential reads. + obs: dict[str, Any] = {} + for cid in channels: + try: + reading = await self._registry.read(device_id, cid) + obs[cid] = reading.value + except Exception: + logger.debug( + "Failed to read channel %s/%s for observation", + device_id, cid, exc_info=True, + ) + return obs + + # -- Action execution ---------------------------------------------- + + async def _execute_action( + self, + device_id: str, + action: Any, + *, + verify: bool = False, + ) -> dict[str, Any]: + """Write action vector to robot and optionally verify.""" + if self._registry is None: + raise RuntimeError("hardware registry is not bound") + + device = await self._resolve_device(device_id) + transport = getattr(device, "transport", None) + if transport is None: + transport = device + + # Build command tuples from the action vector. + commands = _action_to_commands(action, device) + + # Approval gate -- every physical write must be authorised before it + # touches an actuator. Fail closed: an absent or failing gate denies. + if commands and not await self._approve_write(device_id, commands): + return { + "ok": False, + "side_effect_state": "none", + "channels_written": 0, + "error": "Physical operation not approved by hardware approval gate", + } + + from leapflow.hardware.transport import BatchTransport, BatchWriteOutcome + + result: dict[str, Any] + if isinstance(transport, BatchTransport) and commands: + outcome: BatchWriteOutcome = await transport.write_batch(tuple(commands)) + result = { + "ok": outcome.ok, + "side_effect_state": outcome.side_effect_state, + "channels_written": len(commands), + } + else: + # Fallback: sequential writes. + written = 0 + last_error = "" + for cid, val in commands: + try: + w = await self._registry.write(device_id, cid, val) + if w.ok: + written += 1 + else: + last_error = w.error + except Exception as exc: + last_error = str(exc) + result = { + "ok": written == len(commands), + "channels_written": written, + "channels_total": len(commands), + } + if last_error: + result["last_error"] = last_error + + # Optional verification. + if verify and result.get("ok"): + verification = await self._verify_execution(device_id, action, commands) + if verification: + result["verification"] = verification + + return result + + async def _approve_write( + self, + device_id: str, + commands: list[tuple[str, Any]], + ) -> bool: + """Fail-closed approval check for a physical write. + + Mirrors ``HardwareTools._evaluate``: no gate installed, or a gate that + raises, both deny. For a physical device the cost of a wrong "allow" + is not measured in data, so a broken gate must never become an open + door. + """ + if self._gate is None: + logger.error( + "No hardware approval gate is bound; refusing physical write " + "to %s (fail-closed).", + device_id, + ) + return False + + descriptor = self._build_approval_descriptor(device_id, commands) + try: + result = await self._gate.evaluate(descriptor) + except Exception as exc: # noqa: BLE001 - a failing gate must deny, not propagate + logger.error( + "Hardware approval gate raised for %s: %s", + device_id, exc, exc_info=True, + ) + return False + return bool(getattr(result, "approved", False)) + + def _build_approval_descriptor( + self, + device_id: str, + commands: list[tuple[str, Any]], + ) -> Any: + """Build an ActionDescriptor for a batched physical actuation. + + Mirrors the batch descriptor built by ``HardwareTools`` so the same + approval orchestrator evaluates policy-driven writes and direct + hardware writes through one contract. + """ + from leapflow.security.actions import ActionDescriptor, ActionKind + + channel_summaries = [ + {"channel_id": cid, "value": val} for cid, val in commands + ] + return ActionDescriptor.device( + kind=ActionKind.DEVICE_ACTUATE.value, + device_id=device_id, + channel_id=commands[0][0] if commands else "", + quantity="batch", + value=f"{len(commands)} channels", + unit="", + envelope_band="batch", + metadata={ + "batch": True, + "commands": channel_summaries, + "session_id": self._session_id, + "source": "hw_policy_infer", + }, + ) + + async def _verify_execution( + self, + device_id: str, + action: Any, + commands: list[tuple[str, Any]], + ) -> dict[str, Any] | None: + """Run post-execution verification using OperationVerifier.""" + if not commands: + return None + + try: + from leapflow.hardware.verification import collect_evidence + except ImportError: + return None + + # Verify the first commanded channel as representative. + channel_id, intended = commands[0] + try: + evidence = await collect_evidence( + self._registry, + device_id, + channel_id, + intended_value=intended, + outcome=None, + settle_delay_s=0.05, + ) + except Exception as exc: + logger.debug("Evidence collection failed: %s", exc, exc_info=True) + return {"status": "inconclusive", "detail": f"evidence collection failed: {exc}"} + + # Find an applicable verifier. + verifiers = getattr(self._registry, "verifiers", None) + if not verifiers: + return {"status": "skipped", "detail": "no verifiers registered"} + + for v in verifiers: + try: + verdict = await v.verify(evidence) + verdict_dict = verdict.to_dict() + self._update_trust_from_verdict( + device_id, channel_id, verdict_dict.get("status"), + ) + return verdict_dict + except Exception as exc: + logger.debug("Verifier %s failed: %s", v, exc, exc_info=True) + + return {"status": "skipped", "detail": "no applicable verifier succeeded"} + + def _update_trust_from_verdict( + self, + device_id: str, + channel_id: str, + status: str | None, + ) -> None: + """Feed a verification verdict into the progressive-trust gate. + + Only definitive outcomes move trust: ``success`` promotes, ``failure`` + demotes. ``inconclusive`` and ``skipped`` leave trust untouched -- + absence of evidence is not evidence of failure. + """ + if self._trust_gate is None: + return + from leapflow.hardware.verification import VerdictStatus + + try: + if status == VerdictStatus.SUCCESS.value: + self._trust_gate.record_success(device_id, channel_id) + elif status == VerdictStatus.FAILURE.value: + self._trust_gate.record_failure(device_id, channel_id) + except Exception as exc: # noqa: BLE001 - trust bookkeeping must not fail the turn + logger.debug("Trust gate update failed: %s", exc, exc_info=True) + + async def _resolve_device(self, device_id: str) -> Any: + """Resolve a device from the registry by id.""" + get_device = getattr(self._registry, "get_device", None) + if get_device is not None: + device = get_device(device_id) + if device is not None: + return device + + # Fallback: return the registry itself as a proxy. + return self._registry + + def _format_result( + self, + result: InferenceResult, + execution: dict[str, Any] | None, + verification: dict[str, Any] | None, + ) -> dict[str, Any]: + """Build the tool result dict from an :class:`InferenceResult`. + + The ``action`` payload is JSON-serialised (tensors flattened to a + list, dicts kept intact) while the strategy's latency and confidence + metadata surface alongside so callers can drive test-time scaling + without inspecting private fields. + """ + action = result.action + out: dict[str, Any] = {"ok": True} + + # Serialise the action for JSON transport. + if hasattr(action, "tolist"): + out["action"] = action.tolist() + elif hasattr(action, "cpu"): + out["action"] = action.cpu().tolist() + elif isinstance(action, dict): + out["action"] = action + else: + out["action"] = str(action) if action is not None else None + + # Surface strategy telemetry so callers see the true latency and can + # adapt budgets without reaching into the strategy instance. + out["latency_ms"] = round(float(result.latency_ms), 3) + out["confidence"] = float(result.confidence) + out["chunk_size"] = int(result.chunk_size) + if result.metadata: + out["strategy_metadata"] = dict(result.metadata) + + if execution is not None: + out["execution"] = execution + if not execution.get("ok", True): + out["ok"] = False + if verification is not None: + out["verification"] = verification + + return out + + def _register_teardown(self) -> None: + """Register scope cleanup for policy cache and episode state.""" + if self._teardown_registered: + return + if self._scope is None: + return + register = getattr(self._scope, "async_effect", None) or getattr( + self._scope, "effect", None + ) + if register is None: + logger.warning( + "Physical skill plugin received an effect scope without " + "async_effect; policy cache will not be cleaned on teardown" + ) + return + try: + register(self._cleanup) + self._teardown_registered = True + except (RuntimeError, ValueError) as exc: + logger.warning( + "Could not register physical skill teardown effect: %s", + exc, exc_info=True, + ) + + async def _cleanup(self) -> None: + """Release cached policies and reset episode state.""" + await self._close_remote_policies() + self._policies.clear() + self._active_episode = None + logger.debug("Physical skill plugin: cleaned up policy cache and episode state") + + async def _close_remote_policies(self) -> None: + """Close every remote strategy this plugin has referenced. + + Only strategies exposing an ``async close()`` (currently + :class:`VLARemoteStrategy`) are touched; local strategies own no + socket and stay resident in the shared registry so a subsequent + session can reuse the loaded model. Failures are swallowed -- + teardown must not propagate secondary errors. + """ + for path, strategy in list(self._policies.items()): + close = getattr(strategy, "close", None) + if close is None: + continue + try: + await close() + except Exception: # noqa: BLE001 - teardown must not propagate + logger.debug( + "Failed to close inference strategy for %s", path, exc_info=True, + ) + + + + +plugin = PhysicalSkillPlugin() + + +__all__ = ["PhysicalSkillPlugin", "plugin"] diff --git a/src/leapflow/plugins/tool_plugins/self_awareness.py b/src/leapflow/plugins/tool_plugins/self_awareness.py index b90688dc..4e6820e3 100644 --- a/src/leapflow/plugins/tool_plugins/self_awareness.py +++ b/src/leapflow/plugins/tool_plugins/self_awareness.py @@ -1,34 +1,41 @@ # Copyright (c) Alibaba, Inc. and its affiliates. -"""Unified self-awareness plugin — faceted agent self-cognition surface. +"""Unified self-awareness plugin — the agent's single self-cognition surface. -Aggregates registry, daemon, engine, and build_info into two read-only tools: +This is the one place the agent answers questions about *itself* — its identity, +capabilities, runtime state, evolution readiness, or platform connections — from +live runtime evidence rather than from source code, documentation, or memory. + +Two read-only tools: * ``self_describe(facet=...)`` — structured introspection by facet * ``runtime_snapshot()`` — lightweight ~150-token flat dict for quick orientation -All data sources are injected via ``bind_runtime``; missing dependencies degrade -gracefully per facet rather than raising. +Data sources are resolved by *pull*, not by push-DI, so the surface cannot go +silently dark when a bootstrap wiring step is forgotten: + +* capabilities → the process-global tool/scoped registries (``get_registry`` / + ``get_scoped_registry``) — the very evidence source ``plugin_list`` uses. +* identity → this process's captured ``BuildInfo`` + live ``Settings``. +* runtime / evolution / platform → the *session-scoped* daemon ``status`` snapshot, + obtained through an injected ``runtime_status_provider`` keyed by the active + turn's ``session_id``. This honours the "session engine is the only reporting + source" rule: the provider resolves the caller's own session, never a template + engine (whose figures read zero). Absent that provider (in-process CLI), these + facets degrade gracefully instead of reporting stale zeros. """ from __future__ import annotations +import inspect import logging import time -import weakref from collections import Counter -from typing import TYPE_CHECKING, Any +from typing import Any from leapflow.plugins.protocol import ToolMetadata -if TYPE_CHECKING: - pass - logger = logging.getLogger(__name__) -# ── Cache configuration ────────────────────────────────────────────────────── - -_DAEMON_CACHE_TTL_S = 30.0 - # ── Facet enum values ──────────────────────────────────────────────────────── _FACETS = ("identity", "capabilities", "runtime", "evolution", "platform", "all") @@ -65,32 +72,55 @@ def _human(n: int) -> str: return f"{_human(used)}/{_human(total)} ({pct}%)" +def _settings_or_none() -> Any: + """Return live Settings, or None during early bootstrap.""" + try: + from leapflow.config import get_settings + + return get_settings() + except (ImportError, AttributeError, RuntimeError): + return None + + +def _cache_hit_rate_str(value: Any) -> str: + """Render a numeric cache hit rate as a percentage string, else 'unknown'.""" + if isinstance(value, (int, float)) and value >= 0: + return f"{float(value):.1f}%" + return "unknown" + + # ── Plugin class ───────────────────────────────────────────────────────────── class SelfAwarenessPlugin: """Unified self-cognition surface for the agent. - Facade plugin that reads live runtime state from four injected sources - (registry, daemon_client, engine, build_info) and exposes it through - two read-only tools. Registry version gating and daemon TTL caching keep - data current without hot-path cost. + Facade plugin that resolves live runtime state by pull (global registries, + captured build info, live settings) plus an injected session-scoped status + provider, and exposes it through two read-only tools. Registry version + gating keeps the capabilities report current without hot-path cost. """ def __init__(self) -> None: - # Injected dependencies — all optional, degrade per-facet - self._daemon_client: Any = None - self._registry: Any = None - self._engine_ref: weakref.ref | None = None - self._build_info: Any = None + # Injected: a callable ``(session_id: str) -> dict | Awaitable[dict]`` + # returning the session-scoped daemon status snapshot. Optional — the + # runtime/evolution/platform facets degrade when it is unbound. + self._status_provider: Any = None + + # Captured once per process, mirroring how the daemon captures its own + # build fingerprint at startup, so identity works without any wiring. + try: + from leapflow.utils.build_info import capture_build_info + + self._build_info: Any = capture_build_info() + except Exception: # noqa: BLE001 - identity must never fail to load + logger.debug("self_awareness: build info capture failed", exc_info=True) + self._build_info = None - # ── Caches ── + # ── capabilities cache (rebuilt on registry version change) ── self._registry_version: int = -1 self._capabilities_cache: dict[str, Any] = {} - self._daemon_cache: dict[str, Any] = {} - self._daemon_cache_ts: float = 0.0 - # ── Protocol properties ────────────────────────────────────────────── @property @@ -107,9 +137,13 @@ def tools(self) -> list[ToolMetadata]: ToolMetadata( name="self_describe", description=( - "Introspect LeapFlow's own identity, capabilities, runtime state, " - "evolution metrics, or platform connections. Use facet='all' only " - "when a comprehensive self-check is explicitly requested." + "Answer questions about LeapFlow ITSELF from live runtime evidence — its " + "identity/version/model, what tools and plugins it has, whether it supports " + "plugins/self-evolution/hot-reload, its runtime state, or whether it has any " + "form of self-awareness. Prefer this over reading LeapFlow's own source code. " + "Pick facet=capabilities for 'what can you do / do you support X', " + "identity for version/model, runtime for context/posture, or all for a full " + "self-check when explicitly requested." ), parameters_schema={ "type": "object", @@ -119,9 +153,9 @@ def tools(self) -> list[ToolMetadata]: "enum": list(_FACETS), "description": ( "Which aspect to inspect: identity (version/model/uptime), " - "capabilities (tools/plugins/trust), runtime (context/posture/" - "disclosure), evolution (performance/proposals), platform " - "(gateway/hardware/env), or all." + "capabilities (tools/plugins/trust/evolution readiness), runtime " + "(context/posture/disclosure), evolution (performance/proposals), " + "platform (gateway/hardware/env), or all." ), }, }, @@ -132,6 +166,7 @@ def tools(self) -> list[ToolMetadata]: "risk_level": "read_only", "schema_cost": "low", "requires_approval": False, + "summary": "introspect LeapFlow's own identity/capabilities/runtime state", }, provides_capabilities=("system.self_describe",), ), @@ -149,6 +184,7 @@ def tools(self) -> list[ToolMetadata]: "risk_level": "read_only", "schema_cost": "low", "requires_approval": False, + "summary": "flat snapshot of live runtime state", }, provides_capabilities=("system.runtime_snapshot",), ), @@ -156,106 +192,66 @@ def tools(self) -> list[ToolMetadata]: @property def dependencies(self) -> list[str]: - return ["daemon_client"] + return ["runtime_status_provider"] def bind_runtime(self, **deps: Any) -> None: """Receive runtime-injected dependencies. - Accepts: daemon_client, registry, engine (stored as weak ref), build_info. + Accepts ``runtime_status_provider`` — a ``(session_id) -> dict`` callable + (sync or async) resolving the session-scoped daemon status snapshot. """ - if "daemon_client" in deps: - self._daemon_client = deps["daemon_client"] - if "registry" in deps: - self._registry = deps["registry"] - if "engine" in deps: - engine = deps["engine"] - if engine is not None: - try: - self._engine_ref = weakref.ref(engine) - except TypeError: - # Some stub objects cannot be weak-referenced - self._engine_ref = lambda: engine # type: ignore[assignment] - else: - self._engine_ref = None - if "build_info" in deps: - self._build_info = deps["build_info"] + if "runtime_status_provider" in deps: + self._status_provider = deps["runtime_status_provider"] # ── Tool handlers ──────────────────────────────────────────────────── - def _handle_self_describe(self, facet: str = "identity", **_: Any) -> dict[str, Any]: + async def _handle_self_describe(self, facet: str = "identity", **_: Any) -> dict[str, Any]: """Dispatch to facet builders, merging all when facet='all'.""" if facet not in _FACETS: return {"error": f"Unknown facet: {facet!r}. Valid: {', '.join(_FACETS)}"} + status = await self._resolve_status() + if facet == "all": result: dict[str, Any] = {} for f in _FACETS: if f == "all": continue - result[f] = self._build_facet(f) + result[f] = self._build_facet(f, status) return result - return self._build_facet(facet) + return self._build_facet(facet, status) - def _handle_runtime_snapshot(self, **_: Any) -> dict[str, Any]: + async def _handle_runtime_snapshot(self, **_: Any) -> dict[str, Any]: """Return a lightweight flat dict for quick agent orientation.""" - engine = self._resolve_engine() - daemon = self._get_daemon_cache() - - model = "" - context_str = "unknown" - posture = "unknown" - disclosure = "unknown" - turn = 0 - cache_hit_rate = "unknown" - - if engine is not None: - snapshot = getattr(engine, "context_budget_snapshot", None) - if callable(snapshot): - snapshot = snapshot() - if isinstance(snapshot, dict): - used = int(snapshot.get("total_tokens", 0) or 0) - total = int(snapshot.get("context_length", 0) or 0) - context_str = _format_context(used, total) - posture = str(snapshot.get("context_posture", "baseline") or "baseline") - disclosure = str(snapshot.get("disclosure_level", "unknown") or "unknown") - turn = int(getattr(engine, "_session_turn_count", 0) or 0) - # Cache hit rate from usage tracker - tracker = getattr(engine, "_usage_tracker", None) - if tracker is not None: - summary = getattr(tracker, "summary", None) - if callable(summary): - s = summary() - rate = getattr(s, "cache_hit_rate", None) - if rate is not None and rate >= 0: - cache_hit_rate = f"{rate:.1f}%" - - if daemon: - model = str(daemon.get("model", "") or "") - if not model: - model = "unknown" - elif engine is not None: - settings = getattr(engine, "_settings", None) - model = str(getattr(settings, "llm_model", "") or "") if settings else "unknown" - - uptime = "unknown" - if daemon: - uptime_s = daemon.get("uptime_s") - if isinstance(uptime_s, (int, float)) and uptime_s >= 0: - uptime = _format_uptime(uptime_s) - elif self._build_info is not None: - started = getattr(self._build_info, "started_at", 0.0) or 0.0 - if started > 0: - uptime = _format_uptime(time.time() - started) + status = await self._resolve_status() + snapshot = status.get("context_budget_snapshot") if isinstance(status, dict) else None + snapshot = snapshot if isinstance(snapshot, dict) else {} + + used = int(snapshot.get("total_tokens", status.get("context_used", 0)) or 0) + total = int(snapshot.get("context_length", status.get("llm_context_length", 0)) or 0) + context_str = _format_context(used, total) if total > 0 else "unknown" + posture = str(snapshot.get("context_posture", status.get("context_posture", "")) or "unknown") + disclosure = str(snapshot.get("disclosure_level", "") or "unknown") + turn = int(status.get("session_turn_count", 0) or 0) + cache_hit_rate = _cache_hit_rate_str(status.get("cache_hit_rate")) + + model = str(status.get("model", "") or "") + if not model: + settings = _settings_or_none() + model = str(getattr(settings, "llm_model", "") or "unknown") if settings else "unknown" + + uptime = self._uptime(status) tools_available = 0 - if self._registry is not None: + reg = self._registry_or_none() + if reg is not None: try: - tools_available = len(self._registry.tool_handlers) - except Exception: - pass + tools_available = len(reg.tool_handlers) + except Exception: # noqa: BLE001 - snapshot is best-effort + tools_available = 0 - pending = int(daemon.get("pending_approvals", 0) or 0) if daemon else 0 + pending = int(status.get("pending_approvals", 0) or 0) return { "model": model, @@ -271,7 +267,7 @@ def _handle_runtime_snapshot(self, **_: Any) -> dict[str, Any]: # ── Facet builders ─────────────────────────────────────────────────── - def _build_facet(self, facet: str) -> dict[str, Any]: + def _build_facet(self, facet: str, status: dict[str, Any]) -> dict[str, Any]: builder = { "identity": self._facet_identity, "capabilities": self._facet_capabilities, @@ -282,238 +278,229 @@ def _build_facet(self, facet: str) -> dict[str, Any]: if builder is None: return {"error": f"No builder for facet: {facet!r}"} try: - return builder() - except Exception as exc: + return builder(status) + except Exception as exc: # noqa: BLE001 - a facet fault degrades, never raises logger.debug("self_describe facet %s failed: %s", facet, exc, exc_info=True) return {"available": False, "reason": f"Facet {facet!r} raised: {exc}"} - def _facet_identity(self) -> dict[str, Any]: + def _facet_identity(self, status: dict[str, Any]) -> dict[str, Any]: """Version, build, model, provider, context limit, uptime.""" result: dict[str, Any] = {"available": True} - if self._build_info is not None: - result["version"] = getattr(self._build_info, "version", "unknown") - result["commit"] = getattr(self._build_info, "commit", None) or "unknown" - dirty = getattr(self._build_info, "dirty_digest", None) - result["dirty"] = dirty is not None and dirty != "" + # Prefer the daemon's authoritative build fingerprint; fall back to the + # fingerprint this process captured at import time. + build = status.get("build") if isinstance(status.get("build"), dict) else {} + bi = self._build_info + result["version"] = ( + build.get("version") or (getattr(bi, "version", None) if bi else None) or "unknown" + ) + commit = build.get("commit") or (getattr(bi, "commit", None) if bi else None) + result["commit"] = commit or "unknown" + dirty = build.get("dirty_digest") + if dirty is None and bi is not None: + dirty = getattr(bi, "dirty_digest", None) + result["dirty"] = bool(dirty) + + if status: + result["model"] = status.get("model") or "unknown" + result["context_limit"] = status.get("llm_context_length", 0) + result["pid"] = status.get("pid") or build.get("pid") else: - result["version"] = "unknown" - result["commit"] = "unknown" - result["dirty"] = None - - daemon = self._get_daemon_cache() - if daemon: - result["model"] = daemon.get("model", "unknown") - result["context_limit"] = daemon.get("llm_context_length", 0) - uptime_s = daemon.get("uptime_s", 0) - result["uptime"] = _format_uptime(uptime_s) if isinstance(uptime_s, (int, float)) else "unknown" - result["pid"] = daemon.get("pid") - else: - engine = self._resolve_engine() - if engine is not None: - settings = getattr(engine, "_settings", None) - result["model"] = str(getattr(settings, "llm_model", "unknown") or "unknown") if settings else "unknown" - result["context_limit"] = int(getattr(settings, "llm_context_length", 0) or 0) if settings else 0 - else: - result["model"] = "unknown" - result["context_limit"] = 0 - if self._build_info is not None: - started = getattr(self._build_info, "started_at", 0.0) or 0.0 - result["uptime"] = _format_uptime(time.time() - started) if started > 0 else "unknown" - else: - result["uptime"] = "unknown" - + settings = _settings_or_none() + result["model"] = ( + str(getattr(settings, "llm_model", "") or "unknown") if settings else "unknown" + ) + result["context_limit"] = ( + int(getattr(settings, "llm_context_length", 0) or 0) if settings else 0 + ) + result["pid"] = getattr(bi, "pid", None) if bi else None + + result["uptime"] = self._uptime(status) return result - def _facet_capabilities(self) -> dict[str, Any]: - """Tool count by category, plugin count, trust summary, evolution readiness.""" - if self._registry is None: - return {"available": False, "reason": "registry not bound"} + def _facet_capabilities(self, _status: dict[str, Any]) -> dict[str, Any]: + """Tool/plugin inventory, trust summary, evolution readiness — from the + live registry, the same evidence source ``plugin_list`` uses.""" + reg = self._registry_or_none() + if reg is None: + return {"available": False, "reason": "tool registry unavailable"} - # Registry version gate: rebuild only on mismatch - current_version = self._registry.version + # Registry version gate: rebuild only on mismatch. + current_version = reg.version if current_version != self._registry_version: - self._capabilities_cache = self._build_capabilities_report() + self._capabilities_cache = self._build_capabilities_report(reg) self._registry_version = current_version - return {**self._capabilities_cache, "available": True} + # Skills are enumerated live (they carry their own index cache) rather + # than folded into the version-gated report, so a skill added without a + # tool-registry change still shows up. + return {**self._capabilities_cache, "skills": self._skill_inventory(), "available": True} - def _build_capabilities_report(self) -> dict[str, Any]: - """Construct capability report from live registry state.""" - reg = self._registry + def _build_capabilities_report(self, reg: Any) -> dict[str, Any]: + """Construct the capability report from live registry state.""" all_meta = reg.all_metadata plugins = reg.plugins - # Tools by category category_counts: Counter[str] = Counter() + mutating = 0 + approval_required = 0 for meta in all_meta: - cat = meta.x_leapflow.get("category", "uncategorized") if meta.x_leapflow else "uncategorized" + meta_x = meta.x_leapflow or {} + cat = str(meta_x.get("category", "uncategorized") or "uncategorized") category_counts[cat] += 1 + if bool(getattr(meta, "mutates_state", False)): + mutating += 1 + if meta_x.get("requires_approval") is True: + approval_required += 1 - # Trust summary from plugin metadata (if available via scoped registry) + # Trust summary from the scoped registry's fibers, when available. trust_summary: dict[str, int] = {} try: from leapflow.plugins.scoped_registry import get_fiber_registry fiber_reg = get_fiber_registry() if fiber_reg is not None: - for pid, fiber in fiber_reg.fibers.items(): + for _pid, fiber in fiber_reg.fibers.items(): level = str(getattr(fiber, "trust_level", "UNKNOWN")) trust_summary[level] = trust_summary.get(level, 0) + 1 except (ImportError, AttributeError, RuntimeError): pass - # Evolution readiness + # Evolution readiness from live settings. evolution_ready = False try: from leapflow.config import get_settings - settings = get_settings() - evolution_ready = bool(getattr(settings, "evolution_enabled", False)) + + evolution_ready = bool(getattr(get_settings(), "evolution_enabled", False)) except (ImportError, AttributeError, RuntimeError): pass return { "tool_count": len(all_meta), "tools_by_category": dict(category_counts.most_common()), + "mutating_tool_count": mutating, + "approval_required_tool_count": approval_required, "plugin_count": len(plugins), "plugin_ids": sorted(plugins.keys()), + "supports_plugins": "self_management" in plugins, "trust_summary": trust_summary if trust_summary else {"note": "fiber registry unavailable"}, "evolution_ready": evolution_ready, "conflicts": len(reg.conflicts), } - def _facet_runtime(self) -> dict[str, Any]: + def _facet_runtime(self, status: dict[str, Any]) -> dict[str, Any]: """Context budget, disclosure level, posture, session turns, cache hit rate.""" - engine = self._resolve_engine() - if engine is None: - return {"available": False, "reason": "engine not bound"} + if not status: + return {"available": False, "reason": "runtime status unavailable (no daemon bound)"} result: dict[str, Any] = {"available": True} - - # Context budget - snapshot = getattr(engine, "context_budget_snapshot", None) - if callable(snapshot): - snapshot = snapshot() - if isinstance(snapshot, dict): - used = int(snapshot.get("total_tokens", 0) or 0) - total = int(snapshot.get("context_length", 0) or 0) - result["context_used"] = used - result["context_total"] = total - result["context_percentage"] = round(used / total * 100, 1) if total > 0 else 0 - result["context_formatted"] = _format_context(used, total) - result["posture"] = str(snapshot.get("context_posture", "baseline") or "baseline") - result["disclosure_level"] = str(snapshot.get("disclosure_level", "unknown") or "unknown") - else: - result["context_used"] = 0 - result["context_total"] = 0 - result["context_formatted"] = "unknown" - result["posture"] = "unknown" - result["disclosure_level"] = "unknown" - - result["session_turn_count"] = int(getattr(engine, "_session_turn_count", 0) or 0) - - # Cache hit rate - tracker = getattr(engine, "_usage_tracker", None) - if tracker is not None: - summary_fn = getattr(tracker, "summary", None) - if callable(summary_fn): - s = summary_fn() - rate = getattr(s, "cache_hit_rate", None) - result["cache_hit_rate"] = f"{rate:.1f}%" if rate is not None and rate >= 0 else "unknown" - else: - result["cache_hit_rate"] = "unknown" - else: - result["cache_hit_rate"] = "unknown" - + snapshot = status.get("context_budget_snapshot") + snapshot = snapshot if isinstance(snapshot, dict) else {} + + used = int(snapshot.get("total_tokens", status.get("context_used", 0)) or 0) + total = int(snapshot.get("context_length", status.get("llm_context_length", 0)) or 0) + result["context_used"] = used + result["context_total"] = total + result["context_percentage"] = round(used / total * 100, 1) if total > 0 else 0 + result["context_formatted"] = _format_context(used, total) + result["posture"] = str( + snapshot.get("context_posture", status.get("context_posture", "baseline")) or "baseline" + ) + result["disclosure_level"] = str(snapshot.get("disclosure_level", "") or "unknown") + result["session_turn_count"] = int(status.get("session_turn_count", 0) or 0) + result["cache_hit_rate"] = _cache_hit_rate_str(status.get("cache_hit_rate")) return result - def _facet_evolution(self) -> dict[str, Any]: + def _facet_evolution(self, status: dict[str, Any]) -> dict[str, Any]: """Evolution performance metrics and active proposals count.""" - daemon = self._get_daemon_cache() - if not daemon: - if self._daemon_client is None: - return {"available": False, "reason": "daemon_client not bound"} - return {"available": False, "reason": "daemon status unavailable"} + if not status: + return {"available": False, "reason": "evolution status unavailable (no daemon bound)"} + return { + "available": True, + "performance": status.get("evolution_performance", {}), + "pending_approvals": int(status.get("pending_approvals", 0) or 0), + } - result: dict[str, Any] = {"available": True} - result["performance"] = daemon.get("evolution_performance", {}) + def _facet_platform(self, status: dict[str, Any]) -> dict[str, Any]: + """Gateway connections, hardware backend, environment sources, active clients.""" + if not status: + return {"available": False, "reason": "platform status unavailable (no daemon bound)"} + return { + "available": True, + "active_clients": status.get("active_clients", 0), + "connected_clients": status.get("connected_clients", 0), + "host_backend": status.get("host_backend", {}), + "environment_sources": status.get("environment_sources", {}), + } - # Active proposals from watch summary or pending approvals - result["pending_approvals"] = int(daemon.get("pending_approvals", 0) or 0) + # ── Internal helpers ───────────────────────────────────────────────── - return result + def _uptime(self, status: dict[str, Any]) -> str: + """Resolve uptime from the daemon status, else the captured build info.""" + uptime_s = status.get("uptime_s") if status else None + if isinstance(uptime_s, (int, float)) and uptime_s >= 0: + return _format_uptime(uptime_s) + if self._build_info is not None: + started = getattr(self._build_info, "started_at", 0.0) or 0.0 + if started > 0: + return _format_uptime(time.time() - started) + return "unknown" - def _facet_platform(self) -> dict[str, Any]: - """Gateway connections, hardware backend, environment sources, active clients.""" - daemon = self._get_daemon_cache() - if not daemon: - if self._daemon_client is None: - return {"available": False, "reason": "daemon_client not bound"} - return {"available": False, "reason": "daemon status unavailable"} + def _registry_or_none(self) -> Any: + """Return the process-global tool registry, or None if unavailable.""" + try: + from leapflow.plugins import get_registry - result: dict[str, Any] = {"available": True} - result["active_clients"] = daemon.get("active_clients", 0) - result["connected_clients"] = daemon.get("connected_clients", 0) - result["host_backend"] = daemon.get("host_backend", {}) - result["environment_sources"] = daemon.get("environment_sources", {}) + return get_registry() + except (ImportError, AttributeError, RuntimeError): + logger.debug("self_awareness: tool registry unavailable", exc_info=True) + return None - return result + def _skill_inventory(self) -> dict[str, Any]: + """Live skills summary from the skill discovery subsystem (pull). - # ── Internal helpers ───────────────────────────────────────────────── + Skills are a distinct capability surface from tools/plugins, so the + canonical self-cognition facet reports them too — otherwise the agent + must reach for skills_list separately and self_describe under-reports + what LeapFlow can do. + """ + try: + from leapflow.skills.discovery import skill_inventory_summary - def _resolve_engine(self) -> Any: - """Resolve engine from weak ref, returning None if unavailable.""" - if self._engine_ref is None: - return None - engine = self._engine_ref() - return engine + return skill_inventory_summary() + except Exception: # noqa: BLE001 - skills are optional; degrade, never raise + logger.debug("self_awareness: skill inventory unavailable", exc_info=True) + return {"available": False, "reason": "skill discovery unavailable"} - def _get_daemon_cache(self) -> dict[str, Any]: - """Return cached daemon status, refreshing when TTL expires. + async def _resolve_status(self) -> dict[str, Any]: + """Fetch the session-scoped status snapshot via the injected provider. - Uses synchronous access only — the daemon_client.status() is async, - so we store the last-fetched result and expose a sync refresh method - that callers in an async context can await. + The active turn's ``session_id`` is read from the per-turn tool execution + context so the daemon resolves the caller's own session engine — never a + shared/most-recent one. Returns an empty dict when no provider is bound + (in-process CLI) or on any failure; facets degrade accordingly. """ - now = time.monotonic() - if self._daemon_cache and (now - self._daemon_cache_ts) < _DAEMON_CACHE_TTL_S: - return self._daemon_cache - return self._refresh_daemon_cache_sync() - - def _refresh_daemon_cache_sync(self) -> dict[str, Any]: - """Try to refresh daemon cache synchronously via an existing event loop.""" - if self._daemon_client is None: + if self._status_provider is None: return {} + + session_id = "" try: - import asyncio - - asyncio.get_running_loop() # Verify we are in an async context - # We are inside an async context (tool handler runs within the agent loop). - # Schedule the coroutine and use a shim to get the result. - future = asyncio.ensure_future(self._daemon_client.status()) - # Cannot await here in a sync handler; return stale cache and - # schedule background refresh. - future.add_done_callback(self._on_daemon_status_fetched) - return self._daemon_cache - except RuntimeError: - # No running event loop — likely in tests or sync CLI - pass - return self._daemon_cache + from leapflow.tools.execution_context import current_tool_context + + ctx = current_tool_context() + if ctx is not None: + session_id = str(getattr(ctx, "session_id", "") or "") + except Exception: # noqa: BLE001 - context lookup must never break the tool + session_id = "" - def _on_daemon_status_fetched(self, future: Any) -> None: - """Callback when async daemon status completes.""" try: - result = future.result() - if isinstance(result, dict): - self._daemon_cache = result - self._daemon_cache_ts = time.monotonic() - except Exception: - logger.debug("self_awareness: daemon status refresh failed", exc_info=True) - - def inject_daemon_cache(self, status: dict[str, Any]) -> None: - """Inject daemon status directly (used by tests and daemon service).""" - self._daemon_cache = status - self._daemon_cache_ts = time.monotonic() + result = self._status_provider(session_id) + if inspect.isawaitable(result): + result = await result + return result if isinstance(result, dict) else {} + except Exception: # noqa: BLE001 - a status fault degrades, never raises + logger.debug("self_awareness: status provider failed", exc_info=True) + return {} # Module-level instance for plugin discovery diff --git a/src/leapflow/plugins/tool_plugins/self_management.py b/src/leapflow/plugins/tool_plugins/self_management.py index eda06e3d..d5b75a70 100644 --- a/src/leapflow/plugins/tool_plugins/self_management.py +++ b/src/leapflow/plugins/tool_plugins/self_management.py @@ -249,172 +249,11 @@ async def _plugin_list_handler(self, **kwargs: Any) -> Dict[str, Any]: "gateway_adapters": gateway_adapters, "llm_providers": llm_providers, "total_count": len(plugins_info) + len(gateway_adapters) + len(llm_providers), - "capability_report": self._build_capability_report( - reg, - scoped, - plugins_info, - gateway_adapters, - llm_providers, - ), } except (RuntimeError, AttributeError) as exc: logger.warning("plugin_list failed: %s", exc, exc_info=True) return {"ok": False, "error": f"plugin_list failed: {exc}"} - def _build_capability_report( - self, - reg: Any, - scoped: Any, - plugins_info: list[dict[str, Any]], - gateway_adapters: list[dict[str, Any]], - llm_providers: list[dict[str, Any]], - ) -> dict[str, Any]: - """Build a live, evidence-backed capability report for self-questions.""" - tool_categories: dict[str, dict[str, Any]] = {} - self_management_tools: list[str] = [] - mutation_tools: list[str] = [] - approval_required_tools: list[str] = [] - read_only_tools: list[str] = [] - - for tool in reg.all_metadata: - metadata = dict(tool.x_leapflow or {}) - category = str(metadata.get("category") or "general") - bucket = tool_categories.setdefault( - category, - { - "tool_count": 0, - "tools": [], - "approval_required_count": 0, - "mutating_count": 0, - }, - ) - bucket["tool_count"] += 1 - bucket["tools"].append(tool.name) - if bool(tool.mutates_state): - mutation_tools.append(tool.name) - bucket["mutating_count"] += 1 - else: - read_only_tools.append(tool.name) - if metadata.get("requires_approval") is True: - approval_required_tools.append(tool.name) - bucket["approval_required_count"] += 1 - if tool.name.startswith("plugin_") or tool.name == "assess_compatibility": - self_management_tools.append(tool.name) - - for bucket in tool_categories.values(): - bucket["tools"] = sorted(bucket["tools"]) - - profile_layout = self._profile_layout_or_none() - install_dir = self._safe_install_dir() - dependency_state = { - "approval_gate_bound": self._plugin_approval_gate is not None, - "llm_provider_bound": self._llm_provider is not None, - "plugin_generation_enabled": self._plugin_generation_enabled, - "plugin_install_dir": install_dir, - "marketplace_configured": self._marketplace_client is not None, - "trusted_marketplace_pubkeys": len(self._trusted_pubkeys), - "proposal_store_available": self._capability_lifecycle_store is not None, - "version_store_available": ( - self._plugin_version_store is not None or profile_layout is not None - ), - "capability_plan_store_available": ( - self._capability_plan_store is not None or profile_layout is not None - ), - } - limitations = self._capability_limitations(dependency_state) - - return { - "source": "live_runtime_registry", - "registry": { - "version": reg.version, - "plugin_count": len(plugins_info), - "tool_count": len(reg.tool_handlers), - "fiber_count": len(scoped.fibers), - "categories": sorted(tool_categories), - "capability_conflicts": [ - { - "tool_name": c.tool_name, - "kept_plugin": c.kept_plugin, - "rejected_plugin": c.rejected_plugin, - } - for c in getattr(reg, "conflicts", []) - ], - }, - "plugins_supported": { - "supported": "self_management" in reg.plugins, - "evidence_tools": sorted(self_management_tools), - "profile_installs": bool(install_dir), - "hot_reload": "plugin_reload" in self_management_tools, - "versioning": "plugin_versions" in self_management_tools - and "plugin_rollback" in self_management_tools, - "compatibility_assessment": "assess_compatibility" in self_management_tools, - }, - "self_evolution": { - "proposal_flow": "plugin_propose" in self_management_tools, - "generation_tool": "plugin_generate" in self_management_tools, - "generation_ready": self._plugin_generation_enabled - and self._llm_provider is not None, - "install_tool": "plugin_install" in self_management_tools, - "rollback_tool": "plugin_rollback" in self_management_tools, - "behavior_test_gate": True, - }, - "runtime_dependencies": dependency_state, - "tool_categories": dict(sorted(tool_categories.items())), - "read_only_tool_count": len(read_only_tools), - "mutation_tool_count": len(mutation_tools), - "approval_required_tools": sorted(approval_required_tools), - "gateway_adapter_count": len(gateway_adapters), - "llm_provider_count": len(llm_providers), - "limitations": limitations, - "answering_guidance": [ - "Use this live report as the evidence source for questions about LeapFlow capabilities.", - ( - "State configuration-dependent capabilities as available only when " - "their dependency flags are ready." - ), - "If this report is unavailable, say that live capability verification failed instead of guessing.", - ], - } - - def _profile_layout_or_none(self) -> Any: - try: - from leapflow.config import get_settings - - return getattr(get_settings(), "profile_layout", None) - except (RuntimeError, AttributeError, ImportError): - return None - - def _safe_install_dir(self) -> str: - try: - return str(self._resolve_install_dir()) - except (RuntimeError, AttributeError, ImportError): - return "" - - @staticmethod - def _capability_limitations(dependency_state: dict[str, Any]) -> list[str]: - limitations: list[str] = [] - if not dependency_state["approval_gate_bound"]: - limitations.append("Mutation tools fail closed until plugin_approval_gate is bound.") - if not dependency_state["llm_provider_bound"]: - limitations.append("plugin_generate cannot run until an LLM provider is bound.") - if not dependency_state["plugin_generation_enabled"]: - limitations.append("plugin_generate is disabled by configuration.") - if not dependency_state["marketplace_configured"]: - limitations.append("Marketplace installs require a configured marketplace client.") - if not dependency_state["proposal_store_available"]: - limitations.append( - "Plugin proposals require the daemon-injected evolution lifecycle store." - ) - if not dependency_state["version_store_available"]: - limitations.append( - "Plugin versioning requires a profile layout or injected version store." - ) - if not dependency_state["capability_plan_store_available"]: - limitations.append( - "Adaptive capability plan history requires a profile layout or injected plan store." - ) - return limitations - async def _plugin_status_handler(self, plugin_id: str, **kwargs: Any) -> Dict[str, Any]: """Detailed information about a specific plugin.""" from leapflow.plugins import get_registry, get_scoped_registry @@ -2556,9 +2395,10 @@ def tools(self) -> list[ToolMetadata]: ToolMetadata( name="plugin_list", description=( - "List the live plugin registry and cross-subsystem capability evidence. " - "Use this before answering questions about whether LeapFlow supports plugins, " - "self-evolution, plugin installation, hot reload, versioning, or other runtime capabilities." + "List the live plugin registry across Tool/Gateway/LLM subsystems for plugin " + "management: which plugins are loaded, their category and fiber lifecycle state. " + "For questions about LeapFlow's own capabilities or whether it supports X, use " + "self_describe instead." ), parameters_schema={ "type": "object", @@ -2571,7 +2411,7 @@ def tools(self) -> list[ToolMetadata]: "risk_level": "read_only", "schema_cost": "low", "requires_approval": False, - "summary": "list live plugins and self capability evidence", + "summary": "list live plugins across Tool/Gateway/LLM subsystems", }, provides_capabilities=("plugin.list",), ), diff --git a/src/leapflow/prompts/templates.py b/src/leapflow/prompts/templates.py index 461a704b..63c23e51 100644 --- a/src/leapflow/prompts/templates.py +++ b/src/leapflow/prompts/templates.py @@ -78,9 +78,12 @@ def build_react_system(language: str = "en", skill_catalog: str = "") -> str: capability contract; nothing else exists. Only a subset is directly callable this turn (via native tool calling, not a JSON block in your reply). If you need a tool from the index that is not yet callable, call `capability_expand` with its category name first — the matching tools become callable immediately after. -When the user asks what LeapFlow itself supports, whether it supports plugins, or which runtime capabilities -are available, use the live capability evidence exposed by `plugin_list` before making capability claims; report -configuration-dependent or unavailable capabilities as limitations instead of inferring from documentation. +When the user asks about LeapFlow **itself** — its identity/version/model, what tools, plugins, or capabilities +it has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of +self-awareness — answer from live runtime evidence via `self_describe` (facet=capabilities for "what can you +do / do you support X", identity for version/model, runtime for context/posture, or `all` for a full self-check), +never by reading LeapFlow's own source code, documentation, or memory. Report configuration-dependent or +unavailable capabilities as limitations rather than inferring them. {tool_catalog} {app_connector_section}{skill_section} ## Tool Usage diff --git a/src/leapflow/robot/__init__.py b/src/leapflow/robot/__init__.py new file mode 100644 index 00000000..cd7d7c0e --- /dev/null +++ b/src/leapflow/robot/__init__.py @@ -0,0 +1,12 @@ +# Copyright (c) Alibaba, Inc. and its affiliates. +"""LeapRobot: physical robot control and inference for LeapFlow. + +This package provides the core abstractions for robot control, sensor +access, and policy inference. It absorbs and adapts the inference-side +subset of the upstream open-source robot SDK, removing training dependencies and +integrating with LeapFlow's plugin, HCP, and configuration systems. +""" + +__version__ = "0.1.0" + +__all__ = ["__version__"] diff --git a/src/leapflow/robot/base.py b/src/leapflow/robot/base.py new file mode 100644 index 00000000..7a7dd20a --- /dev/null +++ b/src/leapflow/robot/base.py @@ -0,0 +1,166 @@ +# Copyright (c) Alibaba, Inc. and its affiliates. +"""Robot base protocol and configuration for LeapRobot. + +Defines the structural ``Robot`` Protocol that any physical robot must +satisfy to be usable with LeapFlow's hardware stack. The Protocol +replaces the original ABC in accordance with LeapFlow's +"Protocol over ABC" principle. + +Also provides a minimal ``RobotConfig`` frozen dataclass that replaces +the draccus-based configuration from the upstream project. +""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from pathlib import Path +from typing import Any, runtime_checkable + +from typing import Protocol + +from leapflow.robot.types import RobotAction, RobotObservation + + +# --------------------------------------------------------------------------- +# Robot Protocol +# --------------------------------------------------------------------------- + +@runtime_checkable +class Robot(Protocol): + """Protocol for physical robot devices. + + Any object satisfying this protocol can be used with LeapFlow's + hardware stack. The seven core methods map directly to HCP + Transport operations: + + - ``connect`` / ``disconnect`` → Transport ``open`` / ``close`` + - ``get_observation`` → Transport ``read`` / ``read_batch`` + - ``send_action`` → Transport ``write`` / ``write_batch`` + - ``calibrate`` → calibration lifecycle hook + + Properties ``observation_features`` and ``action_features`` declare the + schema so the channel mapper can build the HCP channel set without + connecting to hardware first. + """ + + @property + def name(self) -> str: + """Unique robot type identifier (e.g. ``"so100"``, ``"koch_v1.1"``).""" + ... + + @property + def observation_features(self) -> dict[str, Any]: + """Structure and types of observations produced by the robot. + + Keys match what ``get_observation`` returns. Values are either a + Python type for scalar values or a shape tuple for array values. + Must be callable regardless of connection state. + """ + ... + + @property + def action_features(self) -> dict[str, Any]: + """Structure and types of actions expected by the robot. + + Keys match what ``send_action`` expects. Values follow the same + convention as ``observation_features``. Must be callable + regardless of connection state. + """ + ... + + @property + def is_connected(self) -> bool: + """Whether the robot is currently connected.""" + ... + + def connect(self, calibrate: bool = True) -> None: + """Establish communication with the robot. + + Args: + calibrate: If ``True`` (default), automatically calibrate after + connecting when the hardware requires it. + """ + ... + + def disconnect(self) -> None: + """Disconnect from the robot and release resources.""" + ... + + def get_observation(self) -> RobotObservation: + """Retrieve the current observation from the robot. + + Returns: + Flat dictionary whose structure matches + :pyattr:`observation_features`. + """ + ... + + def send_action(self, action: RobotAction) -> RobotAction: + """Send an action command to the robot. + + Args: + action: Dictionary of actuator commands whose structure + matches :pyattr:`action_features`. + + Returns: + The action actually applied (may be clipped by safety limits). + """ + ... + + def calibrate(self) -> None: + """Calibrate the robot if applicable; no-op otherwise.""" + ... + + +# --------------------------------------------------------------------------- +# RobotConfig +# --------------------------------------------------------------------------- + +@dataclass(frozen=True) +class RobotConfig: + """Minimal robot configuration replacing draccus-based config. + + This is intentionally lean — concrete robot implementations should + subclass or compose their own config with additional fields. + """ + + robot_type: str + """Registered robot type name (e.g. ``"so100"``, ``"moss_v1"``).""" + + id: str = "default" + """Instance identifier distinguishing multiple robots of the same type.""" + + calibration_dir: Path | None = None + """Directory for calibration files. ``None`` uses the default location.""" + + device: str = "cpu" + """Target device for tensor operations (``"cpu"``, ``"cuda"``, etc.).""" + + extra: dict[str, Any] = field(default_factory=dict) + """Vendor-specific or user-supplied configuration key-values.""" + + +def make_robot(robot_type: str, **kwargs: Any) -> Any: + """Construct a robot instance by type name. + + This is the LeapRobot factory entry point that replaces the upstream + ``make_robot`` from the open-source robot SDK. + + .. note:: + + The concrete registry is not yet implemented — callers should + configure ``robot_factory`` directly in the transport config. + This stub exists so that internal imports resolve within the + ``leapflow.robot`` namespace. + """ + raise NotImplementedError( + f"make_robot('{robot_type}') is not yet wired to a robot " + "registry; configure robot_factory directly in transport config" + ) + + +__all__ = [ + "Robot", + "RobotConfig", + "make_robot", +] diff --git a/src/leapflow/robot/cameras.py b/src/leapflow/robot/cameras.py new file mode 100644 index 00000000..28cd2fb7 --- /dev/null +++ b/src/leapflow/robot/cameras.py @@ -0,0 +1,372 @@ +# Copyright (c) Alibaba, Inc. and its affiliates. +"""Camera abstractions for LeapRobot. + +Defines the ``Camera`` Protocol and a concrete ``OpenCVCamera`` +implementation. opencv-python is an optional dependency — the module +is importable without it, but ``OpenCVCamera`` requires it at runtime. +""" + +from __future__ import annotations + +import logging +import math +import platform +import time +from threading import Event, Lock, Thread +from typing import Any, Protocol, runtime_checkable + +# --------------------------------------------------------------------------- +# Optional dependencies +# --------------------------------------------------------------------------- +try: + import numpy as np + from numpy.typing import NDArray + + _NUMPY_AVAILABLE = True +except ImportError: # pragma: no cover + _NUMPY_AVAILABLE = False + np = None # type: ignore[assignment] + NDArray = Any # type: ignore[assignment,misc] + +logger = logging.getLogger(__name__) + + +# --------------------------------------------------------------------------- +# Camera Protocol +# --------------------------------------------------------------------------- + +@runtime_checkable +class Camera(Protocol): + """Protocol for camera devices. + + Any object satisfying this protocol can be used as a camera source + within the LeapFlow hardware stack. + """ + + @property + def is_connected(self) -> bool: + """Whether the camera is currently connected and ready.""" + ... + + def connect(self, warmup: bool = True) -> None: + """Establish connection to the camera. + + Args: + warmup: If ``True``, capture a warm-up frame before + returning. + """ + ... + + def disconnect(self) -> None: + """Disconnect from the camera and release resources.""" + ... + + def read(self) -> Any: + """Capture and return a single frame synchronously (blocking).""" + ... + + def async_read(self, timeout_ms: float = 200) -> Any: + """Return the most recent new frame (may block up to *timeout_ms*).""" + ... + + +# --------------------------------------------------------------------------- +# CameraConfig +# --------------------------------------------------------------------------- + +class CameraConfig: + """Minimal camera configuration. + + Replaces the draccus-based config from the upstream project with a + plain Python class. + """ + + def __init__( + self, + index_or_path: int | str = 0, + *, + fps: int | None = None, + width: int | None = None, + height: int | None = None, + color_mode: str = "rgb", + rotation: int | None = None, + warmup_s: float = 1.0, + ) -> None: + self.index_or_path = index_or_path + self.fps = fps + self.width = width + self.height = height + self.color_mode = color_mode + self.rotation = rotation + self.warmup_s = warmup_s + + +# --------------------------------------------------------------------------- +# OpenCVCamera +# --------------------------------------------------------------------------- + +def _get_cv2_rotation(degrees: int | None) -> int | None: + """Map rotation degrees to an OpenCV constant (or ``None``).""" + if degrees is None: + return None + try: + import cv2 + except ImportError: # pragma: no cover + return None + mapping = { + 90: cv2.ROTATE_90_CLOCKWISE, + 180: cv2.ROTATE_180, + 270: cv2.ROTATE_90_COUNTERCLOCKWISE, + -90: cv2.ROTATE_90_COUNTERCLOCKWISE, + } + return mapping.get(degrees) + + +class OpenCVCamera: + """Camera implementation backed by OpenCV's ``VideoCapture``. + + Supports synchronous and asynchronous (background-thread) frame + capture. The heavy ``cv2`` import happens lazily at connect time so + the module can be imported in environments where OpenCV is absent. + """ + + def __init__(self, config: CameraConfig | None = None, **kwargs: Any) -> None: + cfg = config or CameraConfig(**kwargs) + self._index_or_path = cfg.index_or_path + self._target_fps = cfg.fps + self._target_width = cfg.width + self._target_height = cfg.height + self._color_mode = cfg.color_mode.lower() + self._warmup_s = cfg.warmup_s + self._rotation = _get_cv2_rotation(cfg.rotation) + + # Runtime state + self._cap: Any = None # cv2.VideoCapture + self._thread: Thread | None = None + self._stop_event: Event | None = None + self._frame_lock = Lock() + self._latest_frame: Any = None + self._latest_timestamp: float | None = None + self._new_frame = Event() + + # Resolved after connect + self.fps: int | float | None = cfg.fps + self.width: int | None = cfg.width + self.height: int | None = cfg.height + + def __str__(self) -> str: + return f"OpenCVCamera({self._index_or_path})" + + # -- Protocol properties / methods ------------------------------------ + + @property + def is_connected(self) -> bool: + try: + import cv2 + except ImportError: # pragma: no cover + return False + return isinstance(self._cap, cv2.VideoCapture) and self._cap.isOpened() + + def connect(self, warmup: bool = True) -> None: + """Open the camera and start the background capture thread.""" + import cv2 + + if self.is_connected: + return + + cv2.setNumThreads(1) + self._cap = cv2.VideoCapture(self._index_or_path) + + if not self._cap.isOpened(): + self._cap.release() + self._cap = None + raise ConnectionError( + f"Failed to open camera at {self._index_or_path}." + ) + + try: + self._apply_settings(cv2) + self._start_thread() + + if warmup and self._warmup_s > 0: + deadline = time.time() + self._warmup_s + while time.time() < deadline: + try: + self.async_read(timeout_ms=self._warmup_s * 1000) + except TimeoutError: + pass + time.sleep(0.1) + except BaseException: + self._cleanup() + raise + + logger.info("%s connected.", self) + + def disconnect(self) -> None: + """Stop capture and release the camera.""" + if not self.is_connected and self._thread is None: + return + self._cleanup() + logger.info("%s disconnected.", self) + + def read(self) -> Any: + """Synchronous blocking read (delegates to async_read).""" + if not self.is_connected: + raise RuntimeError(f"{self} is not connected.") + self._new_frame.clear() + return self.async_read(timeout_ms=10_000) + + def async_read(self, timeout_ms: float = 200) -> Any: + """Return the latest frame, waiting up to *timeout_ms*.""" + if not self.is_connected: + raise RuntimeError(f"{self} is not connected.") + if self._thread is None or not self._thread.is_alive(): + raise RuntimeError(f"{self} capture thread is not running.") + + if not self._new_frame.wait(timeout=timeout_ms / 1000.0): + raise TimeoutError( + f"No frame from {self} within {timeout_ms} ms." + ) + with self._frame_lock: + frame = self._latest_frame + self._new_frame.clear() + + if frame is None: + raise RuntimeError(f"Event set but no frame for {self}.") + return frame + + # -- Context manager -------------------------------------------------- + + def __enter__(self): + self.connect() + return self + + def __exit__(self, *exc: Any) -> None: + self.disconnect() + + # -- internals -------------------------------------------------------- + + def _apply_settings(self, cv2_mod: Any) -> None: + """Apply FPS / resolution to the opened capture.""" + cap = self._cap + if self._target_width is not None: + cap.set(cv2_mod.CAP_PROP_FRAME_WIDTH, float(self._target_width)) + if self._target_height is not None: + cap.set(cv2_mod.CAP_PROP_FRAME_HEIGHT, float(self._target_height)) + if self._target_fps is not None: + cap.set(cv2_mod.CAP_PROP_FPS, float(self._target_fps)) + + # Read back the actual values + self.width = int(round(cap.get(cv2_mod.CAP_PROP_FRAME_WIDTH))) + self.height = int(round(cap.get(cv2_mod.CAP_PROP_FRAME_HEIGHT))) + self.fps = cap.get(cv2_mod.CAP_PROP_FPS) + + def _start_thread(self) -> None: + self._stop_thread() + self._stop_event = Event() + self._thread = Thread( + target=self._capture_loop, daemon=True, + name=f"{self}_capture", + ) + self._thread.start() + time.sleep(0.1) + + def _stop_thread(self) -> None: + if self._stop_event is not None: + self._stop_event.set() + if self._thread is not None and self._thread.is_alive(): + self._thread.join(timeout=2.0) + self._thread = None + self._stop_event = None + with self._frame_lock: + self._latest_frame = None + self._latest_timestamp = None + self._new_frame.clear() + + def _cleanup(self) -> None: + thread = self._thread + cap = self._cap + try: + self._stop_thread() + finally: + self._cap = None + if cap is not None: + cap.release() + if thread is not None and thread.is_alive(): + thread.join(timeout=2.0) + + def _capture_loop(self) -> None: + """Background loop that reads frames from hardware.""" + import cv2 + + stop = self._stop_event + failures = 0 + while stop is not None and not stop.is_set(): + try: + ret, frame = self._cap.read() + if not ret: + raise RuntimeError("read() returned False") + + # Color conversion + if self._color_mode == "rgb": + frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) + + # Rotation + if self._rotation is not None: + frame = cv2.rotate(frame, self._rotation) + + with self._frame_lock: + self._latest_frame = frame + self._latest_timestamp = time.perf_counter() + self._new_frame.set() + failures = 0 + except Exception as exc: + failures += 1 + if failures > 10: + logger.error( + "%s exceeded max consecutive failures: %s", self, exc, + ) + break + logger.warning("%s capture error: %s", self, exc) + + # -- static discovery ------------------------------------------------- + + @staticmethod + def find_cameras() -> list[dict[str, Any]]: + """Detect available OpenCV cameras on the system.""" + try: + import cv2 + except ImportError: # pragma: no cover + return [] + + from pathlib import Path + + found: list[dict[str, Any]] = [] + if platform.system() == "Linux": + targets: list[str | int] = [ + str(p) for p in sorted(Path("/dev").glob("video*")) + ] + else: + targets = list(range(60)) + + for target in targets: + cap = cv2.VideoCapture(target) + try: + if cap.isOpened(): + found.append({ + "type": "OpenCV", + "id": target, + "width": int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)), + "height": int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)), + "fps": cap.get(cv2.CAP_PROP_FPS), + }) + finally: + cap.release() + return found + + +__all__ = [ + "Camera", + "CameraConfig", + "OpenCVCamera", +] diff --git a/src/leapflow/robot/drivers/__init__.py b/src/leapflow/robot/drivers/__init__.py new file mode 100644 index 00000000..5a5de84c --- /dev/null +++ b/src/leapflow/robot/drivers/__init__.py @@ -0,0 +1,10 @@ +# Copyright (c) Alibaba, Inc. and its affiliates. +"""Motor driver implementations for LeapRobot. + +This sub-package contains concrete motor bus drivers that implement the +``SerialMotorsBus`` interface for specific motor families. +""" + +from leapflow.robot.drivers.feetech import FeetechMotorsBus + +__all__ = ["FeetechMotorsBus"] diff --git a/src/leapflow/robot/drivers/feetech.py b/src/leapflow/robot/drivers/feetech.py new file mode 100644 index 00000000..cb7db68a --- /dev/null +++ b/src/leapflow/robot/drivers/feetech.py @@ -0,0 +1,636 @@ +# Copyright (c) Alibaba, Inc. and its affiliates. +"""Feetech servo motor bus driver for LeapRobot. + +Implements ``SerialMotorsBus`` for the Feetech STS / SMS / SCS series +servo motors. The Feetech SDK (``scservo_sdk``) is an optional +dependency imported at connection time. + +This module absorbs and adapts the inference-relevant subset of the +upstream open-source Feetech driver (see copyright notice), removing interactive +calibration UI and training-only utilities. +""" + +from __future__ import annotations + +import logging +from copy import deepcopy +from enum import Enum +from pprint import pformat +from typing import TYPE_CHECKING, Any + +from leapflow.robot.motors import ( + Motor, + MotorCalibration, + NameOrID, + SerialMotorsBus, + Value, + get_address, +) + +logger = logging.getLogger(__name__) + + +# --------------------------------------------------------------------------- +# Feetech SDK — optional dependency +# --------------------------------------------------------------------------- +try: + import scservo_sdk as scs + + _SCS_AVAILABLE = True +except ImportError: # pragma: no cover + _SCS_AVAILABLE = False + scs = None # type: ignore[assignment] + + +def _require_scs() -> None: + if not _SCS_AVAILABLE: + raise ImportError( + "feetech-servo-sdk is required for Feetech motors. " + "Install with: pip install feetech-servo-sdk" + ) + + +# --------------------------------------------------------------------------- +# Sign-magnitude encoding helpers +# --------------------------------------------------------------------------- + +def encode_sign_magnitude(value: int, sign_bit: int) -> int: + """Encode a signed integer to sign-magnitude representation.""" + if value < 0: + return (-value) | (1 << sign_bit) + return value + + +def decode_sign_magnitude(value: int, sign_bit: int) -> int: + """Decode a sign-magnitude value to a signed integer.""" + if value & (1 << sign_bit): + return -(value & ~(1 << sign_bit)) + return value + + +# --------------------------------------------------------------------------- +# Control tables +# --------------------------------------------------------------------------- + +FIRMWARE_MAJOR_VERSION = (0, 1) +FIRMWARE_MINOR_VERSION = (1, 1) +MODEL_NUMBER_ADDR = (3, 2) + +# STS / SMS series control table (address, byte_length) +STS_SMS_SERIES_CONTROL_TABLE: dict[str, tuple[int, int]] = { + # EPROM + "Firmware_Major_Version": FIRMWARE_MAJOR_VERSION, + "Firmware_Minor_Version": FIRMWARE_MINOR_VERSION, + "Model_Number": MODEL_NUMBER_ADDR, + "ID": (5, 1), + "Baud_Rate": (6, 1), + "Return_Delay_Time": (7, 1), + "Response_Status_Level": (8, 1), + "Min_Position_Limit": (9, 2), + "Max_Position_Limit": (11, 2), + "Max_Temperature_Limit": (13, 1), + "Max_Voltage_Limit": (14, 1), + "Min_Voltage_Limit": (15, 1), + "Max_Torque_Limit": (16, 2), + "Phase": (18, 1), + "Unloading_Condition": (19, 1), + "LED_Alarm_Condition": (20, 1), + "P_Coefficient": (21, 1), + "D_Coefficient": (22, 1), + "I_Coefficient": (23, 1), + "Minimum_Startup_Force": (24, 2), + "CW_Dead_Zone": (26, 1), + "CCW_Dead_Zone": (27, 1), + "Protection_Current": (28, 2), + "Angular_Resolution": (30, 1), + "Homing_Offset": (31, 2), + "Operating_Mode": (33, 1), + "Protective_Torque": (34, 1), + "Protection_Time": (35, 1), + "Overload_Torque": (36, 1), + "Velocity_closed_loop_P": (37, 1), + "Over_Current_Protection_Time": (38, 1), + "Velocity_closed_loop_I": (39, 1), + # SRAM + "Torque_Enable": (40, 1), + "Acceleration": (41, 1), + "Goal_Position": (42, 2), + "Goal_Time": (44, 2), + "Goal_Velocity": (46, 2), + "Torque_Limit": (48, 2), + "Lock": (55, 1), + "Present_Position": (56, 2), + "Present_Velocity": (58, 2), + "Present_Load": (60, 2), + "Present_Voltage": (62, 1), + "Present_Temperature": (63, 1), + "Status": (65, 1), + "Moving": (66, 1), + "Present_Current": (69, 2), + "Maximum_Acceleration": (85, 1), +} + +# SCS series control table +SCS_SERIES_CONTROL_TABLE: dict[str, tuple[int, int]] = { + "Firmware_Major_Version": FIRMWARE_MAJOR_VERSION, + "Firmware_Minor_Version": FIRMWARE_MINOR_VERSION, + "Model_Number": MODEL_NUMBER_ADDR, + "ID": (5, 1), + "Baud_Rate": (6, 1), + "Return_Delay_Time": (7, 1), + "Response_Status_Level": (8, 1), + "Min_Position_Limit": (9, 2), + "Max_Position_Limit": (11, 2), + "Max_Temperature_Limit": (13, 1), + "Max_Voltage_Limit": (14, 1), + "Min_Voltage_Limit": (15, 1), + "Max_Torque_Limit": (16, 2), + "Phase": (18, 1), + "Unloading_Condition": (19, 1), + "LED_Alarm_Condition": (20, 1), + "P_Coefficient": (21, 1), + "D_Coefficient": (22, 1), + "I_Coefficient": (23, 1), + "Minimum_Startup_Force": (24, 2), + "CW_Dead_Zone": (26, 1), + "CCW_Dead_Zone": (27, 1), + "Protective_Torque": (37, 1), + "Protection_Time": (38, 1), + "Torque_Enable": (40, 1), + "Acceleration": (41, 1), + "Goal_Position": (42, 2), + "Running_Time": (44, 2), + "Goal_Velocity": (46, 2), + "Lock": (48, 1), + "Present_Position": (56, 2), + "Present_Velocity": (58, 2), + "Present_Load": (60, 2), + "Present_Voltage": (62, 1), + "Present_Temperature": (63, 1), + "Status": (65, 1), + "Moving": (66, 1), +} + +STS_SMS_SERIES_BAUDRATE_TABLE: dict[int, int] = { + 1_000_000: 0, 500_000: 1, 250_000: 2, 128_000: 3, + 115_200: 4, 57_600: 5, 38_400: 6, 19_200: 7, +} + +SCS_SERIES_BAUDRATE_TABLE: dict[int, int] = { + 1_000_000: 0, 500_000: 1, 250_000: 2, 128_000: 3, + 115_200: 4, 57_600: 5, 38_400: 6, 19_200: 7, +} + +MODEL_CONTROL_TABLE: dict[str, dict[str, tuple[int, int]]] = { + "sts_series": STS_SMS_SERIES_CONTROL_TABLE, + "scs_series": SCS_SERIES_CONTROL_TABLE, + "sms_series": STS_SMS_SERIES_CONTROL_TABLE, + "sts3215": STS_SMS_SERIES_CONTROL_TABLE, + "sts3250": STS_SMS_SERIES_CONTROL_TABLE, + "scs0009": SCS_SERIES_CONTROL_TABLE, + "sm8512bl": STS_SMS_SERIES_CONTROL_TABLE, +} + +MODEL_RESOLUTION: dict[str, int] = { + "sts_series": 4096, "sms_series": 4096, "scs_series": 1024, + "sts3215": 4096, "sts3250": 4096, "sm8512bl": 4096, "scs0009": 1024, +} + +MODEL_BAUDRATE_TABLE: dict[str, dict[int, int]] = { + "sts_series": STS_SMS_SERIES_BAUDRATE_TABLE, + "sms_series": STS_SMS_SERIES_BAUDRATE_TABLE, + "scs_series": SCS_SERIES_BAUDRATE_TABLE, + "sm8512bl": STS_SMS_SERIES_BAUDRATE_TABLE, + "sts3215": STS_SMS_SERIES_BAUDRATE_TABLE, + "sts3250": STS_SMS_SERIES_BAUDRATE_TABLE, + "scs0009": SCS_SERIES_BAUDRATE_TABLE, +} + +STS_SMS_SERIES_ENCODINGS: dict[str, int] = { + "Present_Load": 10, "Homing_Offset": 11, + "Goal_Position": 15, "Goal_Velocity": 15, "Goal_Speed": 15, + "Present_Position": 15, "Present_Velocity": 15, "Present_Speed": 15, +} + +MODEL_ENCODING_TABLE: dict[str, dict[str, int]] = { + "sts_series": STS_SMS_SERIES_ENCODINGS, + "sms_series": STS_SMS_SERIES_ENCODINGS, + "scs_series": {}, + "sts3215": STS_SMS_SERIES_ENCODINGS, + "sts3250": STS_SMS_SERIES_ENCODINGS, + "sm8512bl": STS_SMS_SERIES_ENCODINGS, + "scs0009": {}, +} + +SCAN_BAUDRATES: list[int] = [ + 4_800, 9_600, 14_400, 19_200, 38_400, 57_600, + 115_200, 128_000, 250_000, 500_000, 1_000_000, +] + +MODEL_NUMBER_TABLE: dict[str, int] = { + "sts3215": 777, "sts3250": 2825, "sm8512bl": 11272, "scs0009": 1284, +} + +MODEL_PROTOCOL: dict[str, int] = { + "sts_series": 0, "sms_series": 0, "scs_series": 1, + "sts3215": 0, "sts3250": 0, "sm8512bl": 0, "scs0009": 1, +} + +DEFAULT_PROTOCOL_VERSION = 0 +DEFAULT_BAUDRATE = 1_000_000 +DEFAULT_TIMEOUT_MS = 1000 +NORMALIZED_DATA = ["Goal_Position", "Present_Position"] + + +# --------------------------------------------------------------------------- +# Enums +# --------------------------------------------------------------------------- + +class OperatingMode(Enum): + """Feetech motor operating modes.""" + POSITION = 0 + VELOCITY = 1 + PWM = 2 + STEP = 3 + + +class DriveMode(Enum): + NON_INVERTED = 0 + INVERTED = 1 + + +class TorqueMode(Enum): + ENABLED = 1 + DISABLED = 0 + + +# --------------------------------------------------------------------------- +# SDK timeout patch +# --------------------------------------------------------------------------- + +def _patch_set_packet_timeout(port_handler: Any, packet_length: int) -> None: + """Fix scservo_sdk timeout calculation bug. + + The official Feetech SDK on PyPI has incorrect timeout maths; + this patch matches the fix in the canonical Gitee repository. + """ + port_handler.packet_start_time = port_handler.getCurrentTime() + port_handler.packet_timeout = ( + (port_handler.tx_time_per_byte * packet_length) + + (port_handler.tx_time_per_byte * 3.0) + + 50 + ) + + +# --------------------------------------------------------------------------- +# FeetechMotorsBus +# --------------------------------------------------------------------------- + +class FeetechMotorsBus(SerialMotorsBus): + """Feetech STS / SMS / SCS motor bus driver. + + Wraps the ``scservo_sdk`` library behind the ``SerialMotorsBus`` + interface. The SDK is imported lazily at connection time. + """ + + apply_drive_mode = True + available_baudrates = deepcopy(SCAN_BAUDRATES) + default_baudrate = DEFAULT_BAUDRATE + default_timeout = DEFAULT_TIMEOUT_MS + model_baudrate_table = deepcopy(MODEL_BAUDRATE_TABLE) + model_ctrl_table = deepcopy(MODEL_CONTROL_TABLE) + model_encoding_table = deepcopy(MODEL_ENCODING_TABLE) + model_number_table = deepcopy(MODEL_NUMBER_TABLE) + model_resolution_table = deepcopy(MODEL_RESOLUTION) + normalized_data = deepcopy(NORMALIZED_DATA) + + def __init__( + self, + port: str, + motors: dict[str, Motor], + calibration: dict[str, MotorCalibration] | None = None, + protocol_version: int = DEFAULT_PROTOCOL_VERSION, + ) -> None: + _require_scs() + super().__init__(port, motors, calibration) + self.protocol_version = protocol_version + + # Validate protocol consistency + for model in self.models: + expected = MODEL_PROTOCOL.get(model, 0) + if expected != self.protocol_version: + raise ValueError( + f"Motor model {model!r} requires protocol {expected}, " + f"but bus uses {self.protocol_version}." + ) + + # SDK handles + self.port_handler = scs.PortHandler(self.port) + self.port_handler.setPacketTimeout = ( + _patch_set_packet_timeout.__get__(self.port_handler) + ) + self.packet_handler = scs.PacketHandler(protocol_version) + self.sync_reader = scs.GroupSyncRead( + self.port_handler, self.packet_handler, 0, 0, + ) + self.sync_writer = scs.GroupSyncWrite( + self.port_handler, self.packet_handler, 0, 0, + ) + self._comm_success = scs.COMM_SUCCESS + self._no_error = 0x00 + + # -- handshake -------------------------------------------------------- + + def _handshake(self) -> None: + self._assert_motors_exist() + + def _assert_motors_exist(self) -> None: + """Ping every registered motor and verify model numbers.""" + expected = {m.id: self.model_number_table[m.model] for m in self.motors.values()} + found: dict[int, int] = {} + for id_ in self.ids: + model_nb = self._ping(id_) + if model_nb is not None: + found[id_] = model_nb + + missing = [id_ for id_ in self.ids if id_ not in found] + wrong = { + id_: (expected[id_], found[id_]) + for id_ in found if expected.get(id_) != found[id_] + } + if missing or wrong: + parts = [f"{type(self).__name__} check failed on '{self.port}':"] + if missing: + parts.append(f" Missing IDs: {missing}") + if wrong: + parts.append(f" Wrong models: {wrong}") + raise RuntimeError("\n".join(parts)) + + def _ping(self, motor_id: int, num_retry: int = 0) -> int | None: + """Ping a motor and return its model number, or ``None``.""" + for _ in range(1 + num_retry): + model_nb, comm, error = self.packet_handler.ping( + self.port_handler, motor_id, + ) + if self._is_comm_success(comm): + return model_nb + return None + + # -- protocol compatibility ------------------------------------------- + + def _assert_protocol_compatible(self, instruction: str) -> None: + if instruction == "sync_read" and self.protocol_version == 1: + raise NotImplementedError( + "Sync Read is not available with Feetech Protocol 1." + ) + + # -- low-level read / write ------------------------------------------- + + def _read( + self, + address: int, + length: int, + motor_id: int, + *, + num_retry: int = 0, + raise_on_error: bool = True, + ) -> tuple[int, int, int]: + read_fn = { + 1: self.packet_handler.read1ByteTxRx, + 2: self.packet_handler.read2ByteTxRx, + 4: self.packet_handler.read4ByteTxRx, + }.get(length) + if read_fn is None: + raise ValueError(f"Unsupported register length: {length}") + + comm = error = 0 + value = 0 + for _ in range(1 + num_retry): + value, comm, error = read_fn(self.port_handler, motor_id, address) + if self._is_comm_success(comm): + break + + if not self._is_comm_success(comm) and raise_on_error: + raise ConnectionError( + f"Read @{address} (len={length}) failed on id={motor_id}: " + f"{self.packet_handler.getTxRxResult(comm)}" + ) + return value, comm, error + + def _write( + self, + addr: int, + length: int, + motor_id: int, + value: int, + *, + num_retry: int = 0, + raise_on_error: bool = True, + ) -> tuple[int, int]: + data = self._serialize_data(value, length) + comm = error = 0 + for _ in range(1 + num_retry): + comm, error = self.packet_handler.writeTxRx( + self.port_handler, motor_id, addr, length, data, + ) + if self._is_comm_success(comm): + break + + if not self._is_comm_success(comm) and raise_on_error: + raise ConnectionError( + f"Write @{addr} (len={length}) failed on id={motor_id}: " + f"{self.packet_handler.getTxRxResult(comm)}" + ) + return comm, error + + # -- sync read / write ------------------------------------------------ + + def _sync_read( + self, + addr: int, + length: int, + motor_ids: list[int], + *, + num_retry: int = 0, + ) -> tuple[dict[int, int], int]: + self._assert_protocol_compatible("sync_read") + self.sync_reader.clearParam() + self.sync_reader.start_address = addr + self.sync_reader.data_length = length + for id_ in motor_ids: + self.sync_reader.addParam(id_) + + comm = 0 + for _ in range(1 + num_retry): + comm = self.sync_reader.txRxPacket() + if self._is_comm_success(comm): + break + + values = { + id_: self.sync_reader.getData(id_, addr, length) + for id_ in motor_ids + } + return values, comm + + def _sync_write( + self, + addr: int, + length: int, + ids_values: dict[int, int], + *, + num_retry: int = 0, + ) -> int: + self.sync_writer.clearParam() + self.sync_writer.start_address = addr + self.sync_writer.data_length = length + for id_, value in ids_values.items(): + data = self._serialize_data(value, length) + self.sync_writer.addParam(id_, data) + + comm = 0 + for _ in range(1 + num_retry): + comm = self.sync_writer.txPacket() + if self._is_comm_success(comm): + break + return comm + + # -- byte splitting (little-endian) ----------------------------------- + + def _split_into_byte_chunks(self, value: int, length: int) -> list[int]: + if length == 1: + return [value] + elif length == 2: + return [scs.SCS_LOBYTE(value), scs.SCS_HIBYTE(value)] + elif length == 4: + return [ + scs.SCS_LOBYTE(scs.SCS_LOWORD(value)), + scs.SCS_HIBYTE(scs.SCS_LOWORD(value)), + scs.SCS_LOBYTE(scs.SCS_HIWORD(value)), + scs.SCS_HIBYTE(scs.SCS_HIWORD(value)), + ] + raise NotImplementedError(f"Unsupported length: {length}") + + # -- sign encoding / decoding ----------------------------------------- + + def _encode_sign( + self, data_name: str, ids_values: dict[int, int], + ) -> dict[int, int]: + for id_ in ids_values: + model = self._id_to_model(id_) + enc_table = self.model_encoding_table.get(model) + if enc_table and data_name in enc_table: + ids_values[id_] = encode_sign_magnitude( + ids_values[id_], enc_table[data_name], + ) + return ids_values + + def _decode_sign( + self, data_name: str, ids_values: dict[int, int], + ) -> dict[int, int]: + for id_ in ids_values: + model = self._id_to_model(id_) + enc_table = self.model_encoding_table.get(model) + if enc_table and data_name in enc_table: + ids_values[id_] = decode_sign_magnitude( + ids_values[id_], enc_table[data_name], + ) + return ids_values + + # -- torque control --------------------------------------------------- + + def enable_torque( + self, motors: int | str | list[str] | None = None, num_retry: int = 0, + ) -> None: + for motor in self._get_motors_list(motors): + self.write( + "Torque_Enable", motor, + TorqueMode.ENABLED.value, num_retry=num_retry, + ) + self.write("Lock", motor, 1, num_retry=num_retry) + + def disable_torque( + self, motors: int | str | list[str] | None = None, num_retry: int = 0, + ) -> None: + for motor in self._get_motors_list(motors): + self.write( + "Torque_Enable", motor, + TorqueMode.DISABLED.value, num_retry=num_retry, + ) + self.write("Lock", motor, 0, num_retry=num_retry) + + # -- calibration ------------------------------------------------------ + + @property + def is_calibrated(self) -> bool: + if not self.calibration: + return False + hw_cal = self.read_calibration() + if set(hw_cal) != set(self.calibration): + return False + return all( + self.calibration[m].range_min == c.range_min + and self.calibration[m].range_max == c.range_max + for m, c in hw_cal.items() + ) + + def read_calibration(self) -> dict[str, MotorCalibration]: + calibration: dict[str, MotorCalibration] = {} + for motor, m in self.motors.items(): + min_pos = self.read("Min_Position_Limit", motor, normalize=False) + max_pos = self.read("Max_Position_Limit", motor, normalize=False) + offset = ( + self.read("Homing_Offset", motor, normalize=False) + if self.protocol_version == 0 + else 0 + ) + calibration[motor] = MotorCalibration( + id=m.id, drive_mode=0, homing_offset=int(offset), + range_min=int(min_pos), range_max=int(max_pos), + ) + return calibration + + def write_calibration( + self, + calibration_dict: dict[str, MotorCalibration], + cache: bool = True, + ) -> None: + for motor, cal in calibration_dict.items(): + if self.protocol_version == 0: + self.write("Homing_Offset", motor, cal.homing_offset) + self.write("Min_Position_Limit", motor, cal.range_min) + self.write("Max_Position_Limit", motor, cal.range_max) + if cache: + self.calibration = calibration_dict + + # -- motor configuration ---------------------------------------------- + + def configure_motors( + self, + return_delay_time: int = 0, + maximum_acceleration: int = 254, + acceleration: int = 254, + ) -> None: + """Apply recommended runtime configuration to all motors.""" + for motor in self.motors: + self.write("Return_Delay_Time", motor, return_delay_time) + if self.protocol_version == 0: + self.write("Maximum_Acceleration", motor, maximum_acceleration) + self.write("Acceleration", motor, acceleration) + + +__all__ = [ + "FeetechMotorsBus", + "OperatingMode", + "DriveMode", + "TorqueMode", + "encode_sign_magnitude", + "decode_sign_magnitude", + # Tables + "MODEL_CONTROL_TABLE", + "MODEL_RESOLUTION", + "MODEL_BAUDRATE_TABLE", + "MODEL_ENCODING_TABLE", + "MODEL_NUMBER_TABLE", + "MODEL_PROTOCOL", + "SCAN_BAUDRATES", +] diff --git a/src/leapflow/robot/episode_export.py b/src/leapflow/robot/episode_export.py new file mode 100644 index 00000000..d4ba4a99 --- /dev/null +++ b/src/leapflow/robot/episode_export.py @@ -0,0 +1,384 @@ +# Copyright (c) Alibaba, Inc. and its affiliates. +"""Episode export: physical trajectories to standard training formats. + +Exports recorded PhysicalTrajectory / PhysicalEpisode data to the standard +Parquet + MP4 layout used by offline imitation learning pipelines (the same +layout LeapRobot's absorbed policy loader expects). Training itself is out +of scope — LeapFlow records and exports; a separate offline pipeline trains. + +pyarrow and opencv are *optional* dependencies. When unavailable, export +falls back to JSON + individual frame files and logs a degradation warning. +""" + +from __future__ import annotations + +import json +import logging +import os +from pathlib import Path +from typing import Any + +logger = logging.getLogger(__name__) + +# --------------------------------------------------------------------------- +# Optional dependency probing +# --------------------------------------------------------------------------- + +_PYARROW_AVAILABLE = False +try: + import pyarrow as pa # noqa: F401 + import pyarrow.parquet as pq # noqa: F401 + + _PYARROW_AVAILABLE = True +except ImportError: + pa = None # type: ignore[assignment] + pq = None # type: ignore[assignment] + +_CV2_AVAILABLE = False +try: + import cv2 # noqa: F401 + + _CV2_AVAILABLE = True +except ImportError: + cv2 = None # type: ignore[assignment] + +_NUMPY_AVAILABLE = False +try: + import numpy as np # noqa: F401 + + _NUMPY_AVAILABLE = True +except ImportError: + np = None # type: ignore[assignment] + + +# --------------------------------------------------------------------------- +# EpisodeExporter +# --------------------------------------------------------------------------- + + +class EpisodeExporter: + """Exports physical episodes to Parquet (state/action) + MP4 (frames). + + Directory layout:: + + export_dir/ + meta/info.json # schema, fps, features + meta/episodes.parquet # per-episode metadata (or .json) + data/episode_NNN.parquet # per-step state/action rows (or .json) + videos/episode_NNN/{cam}.mp4 # camera frames (or raw frames/) + + When ``pyarrow`` is not installed, ``.parquet`` files are replaced with + ``.json`` and a warning is logged. When ``opencv-python`` is not + installed, MP4 encoding is skipped and raw frame references are + preserved. + """ + + def __init__(self, export_dir: str | Path, *, fps: float = 30.0) -> None: + self._export_dir = Path(export_dir) + self._fps = max(1.0, float(fps)) + self._episodes_exported = 0 + + @property + def export_dir(self) -> Path: + return self._export_dir + + @property + def episodes_exported(self) -> int: + return self._episodes_exported + + # -- Public API ----------------------------------------------------------- + + async def export_episode(self, episode: Any) -> dict[str, Any]: + """Export one :class:`PhysicalEpisode`. + + Returns a summary dict: ``{episode_id, files_written, format, ...}``. + """ + episode_id: str = getattr(episode, "episode_id", "unknown") + trajectory = getattr(episode, "trajectory", None) + task: str = getattr(episode, "task", "") + success: bool = getattr(episode, "success", False) + started_at: float = getattr(episode, "started_at", 0.0) + ended_at: float = getattr(episode, "ended_at", 0.0) + + steps: tuple[Any, ...] = () + if trajectory is not None: + steps = getattr(trajectory, "steps", ()) + + files_written: list[str] = [] + + # 1. State/action Parquet (or JSON). + data_dir = self._export_dir / "data" + data_dir.mkdir(parents=True, exist_ok=True) + if _PYARROW_AVAILABLE: + parquet_path = data_dir / f"episode_{episode_id}.parquet" + self._write_state_action_parquet(steps, parquet_path) + files_written.append(str(parquet_path)) + else: + json_path = data_dir / f"episode_{episode_id}.json" + self._write_state_action_json(steps, json_path) + files_written.append(str(json_path)) + logger.warning( + "pyarrow not available; exported episode %s as JSON instead of Parquet.", + episode_id, + ) + + # 2. Camera frames → MP4 (or raw frame references). + camera_ids = self._collect_camera_ids(steps) + for camera_id in camera_ids: + video_dir = self._export_dir / "videos" / f"episode_{episode_id}" + video_dir.mkdir(parents=True, exist_ok=True) + if _CV2_AVAILABLE and _NUMPY_AVAILABLE: + mp4_path = video_dir / f"{camera_id}.mp4" + self._write_frames_mp4(steps, camera_id, mp4_path) + files_written.append(str(mp4_path)) + else: + refs_path = video_dir / f"{camera_id}_refs.json" + self._write_frame_refs_json(steps, camera_id, refs_path) + files_written.append(str(refs_path)) + logger.warning( + "opencv/numpy not available; wrote frame references for %s/%s.", + episode_id, camera_id, + ) + + # 3. Episode metadata → episodes manifest. + meta_dir = self._export_dir / "meta" + meta_dir.mkdir(parents=True, exist_ok=True) + ep_meta = { + "episode_id": episode_id, + "task": task, + "success": success, + "started_at": started_at, + "ended_at": ended_at, + "step_count": len(steps), + "fps": self._fps, + } + self._append_episode_manifest(meta_dir, ep_meta) + + # 4. Info JSON. + self._write_info_json( + meta_dir, + episode_id=episode_id, + step_count=len(steps), + camera_ids=camera_ids, + ) + + self._episodes_exported += 1 + return { + "episode_id": episode_id, + "files_written": files_written, + "format": "parquet" if _PYARROW_AVAILABLE else "json", + "step_count": len(steps), + } + + async def export_trajectory( + self, + trajectory: Any, + *, + task: str = "", + ) -> dict[str, Any]: + """Export a raw :class:`PhysicalTrajectory` (demonstration without outcome). + + Wraps the trajectory in a minimal episode-like structure and delegates + to :meth:`export_episode`. + """ + trajectory_id: str = getattr(trajectory, "trajectory_id", "unknown") + + class _MinimalEpisode: + """Lightweight shim so export_episode can consume a raw trajectory.""" + + def __init__(self, traj: Any, task_: str) -> None: + self.episode_id = trajectory_id + self.trajectory = traj + self.task = task_ + self.success = False + self.started_at = getattr(traj, "started_at", 0.0) + self.ended_at = getattr(traj, "ended_at", 0.0) + + shim = _MinimalEpisode(trajectory, task) + return await self.export_episode(shim) + + # -- Internal: Parquet / JSON writers ------------------------------------- + + def _write_state_action_parquet( + self, + steps: tuple[Any, ...] | list[Any], + path: Path, + ) -> None: + """Write per-step joint positions/velocities/gripper/actions to Parquet.""" + if not _PYARROW_AVAILABLE or not steps: + return + + timestamps: list[float] = [] + sequences: list[int] = [] + action_sources: list[str] = [] + gripper_states: list[float | None] = [] + joint_positions_list: list[str] = [] + joint_velocities_list: list[str] = [] + + for step in steps: + timestamps.append(float(getattr(step, "timestamp", 0.0))) + sequences.append(int(getattr(step, "sequence", 0))) + action_sources.append(str(getattr(step, "action_source", ""))) + gripper_states.append(getattr(step, "gripper_state", None)) + positions = dict(getattr(step, "joint_positions", {})) + velocities = dict(getattr(step, "joint_velocities", {})) + joint_positions_list.append(json.dumps(positions, ensure_ascii=False)) + joint_velocities_list.append(json.dumps(velocities, ensure_ascii=False)) + + table = pa.table({ + "timestamp": pa.array(timestamps, type=pa.float64()), + "sequence": pa.array(sequences, type=pa.int32()), + "action_source": pa.array(action_sources, type=pa.string()), + "gripper_state": pa.array( + [g if g is not None else float("nan") for g in gripper_states], + type=pa.float64(), + ), + "joint_positions": pa.array(joint_positions_list, type=pa.string()), + "joint_velocities": pa.array(joint_velocities_list, type=pa.string()), + }) + pq.write_table(table, str(path)) + + def _write_state_action_json( + self, + steps: tuple[Any, ...] | list[Any], + path: Path, + ) -> None: + """Fallback: write step data as JSON when pyarrow is unavailable.""" + rows: list[dict[str, Any]] = [] + for step in steps: + to_dict_fn = getattr(step, "to_dict", None) + rows.append(to_dict_fn() if callable(to_dict_fn) else {"raw": str(step)}) + path.write_text( + json.dumps(rows, ensure_ascii=False, indent=2, default=str), + encoding="utf-8", + ) + + def _write_frames_mp4( + self, + steps: tuple[Any, ...] | list[Any], + camera_id: str, + path: Path, + ) -> None: + """Encode a camera's frame sequence to MP4 (best-effort). + + Frames are expected to be file paths (as per PhysicalTrajectoryStep + convention: camera_frames stores references, not inline bytes). + Reads each frame from disk, encodes to MP4. + """ + if not _CV2_AVAILABLE or not _NUMPY_AVAILABLE: + return + + writer = None + try: + for step in steps: + camera_frames = dict(getattr(step, "camera_frames", {})) + frame_ref = camera_frames.get(camera_id) + if not frame_ref: + continue + frame_path = Path(frame_ref) + if not frame_path.exists(): + continue + + img = cv2.imread(str(frame_path)) + if img is None: + continue + + if writer is None: + h, w = img.shape[:2] + fourcc = cv2.VideoWriter_fourcc(*"mp4v") + writer = cv2.VideoWriter(str(path), fourcc, self._fps, (w, h)) + + writer.write(img) + except Exception as exc: # noqa: BLE001 + logger.warning("MP4 encoding failed for %s: %s", camera_id, exc) + finally: + if writer is not None: + writer.release() + + def _write_frame_refs_json( + self, + steps: tuple[Any, ...] | list[Any], + camera_id: str, + path: Path, + ) -> None: + """Fallback: write frame references as JSON.""" + refs: list[dict[str, Any]] = [] + for idx, step in enumerate(steps): + camera_frames = dict(getattr(step, "camera_frames", {})) + frame_ref = camera_frames.get(camera_id) + if frame_ref: + refs.append({ + "sequence": idx, + "timestamp": float(getattr(step, "timestamp", 0.0)), + "frame_ref": str(frame_ref), + }) + path.write_text( + json.dumps(refs, ensure_ascii=False, indent=2), + encoding="utf-8", + ) + + def _write_info_json( + self, + meta_dir: Path, + *, + episode_id: str, + step_count: int, + camera_ids: list[str], + ) -> None: + """Write the info.json schema descriptor.""" + info = { + "fps": self._fps, + "format_version": 1, + "last_episode_id": episode_id, + "last_step_count": step_count, + "camera_ids": camera_ids, + "features": { + "timestamp": "float64", + "sequence": "int32", + "action_source": "string", + "gripper_state": "float64", + "joint_positions": "json_string", + "joint_velocities": "json_string", + }, + "data_format": "parquet" if _PYARROW_AVAILABLE else "json", + "video_format": "mp4" if _CV2_AVAILABLE else "frame_refs", + } + info_path = meta_dir / "info.json" + info_path.write_text( + json.dumps(info, ensure_ascii=False, indent=2), + encoding="utf-8", + ) + + def _append_episode_manifest( + self, + meta_dir: Path, + ep_meta: dict[str, Any], + ) -> None: + """Append an episode entry to the manifest file. + + Uses JSON lines for simplicity and append-friendliness. A Parquet + manifest can be built as a post-processing step. + """ + manifest_path = meta_dir / "episodes.jsonl" + with manifest_path.open("a", encoding="utf-8") as f: + f.write(json.dumps(ep_meta, ensure_ascii=False) + "\n") + + # -- Helpers -------------------------------------------------------------- + + @staticmethod + def _collect_camera_ids(steps: tuple[Any, ...] | list[Any]) -> list[str]: + """Collect unique camera ids across all steps.""" + seen: set[str] = set() + ordered: list[str] = [] + for step in steps: + camera_frames = getattr(step, "camera_frames", {}) + for cam_id in camera_frames: + if cam_id not in seen: + seen.add(cam_id) + ordered.append(cam_id) + return ordered + + +__all__ = [ + "EpisodeExporter", +] diff --git a/src/leapflow/robot/inference/__init__.py b/src/leapflow/robot/inference/__init__.py new file mode 100644 index 00000000..331127a7 --- /dev/null +++ b/src/leapflow/robot/inference/__init__.py @@ -0,0 +1,32 @@ +# Copyright (c) Alibaba, Inc. and its affiliates. +"""LeapRobot inference strategy framework. + +Provides pluggable physical AI inference strategies for test-time scaling. +Each strategy maps observation → action through a different compute path +(local VLA, remote PolicyServer, classical controller, etc.) and declares +its compute profile so the system can adapt the budget to task complexity. +""" + +from __future__ import annotations + +from leapflow.robot.inference.position_control import PositionControlStrategy +from leapflow.robot.inference.registry import InferenceStrategyRegistry +from leapflow.robot.inference.strategy import ( + ComputeBudget, + ComputeProfile, + InferenceResult, + InferenceStrategy, +) +from leapflow.robot.inference.vla_local import VLALocalStrategy +from leapflow.robot.inference.vla_remote import VLARemoteStrategy + +__all__ = [ + "ComputeBudget", + "ComputeProfile", + "InferenceResult", + "InferenceStrategy", + "InferenceStrategyRegistry", + "PositionControlStrategy", + "VLALocalStrategy", + "VLARemoteStrategy", +] diff --git a/src/leapflow/robot/inference/position_control.py b/src/leapflow/robot/inference/position_control.py new file mode 100644 index 00000000..1631e144 --- /dev/null +++ b/src/leapflow/robot/inference/position_control.py @@ -0,0 +1,129 @@ +# Copyright (c) Alibaba, Inc. and its affiliates. +"""Classical joint-space position control strategy. + +No neural network is involved: the strategy computes a position delta between a +target and the current joint state, optionally applying a proportional gain and +clamping each joint step for smoother, safer motion. It is useful for precise +positioning and as a zero-dependency fallback when no VLA model is loaded. +""" + +from __future__ import annotations + +import logging +import time +from typing import Any, Mapping + +from leapflow.robot.inference.strategy import ( + ComputeBudget, + ComputeProfile, + InferenceResult, +) + +logger = logging.getLogger(__name__) + +__all__ = ["PositionControlStrategy"] + +# Observation keys probed, in order, for the target and current joint vectors. +_TARGET_KEYS = ("target", "target_position", "goal") +_CURRENT_KEYS = ("position", "current", "state", "joint_position") + + +class PositionControlStrategy: + """Simple joint-space position controller. + + No neural network -- directly maps target positions to action vectors. + Useful for precise positioning tasks and as a fallback when no VLA + model is loaded. + + Supports optional P-control interpolation for smoother motion. + """ + + strategy_id = "position_control" + + def __init__(self, *, gain: float = 1.0, max_step_rad: float = 0.1) -> None: + self._gain = gain + self._max_step = max_step_rad + + @property + def compute_profile(self) -> ComputeProfile: + return ComputeProfile( + latency_range_ms=(0.1, 1.0), + supports_chunking=False, + gpu_required=False, + ) + + async def infer( + self, + observation: Mapping[str, Any], + *, + budget: ComputeBudget | None = None, + ) -> InferenceResult: + """Compute position delta: target - current, clamp by max_step.""" + t0 = time.monotonic() + + target = self._extract(observation, _TARGET_KEYS) + current = self._extract(observation, _CURRENT_KEYS) + if target is None: + raise RuntimeError( + "position_control requires a target in the observation " + f"(one of {_TARGET_KEYS})" + ) + if current is None: + # No current state: command the target directly, clamped from zero. + current = [0.0] * len(target) + + action = self._step_towards(current, target) + latency_ms = (time.monotonic() - t0) * 1000.0 + + return InferenceResult( + action=action, + latency_ms=latency_ms, + confidence=1.0, + chunk_size=1, + metadata={"strategy": self.strategy_id, "gain": self._gain}, + ) + + async def reset(self) -> None: + pass + + # ------------------------------------------------------------------ + # Internal helpers + # ------------------------------------------------------------------ + + def _step_towards( + self, + current: list[float], + target: list[float], + ) -> list[float]: + """Return a gain-scaled, per-joint clamped position command.""" + n = min(len(current), len(target)) + command: list[float] = [] + for i in range(n): + delta = (target[i] - current[i]) * self._gain + if delta > self._max_step: + delta = self._max_step + elif delta < -self._max_step: + delta = -self._max_step + command.append(current[i] + delta) + return command + + @staticmethod + def _extract( + observation: Mapping[str, Any], + keys: tuple[str, ...], + ) -> list[float] | None: + """Pull the first present key and coerce it to a list of floats.""" + for key in keys: + if key not in observation: + continue + val = observation[key] + if hasattr(val, "tolist"): + val = val.tolist() + if isinstance(val, (int, float)): + return [float(val)] + if isinstance(val, (list, tuple)): + try: + return [float(x) for x in val] + except (ValueError, TypeError): + return None + return None diff --git a/src/leapflow/robot/inference/registry.py b/src/leapflow/robot/inference/registry.py new file mode 100644 index 00000000..0e1e21d9 --- /dev/null +++ b/src/leapflow/robot/inference/registry.py @@ -0,0 +1,227 @@ +# Copyright (c) Alibaba, Inc. and its affiliates. +"""Discovery and selection registry for inference strategies. + +Mirrors the transport factory table: strategies arrive from three sources +(built-in registration at import time, the ``leapflow.inference.strategies`` +entry-point group, and runtime :meth:`InferenceStrategyRegistry.register`) and +are keyed by their ``strategy_id`` in one global namespace. The registry is +first-wins on name collisions -- an installed package cannot hijack a built-in +id -- and thread-safe under an ``RLock`` so concurrent sessions can share one +instance. +""" + +from __future__ import annotations + +import logging +import threading +from typing import Any + +from leapflow.robot.inference.strategy import ComputeBudget, InferenceStrategy + +logger = logging.getLogger(__name__) + +_EP_GROUP = "leapflow.inference.strategies" + +__all__ = ["InferenceStrategyRegistry", "get_default_registry"] + + +class InferenceStrategyRegistry: + """Discovers and manages physical AI inference strategies. + + Discovery sources (in priority order): + 1. Built-in strategies (registered at import time) + 2. Entry-point group 'leapflow.inference.strategies' + 3. Runtime registration via register() + + Thread-safe (RLock protected). + """ + + def __init__(self) -> None: + self._strategies: dict[str, InferenceStrategy] = {} + self._lock = threading.RLock() + self._ep_scanned = False + + # ------------------------------------------------------------------ + # Registration + # ------------------------------------------------------------------ + + def register(self, strategy: InferenceStrategy) -> None: + """Register a strategy under its ``strategy_id``. + + First-wins: a strategy whose id is already registered is skipped with a + warning rather than overwriting the incumbent, so an out-of-tree plugin + cannot silently replace a built-in path. + """ + strategy_id = getattr(strategy, "strategy_id", "") + if not strategy_id: + raise ValueError("strategy must expose a non-empty 'strategy_id'") + if not isinstance(strategy, InferenceStrategy): + raise TypeError( + f"object for {strategy_id!r} does not satisfy InferenceStrategy" + ) + with self._lock: + if strategy_id in self._strategies: + logger.warning( + "inference strategy %r already registered; keeping incumbent", + strategy_id, + ) + return + self._strategies[strategy_id] = strategy + logger.debug("inference strategy %r registered", strategy_id) + + def unregister(self, strategy_id: str) -> None: + """Remove a strategy by id. A no-op when the id is unknown.""" + with self._lock: + self._strategies.pop(strategy_id, None) + + # ------------------------------------------------------------------ + # Lookup + # ------------------------------------------------------------------ + + def get(self, strategy_id: str) -> InferenceStrategy | None: + """Return the strategy registered for *strategy_id*, or ``None``.""" + self._discover_entry_points() + with self._lock: + return self._strategies.get(strategy_id) + + def list_strategies(self) -> list[dict[str, Any]]: + """Return a serialisable summary of every registered strategy. + + Each entry carries the id plus the declared compute profile, so a + caller can inspect capabilities without instantiating anything heavy. + """ + self._discover_entry_points() + with self._lock: + items = list(self._strategies.items()) + + summary: list[dict[str, Any]] = [] + for strategy_id, strategy in items: + profile = getattr(strategy, "compute_profile", None) + entry: dict[str, Any] = {"strategy_id": strategy_id} + if profile is not None: + entry["compute_profile"] = { + "latency_range_ms": list(profile.latency_range_ms), + "supports_chunking": profile.supports_chunking, + "supports_ensemble": profile.supports_ensemble, + "supports_refinement": profile.supports_refinement, + "gpu_required": profile.gpu_required, + "max_chunk_size": profile.max_chunk_size, + } + summary.append(entry) + return summary + + def select( + self, + *, + budget: ComputeBudget | None = None, + gpu_available: bool = False, + ) -> InferenceStrategy | None: + """Auto-select the best strategy matching the budget constraints. + + A strategy is a candidate when it satisfies both constraints: + - its declared minimum latency fits within ``budget.max_latency_ms``; + - it does not require a GPU that is unavailable in this process. + + Among the candidates the one with the lowest typical minimum latency + wins (fastest declared path). Returns ``None`` when nothing qualifies. + """ + self._discover_entry_points() + with self._lock: + candidates = list(self._strategies.values()) + + max_latency = budget.max_latency_ms if budget is not None else None + + best: InferenceStrategy | None = None + best_latency = float("inf") + for strategy in candidates: + profile = getattr(strategy, "compute_profile", None) + if profile is None: + continue + if profile.gpu_required and not gpu_available: + continue + typical_min = profile.latency_range_ms[0] + if max_latency is not None and typical_min > max_latency: + continue + if typical_min < best_latency: + best = strategy + best_latency = typical_min + return best + + # ------------------------------------------------------------------ + # Entry-point discovery + # ------------------------------------------------------------------ + + def _discover_entry_points(self) -> None: + """Merge entry-point declared strategies into the table, once. + + Idempotent: guarded by ``_ep_scanned`` so the metadata scan runs only on + first lookup. Built-in and runtime registrations take precedence -- an + entry-point whose id collides with an existing key is skipped. + """ + with self._lock: + if self._ep_scanned: + return + self._ep_scanned = True + + try: + from importlib.metadata import entry_points + except ImportError: # defensive: should never happen on 3.11+ + return + + try: + eps = entry_points(group=_EP_GROUP) + except TypeError: + # Ancient importlib.metadata (not reachable on >=3.11). + eps = entry_points().get(_EP_GROUP, []) # type: ignore[arg-type,union-attr] + + for ep in eps: + try: + factory = ep.load() + except Exception as exc: # noqa: BLE001 - one bad plugin must not break discovery + logger.warning( + "inference strategy entry-point %r failed to load: %s", + ep.name, exc, exc_info=True, + ) + continue + try: + strategy = factory() if callable(factory) else factory + except Exception as exc: # noqa: BLE001 - construction must not break discovery + logger.warning( + "inference strategy entry-point %r failed to construct: %s", + ep.name, exc, exc_info=True, + ) + continue + try: + self.register(strategy) + except (TypeError, ValueError) as exc: + logger.warning( + "inference strategy entry-point %r rejected: %s", ep.name, exc, + ) + + +# --------------------------------------------------------------------------- +# Module-level default registry +# --------------------------------------------------------------------------- + +_default_registry: InferenceStrategyRegistry | None = None +_default_registry_lock = threading.Lock() + + +def get_default_registry() -> InferenceStrategyRegistry: + """Return the process-wide default strategy registry. + + Lazily created on first access. Built-in strategies are NOT + auto-registered here (they require a policy path or server address + at construction) -- the registry starts empty and strategies are + registered on demand as policies are loaded. + + Sharing one registry across sessions lets a heavy local model that + is loaded once be reused by every plugin instance in the process, + without forcing callers through daemon-level dependency injection. + """ + global _default_registry + if _default_registry is None: + with _default_registry_lock: + if _default_registry is None: + _default_registry = InferenceStrategyRegistry() + return _default_registry diff --git a/src/leapflow/robot/inference/strategy.py b/src/leapflow/robot/inference/strategy.py new file mode 100644 index 00000000..a9e8e3ba --- /dev/null +++ b/src/leapflow/robot/inference/strategy.py @@ -0,0 +1,127 @@ +# Copyright (c) Alibaba, Inc. and its affiliates. +"""Core contracts for the LeapRobot inference strategy framework. + +An :class:`InferenceStrategy` maps an observation to an action through some +compute path -- a local VLA policy, a remote PolicyServer, a classical +controller, and so on. The strategy declares a :class:`ComputeProfile` +describing what it can do (chunking, ensembling, refinement, GPU need) so the +system can match a :class:`ComputeBudget` to task complexity at inference time. + +The Protocol is intentionally narrow: two async methods (``infer`` and +``reset``) plus two read-only properties (``strategy_id`` and +``compute_profile``). Everything else -- policy loading, transport lifecycle, +budget interpretation -- is an implementation detail owned by the concrete +strategy, not the contract. +""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import Any, Mapping, Protocol, runtime_checkable + +__all__ = [ + "ComputeProfile", + "ComputeBudget", + "InferenceResult", + "InferenceStrategy", +] + + +@dataclass(frozen=True) +class ComputeProfile: + """Declared compute characteristics of an inference strategy. + + These are static declarations, not runtime measurements: the registry uses + them to filter and rank candidate strategies against a + :class:`ComputeBudget` before any inference call is made. ``gpu_required`` + is about *this process* -- a remote strategy runs the model on the server, + so it declares ``gpu_required=False`` even though a GPU is used elsewhere. + """ + + latency_range_ms: tuple[float, float] # (typical_min, typical_max) + supports_chunking: bool = False # can predict N-step action chunks + supports_ensemble: bool = False # can run multiple samples + vote + supports_refinement: bool = False # can iteratively improve output + gpu_required: bool = False + max_chunk_size: int = 1 + + +@dataclass(frozen=True) +class ComputeBudget: + """Test-time compute budget for one inference call. + + Carries the knobs a caller uses to trade latency for quality: how long the + call may take, how many refinement steps to run, how many ensemble samples + to draw, the confidence below which more compute should be spent, and how + many future action steps to predict in one chunk. + + A strategy that does not support a given knob (see :class:`ComputeProfile`) + ignores it rather than failing -- the budget is advisory, and the profile is + the authoritative statement of what a strategy can honour. + """ + + max_latency_ms: float = 1000.0 + max_refinement_steps: int = 1 + ensemble_size: int = 1 + confidence_threshold: float = 0.8 + chunk_size: int = 1 + + +@dataclass(frozen=True) +class InferenceResult: + """Output of one inference call. + + ``action`` is deliberately typed ``Any``: a strategy may return a dict of + channel -> value, a torch tensor, a numpy array, or a plain list, and the + caller adapts it to hardware commands downstream. The remaining fields are + telemetry the system uses to drive budget adaptation and trust: measured + ``latency_ms``, a ``confidence`` score in ``[0, 1]``, the ``chunk_size`` the + action represents, and strategy-specific ``metadata``. + """ + + action: Any # typically a dict or tensor + latency_ms: float = 0.0 + confidence: float = 1.0 + chunk_size: int = 1 + metadata: Mapping[str, Any] = field(default_factory=dict) + + +@runtime_checkable +class InferenceStrategy(Protocol): + """Pluggable physical AI inference strategy. + + Any object satisfying this protocol can be registered with the + :class:`~leapflow.robot.inference.registry.InferenceStrategyRegistry` and + used by ``PhysicalSkillPlugin`` in place of the two hardcoded local/remote + paths. Implementations are free to lazily load heavy resources (models, + transports) inside :meth:`infer` so that construction stays cheap and + dependency-free. + """ + + @property + def strategy_id(self) -> str: + """Stable, unique identifier used as the registry key.""" + ... + + @property + def compute_profile(self) -> ComputeProfile: + """Declared compute characteristics used for budget-aware selection.""" + ... + + async def infer( + self, + observation: Mapping[str, Any], + *, + budget: ComputeBudget | None = None, + ) -> InferenceResult: + """Map an observation to an action under an optional compute budget. + + When *budget* is ``None`` the strategy uses its own defaults. A + strategy must honour the knobs its :class:`ComputeProfile` advertises + and may ignore the rest. + """ + ... + + async def reset(self) -> None: + """Reset per-episode state (action queues, hidden states, caches).""" + ... diff --git a/src/leapflow/robot/inference/vla_local.py b/src/leapflow/robot/inference/vla_local.py new file mode 100644 index 00000000..0b2fb5b1 --- /dev/null +++ b/src/leapflow/robot/inference/vla_local.py @@ -0,0 +1,137 @@ +# Copyright (c) Alibaba, Inc. and its affiliates. +"""In-process inference strategy wrapping a local ``PreTrainedPolicy``. + +Loads a policy from a HuggingFace-format checkpoint directory and runs +inference in the current process. The policy is loaded lazily on first +``infer`` call (off the event loop via ``asyncio.to_thread``) so constructing +the strategy stays cheap and does not require torch to be installed until an +inference actually runs. +""" + +from __future__ import annotations + +import asyncio +import logging +import time +from typing import Any, Mapping + +from leapflow.robot.inference.strategy import ( + ComputeBudget, + ComputeProfile, + InferenceResult, +) + +logger = logging.getLogger(__name__) + +__all__ = ["VLALocalStrategy"] + + +class VLALocalStrategy: + """Inference strategy wrapping a local PreTrainedPolicy. + + Loads a policy from a HuggingFace-format checkpoint and runs + inference in-process. Supports action chunking if the policy + implements predict_action_chunk(). + """ + + strategy_id = "vla_local" + + def __init__(self, policy_path: str, *, device: str = "cpu") -> None: + self._path = policy_path + self._device = device + self._policy: Any = None # PreTrainedPolicy, loaded lazily + self._load_lock = asyncio.Lock() + + @property + def compute_profile(self) -> ComputeProfile: + return ComputeProfile( + latency_range_ms=(10.0, 200.0), + supports_chunking=True, + gpu_required=self._device != "cpu", + max_chunk_size=100, + ) + + async def infer( + self, + observation: Mapping[str, Any], + *, + budget: ComputeBudget | None = None, + ) -> InferenceResult: + """Load policy if needed, run select_action or predict_action_chunk.""" + await self._ensure_loaded() + + chunk_size = max(1, int(budget.chunk_size) if budget is not None else 1) + obs = self._prepare_observation(observation) + + t0 = time.monotonic() + if chunk_size > 1 and hasattr(self._policy, "predict_action_chunk"): + action = await asyncio.to_thread( + self._policy.predict_action_chunk, obs + ) + else: + select = getattr(self._policy, "select_action", None) + if select is None: + raise RuntimeError( + f"policy at {self._path!r} does not implement select_action()" + ) + action = await asyncio.to_thread(select, obs) + latency_ms = (time.monotonic() - t0) * 1000.0 + + return InferenceResult( + action=action, + latency_ms=latency_ms, + confidence=1.0, + chunk_size=chunk_size, + metadata={"strategy": self.strategy_id, "device": self._device}, + ) + + async def reset(self) -> None: + """Call policy.reset().""" + if self._policy is None: + return + reset = getattr(self._policy, "reset", None) + if reset is None: + return + try: + await asyncio.to_thread(reset) + except Exception: # noqa: BLE001 - reset failure must not abort the episode + logger.debug("policy.reset() raised; suppressed", exc_info=True) + + async def _ensure_loaded(self) -> None: + """Lazy-load the policy via asyncio.to_thread.""" + if self._policy is not None: + return + async with self._load_lock: + if self._policy is not None: + return + try: + from leapflow.robot.policy import make_policy + except ImportError as exc: + raise RuntimeError( + "LeapRobot policy package is not available; " + "install with: pip install leapflow[robot]" + ) from exc + self._policy = await asyncio.to_thread( + make_policy, self._path, device=self._device, + ) + logger.info("VLALocalStrategy loaded policy: %s", self._path) + + def _prepare_observation(self, observation: Mapping[str, Any]) -> dict[str, Any]: + """Convert numeric observation values to tensors when torch is present.""" + try: + import torch # type: ignore[import-untyped] + except ImportError: + return dict(observation) + + prepared: dict[str, Any] = {} + for key, val in observation.items(): + if isinstance(val, (int, float)): + prepared[key] = torch.tensor([val], dtype=torch.float32) + elif isinstance(val, list): + try: + prepared[key] = torch.tensor(val, dtype=torch.float32) + except (ValueError, TypeError): + prepared[key] = val + else: + prepared[key] = val + return prepared diff --git a/src/leapflow/robot/inference/vla_remote.py b/src/leapflow/robot/inference/vla_remote.py new file mode 100644 index 00000000..f8e82544 --- /dev/null +++ b/src/leapflow/robot/inference/vla_remote.py @@ -0,0 +1,163 @@ +# Copyright (c) Alibaba, Inc. and its affiliates. +"""Remote inference strategy delegating to a ``PolicyServer``. + +Adapts :class:`~leapflow.hardware.transports.vla_inference.VLAInferenceTransport` +to the :class:`~leapflow.robot.inference.strategy.InferenceStrategy` contract. +The model runs on a GPU server, so this process needs no GPU and no torch -- +the transport is imported lazily and opened on first use, mirroring the +construct-cheap / connect-late pattern of the local strategy. +""" + +from __future__ import annotations + +import logging +from typing import Any, Mapping + +from leapflow.robot.inference.strategy import ( + ComputeBudget, + ComputeProfile, + InferenceResult, +) + +logger = logging.getLogger(__name__) + +__all__ = ["VLARemoteStrategy"] + + +class VLARemoteStrategy: + """Inference strategy delegating to a remote PolicyServer. + + Wraps VLAInferenceTransport for the InferenceStrategy interface. + """ + + strategy_id = "vla_remote" + + def __init__( + self, + server_address: str, + *, + model_name: str = "", + timeout_s: float = 10.0, + ) -> None: + self._address = server_address + self._model_name = model_name + self._timeout_s = timeout_s + self._transport: Any = None # VLAInferenceTransport, created lazily + self._connected = False + + @property + def compute_profile(self) -> ComputeProfile: + return ComputeProfile( + latency_range_ms=(20.0, 500.0), + supports_chunking=True, + gpu_required=False, # GPU is on the server side + max_chunk_size=100, + ) + + async def infer( + self, + observation: Mapping[str, Any], + *, + budget: ComputeBudget | None = None, + ) -> InferenceResult: + """Delegate to VLAInferenceTransport.write("inference", ...).""" + await self._ensure_open() + + chunk_size = max(1, int(budget.chunk_size) if budget is not None else 1) + payload: dict[str, Any] = {"observation": dict(observation)} + if self._model_name: + payload["model_name"] = self._model_name + if chunk_size > 1: + payload["chunk_size"] = chunk_size + + outcome = await self._transport.write("inference", payload) + if not getattr(outcome, "ok", False): + raise RuntimeError( + f"remote inference failed: {getattr(outcome, 'error', '')}" + ) + + action = outcome.readback.value if outcome.readback is not None else None + raw = getattr(outcome, "raw", None) or {} + latency_ms = float(raw.get("latency_ms", 0.0)) + + return InferenceResult( + action=action, + latency_ms=latency_ms, + confidence=1.0, + chunk_size=chunk_size, + metadata={"strategy": self.strategy_id, "address": self._address}, + ) + + async def reset(self) -> None: + """Reset is a no-op for a stateless remote server contract.""" + return None + + async def close(self) -> None: + """Close the underlying transport, if one has been opened. + + Called from ``PhysicalSkillPlugin`` scope teardown so a session that + went through a remote inference path does not leave the gRPC channel + holding sockets after the plugin's effect scope tears down. The + method is idempotent and swallows transport errors -- teardown must + not surface secondary failures. + """ + transport = self._transport + self._transport = None + self._connected = False + if transport is None: + return + close = getattr(transport, "close", None) + if close is None: + return + try: + await close() + except Exception: # noqa: BLE001 - teardown must not propagate + logger.debug( + "VLARemoteStrategy: transport close raised for %s", + self._address, exc_info=True, + ) + + async def _ensure_open(self) -> None: + """Create and open the remote transport lazily on first use.""" + if self._connected and self._transport is not None: + return + if self._transport is None: + try: + from leapflow.hardware.transports.vla_inference import ( + VLAInferenceTransport, + ) + except ImportError as exc: + raise RuntimeError( + "VLAInferenceTransport is not available" + ) from exc + self._transport = VLAInferenceTransport( + server_address=self._address, + model_name=self._model_name, + timeout_s=self._timeout_s, + ) + + from leapflow.hardware.context import ( + ContextProvenance, + ContextSource, + HardwareContext, + TransportRef, + ) + + ctx = HardwareContext( + device_id=f"vla_inference.{self.strategy_id}", + display_name="VLA inference server", + transport=TransportRef( + kind="vla_inference", + config={"server_address": self._address}, + ), + channels=(), + halt_supported=True, + provenance=ContextProvenance(source=ContextSource.IMPORTED.value), + ) + status = await self._transport.open(ctx) + self._connected = bool(getattr(status, "connected", False)) + if not self._connected: + raise RuntimeError( + "Remote VLA inference transport failed to connect: " + f"{getattr(status, 'detail', '')}" + ) diff --git a/src/leapflow/robot/motors.py b/src/leapflow/robot/motors.py new file mode 100644 index 00000000..2e4c6401 --- /dev/null +++ b/src/leapflow/robot/motors.py @@ -0,0 +1,637 @@ +# Copyright (c) Alibaba, Inc. and its affiliates. +"""Motor bus abstractions for LeapRobot. + +Defines the ``MotorsBus`` Protocol (replacing the upstream ABC), +supporting data types (``Motor``, ``MotorCalibration``, +``MotorNormMode``), and a ``SerialMotorsBus`` base implementation for +serial-protocol motor buses (Feetech, Dynamixel, …). + +pyserial is an optional dependency — the module is importable without +it, but ``SerialMotorsBus.connect()`` requires it at runtime. +""" + +from __future__ import annotations + +import logging +import time +from collections.abc import Sequence +from contextlib import contextmanager +from dataclasses import dataclass +from enum import Enum +from typing import Any, Protocol, runtime_checkable + +logger = logging.getLogger(__name__) + +# --------------------------------------------------------------------------- +# Value / NameOrID aliases +# --------------------------------------------------------------------------- + +Value = int | float +"""Motor register value — raw int from hardware or normalised float.""" + +NameOrID = str | int +"""Motor identifier — either the human-readable name or the bus ID.""" + + +# --------------------------------------------------------------------------- +# Motor data types +# --------------------------------------------------------------------------- + +class MotorNormMode(str, Enum): + """Normalisation modes for motor position values.""" + + RANGE_0_100 = "range_0_100" + RANGE_M100_100 = "range_m100_100" + DEGREES = "degrees" + + +@dataclass +class MotorCalibration: + """Persisted calibration parameters for a single motor.""" + + id: int + drive_mode: int + homing_offset: int + range_min: int + range_max: int + + +@dataclass +class Motor: + """Descriptor for a motor connected to a bus.""" + + id: int + model: str + norm_mode: MotorNormMode + motor_type_str: str | None = None + recv_id: int | None = None + + +# --------------------------------------------------------------------------- +# MotorsBus Protocol +# --------------------------------------------------------------------------- + +@runtime_checkable +class MotorsBus(Protocol): + """Protocol for motor bus implementations. + + This is the minimal interface that all motor buses must satisfy, + regardless of their communication protocol (serial, CAN, etc.). + """ + + def connect(self, handshake: bool = True) -> None: + """Establish connection to the motors.""" + ... + + def disconnect(self, disable_torque: bool = True) -> None: + """Disconnect from the motors.""" + ... + + @property + def is_connected(self) -> bool: + """Check whether the bus is currently connected.""" + ... + + def read(self, data_name: str, motor: str, **kwargs: Any) -> Value: + """Read a value from a single motor.""" + ... + + def write(self, data_name: str, motor: str, value: Value, **kwargs: Any) -> None: + """Write a value to a single motor.""" + ... + + def sync_read( + self, data_name: str, motors: str | list[str] | None = None, **kwargs: Any, + ) -> dict[str, Value]: + """Read a value from multiple motors at once.""" + ... + + def sync_write( + self, data_name: str, values: Value | dict[str, Value], **kwargs: Any, + ) -> None: + """Write values to multiple motors at once.""" + ... + + def enable_torque( + self, motors: str | list[str] | None = None, num_retry: int = 0, + ) -> None: + """Enable torque on selected motors.""" + ... + + def disable_torque( + self, motors: str | list[str] | None = None, num_retry: int = 0, + ) -> None: + """Disable torque on selected motors.""" + ... + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + +def get_ctrl_table( + model_ctrl_table: dict[str, dict[str, tuple[int, int]]], + model: str, +) -> dict[str, tuple[int, int]]: + """Look up the control table for a motor model. + + Raises: + KeyError: If *model* is not found. + """ + ctrl_table = model_ctrl_table.get(model) + if ctrl_table is None: + raise KeyError(f"Control table for model={model!r} not found.") + return ctrl_table + + +def get_address( + model_ctrl_table: dict[str, dict[str, tuple[int, int]]], + model: str, + data_name: str, +) -> tuple[int, int]: + """Return ``(address, byte_length)`` for a register on a motor model. + + Raises: + KeyError: If *data_name* is not in the model's control table. + """ + ctrl_table = get_ctrl_table(model_ctrl_table, model) + addr_bytes = ctrl_table.get(data_name) + if addr_bytes is None: + raise KeyError( + f"Address for '{data_name}' not found in {model} control table." + ) + return addr_bytes + + +# --------------------------------------------------------------------------- +# SerialMotorsBus +# --------------------------------------------------------------------------- + +class SerialMotorsBus: + """Base implementation for serial-based motor buses. + + This class captures the common read/write, sync-read/write, + normalisation, and lifecycle logic shared between Feetech and + Dynamixel buses. Subclasses must populate the class-level table + attributes and implement the abstract hooks. + + pyserial (``serial``) is imported at *connection* time so the + module can be imported in environments where hardware is absent. + """ + + # -- Subclasses MUST populate these class attributes -- + apply_drive_mode: bool = False + available_baudrates: list[int] = [] + default_baudrate: int = 1_000_000 + default_timeout: int = 1000 + model_baudrate_table: dict[str, dict[int, int]] = {} + model_ctrl_table: dict[str, dict[str, tuple[int, int]]] = {} + model_encoding_table: dict[str, dict[str, int]] = {} + model_number_table: dict[str, int] = {} + model_resolution_table: dict[str, int] = {} + normalized_data: list[str] = [] + + def __init__( + self, + port: str, + motors: dict[str, Motor], + calibration: dict[str, MotorCalibration] | None = None, + ) -> None: + self.port = port + self.motors = motors + self.calibration: dict[str, MotorCalibration] = calibration or {} + + # Internal look-up tables + self._id_to_model_dict = {m.id: m.model for m in self.motors.values()} + self._id_to_name_dict = {m.id: name for name, m in self.motors.items()} + self._model_nb_to_model_dict: dict[int, str] = ( + {v: k for k, v in self.model_number_table.items()} + if self.model_number_table + else {} + ) + + self._validate_motors() + + # Populated by _connect / subclass __init__ + self.port_handler: Any = None + self.packet_handler: Any = None + self.sync_reader: Any = None + self.sync_writer: Any = None + self._comm_success: int = 0 + self._no_error: int = 0 + + # -- identification helpers ------------------------------------------- + + @property + def models(self) -> list[str]: + return [m.model for m in self.motors.values()] + + @property + def ids(self) -> list[int]: + return [m.id for m in self.motors.values()] + + def _id_to_model(self, motor_id: int) -> str: + return self._id_to_model_dict[motor_id] + + def _id_to_name(self, motor_id: int) -> str: + return self._id_to_name_dict[motor_id] + + def _get_motor_id(self, motor: NameOrID) -> int: + if isinstance(motor, str): + return self.motors[motor].id + return motor + + def _get_motor_model(self, motor: NameOrID) -> str: + if isinstance(motor, str): + return self.motors[motor].model + return self._id_to_model_dict[motor] + + def _get_motors_list( + self, motors: NameOrID | Sequence[NameOrID] | None, + ) -> list[str]: + if motors is None: + return list(self.motors) + if isinstance(motors, str): + return [motors] + if isinstance(motors, int): + return [self._id_to_name(motors)] + return [ + m if isinstance(m, str) else self._id_to_name(m) for m in motors + ] + + def _get_ids_values_dict( + self, values: Value | dict[str, Value], + ) -> dict[int, Value]: + if isinstance(values, (int, float)): + return dict.fromkeys(self.ids, values) + if isinstance(values, dict): + return { + self.motors[motor].id: val for motor, val in values.items() + } + raise TypeError(f"'values' must be a scalar or dict, got {type(values)}") + + # -- validation ------------------------------------------------------- + + def _validate_motors(self) -> None: + if len(self.ids) != len(set(self.ids)): + raise ValueError(f"Duplicate motor IDs detected: {self.ids}") + for model in self.models: + if self.model_ctrl_table: + get_ctrl_table(self.model_ctrl_table, model) + + # -- communication predicates ----------------------------------------- + + def _is_comm_success(self, comm: int) -> bool: + return comm == self._comm_success + + def _is_error(self, error: int) -> bool: + return error != self._no_error + + # -- connection lifecycle --------------------------------------------- + + @property + def is_connected(self) -> bool: + """``True`` if the underlying serial port is open.""" + if self.port_handler is None: + return False + return getattr(self.port_handler, "is_open", False) + + def connect(self, handshake: bool = True) -> None: + """Open the serial port and initialise communication. + + Args: + handshake: If ``True``, ping every motor and run + subclass-specific integrity checks. + """ + if self.is_connected: + return + self._connect(handshake) + self._set_timeout() + logger.debug("%s connected on %s.", type(self).__name__, self.port) + + def _connect(self, handshake: bool = True) -> None: + """Low-level port open — subclasses override to set SDK handles.""" + try: + if not self.port_handler.openPort(): + raise OSError(f"Failed to open port '{self.port}'.") + if handshake: + self._handshake() + except Exception as exc: + raise ConnectionError( + f"Could not connect on port '{self.port}'." + ) from exc + + def _handshake(self) -> None: + """Subclass hook called after the port is opened.""" + + def disconnect(self, disable_torque: bool = True) -> None: + """Close the serial port. + + Args: + disable_torque: If ``True`` (default) torque is disabled on + all motors before closing to avoid damage. + """ + if not self.is_connected: + return + if disable_torque: + try: + self.disable_torque(num_retry=5) + except Exception: + logger.warning("Failed to disable torque during disconnect.") + self.port_handler.closePort() + logger.debug("%s disconnected.", type(self).__name__) + + def _set_timeout(self, timeout_ms: int | None = None) -> None: + timeout_ms = timeout_ms if timeout_ms is not None else self.default_timeout + if self.port_handler is not None: + self.port_handler.setPacketTimeoutMillis(timeout_ms) + + # -- normalisation ---------------------------------------------------- + + def _normalize(self, ids_values: dict[int, int]) -> dict[int, float]: + if not self.calibration: + raise RuntimeError("No calibration registered.") + result: dict[int, float] = {} + for id_, val in ids_values.items(): + motor = self._id_to_name(id_) + cal = self.calibration[motor] + if cal.range_max == cal.range_min: + raise ValueError( + f"Invalid calibration for motor '{motor}': " + f"min and max are equal." + ) + bounded = min(cal.range_max, max(cal.range_min, val)) + drive = self.apply_drive_mode and cal.drive_mode + nm = self.motors[motor].norm_mode + if nm is MotorNormMode.RANGE_M100_100: + norm = ((bounded - cal.range_min) / (cal.range_max - cal.range_min)) * 200 - 100 + result[id_] = -norm if drive else norm + elif nm is MotorNormMode.RANGE_0_100: + norm = ((bounded - cal.range_min) / (cal.range_max - cal.range_min)) * 100 + result[id_] = 100 - norm if drive else norm + elif nm is MotorNormMode.DEGREES: + mid = (cal.range_min + cal.range_max) / 2 + max_res = self.model_resolution_table[self._id_to_model(id_)] - 1 + result[id_] = (val - mid) * 360 / max_res + else: + raise NotImplementedError(f"Unsupported norm mode: {nm}") + return result + + def _unnormalize(self, ids_values: dict[int, float]) -> dict[int, int]: + if not self.calibration: + raise RuntimeError("No calibration registered.") + result: dict[int, int] = {} + for id_, val in ids_values.items(): + motor = self._id_to_name(id_) + cal = self.calibration[motor] + if cal.range_max == cal.range_min: + raise ValueError( + f"Invalid calibration for motor '{motor}': " + f"min and max are equal." + ) + drive = self.apply_drive_mode and cal.drive_mode + nm = self.motors[motor].norm_mode + if nm is MotorNormMode.RANGE_M100_100: + v = -val if drive else val + bounded = min(100.0, max(-100.0, v)) + result[id_] = int( + ((bounded + 100) / 200) * (cal.range_max - cal.range_min) + cal.range_min + ) + elif nm is MotorNormMode.RANGE_0_100: + v = 100 - val if drive else val + bounded = min(100.0, max(0.0, v)) + result[id_] = int( + (bounded / 100) * (cal.range_max - cal.range_min) + cal.range_min + ) + elif nm is MotorNormMode.DEGREES: + mid = (cal.range_min + cal.range_max) / 2 + max_res = self.model_resolution_table[self._id_to_model(id_)] - 1 + result[id_] = int((val * max_res / 360) + mid) + else: + raise NotImplementedError(f"Unsupported norm mode: {nm}") + return result + + # -- serial data helpers ---------------------------------------------- + + def _serialize_data(self, value: int, length: int) -> list[int]: + """Convert an unsigned int to a list of byte-sized ints.""" + if value < 0: + raise ValueError(f"Negative values not allowed: {value}") + max_value = {1: 0xFF, 2: 0xFFFF, 4: 0xFFFFFFFF}.get(length) + if max_value is None: + raise NotImplementedError(f"Unsupported byte size: {length}") + if value > max_value: + raise ValueError(f"Value {value} exceeds max for {length} bytes.") + return self._split_into_byte_chunks(value, length) + + def _split_into_byte_chunks(self, value: int, length: int) -> list[int]: + """Subclasses must override with endianness-specific logic.""" + raise NotImplementedError + + # -- read / write (single motor) -------------------------------------- + + def read( + self, + data_name: str, + motor: str, + *, + normalize: bool = True, + num_retry: int = 0, + ) -> Value: + """Read a register from a single motor.""" + id_ = self.motors[motor].id + model = self.motors[motor].model + addr, length = get_address(self.model_ctrl_table, model, data_name) + value, _, _ = self._read( + addr, length, id_, num_retry=num_retry, raise_on_error=True, + ) + decoded = self._decode_sign(data_name, {id_: value}) + if normalize and data_name in self.normalized_data: + return self._normalize(decoded)[id_] + return decoded[id_] + + def _read( + self, + address: int, + length: int, + motor_id: int, + *, + num_retry: int = 0, + raise_on_error: bool = True, + ) -> tuple[int, int, int]: + """Low-level register read — subclasses should override.""" + raise NotImplementedError + + def write( + self, + data_name: str, + motor: str, + value: Value, + *, + normalize: bool = True, + num_retry: int = 0, + ) -> None: + """Write a value to a single motor's register.""" + id_ = self.motors[motor].id + model = self.motors[motor].model + addr, length = get_address(self.model_ctrl_table, model, data_name) + int_value = int(value) + if normalize and data_name in self.normalized_data: + int_value = self._unnormalize({id_: value})[id_] + int_value = self._encode_sign(data_name, {id_: int_value})[id_] + self._write( + addr, length, id_, int_value, + num_retry=num_retry, raise_on_error=True, + ) + + def _write( + self, + addr: int, + length: int, + motor_id: int, + value: int, + *, + num_retry: int = 0, + raise_on_error: bool = True, + ) -> tuple[int, int]: + """Low-level register write — subclasses should override.""" + raise NotImplementedError + + # -- sync read / write ------------------------------------------------ + + def sync_read( + self, + data_name: str, + motors: NameOrID | Sequence[NameOrID] | None = None, + *, + normalize: bool = True, + num_retry: int = 0, + ) -> dict[str, Value]: + """Read the same register from multiple motors.""" + names = self._get_motors_list(motors) + ids = [self.motors[m].id for m in names] + models = [self.motors[m].model for m in names] + model = models[0] + addr, length = get_address(self.model_ctrl_table, model, data_name) + raw, _ = self._sync_read(addr, length, ids, num_retry=num_retry) + decoded = self._decode_sign(data_name, raw) + if normalize and data_name in self.normalized_data: + normed = self._normalize(decoded) + return {self._id_to_name(id_): v for id_, v in normed.items()} + return {self._id_to_name(id_): v for id_, v in decoded.items()} + + def _sync_read( + self, + addr: int, + length: int, + motor_ids: list[int], + *, + num_retry: int = 0, + ) -> tuple[dict[int, int], int]: + """Low-level sync read — subclasses should override.""" + raise NotImplementedError + + def sync_write( + self, + data_name: str, + values: Value | dict[str, Value], + *, + normalize: bool = True, + num_retry: int = 0, + ) -> None: + """Write the same register on multiple motors.""" + raw_ids = self._get_ids_values_dict(values) + models = [self._id_to_model(id_) for id_ in raw_ids] + model = models[0] + addr, length = get_address(self.model_ctrl_table, model, data_name) + int_ids = {id_: int(v) for id_, v in raw_ids.items()} + if normalize and data_name in self.normalized_data: + int_ids = self._unnormalize(raw_ids) + int_ids = self._encode_sign(data_name, int_ids) + self._sync_write(addr, length, int_ids, num_retry=num_retry) + + def _sync_write( + self, + addr: int, + length: int, + ids_values: dict[int, int], + *, + num_retry: int = 0, + ) -> int: + """Low-level sync write — subclasses should override.""" + raise NotImplementedError + + # -- torque control --------------------------------------------------- + + def enable_torque( + self, motors: str | list[str] | None = None, num_retry: int = 0, + ) -> None: + """Enable torque on selected motors — subclasses should override.""" + raise NotImplementedError + + def disable_torque( + self, motors: str | list[str] | None = None, num_retry: int = 0, + ) -> None: + """Disable torque on selected motors — subclasses should override.""" + raise NotImplementedError + + @contextmanager + def torque_disabled(self, motors: str | list[str] | None = None): + """Context manager that guarantees torque is re-enabled.""" + self.disable_torque(motors) + try: + yield + finally: + self.enable_torque(motors) + + # -- sign encoding (subclass hooks) ----------------------------------- + + def _encode_sign( + self, data_name: str, ids_values: dict[int, int], + ) -> dict[int, int]: + """Encode signed values — subclasses override per protocol.""" + return ids_values + + def _decode_sign( + self, data_name: str, ids_values: dict[int, int], + ) -> dict[int, int]: + """Decode signed values — subclasses override per protocol.""" + return ids_values + + # -- calibration (subclass hooks) ------------------------------------- + + def read_calibration(self) -> dict[str, MotorCalibration]: + """Read calibration from hardware — subclasses should override.""" + raise NotImplementedError + + def write_calibration( + self, + calibration_dict: dict[str, MotorCalibration], + cache: bool = True, + ) -> None: + """Write calibration to hardware — subclasses should override.""" + raise NotImplementedError + + # -- repr / len ------------------------------------------------------- + + def __len__(self) -> int: + return len(self.motors) + + def __repr__(self) -> str: + return ( + f"{type(self).__name__}(port={self.port!r}, " + f"motors={list(self.motors.keys())})" + ) + + +__all__ = [ + "Value", + "NameOrID", + "MotorNormMode", + "MotorCalibration", + "Motor", + "MotorsBus", + "SerialMotorsBus", + "get_ctrl_table", + "get_address", +] diff --git a/src/leapflow/robot/orchestrator.py b/src/leapflow/robot/orchestrator.py new file mode 100644 index 00000000..f3098857 --- /dev/null +++ b/src/leapflow/robot/orchestrator.py @@ -0,0 +1,516 @@ +# Copyright (c) Alibaba, Inc. and its affiliates. +"""Multi-device orchestration: coordinated control across multiple robots. + +Sequences, parallelizes, and synchronizes physical operations across +multiple devices. A single manipulation task may involve two arms +handing off an object, a leader-follower teleoperation pair, or a +pipeline where one device's completion triggers the next. + +Safety is the orchestrator's first concern: any failure in a parallel +group triggers ``halt_all`` on every participating device, because a +half-completed multi-device operation can leave the workspace in a +physically unsafe state (one arm holding, the other released). +""" + +from __future__ import annotations + +import asyncio +import logging +import time +from dataclasses import dataclass +from enum import Enum +from typing import Any + +logger = logging.getLogger(__name__) + + +# --------------------------------------------------------------------------- +# Orchestration primitives +# --------------------------------------------------------------------------- + + +class ExecutionMode(str, Enum): + """How a group of device operations is coordinated.""" + + SEQUENTIAL = "sequential" + """One device completes before the next starts.""" + + PARALLEL = "parallel" + """All devices act simultaneously.""" + + BARRIER = "barrier" + """All devices reach a state, then proceed together.""" + + +@dataclass(frozen=True) +class DeviceOperation: + """One operation targeting one device in an orchestration. + + ``commands`` is a tuple of ``(channel_id, value)`` pairs that will be + written to the device. ``verify`` enables post-write verification + through the existing ``OperationVerifier`` path. ``settle_after_s`` + adds a settling delay after the write, matching the HCP Envelope + convention. + """ + + device_id: str + commands: tuple[tuple[str, Any], ...] + verify: bool = False + settle_after_s: float = 0.0 + + +@dataclass(frozen=True) +class OrchestrationStep: + """One step of a multi-device orchestration. + + ``mode`` is an :class:`ExecutionMode` value controlling how + ``operations`` execute relative to one another. ``barrier_timeout_s`` + applies only when ``mode`` is ``BARRIER``. + """ + + mode: str + operations: tuple[DeviceOperation, ...] + barrier_timeout_s: float = 10.0 + label: str = "" + + +@dataclass(frozen=True) +class OrchestrationResult: + """Outcome of an orchestration step or plan. + + ``ok`` is True only when every operation succeeded. + ``halted`` indicates that ``halt_all`` was triggered as a safety + response to a failure. + """ + + ok: bool + step_results: tuple[dict, ...] + halted: bool = False + halt_reason: str = "" + elapsed_s: float = 0.0 + + +# --------------------------------------------------------------------------- +# Orchestrator +# --------------------------------------------------------------------------- + + +class MultiDeviceOrchestrator: + """Coordinates operations across multiple hardware devices. + + Dependencies: + + - ``registry``: :class:`~leapflow.hardware.registry.HardwareRegistry` + for device access, I/O locks, and transport retrieval. + - ``hardware_tools``: :class:`~leapflow.hardware.tools.HardwareTools` + for approval-gated batch writes (optional). When provided, every + device write flows through the approval chain. + - ``capability_index``: :class:`~leapflow.hardware.capability_router.CapabilityIndex` + for affordance-based device routing (optional). + """ + + def __init__( + self, + registry: Any, + *, + hardware_tools: Any = None, + capability_index: Any = None, + ) -> None: + self._registry = registry + self._hardware_tools = hardware_tools + self._capability_index = capability_index + + # ── Public API ──────────────────────────────────────────────── + + async def execute_step(self, step: OrchestrationStep) -> OrchestrationResult: + """Execute one orchestration step according to its mode. + + - ``SEQUENTIAL``: execute operations one at a time, stopping on first failure. + - ``PARALLEL``: execute all operations concurrently via ``asyncio.gather``; + on any failure trigger ``halt_all`` on every participating device. + - ``BARRIER``: command all devices, then poll until all reach the target + state (within ``barrier_timeout_s``), or halt on timeout. + + Returns an :class:`OrchestrationResult` summarising the outcome. + """ + t0 = time.monotonic() + mode = step.mode.value if isinstance(step.mode, ExecutionMode) else str(step.mode) + + try: + if mode == ExecutionMode.SEQUENTIAL.value: + results = await self._execute_sequential(step.operations) + elif mode == ExecutionMode.PARALLEL.value: + results = await self._execute_parallel(step.operations) + elif mode == ExecutionMode.BARRIER.value: + results = await self._execute_barrier( + step.operations, step.barrier_timeout_s + ) + else: + return OrchestrationResult( + ok=False, + step_results=({"error": f"unknown mode: {mode}"},), + elapsed_s=time.monotonic() - t0, + ) + except Exception as exc: + logger.error( + "Orchestration step %r raised unexpectedly: %s", + step.label or mode, + exc, + exc_info=True, + ) + device_ids = tuple(op.device_id for op in step.operations) + halt_map = await self.halt_all(device_ids) + return OrchestrationResult( + ok=False, + step_results=({"error": str(exc), "halt_map": halt_map},), + halted=True, + halt_reason=str(exc), + elapsed_s=time.monotonic() - t0, + ) + + ok = all(r.get("ok", False) for r in results) + halted = any(r.get("halted", False) for r in results) + halt_reason = next( + (r.get("halt_reason", "") for r in results if r.get("halted")), + "", + ) + return OrchestrationResult( + ok=ok, + step_results=tuple(results), + halted=halted, + halt_reason=halt_reason, + elapsed_s=time.monotonic() - t0, + ) + + async def execute_plan( + self, steps: tuple[OrchestrationStep, ...] + ) -> OrchestrationResult: + """Execute a sequence of orchestration steps. + + Steps run sequentially (each step may itself be parallel internally). + A failed step halts the plan and triggers ``halt_all`` on all devices + that participated in the failed step. + """ + t0 = time.monotonic() + all_results: list[dict] = [] + + for step in steps: + result = await self.execute_step(step) + all_results.extend(result.step_results) + if not result.ok: + return OrchestrationResult( + ok=False, + step_results=tuple(all_results), + halted=result.halted, + halt_reason=result.halt_reason or f"step '{step.label}' failed", + elapsed_s=time.monotonic() - t0, + ) + + return OrchestrationResult( + ok=True, + step_results=tuple(all_results), + elapsed_s=time.monotonic() - t0, + ) + + async def halt_all(self, device_ids: tuple[str, ...]) -> dict: + """Emergency stop all specified devices concurrently. + + Calls ``transport.halt()`` on every device in parallel. ``halt()`` + is lock-free (HCP requirement) so this does not wait for in-flight + operations. Returns a per-device halt status map. + + This is the safety backstop: called automatically on any parallel + group failure, and callable directly for emergency stop. + """ + if not device_ids: + return {} + + async def _halt_one(device_id: str) -> tuple[str, dict]: + try: + transport = await self._registry.transport(device_id) + status = await transport.halt() + return device_id, { + "halted": True, + "halt_supported": getattr(status, "halt_supported", True), + } + except Exception as exc: + logger.warning( + "halt_all: device %r halt failed: %s", + device_id, + exc, + exc_info=True, + ) + return device_id, {"halted": False, "error": str(exc)} + + unique_ids = tuple(dict.fromkeys(device_ids)) + results = await asyncio.gather( + *(_halt_one(did) for did in unique_ids), return_exceptions=True + ) + halt_map: dict[str, Any] = {} + for item in results: + if isinstance(item, Exception): + logger.warning("halt_all: gather exception: %s", item, exc_info=True) + continue + did, status = item + halt_map[did] = status + return halt_map + + def resolve_by_affordance(self, affordance: str) -> tuple[str, ...]: + """Return device_ids that can perform the given affordance. + + Uses ``capability_index`` to find candidate devices for multi-device + task planning (e.g. "which arms can grasp?"). + """ + if self._capability_index is None: + return () + entries = self._capability_index.resolve(affordance) + return tuple(entry.device_id for entry in entries) + + # ── Internal execution modes ────────────────────────────────── + + async def _execute_sequential( + self, operations: tuple[DeviceOperation, ...] + ) -> list[dict]: + """Execute operations one at a time, stopping on first failure.""" + results: list[dict] = [] + for op in operations: + result = await self._execute_operation(op) + results.append(result) + if not result.get("ok", False): + break + return results + + async def _execute_parallel( + self, operations: tuple[DeviceOperation, ...] + ) -> list[dict]: + """Execute operations concurrently via ``asyncio.gather``. + + Uses ``return_exceptions=True`` so one failure does not cancel the + others mid-flight (which could leave devices in inconsistent + states). After gather, if any failed, calls ``halt_all``. + """ + if not operations: + return [] + + raw_results = await asyncio.gather( + *(self._execute_operation(op) for op in operations), + return_exceptions=True, + ) + + results: list[dict] = [] + for i, raw in enumerate(raw_results): + if isinstance(raw, Exception): + results.append({ + "ok": False, + "device_id": operations[i].device_id, + "error": str(raw), + }) + else: + results.append(raw) + + any_failed = any(not r.get("ok", False) for r in results) + if any_failed: + device_ids = tuple(op.device_id for op in operations) + halt_map = await self.halt_all(device_ids) + failure_reasons = [ + r.get("error", "unknown") + for r in results + if not r.get("ok", False) + ] + for r in results: + r["halted"] = True + r["halt_reason"] = "; ".join(failure_reasons) + r["halt_map"] = halt_map + + return results + + async def _execute_barrier( + self, operations: tuple[DeviceOperation, ...], timeout_s: float + ) -> list[dict]: + """Command all devices, then poll until all reach target or timeout. + + Phase 1: send commands to all devices concurrently. + Phase 2: poll each device until the commanded value is reached or + the barrier timeout expires. + """ + if not operations: + return [] + + # Phase 1: command all devices concurrently. + raw_results = await asyncio.gather( + *(self._execute_operation(op) for op in operations), + return_exceptions=True, + ) + + results: list[dict] = [] + for i, raw in enumerate(raw_results): + if isinstance(raw, Exception): + results.append({ + "ok": False, + "device_id": operations[i].device_id, + "error": str(raw), + }) + else: + results.append(raw) + + # If any command failed, halt and return immediately. + any_failed = any(not r.get("ok", False) for r in results) + if any_failed: + device_ids = tuple(op.device_id for op in operations) + halt_map = await self.halt_all(device_ids) + for r in results: + r["halted"] = True + r["halt_reason"] = "barrier command phase failed" + r["halt_map"] = halt_map + return results + + # Phase 2: wait for all devices to reach their target. + deadline = time.monotonic() + timeout_s + settled = set[int]() + poll_interval = min(0.1, timeout_s / 10) if timeout_s > 0 else 0 + + while len(settled) < len(operations): + remaining = deadline - time.monotonic() + if remaining <= 0: + break + + for idx, op in enumerate(operations): + if idx in settled: + continue + if await self._check_reached(op): + settled.add(idx) + + if len(settled) < len(operations) and poll_interval > 0: + await asyncio.sleep(poll_interval) + + if len(settled) < len(operations): + device_ids = tuple(op.device_id for op in operations) + halt_map = await self.halt_all(device_ids) + unsettled = [ + operations[i].device_id + for i in range(len(operations)) + if i not in settled + ] + for r in results: + r["halted"] = True + r["halt_reason"] = ( + f"barrier timeout ({timeout_s}s); " + f"unsettled devices: {unsettled}" + ) + r["halt_map"] = halt_map + r["ok"] = False + return results + + for r in results: + r["barrier_settled"] = True + return results + + # ── Single operation execution ──────────────────────────────── + + async def _execute_operation(self, op: DeviceOperation) -> dict: + """Execute one device operation through the approval-gated path. + + Delegates to ``hardware_tools.batch_actuate`` if available (for the + approval chain), otherwise uses ``transport.write`` directly. + """ + if self._hardware_tools is not None: + return await self._execute_via_tools(op) + return await self._execute_direct(op) + + async def _execute_via_tools(self, op: DeviceOperation) -> dict: + """Write through HardwareTools.batch_actuate for approval gating.""" + commands = [ + {"channel_id": ch_id, "value": value} + for ch_id, value in op.commands + ] + try: + result = await self._hardware_tools.batch_actuate({ + "device_id": op.device_id, + "commands": commands, + "verify": op.verify, + }) + if not isinstance(result, dict): + result = {"ok": False, "error": "unexpected result type"} + result["device_id"] = op.device_id + return result + except Exception as exc: + logger.warning( + "Orchestrator: batch_actuate for %r failed: %s", + op.device_id, + exc, + exc_info=True, + ) + return {"ok": False, "device_id": op.device_id, "error": str(exc)} + + async def _execute_direct(self, op: DeviceOperation) -> dict: + """Write directly through the transport (no approval chain).""" + try: + transport = await self._registry.transport(op.device_id) + except Exception as exc: + return {"ok": False, "device_id": op.device_id, "error": str(exc)} + + outcomes: list[dict] = [] + all_ok = True + for channel_id, value in op.commands: + try: + outcome = await transport.write(channel_id, value) + ok = getattr(outcome, "ok", False) + outcomes.append({ + "channel_id": channel_id, + "ok": ok, + "side_effect_state": getattr( + outcome, "side_effect_state", "unknown" + ), + }) + if not ok: + all_ok = False + except Exception as exc: + outcomes.append({ + "channel_id": channel_id, + "ok": False, + "error": str(exc), + }) + all_ok = False + + if op.settle_after_s > 0: + await asyncio.sleep(op.settle_after_s) + + return { + "ok": all_ok, + "device_id": op.device_id, + "outcomes": outcomes, + } + + async def _check_reached(self, op: DeviceOperation) -> bool: + """Check whether all commanded channels reached their targets. + + Used by the barrier mode to poll for convergence. Returns True + when every commanded channel's current value is within a default + tolerance of the target. Returns True on read failure (fail-open) + because a missing reading should not block the barrier indefinitely; + the barrier's timeout is the safety backstop. + """ + tolerance = 0.05 + for channel_id, target_value in op.commands: + try: + reading = await self._registry.read(op.device_id, channel_id) + actual = getattr(reading, "value", None) + if actual is None: + continue + target_f = float(target_value) + actual_f = float(actual) + if abs(actual_f - target_f) > tolerance: + return False + except Exception: + # Read failure: do not block, let the timeout handle it. + continue + return True + + +__all__ = [ + "DeviceOperation", + "ExecutionMode", + "MultiDeviceOrchestrator", + "OrchestrationResult", + "OrchestrationStep", +] diff --git a/src/leapflow/robot/policy.py b/src/leapflow/robot/policy.py new file mode 100644 index 00000000..2cfb53ba --- /dev/null +++ b/src/leapflow/robot/policy.py @@ -0,0 +1,315 @@ +# Copyright (c) Alibaba, Inc. and its affiliates. +"""Policy inference abstractions for LeapRobot. + +Provides the ``PreTrainedPolicy`` base class (inference-only subset) and +the ``make_policy`` factory function. All training-related functionality +(forward pass with loss, optimizer params, PEFT/LoRA, FSDP, gradient +checkpointing, Hub push) has been removed. + +torch is an optional dependency — the module is importable without it, +but instantiating a policy requires torch at runtime. +""" + +from __future__ import annotations + +import abc +import logging +import os +from collections import deque +from pathlib import Path +from typing import Any, ClassVar, TypeVar + +# --------------------------------------------------------------------------- +# torch — optional dependency +# --------------------------------------------------------------------------- +try: + import torch + from torch import Tensor, nn + + _TORCH_AVAILABLE = True +except ImportError: # pragma: no cover + _TORCH_AVAILABLE = False + torch = None # type: ignore[assignment] + nn = None # type: ignore[assignment] + Tensor = Any # type: ignore[assignment,misc] + +logger = logging.getLogger(__name__) + +T = TypeVar("T", bound="PreTrainedPolicy") + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + +_ACTION_KEY = "action" + + +def _require_torch() -> None: + """Raise early if torch is not installed.""" + if not _TORCH_AVAILABLE: + raise ImportError( + "torch is required for policy inference. " + "Install it with: pip install leapflow[robot]" + ) + + +# --------------------------------------------------------------------------- +# PreTrainedPolicy — inference subset +# --------------------------------------------------------------------------- + +class PreTrainedPolicy(nn.Module if _TORCH_AVAILABLE else object, abc.ABC): # type: ignore[misc] + """Base class for inference-only policy models. + + Concrete policies must set ``name`` (str) as a class variable and + implement the four abstract methods: + + - :meth:`reset` + - :meth:`select_action` + - :meth:`predict_action_chunk` + - :meth:`forward_inference` + + Loading from a local directory of safetensors weights is supported + via :meth:`from_pretrained`. + """ + + name: ClassVar[str | None] = None + + # Attribute names that ``drop_queued_actions`` clears. + _action_queue_attrs: ClassVar[tuple[str, ...]] = ("_queues", "_action_queue") + + def __init__(self, config: dict[str, Any] | None = None, **kwargs: Any) -> None: + _require_torch() + super().__init__() + self.config: dict[str, Any] = config or {} + + # ------------------------------------------------------------------ + # Persistence — inference-only (load, not save) + # ------------------------------------------------------------------ + + @classmethod + def from_pretrained( + cls: type[T], + pretrained_path: str | Path, + *, + config: dict[str, Any] | None = None, + device: str = "cpu", + strict: bool = False, + **kwargs: Any, + ) -> T: + """Load a pre-trained policy from a local directory. + + The directory must contain a ``model.safetensors`` file (or + compatible weight format). The policy is placed in eval mode. + + Args: + pretrained_path: Path to the directory with saved weights. + config: Optional config dict; when ``None`` a default is + used. + device: Target device (``"cpu"``, ``"cuda"``). + strict: Whether to require an exact key match when loading. + **kwargs: Extra keyword arguments forwarded to the + constructor. + + Returns: + An instance of this policy, weights loaded and in eval mode. + """ + _require_torch() + + model_dir = Path(pretrained_path) + if not model_dir.is_dir(): + raise FileNotFoundError( + f"Pretrained path does not exist or is not a directory: {model_dir}" + ) + + # Locate weight file + safetensors_file = model_dir / "model.safetensors" + if not safetensors_file.exists(): + raise FileNotFoundError( + f"Weight file not found: {safetensors_file}" + ) + + instance = cls(config=config, **kwargs) + + # Load weights via safetensors + try: + from safetensors.torch import load_model as load_model_safetensor + missing, unexpected = load_model_safetensor( + instance, str(safetensors_file), strict=strict, device=device, + ) + if missing: + logger.warning("Missing keys when loading weights: %s", missing) + if unexpected: + logger.warning("Unexpected keys when loading weights: %s", unexpected) + except ImportError: + # Fallback to vanilla torch.load + state_dict = torch.load( + safetensors_file, map_location=device, weights_only=True, + ) + instance.load_state_dict(state_dict, strict=strict) + + instance.to(device) + instance.eval() + return instance + + # ------------------------------------------------------------------ + # Inference interface + # ------------------------------------------------------------------ + + @abc.abstractmethod + def reset(self) -> None: + """Reset episode state (caches, queues, hidden states). + + Called at the beginning of every episode. + """ + raise NotImplementedError + + @abc.abstractmethod + def select_action(self, batch: dict[str, Any], **kwargs: Any) -> Any: + """Return one action to execute in the environment. + + For chunking policies this pops from an internal queue, + only calling :meth:`predict_action_chunk` when the queue is + empty. + + Args: + batch: Preprocessed observation dictionary. + **kwargs: Policy-specific inference options (e.g. noise). + + Returns: + A single action tensor. + """ + raise NotImplementedError + + @abc.abstractmethod + def predict_action_chunk(self, batch: dict[str, Any], **kwargs: Any) -> Any: + """Predict a chunk of future actions for the given observation. + + Child classes using action chunking should use this inside + :meth:`select_action` to fill the queue. + + Args: + batch: Preprocessed observation dictionary. + **kwargs: Policy-specific inference options. + + Returns: + Tensor of shape ``(chunk_size, action_dim)``. + """ + raise NotImplementedError + + def forward_inference(self, batch: dict[str, Any]) -> Any: + """Run a single-step forward pass for inference. + + Default implementation delegates to :meth:`select_action`. + Override to provide a more efficient single-step path. + """ + return self.select_action(batch) + + # ------------------------------------------------------------------ + # Action queue management + # ------------------------------------------------------------------ + + def drop_queued_actions(self) -> None: + """Discard pre-computed actions so the next call recomputes. + + Useful when a mid-episode conditioning change (e.g. new language + instruction) must take effect immediately rather than after the + queue drains. + """ + for attr in self._action_queue_attrs: + queue = getattr(self, attr, None) + if isinstance(queue, dict): + action_deque = queue.get(_ACTION_KEY) + if action_deque is not None: + action_deque.clear() + elif queue is not None: + queue.clear() + + def count_queued_actions(self) -> int: + """Number of pre-computed actions waiting to be served.""" + total = 0 + for attr in self._action_queue_attrs: + queue = getattr(self, attr, None) + if isinstance(queue, dict): + action_deque = queue.get(_ACTION_KEY) + total += len(action_deque) if action_deque is not None else 0 + elif queue is not None: + total += len(queue) + return total + + +# --------------------------------------------------------------------------- +# Factory +# --------------------------------------------------------------------------- + +# Registry of known policy classes keyed by name. +_POLICY_REGISTRY: dict[str, type[PreTrainedPolicy]] = {} + + +def register_policy(cls: type[PreTrainedPolicy]) -> type[PreTrainedPolicy]: + """Class decorator that registers a policy in the factory. + + Usage:: + + @register_policy + class MyPolicy(PreTrainedPolicy): + name = "my_policy" + ... + """ + if not cls.name: + raise TypeError(f"{cls.__name__} must define a 'name' class variable") + _POLICY_REGISTRY[cls.name] = cls + return cls + + +def make_policy( + policy_type: str, + pretrained_path: str | Path | None = None, + *, + config: dict[str, Any] | None = None, + device: str = "cpu", + **kwargs: Any, +) -> PreTrainedPolicy: + """Instantiate a policy by registered name. + + When *pretrained_path* is supplied the weights are loaded from disk; + otherwise a freshly initialised instance is returned. + + Args: + policy_type: Registered policy name (e.g. ``"act"``, + ``"diffusion"``). + pretrained_path: Optional directory containing saved weights. + config: Policy configuration dict. + device: Target device. + **kwargs: Extra arguments forwarded to the constructor. + + Returns: + A ready-to-use policy instance. + + Raises: + ValueError: If *policy_type* is not registered. + """ + _require_torch() + + policy_cls = _POLICY_REGISTRY.get(policy_type) + if policy_cls is None: + available = sorted(_POLICY_REGISTRY.keys()) + raise ValueError( + f"Unknown policy type '{policy_type}'. " + f"Available: {available}" + ) + + if pretrained_path is not None: + return policy_cls.from_pretrained( + pretrained_path, config=config, device=device, **kwargs, + ) + + policy = policy_cls(config=config, **kwargs) + policy.to(device) + return policy + + +__all__ = [ + "PreTrainedPolicy", + "register_policy", + "make_policy", +] diff --git a/src/leapflow/robot/processor.py b/src/leapflow/robot/processor.py new file mode 100644 index 00000000..e45a5977 --- /dev/null +++ b/src/leapflow/robot/processor.py @@ -0,0 +1,263 @@ +# Copyright (c) Alibaba, Inc. and its affiliates. +"""Pre/post-processing pipeline for policy inference. + +Provides ``ProcessorStep`` (Protocol), concrete processors +(``NormalizeProcessor``, ``DeviceProcessor``, ``UnnormalizeProcessor``), +and the ``ProcessorPipeline`` that chains them. + +torch and numpy are optional dependencies — the module is importable +without them, but concrete processors require them at runtime. +""" + +from __future__ import annotations + +import copy +import logging +from typing import Any, Protocol, runtime_checkable + +# --------------------------------------------------------------------------- +# Optional dependencies +# --------------------------------------------------------------------------- +try: + import torch + from torch import Tensor + + _TORCH_AVAILABLE = True +except ImportError: # pragma: no cover + _TORCH_AVAILABLE = False + torch = None # type: ignore[assignment] + Tensor = Any # type: ignore[assignment,misc] + +try: + import numpy as np + + _NUMPY_AVAILABLE = True +except ImportError: # pragma: no cover + _NUMPY_AVAILABLE = False + np = None # type: ignore[assignment] + +logger = logging.getLogger(__name__) + + +# --------------------------------------------------------------------------- +# ProcessorStep Protocol +# --------------------------------------------------------------------------- + +@runtime_checkable +class ProcessorStep(Protocol): + """One step in the pre/post-processing pipeline. + + ``forward`` transforms data *towards* the model (pre-processing); + ``reverse`` transforms data *away from* the model (post-processing). + Both must be pure in the sense that they do not mutate global state, + although they may update internal running statistics. + """ + + def forward(self, batch: dict[str, Any]) -> dict[str, Any]: + """Transform *batch* in the forward (pre-processing) direction.""" + ... + + def reverse(self, batch: dict[str, Any]) -> dict[str, Any]: + """Transform *batch* in the reverse (post-processing) direction.""" + ... + + +# --------------------------------------------------------------------------- +# NormalizeProcessor +# --------------------------------------------------------------------------- + +class NormalizeProcessor: + """Normalize observation values to a model-expected distribution. + + Supports two modes selectable via *mode*: + + - ``"mean_std"`` — classic z-score normalisation: ``(x - mean) / std`` + - ``"min_max"`` — scale to [-1, 1]: ``2 * (x - min) / (max - min) - 1`` + + Statistics are supplied at construction time as plain dicts so they + can be loaded from a JSON config without a torch dependency at + import time. + """ + + def __init__( + self, + stats: dict[str, dict[str, Any]], + *, + mode: str = "mean_std", + keys: list[str] | None = None, + ) -> None: + """ + Args: + stats: Mapping ``feature_key -> {"mean": ..., "std": ...}`` + or ``feature_key -> {"min": ..., "max": ...}`` depending + on *mode*. Values may be scalars, lists, or tensors. + mode: ``"mean_std"`` or ``"min_max"``. + keys: If supplied, only these keys are normalised. ``None`` + normalises every key present in *stats*. + """ + if mode not in ("mean_std", "min_max"): + raise ValueError(f"Unsupported normalization mode: {mode!r}") + self._stats = stats + self._mode = mode + self._keys = set(keys) if keys else None + + def _should_process(self, key: str) -> bool: + if self._keys is not None and key not in self._keys: + return False + return key in self._stats + + def forward(self, batch: dict[str, Any]) -> dict[str, Any]: + """Normalise *batch* values in-place and return it.""" + for key, value in batch.items(): + if not self._should_process(key): + continue + s = self._stats[key] + if self._mode == "mean_std": + batch[key] = self._z_normalize(value, s["mean"], s["std"]) + else: + batch[key] = self._minmax_normalize(value, s["min"], s["max"]) + return batch + + def reverse(self, batch: dict[str, Any]) -> dict[str, Any]: + """Un-normalise *batch* values in-place and return it.""" + for key, value in batch.items(): + if not self._should_process(key): + continue + s = self._stats[key] + if self._mode == "mean_std": + batch[key] = self._z_unnormalize(value, s["mean"], s["std"]) + else: + batch[key] = self._minmax_unnormalize(value, s["min"], s["max"]) + return batch + + # -- arithmetic helpers (work with scalars, numpy, or torch) ---------- + + @staticmethod + def _z_normalize(x: Any, mean: Any, std: Any) -> Any: + return (x - mean) / (std + 1e-8) + + @staticmethod + def _z_unnormalize(x: Any, mean: Any, std: Any) -> Any: + return x * (std + 1e-8) + mean + + @staticmethod + def _minmax_normalize(x: Any, lo: Any, hi: Any) -> Any: + return 2.0 * (x - lo) / (hi - lo + 1e-8) - 1.0 + + @staticmethod + def _minmax_unnormalize(x: Any, lo: Any, hi: Any) -> Any: + return (x + 1.0) / 2.0 * (hi - lo + 1e-8) + lo + + +# --------------------------------------------------------------------------- +# UnnormalizeProcessor +# --------------------------------------------------------------------------- + +class UnnormalizeProcessor: + """Reverse a NormalizeProcessor in the forward direction. + + Useful as a post-processor: its ``forward`` un-normalises, and its + ``reverse`` normalises — the exact mirror of ``NormalizeProcessor``. + """ + + def __init__( + self, + stats: dict[str, dict[str, Any]], + *, + mode: str = "mean_std", + keys: list[str] | None = None, + ) -> None: + self._inner = NormalizeProcessor(stats, mode=mode, keys=keys) + + def forward(self, batch: dict[str, Any]) -> dict[str, Any]: + return self._inner.reverse(batch) + + def reverse(self, batch: dict[str, Any]) -> dict[str, Any]: + return self._inner.forward(batch) + + +# --------------------------------------------------------------------------- +# DeviceProcessor +# --------------------------------------------------------------------------- + +class DeviceProcessor: + """Move tensors between devices (CPU ↔ GPU). + + Non-tensor values are passed through unchanged. + """ + + def __init__(self, device: str = "cpu") -> None: + if not _TORCH_AVAILABLE: + raise ImportError("DeviceProcessor requires torch") + self._device = torch.device(device) + + def forward(self, batch: dict[str, Any]) -> dict[str, Any]: + """Move all tensor values to the configured device.""" + return { + k: v.to(self._device) if isinstance(v, Tensor) else v + for k, v in batch.items() + } + + def reverse(self, batch: dict[str, Any]) -> dict[str, Any]: + """Move all tensor values to CPU (inverse of forward).""" + cpu = torch.device("cpu") + return { + k: v.to(cpu) if isinstance(v, Tensor) else v + for k, v in batch.items() + } + + +# --------------------------------------------------------------------------- +# ProcessorPipeline +# --------------------------------------------------------------------------- + +class ProcessorPipeline: + """Chain of ``ProcessorStep``-compatible objects. + + ``forward()`` runs every step's ``forward`` in order; + ``reverse()`` runs every step's ``reverse`` in *reverse* order. + """ + + def __init__(self, steps: list[Any] | None = None) -> None: + """ + Args: + steps: Ordered list of processor step instances. Each must + expose ``forward(batch)`` and ``reverse(batch)`` methods. + """ + self._steps: list[Any] = list(steps) if steps else [] + + def __len__(self) -> int: + return len(self._steps) + + def __repr__(self) -> str: + step_names = [type(s).__name__ for s in self._steps] + return f"ProcessorPipeline(steps={step_names})" + + def add(self, step: Any) -> None: + """Append a step to the pipeline.""" + self._steps.append(step) + + def forward(self, batch: dict[str, Any]) -> dict[str, Any]: + """Run all steps' ``forward`` in declaration order.""" + for step in self._steps: + batch = step.forward(batch) + return batch + + def reverse(self, batch: dict[str, Any]) -> dict[str, Any]: + """Run all steps' ``reverse`` in reverse declaration order.""" + for step in reversed(self._steps): + batch = step.reverse(batch) + return batch + + def __call__(self, batch: dict[str, Any]) -> dict[str, Any]: + """Alias for :meth:`forward`.""" + return self.forward(batch) + + +__all__ = [ + "ProcessorStep", + "NormalizeProcessor", + "UnnormalizeProcessor", + "DeviceProcessor", + "ProcessorPipeline", +] diff --git a/src/leapflow/robot/teleop.py b/src/leapflow/robot/teleop.py new file mode 100644 index 00000000..60c1bda1 --- /dev/null +++ b/src/leapflow/robot/teleop.py @@ -0,0 +1,470 @@ +# Copyright (c) Alibaba, Inc. and its affiliates. +"""Teleoperation bridge: leader-follower device pairing for demonstration. + +A teleoperation session reads commands from a leader device (a human moves +the leader arm) and mirrors them to a follower device (the robot being +taught), while recording the resulting trajectory. This is the physical +analogue of demonstrating a task by doing it. + +Kinesthetic mode is a variant where there is no separate leader: the human +physically moves the follower itself (with torque disabled), and the source +of truth is the follower's own joint encoders. + +Both modes produce a :class:`PhysicalTrajectory` as their output, which +can later be exported as an episode for policy learning. + +The control loop uses deadline-based scheduling identical to +``BatchStreamCoordinator._run()``: a fixed sleep after each iteration would +add the I/O duration to every period, so a loop declared at 30 Hz would run +slower — silently. +""" + +from __future__ import annotations + +import asyncio +import logging +import time +from typing import Any, Mapping, Protocol, runtime_checkable + +from leapflow.robot.trajectory import ( + PhysicalTrajectory, + PhysicalTrajectoryStep, + make_trajectory_id, +) + +logger = logging.getLogger(__name__) + + +# --------------------------------------------------------------------------- +# TeleopBridge Protocol +# --------------------------------------------------------------------------- + +@runtime_checkable +class TeleopBridge(Protocol): + """Bridges a leader device's motion to a follower device. + + Implementations must be safe to start and stop from an ``asyncio`` event + loop. ``stop()`` returns the finalized trajectory so the caller can + persist it or hand it to an episode recorder. + """ + + @property + def session_id(self) -> str: + """Unique identifier for this teleop session.""" + ... + + async def start(self) -> None: + """Begin the control loop. Returns promptly; the loop runs as a task.""" + ... + + async def stop(self) -> PhysicalTrajectory: + """Stop the control loop and return the recorded trajectory.""" + ... + + @property + def is_active(self) -> bool: + """Whether the control loop is currently running.""" + ... + + +# --------------------------------------------------------------------------- +# LeaderFollowerBridge +# --------------------------------------------------------------------------- + +class LeaderFollowerBridge: + """Teleop bridge that mirrors leader joint positions to follower. + + Loop (at the follower's declared control rate): + + 1. ``read_batch(leader_device, joint_channels)`` + 2. Optionally transform/scale (leader and follower may differ) + 3. Write to follower through the approval-gated path + (``hardware_tools.batch_actuate``) — teleop does **not** bypass gating + 4. ``read_batch(follower)`` for actual state + 5. Append ``PhysicalTrajectoryStep`` to trajectory + + Safety: the follower write goes through the normal HCP approval/trust + path. ``halt()`` on either device stops the session. + """ + + def __init__( + self, + registry: Any, + *, + leader_device_id: str, + follower_device_id: str, + goal: str = "", + control_rate_hz: float = 30.0, + joint_map: Mapping[str, str] | None = None, + hardware_tools: Any = None, + ) -> None: + self._registry = registry + self._leader_device_id = leader_device_id + self._follower_device_id = follower_device_id + self._goal = goal + self._control_rate_hz = max(1.0, float(control_rate_hz)) + self._joint_map = dict(joint_map) if joint_map else {} + self._hardware_tools = hardware_tools + + self._session_id = make_trajectory_id() + self._trajectory = PhysicalTrajectory( + trajectory_id=self._session_id, + device_id=follower_device_id, + goal=goal, + started_at=0.0, + metadata={ + "mode": "teleop", + "leader_device_id": leader_device_id, + "follower_device_id": follower_device_id, + "control_rate_hz": control_rate_hz, + "joint_map": self._joint_map or {}, + }, + ) + self._task: asyncio.Task[None] | None = None + self._stopping = asyncio.Event() + self._sequence = 0 + + @property + def session_id(self) -> str: + return self._session_id + + @property + def is_active(self) -> bool: + return self._task is not None and not self._task.done() + + async def start(self) -> None: + """Begin the leader→follower mirror loop.""" + if self._task is not None: + return + self._stopping.clear() + self._trajectory = PhysicalTrajectory( + trajectory_id=self._trajectory.trajectory_id, + device_id=self._trajectory.device_id, + goal=self._trajectory.goal, + started_at=time.time(), + metadata=self._trajectory.metadata, + ) + self._task = asyncio.create_task( + self._run(), name=f"teleop:{self._session_id}" + ) + + async def stop(self) -> PhysicalTrajectory: + """Stop the mirror loop and return the finalized trajectory.""" + self._stopping.set() + task = self._task + self._task = None + if task is not None: + task.cancel() + try: + await asyncio.wait_for(task, timeout=3.0) + except (asyncio.CancelledError, asyncio.TimeoutError): + pass + except Exception as exc: # noqa: BLE001 + logger.warning("Teleop task stop raised: %s", exc, exc_info=True) + return self._trajectory.finalized() + + # -- Control loop --------------------------------------------------------- + + async def _run(self) -> None: + """Core mirror loop using deadline-based scheduling.""" + interval = 1.0 / self._control_rate_hz + next_at = time.monotonic() + consecutive_failures = 0 + + while not self._stopping.is_set(): + try: + step = await self._one_cycle() + if step is not None: + self._trajectory = self._trajectory.with_step(step) + consecutive_failures = 0 + except asyncio.CancelledError: + raise + except Exception as exc: # noqa: BLE001 + consecutive_failures += 1 + if consecutive_failures == 1: + logger.warning( + "Teleop cycle failed: %s", exc, exc_info=True + ) + backoff = min(interval * (2 ** consecutive_failures), 5.0) + await self._sleep(backoff) + next_at = time.monotonic() + continue + + # Deadline-based scheduling (identical to BatchStreamCoordinator). + next_at += interval + delay = next_at - time.monotonic() + if delay < 0: + missed = int(-delay // interval) + 1 + next_at += missed * interval + delay = max(0.0, next_at - time.monotonic()) + await self._sleep(delay) + + async def _one_cycle(self) -> PhysicalTrajectoryStep | None: + """Execute one leader-read → follower-write → follower-read cycle.""" + registry = self._registry + + # 1. Read leader joint positions. + leader_context = registry.context(self._leader_device_id) + if leader_context is None: + return None + leader_channels = tuple( + ch.channel_id for ch in leader_context.channels if ch.is_readable + ) + if not leader_channels: + return None + leader_batch = await registry.read_batch( + self._leader_device_id, leader_channels + ) + + # 2. Map leader readings to follower commands. + commands: list[tuple[str, Any]] = [] + leader_positions: dict[str, float] = {} + for reading in leader_batch.readings: + leader_positions[reading.channel_id] = float(reading.value) + follower_ch = self._joint_map.get(reading.channel_id, reading.channel_id) + commands.append((follower_ch, reading.value)) + + # 3. Write to follower through approval-gated path. + if commands and self._hardware_tools is not None: + await self._hardware_tools.batch_actuate( + self._follower_device_id, tuple(commands) + ) + elif commands: + # Direct write fallback when no hardware_tools provided (testing). + from leapflow.hardware.transport import BatchTransport + + transport = await registry.transport(self._follower_device_id) + if isinstance(transport, BatchTransport): + async with registry.device_io_batch(self._follower_device_id): + await transport.write_batch(tuple(commands)) + else: + async with registry.device_io(self._follower_device_id): + for ch_id, val in commands: + await transport.write(ch_id, val) + + # 4. Read follower actual state. + follower_context = registry.context(self._follower_device_id) + if follower_context is None: + return None + follower_channels = tuple( + ch.channel_id for ch in follower_context.channels if ch.is_readable + ) + follower_batch = await registry.read_batch( + self._follower_device_id, follower_channels + ) + + # 5. Build step from follower state. + now = time.time() + mono = time.monotonic() + positions: dict[str, float] = {} + for reading in follower_batch.readings: + positions[reading.channel_id] = float(reading.value) + + step = PhysicalTrajectoryStep( + timestamp=now, + monotonic_at=mono, + joint_positions=positions, + action_source="teleop", + sequence=self._sequence, + ) + self._sequence += 1 + return step + + async def _sleep(self, seconds: float) -> None: + """Sleep interruptibly via the stopping event.""" + try: + await asyncio.wait_for(self._stopping.wait(), timeout=seconds) + except asyncio.TimeoutError: + return + + +# --------------------------------------------------------------------------- +# KinestheticRecorder +# --------------------------------------------------------------------------- + +class KinestheticRecorder: + """Records a demonstration where the human moves the follower directly. + + No leader device. Torque is disabled on the follower (via a configure + channel if available), the human moves it by hand, and the recorder + samples the follower's joint encoders at the control rate. + + Output is a ``PhysicalTrajectory`` with ``action_source="kinesthetic"`` + on every step. + """ + + def __init__( + self, + registry: Any, + *, + device_id: str, + goal: str = "", + sample_rate_hz: float = 30.0, + ) -> None: + self._registry = registry + self._device_id = device_id + self._goal = goal + self._sample_rate_hz = max(1.0, float(sample_rate_hz)) + + self._session_id = make_trajectory_id() + self._trajectory = PhysicalTrajectory( + trajectory_id=self._session_id, + device_id=device_id, + goal=goal, + started_at=0.0, + metadata={ + "mode": "kinesthetic", + "device_id": device_id, + "sample_rate_hz": sample_rate_hz, + }, + ) + self._task: asyncio.Task[None] | None = None + self._stopping = asyncio.Event() + self._sequence = 0 + + @property + def session_id(self) -> str: + return self._session_id + + @property + def is_active(self) -> bool: + return self._task is not None and not self._task.done() + + async def start(self) -> None: + """Begin sampling the follower's joint encoders.""" + if self._task is not None: + return + self._stopping.clear() + self._trajectory = PhysicalTrajectory( + trajectory_id=self._trajectory.trajectory_id, + device_id=self._trajectory.device_id, + goal=self._trajectory.goal, + started_at=time.time(), + metadata=self._trajectory.metadata, + ) + self._task = asyncio.create_task( + self._run(), name=f"kinesthetic:{self._session_id}" + ) + + async def stop(self) -> PhysicalTrajectory: + """Stop sampling and return the finalized trajectory.""" + self._stopping.set() + task = self._task + self._task = None + if task is not None: + task.cancel() + try: + await asyncio.wait_for(task, timeout=3.0) + except (asyncio.CancelledError, asyncio.TimeoutError): + pass + except Exception as exc: # noqa: BLE001 + logger.warning( + "Kinesthetic task stop raised: %s", exc, exc_info=True + ) + return self._trajectory.finalized() + + # -- Sampling loop -------------------------------------------------------- + + async def _run(self) -> None: + """Core sampling loop using deadline-based scheduling.""" + interval = 1.0 / self._sample_rate_hz + next_at = time.monotonic() + consecutive_failures = 0 + + while not self._stopping.is_set(): + try: + step = await self._sample() + if step is not None: + self._trajectory = self._trajectory.with_step(step) + consecutive_failures = 0 + except asyncio.CancelledError: + raise + except Exception as exc: # noqa: BLE001 + consecutive_failures += 1 + if consecutive_failures == 1: + logger.warning( + "Kinesthetic sample failed: %s", exc, exc_info=True + ) + backoff = min(interval * (2 ** consecutive_failures), 5.0) + await self._sleep(backoff) + next_at = time.monotonic() + continue + + next_at += interval + delay = next_at - time.monotonic() + if delay < 0: + missed = int(-delay // interval) + 1 + next_at += missed * interval + delay = max(0.0, next_at - time.monotonic()) + await self._sleep(delay) + + async def _sample(self) -> PhysicalTrajectoryStep | None: + """Read the follower's current joint state.""" + context = self._registry.context(self._device_id) + if context is None: + return None + channels = tuple( + ch.channel_id for ch in context.channels if ch.is_readable + ) + if not channels: + return None + batch = await self._registry.read_batch(self._device_id, channels) + + now = time.time() + mono = time.monotonic() + positions: dict[str, float] = {} + for reading in batch.readings: + positions[reading.channel_id] = float(reading.value) + + step = PhysicalTrajectoryStep( + timestamp=now, + monotonic_at=mono, + joint_positions=positions, + action_source="kinesthetic", + sequence=self._sequence, + ) + self._sequence += 1 + return step + + async def _sleep(self, seconds: float) -> None: + try: + await asyncio.wait_for(self._stopping.wait(), timeout=seconds) + except asyncio.TimeoutError: + return + + +# --------------------------------------------------------------------------- +# Module-level active session tracking +# --------------------------------------------------------------------------- + +_active_physical_sessions: dict[str, TeleopBridge | LeaderFollowerBridge | KinestheticRecorder] = {} +"""Tracks active physical teach sessions, keyed by a context identifier. + +Used by the /teach command dispatcher to start/stop physical sessions +without polluting the desktop teach session state. +""" + + +def register_physical_session(key: str, bridge: Any) -> None: + """Register an active physical teach session.""" + _active_physical_sessions[key] = bridge + + +def get_physical_session(key: str) -> Any: + """Return the active physical session for *key*, or None.""" + return _active_physical_sessions.get(key) + + +def remove_physical_session(key: str) -> Any: + """Remove and return the active physical session for *key*, or None.""" + return _active_physical_sessions.pop(key, None) + + +__all__ = [ + "KinestheticRecorder", + "LeaderFollowerBridge", + "TeleopBridge", + "get_physical_session", + "register_physical_session", + "remove_physical_session", +] diff --git a/src/leapflow/robot/trajectory.py b/src/leapflow/robot/trajectory.py new file mode 100644 index 00000000..b77e8f10 --- /dev/null +++ b/src/leapflow/robot/trajectory.py @@ -0,0 +1,198 @@ +# Copyright (c) Alibaba, Inc. and its affiliates. +"""Physical demonstration trajectory types. + +Parallel to the desktop ``TrajectoryStep`` (which captures GUI events), these +types capture physical robot state: joint positions, velocities, gripper +state, camera frames, and sensor readings. Kept separate from the desktop +trajectory model so physical demonstration learning does not perturb the +existing GUI teach/learn regression. + +Timebase convention follows ``Reading`` / ``HardwareEvent``: + +- ``timestamp``: wall-clock (``time.time()`` epoch seconds). The only clock + that may be persisted, rendered, or correlated outside this process. +- ``monotonic_at``: ``time.monotonic()``. The only clock that may be used + for intervals, since wall-clock jumps fabricate rates no device produced. + +``camera_frames`` stores file references, not inline bytes. Frames are large +binary blobs that would inflate every serialization path; the ``FrameReading`` +not-a-``Reading`` principle from HCP applies identically here. +""" + +from __future__ import annotations + +import time +import uuid +from dataclasses import dataclass, field +from typing import Any, Mapping + + +# --------------------------------------------------------------------------- +# PhysicalTrajectoryStep +# --------------------------------------------------------------------------- + +@dataclass(frozen=True) +class PhysicalTrajectoryStep: + """One timestep of a physical demonstration. + + Captures the complete observable state of a robot at a single instant: + joint positions and velocities, gripper aperture, camera frame + references, and arbitrary sensor readings. + + ``action_source`` classifies how this step was produced: + + - ``"teleop"``: a leader device was mirrored to the follower. + - ``"kinesthetic"``: the human physically moved the robot. + - ``"policy"``: a learned policy produced the action. + - ``"manual"``: a single manual command (e.g. jog). + """ + + timestamp: float + """Wall-clock (``time.time()`` epoch seconds).""" + + monotonic_at: float + """``time.monotonic()`` — for interval computation only.""" + + joint_positions: Mapping[str, float] + """Channel-id → position (radians or meters, per channel declaration).""" + + joint_velocities: Mapping[str, float] = field(default_factory=dict) + """Channel-id → velocity. Empty when not sampled.""" + + gripper_state: float | None = None + """Gripper aperture, 0.0 = closed … 1.0 = open. ``None`` = no gripper.""" + + camera_frames: Mapping[str, str] = field(default_factory=dict) + """Channel-id → frame file reference (path or URI, never inline bytes).""" + + sensor_readings: Mapping[str, Any] = field(default_factory=dict) + """Arbitrary sensor values keyed by channel-id.""" + + action_source: str = "teleop" + """How this step was produced: ``teleop`` | ``kinesthetic`` | ``policy`` | ``manual``.""" + + sequence: int = 0 + """Zero-based index within the trajectory, for gap detection.""" + + def to_dict(self) -> dict[str, Any]: + """Serialize to a plain dict suitable for JSON / NDJSON persistence.""" + payload: dict[str, Any] = { + "timestamp": self.timestamp, + "monotonic_at": self.monotonic_at, + "joint_positions": dict(self.joint_positions), + "sequence": self.sequence, + "action_source": self.action_source, + } + if self.joint_velocities: + payload["joint_velocities"] = dict(self.joint_velocities) + if self.gripper_state is not None: + payload["gripper_state"] = self.gripper_state + if self.camera_frames: + payload["camera_frames"] = dict(self.camera_frames) + if self.sensor_readings: + payload["sensor_readings"] = dict(self.sensor_readings) + return payload + + +# --------------------------------------------------------------------------- +# PhysicalTrajectory +# --------------------------------------------------------------------------- + +@dataclass(frozen=True) +class PhysicalTrajectory: + """A complete physical demonstration episode. + + Frozen so that a recorded trajectory is an immutable evidence artifact. + Mutation uses the builder helpers ``with_step`` and ``finalized`` which + return new instances — allocation is negligible compared to the cost of + the physical sampling loop that produced the data. + """ + + trajectory_id: str + """Unique identifier for this trajectory.""" + + device_id: str + """The follower device that was commanded (or recorded in kinesthetic mode).""" + + goal: str + """Human-supplied description of what the demonstration achieves.""" + + steps: tuple[PhysicalTrajectoryStep, ...] = () + """Ordered timesteps, oldest first.""" + + started_at: float = 0.0 + """Wall-clock epoch when recording began.""" + + ended_at: float = 0.0 + """Wall-clock epoch when recording ended. 0.0 while still recording.""" + + metadata: Mapping[str, Any] = field(default_factory=dict) + """Free-form metadata (leader device id, control rate, joint map, etc.).""" + + # -- Derived properties --------------------------------------------------- + + @property + def duration_s(self) -> float: + """Duration in seconds. Zero while still recording.""" + if self.ended_at > self.started_at: + return self.ended_at - self.started_at + return 0.0 + + @property + def sample_count(self) -> int: + """Number of recorded timesteps.""" + return len(self.steps) + + # -- Serialization -------------------------------------------------------- + + def to_dict(self) -> dict[str, Any]: + """Serialize the full trajectory for JSON persistence or export.""" + return { + "trajectory_id": self.trajectory_id, + "device_id": self.device_id, + "goal": self.goal, + "started_at": self.started_at, + "ended_at": self.ended_at, + "duration_s": self.duration_s, + "sample_count": self.sample_count, + "metadata": dict(self.metadata), + "steps": [step.to_dict() for step in self.steps], + } + + # -- Builder pattern (frozen, so returns new instances) -------------------- + + def with_step(self, step: PhysicalTrajectoryStep) -> "PhysicalTrajectory": + """Return a new trajectory with *step* appended.""" + return PhysicalTrajectory( + trajectory_id=self.trajectory_id, + device_id=self.device_id, + goal=self.goal, + steps=(*self.steps, step), + started_at=self.started_at, + ended_at=self.ended_at, + metadata=self.metadata, + ) + + def finalized(self, ended_at: float | None = None) -> "PhysicalTrajectory": + """Return a new trajectory with ``ended_at`` set.""" + return PhysicalTrajectory( + trajectory_id=self.trajectory_id, + device_id=self.device_id, + goal=self.goal, + steps=self.steps, + started_at=self.started_at, + ended_at=ended_at if ended_at is not None else time.time(), + metadata=self.metadata, + ) + + +def make_trajectory_id() -> str: + """Generate a short unique trajectory identifier.""" + return uuid.uuid4().hex[:16] + + +__all__ = [ + "PhysicalTrajectory", + "PhysicalTrajectoryStep", + "make_trajectory_id", +] diff --git a/src/leapflow/robot/types.py b/src/leapflow/robot/types.py new file mode 100644 index 00000000..8691a913 --- /dev/null +++ b/src/leapflow/robot/types.py @@ -0,0 +1,71 @@ +# Copyright (c) Alibaba, Inc. and its affiliates. +"""Core type aliases for the LeapRobot package. + +These types mirror the inference-side subset of the upstream open-source robot +type vocabulary. Heavy dependencies (torch, numpy) are conditionally +imported so the module remains importable in environments where only +standard-library Python is available. +""" + +from __future__ import annotations + +from typing import Any + +# --------------------------------------------------------------------------- +# Torch — optional dependency +# --------------------------------------------------------------------------- +try: + import torch + + PolicyAction = torch.Tensor + """Action tensor produced by a policy network.""" + + _TORCH_AVAILABLE = True +except ImportError: # pragma: no cover + _TORCH_AVAILABLE = False + PolicyAction = Any # type: ignore[assignment,misc] + +# --------------------------------------------------------------------------- +# NumPy — optional dependency +# --------------------------------------------------------------------------- +try: + import numpy as np + + EnvAction = np.ndarray + """Action array used by simulation environments.""" + + _NUMPY_AVAILABLE = True +except ImportError: # pragma: no cover + _NUMPY_AVAILABLE = False + EnvAction = Any # type: ignore[assignment,misc] + +# --------------------------------------------------------------------------- +# Robot-level types (always available) +# --------------------------------------------------------------------------- + +RobotObservation = dict[str, Any] +"""Flat mapping of sensor name to value returned by a robot.""" + +RobotAction = dict[str, Any] +"""Flat mapping of actuator name to commanded value.""" + +BatchType = dict[str, Any] +"""Generic dict batch fed to / returned by policy inference.""" + +ObservationFeatures = dict[str, tuple[int, ...]] +"""Maps observation key to its shape tuple.""" + +ActionFeatures = dict[str, tuple[int, ...]] +"""Maps action key to its shape tuple.""" + +__all__ = [ + "PolicyAction", + "EnvAction", + "RobotObservation", + "RobotAction", + "BatchType", + "ObservationFeatures", + "ActionFeatures", + "_TORCH_AVAILABLE", + "_NUMPY_AVAILABLE", +] diff --git a/src/leapflow/skills/discovery.py b/src/leapflow/skills/discovery.py index 29b40165..7ce73538 100644 --- a/src/leapflow/skills/discovery.py +++ b/src/leapflow/skills/discovery.py @@ -46,64 +46,48 @@ def configure(index: Any, injector: Any, *, registry: Any = None, skill_view_max # ------------------------------------------------------------------ -async def skills_list(params: Dict[str, Any]) -> Dict[str, Any]: - """Layer 1: List available skills with compact metadata. - - Params: - query (str, optional): Keyword filter applied to name and description. - category (str, optional): Filter by category. - source (str, optional): Filter by source ("learned"|"manual"|"hub"). +def _collect_skills( + query: str = "", category_filter: str = "", source_filter: str = "" +) -> List[Dict[str, Any]]: + """Enumerate skills from the SkillIndex plus the builtin registry, filtered. - Returns dict with ok, count, skills[] fields. + Shared by ``skills_list`` (the async LLM tool) and ``skill_inventory_summary`` + (sync self-introspection) so both report the exact same inventory. """ - if _skill_index is None: - return {"ok": False, "error": "Skill index not initialized"} + skills: List[Dict[str, Any]] = [] + + if _skill_index is not None: + entries = _skill_index.get_entries() + if query: + ql = query.lower() + entries = [e for e in entries if ql in e.name.lower() or ql in e.description.lower()] + if category_filter: + cl = category_filter.lower() + entries = [e for e in entries if e.category.lower() == cl] + if source_filter: + sl = source_filter.lower() + entries = [e for e in entries if e.source.lower() == sl] + skills.extend( + { + "name": e.name, + "description": e.description, + "tags": list(e.tags), + "category": e.category, + "source": e.source, + } + for e in entries + ) - query = str(params.get("query", "")).strip() - category_filter = str(params.get("category", "")).strip() - source_filter = str(params.get("source", "")).strip() - entries = _skill_index.get_entries() - - # Optional keyword filter (case-insensitive substring match) - if query: - query_lower = query.lower() - entries = [ - e for e in entries - if query_lower in e.name.lower() or query_lower in e.description.lower() - ] - - # Category filter - if category_filter: - cat_lower = category_filter.lower() - entries = [e for e in entries if e.category.lower() == cat_lower] - - # Source filter - if source_filter: - src_lower = source_filter.lower() - entries = [e for e in entries if e.source.lower() == src_lower] - - skills = [ - { - "name": e.name, - "description": e.description, - "tags": list(e.tags), - "category": e.category, - "source": e.source, - } - for e in entries - ] - - # Include builtin skills from registry (always available) - if _skill_registry and hasattr(_skill_registry, 'list_all'): + # Builtin Python-class skills from the registry (always available) + if _skill_registry and hasattr(_skill_registry, "list_all"): for s in _skill_registry.list_all(): reg_entry = { "name": s.name, - "description": getattr(s, 'description', s.name), + "description": getattr(s, "description", s.name), "tags": [], "category": "builtin", "source": "builtin", } - # Apply filters if query and query.lower() not in reg_entry["name"].lower() and query.lower() not in reg_entry["description"].lower(): continue if source_filter and source_filter.lower() != "builtin": @@ -112,9 +96,54 @@ async def skills_list(params: Dict[str, Any]) -> Dict[str, Any]: continue skills.append(reg_entry) + return skills + + +async def skills_list(params: Dict[str, Any]) -> Dict[str, Any]: + """Layer 1: List available skills with compact metadata. + + Params: + query (str, optional): Keyword filter applied to name and description. + category (str, optional): Filter by category. + source (str, optional): Filter by source ("learned"|"manual"|"hub"). + + Returns dict with ok, count, skills[] fields. + """ + if _skill_index is None: + return {"ok": False, "error": "Skill index not initialized"} + + skills = _collect_skills( + str(params.get("query", "")).strip(), + str(params.get("category", "")).strip(), + str(params.get("source", "")).strip(), + ) return {"ok": True, "count": len(skills), "skills": skills} +def skill_inventory_summary() -> Dict[str, Any]: + """Compact, synchronous skills summary for self-introspection. + + Returns ``{available, count, by_source, by_category, names}``. Degrades to + ``available=False`` when neither a skill index nor a builtin registry is + wired, so the self_describe capabilities facet can report skills as a + limitation rather than raising. + """ + if _skill_index is None and _skill_registry is None: + return {"available": False, "reason": "skill discovery not initialized"} + from collections import Counter + + skills = _collect_skills() + by_source = Counter(s["source"] for s in skills) + by_category = Counter(s["category"] for s in skills if s.get("category")) + return { + "available": True, + "count": len(skills), + "by_source": dict(by_source), + "by_category": dict(by_category), + "names": sorted(s["name"] for s in skills), + } + + async def skill_view(params: Dict[str, Any]) -> Dict[str, Any]: """Layer 2: View full SKILL.md content for a specific skill. diff --git a/src/leapflow/skills/index.py b/src/leapflow/skills/index.py index 322281a9..803b33ee 100644 --- a/src/leapflow/skills/index.py +++ b/src/leapflow/skills/index.py @@ -7,6 +7,7 @@ from __future__ import annotations import dataclasses +import hashlib import json import logging import time @@ -20,6 +21,10 @@ # the user-profile skills_dir so they ship with the installed package. _BUILTIN_SKILLS_DIR = Path(__file__).resolve().parent / "builtin_skills" +# Bumped when the snapshot layout or signing scheme changes; a mismatch forces +# a fresh L3 rescan so an old on-disk snapshot can never mask new skills. +_SNAPSHOT_SCHEMA = 2 + @dataclass(frozen=True) class SkillEntry: @@ -244,30 +249,78 @@ def _extract_first_heading(content: str) -> Optional[str]: return line[2:].strip() return None + def _compute_source_signature(self) -> str: + """Fingerprint the scanned skill sources so a stale snapshot is detected. + + Covers add / remove / edit of any SKILL.md across the package builtin_skills + dir, the user skills_dir, and the MCP-bridged dir — the exact sources + ``_scan_skills_dir`` reads — using only ``stat()`` (no file reads), so it + stays cheap on the L2 hot path. Without it, an ``.skills_index.json`` + written while a source dir was empty (e.g. before builtin skills shipped, + or before a package upgrade) is trusted forever and silently hides every + skill added since. + """ + parts: List[str] = [] + for label, directory in ( + ("builtin", _BUILTIN_SKILLS_DIR), + ("user", self._skills_dir), + ("mcp", self._skills_dir / "_mcp_skills"), + ): + if not directory.exists(): + continue + for skill_dir in sorted(directory.iterdir()): + skill_md = skill_dir / "SKILL.md" + try: + st = skill_md.stat() + except OSError: + continue + parts.append(f"{label}/{skill_dir.name}:{st.st_mtime_ns}:{st.st_size}") + digest = hashlib.sha256("\n".join(parts).encode("utf-8")).hexdigest() + return f"{_SNAPSHOT_SCHEMA}:{digest}" + def _load_from_snapshot(self) -> Optional[List[SkillEntry]]: - """L2: Load from disk snapshot.""" + """L2: Load from disk snapshot, rejecting a legacy or stale one. + + Returning ``None`` forces an L3 rescan that rewrites a fresh, signed + snapshot, so this path is self-healing across package upgrades and after + skills are added, edited, or removed. + """ if not self._snapshot_path.exists(): return None try: data = json.loads(self._snapshot_path.read_text(encoding="utf-8")) - # Convert list[str] back to tuple for frozen dataclass + except Exception as exc: + logger.debug("skill_index.snapshot_load_failed path=%s error=%s", self._snapshot_path, exc) + return None + # Legacy bare-list snapshots carry no signature — treat as stale and rescan. + if not isinstance(data, dict) or data.get("schema") != _SNAPSHOT_SCHEMA: + logger.info("skill_index.snapshot_legacy_or_schema_drift; rescanning path=%s", self._snapshot_path) + return None + if data.get("signature") != self._compute_source_signature(): + logger.info("skill_index.snapshot_stale (skill sources changed); rescanning path=%s", self._snapshot_path) + return None + try: entries: List[SkillEntry] = [] - for raw in data: + for raw in data.get("entries", []): raw["tags"] = tuple(raw.get("tags", ())) raw["requires_tools"] = tuple(raw.get("requires_tools", ())) raw["platforms"] = tuple(raw.get("platforms", ())) entries.append(SkillEntry(**raw)) return entries except Exception as exc: - logger.debug("skill_index.snapshot_load_failed path=%s error=%s", self._snapshot_path, exc) + logger.debug("skill_index.snapshot_parse_failed path=%s error=%s", self._snapshot_path, exc) return None def _save_snapshot(self, entries: List[SkillEntry]) -> None: - """Save snapshot for L2 cache.""" + """Save a signed snapshot for the L2 cache.""" try: self._skills_dir.mkdir(parents=True, exist_ok=True) - data = [dataclasses.asdict(e) for e in entries] - self._snapshot_path.write_text(json.dumps(data, ensure_ascii=False), encoding="utf-8") + payload = { + "schema": _SNAPSHOT_SCHEMA, + "signature": self._compute_source_signature(), + "entries": [dataclasses.asdict(e) for e in entries], + } + self._snapshot_path.write_text(json.dumps(payload, ensure_ascii=False), encoding="utf-8") except Exception: pass # Non-critical — next scan will rebuild diff --git a/tests/_fixtures/cassettes/r1_conversation/cassette-model-1fa2f8d8bcfe7801.cassette.json b/tests/_fixtures/cassettes/r1_conversation/cassette-model-1fa2f8d8bcfe7801.cassette.json new file mode 100644 index 00000000..d020467a --- /dev/null +++ b/tests/_fixtures/cassettes/r1_conversation/cassette-model-1fa2f8d8bcfe7801.cassette.json @@ -0,0 +1,79 @@ +{ + "fingerprint": "1fa2f8d8bcfe7801c68976e4500788a3fe4d049666168c15b46ce6bab8984a06", + "note": "captured in seed mode", + "request": { + "model": "cassette-model", + "stream": false, + "messages": [ + { + "role": "system", + "content": "You are LeapFlow, an intelligent assistant that can both converse naturally and take real actions on the user's computer.\n\n## Capabilities\nThe tool index below lists **every** registered tool by name and a one-line summary — this is the complete\ncapability contract; nothing else exists. Only a subset is directly callable this turn (via native tool calling,\nnot a JSON block in your reply). If you need a tool from the index that is not yet callable, call\n`capability_expand` with its category name first — the matching tools become callable immediately after.\nWhen the user asks about LeapFlow **itself** — its identity/version/model, what tools, plugins, or capabilities\nit has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of\nself-awareness — answer from live runtime evidence via `self_describe` (facet=capabilities for \"what can you\ndo / do you support X\", identity for version/model, runtime for context/posture, or `all` for a full self-check),\nnever by reading LeapFlow's own source code, documentation, or memory. Report configuration-dependent or\nunavailable capabilities as limitations rather than inferring them.\n- **text_search**(text, pattern): Search for a regex pattern in text.\n- **text_replace**(text, old, new, count) [capability_expand category: write]: Replace occurrences of a substring in text.\n- **time_get**(): Get current date and time.\n- **env_info**(): Get system environment information (OS, Python version, cwd).\n- **skills_list**(query, category, source): List available learned skills. Use when user asks about capabilities or you need a specific skill.\n- **skill_view**(name): View the full content of a specific skill document.\n- **skill_list_files**(name) [capability_expand category: skills]: List all support files in a skill directory (configs, templates, examples). Use before skill_file_read to discover available files.\n- **skill_file_read**(name, file_path) [capability_expand category: skills]: Read an individual support file from a skill directory. Use skill_list_files first to discover available files.\n- **code_intel**(path, operation): Precise document symbols (outline) for a source file: classes, functions, and methods with line ranges. Python uses an exact AST parse; other languages use a keyword-prefix scan. Prefer over file_read mode=symbols for accurate navigation before editing. Read-only.\n- **repo_map**(path): Compact project orientation for a repository root: languages, detected test/lint commands, top-level structure, entry points, manifest, and VCS branch. Call this first when entering an unfamiliar codebase. Read-only.\n- **scm_sync**(action, cwd, remote, pull_ref, push_ref, timeout) [capability_expand category: scm]: Run a typed git SCM action. Use this instead of shell_run for git pull/push/status. For 'pull origin main then push', set action='pull_then_push', remote='origin', pull_ref='main', and omit push_ref so LeapFlow pushes the current local branch.\n- **git_query**(action, cwd, ref, path, staged, max_count, stat): Read-only structured git inspection: action=diff|log|status|branch|show. Prefer over shell_run for reading repo state — output is clipped, redacted, and log/branch are parsed into structured fields. Use scm_sync for pull/push.\n- **git_write**(action, cwd, message, stage_all, name, ref, create) [capability_expand category: scm]: Mutating git actions: action=commit (message, stage_all), branch (create+switch), checkout (switch; create=true for -b). Approval-gated. Use scm_sync for pull/push and git_query for reads.\n- **test_run**(command, cwd, timeout): Run the project's test suite and return structured results (framework, passed/failed counts, failing tests). Auto-detects the runner (pytest/npm/go/cargo) or uses a configured/explicit command; executes via the governed shell. ok=true means the runner executed — see 'success' for pass/fail.\n- **lint_check**(command, cwd, timeout): Run the project's linter and return a structured clean/issue result. Auto-detects the linter (ruff/eslint/go vet/clippy) or uses a configured/explicit command; executes via the governed shell. ok=true means the linter ran — see 'clean'.\n- **file_list**(path, pattern, depth): List files and directories at a given path. Use depth=1 or depth=2 to get a recursive tree in one call instead of listing each sub-directory separately.\n- **file_read**(path, max_lines, start_line, max_chars, mode): Read text file content with adaptive context governance. For large or unfamiliar files, prefer mode='outline' or mode='symbols' first, then use mode='raw' with start_line/max_lines for the specific range you actually need. For LeapFlow's own settings, use config_list / config_get / config_set — its config files are outside the workspace and not readable here.\n- **file_write**(path, content, mode) [capability_expand category: write]: Write content to a file (overwrite or append).\n- **code_search**(pattern, patterns, path, glob, ignore_case, multiline, max_results, context_lines): Search file CONTENTS by regex pattern across a directory tree (ripgrep-backed). Requires a regex pattern. NOT for listing or browsing directory contents — use file_list for that. Prefer this over shell_run grep: faster, skips VCS/dependency/build dirs, and returns structured path:line:column matches. Batch related lookups into ONE call via `patterns` (OR-combined, single pass) instead of issuing several separate searches. Use file_read for the surrounding context of a hit.\n- **file_find**(glob, path, max_results): Find files by a recursive glob pattern under a base path (e.g. '**/test_*.py' or '*.md'). Prefer this over shell_run find; skips VCS/dependency/build dirs.\n- **edit_file**(path, edits, dry_run, diff) [capability_expand category: write]: Apply targeted, anchored search-replace edits to an EXISTING text file (use file_write to create/overwrite). Each edit is {original_text, new_text, replace_all?}; original_text must match exactly and uniquely (or set replace_all) — a non-unique or missing anchor is rejected so files are never corrupted. Set dry_run to preview. Alternatively pass a unified 'diff' to apply its hunks as anchored edits. Far cheaper and safer than rewriting a whole file.\n- **shell_run**(command, cwd, timeout) [capability_expand category: shell]: Execute a one-shot shell command with timeout protection. Runs in the active workspace; paths resolving outside it are refused. Reach for a structured tool first when one fits — web_fetch for anything over HTTP(S), git_query/scm_sync for git, code_search/file_find/file_read for the repo, config_get/config_set for LeapFlow's own settings — because those report typed results, while a failed shell command can only be diagnosed from its exit code and stderr. Every shell run counts as an external side effect, so a failure stops the rest of the batch and is not retried automatically.\n- **terminal_open**(command, cwd, shell) [capability_expand category: terminal]: Open a PERSISTENT shell session (REPL/dev server/watch), returning a session_id for terminal_send/read/close. Disabled unless tools.terminal_session_enabled is set. For one-shot commands use shell_run instead.\n- **terminal_send**(session_id, input, wait) [capability_expand category: terminal]: Send a line of input to a persistent terminal session and return output captured shortly after.\n- **terminal_read**(session_id, wait): Drain buffered output from a persistent terminal session (optionally waiting briefly first).\n- **terminal_close**(session_id) [capability_expand category: terminal]: Terminate a persistent terminal session and release its process group.\n- **terminal_list**(): List active persistent terminal sessions.\n- **config_list**(category, limit): List LeapFlow's own writable settings (LLM model and endpoint, daemon, memory, perception, gateway, …) with current values. Use this to discover the exact key before changing anything. Optionally narrow by `category`. This is the only correct way to inspect LeapFlow configuration — never read config files from disk.\n- **config_get**(key): Read one LeapFlow setting by key (e.g. 'llm.model', 'llm.base_url', 'daemon.log_level'), returning its current value, type, scopes, and whether a change needs a daemon restart. There is no 'llm.provider' key: provider behavior is inferred from the OpenAI-compatible 'llm.base_url'. Never read LeapFlow config files from disk — use this.\n- **config_set**(key, value, scope) [capability_expand category: config]: Change one LeapFlow setting by key, e.g. switch an OpenAI-compatible LLM with key='llm.model' and key='llm.base_url'. There is no 'llm.provider' key: provider behavior is inferred from the endpoint. Values are validated and coerced; credentials are stored in the vault automatically. Call config_list or config_get first if unsure of the key. The result states whether a `leap daemon restart` is required. Never edit LeapFlow config files directly.\n- **web_fetch**(url, select, timeout, max_bytes): Read a URL over HTTP(S) and get back extracted, context-sized content: parsed JSON for API endpoints, readable text plus links for web pages. Use this for anything on the internet — prices, docs, releases, articles — instead of running curl through shell_run: it reports real HTTP status codes, retries rate limits on its own, and is a plain read so a retry is always safe. For JSON APIs pass `select` with a dotted path (e.g. 'chart.result.0.meta') to return just that part instead of the whole payload.\n- **memory_search**(query, limit): Search agent memory for relevant past experiences, observations, and facts.\n- **memory_add**(content, kind) [capability_expand category: write]: Store a new observation or insight in memory for future reference.\n- **research_note**(kind, text): Record a compact, structured note about the current task's state so it survives context compression on long / multi-step tasks. Use for durable findings, open questions still to resolve, decisions / excluded paths, and the immediate next step. One concise sentence per note.\n- **capability_expand**(category): Fetch the full callable schema for every tool in a capability category (e.g. 'hub', 'gateway', 'desktop', 'delegate', 'file', 'memory', 'skill'). The compact tool index always lists every registered tool by name and a one-line summary, but only a static low-risk subset is directly callable each turn. If you need a tool from the index that is not yet callable, call capability_expand with its category first; the matching tools become callable in this turn. Never invent a tool name — expand the category instead.\n- **delegate_task**(goal, context) [capability_expand category: delegate]: Delegate a complex sub-task to an isolated subagent. The subagent gets a fresh context and restricted tool access. Use when a task is self-contained and can be solved independently.\n- **schedule_reentry**(kind, reason, delay_seconds, event_match, max_reentries, deadline_seconds): Register a re-entry so this task can resume later from its current orientation (findings / open questions / next step). Use when work must pause and continue after a delay (kind=time) or when a matching platform event arrives (kind=event), instead of finishing now. The research-ledger state is carried over automatically.\n- **hub_push**(skill_name, visibility, version) [capability_expand category: hub]: Push a local skill to the ModelScope Hub for sharing or backup.\n- **hub_pull**(repo_id, version) [capability_expand category: hub]: Pull a skill from the ModelScope Hub to install locally.\n- **hub_search**(query) [capability_expand category: hub]: Search for skills on the Hub by keyword or description.\n- **hub_sync**(mode, dry_run) [capability_expand category: hub]: Preview or execute sync between local skills and Hub.\n- **platform_action**(platform, action, payload, backend_kind) [capability_expand category: gateway]: Execute an exact registered business action on an external platform through LeapFlow's App Connector layer. Actions must be copied from the App Connector Capability Index and are addressed as domain.operation, e.g. im.send_message or docs.create_markdown. All business fields (chat_id, text, query, etc.) MUST be placed inside `payload`, never at the top level. Example: {\"platform\":\"feishu\",\"action\":\"im.send_message\",\"payload\":{\"chat_id\":\"oc_xxx\",\"text\":\"hello\"}}. Do not invent action names, do not use management actions such as list/guide/connect/status here.\n- **platform_connect**(action, platform, credentials, options, checkpoint) [capability_expand category: gateway]: List, guide, connect, disconnect, remove, or check status for external platforms using the App Connector management namespace. Supports REST and CLI backends. Use this for management actions such as list/guide/preflight/connect/status; use platform_action only for exact registered business actions.\n- **gateway_send**(platform, chat_id, text, thread_id) [capability_expand category: gateway]: Send a message to a connected external platform (Feishu group, Telegram chat, DingTalk conversation, etc.). Requires the platform to be connected via gateway_connect first. Use gateway_connect with action='list' to see connected platforms and available chat IDs.\n- **gateway_connect**(action, platform, credentials, options) [capability_expand category: gateway]: Connect, configure, or manage external platform integrations (Feishu, DingTalk, Telegram, Slack, Discord, etc.). Conversational flow: 1) call 'guide' to get setup steps + required fields, 2) present the steps to the user and ask for ALL required credentials in a single message, 3) call 'connect' with the credentials. Goal: complete in 1–2 user turns. NEVER include credential values in your text response.\n- **plugin_list**(): List the live plugin registry across Tool/Gateway/LLM subsystems for plugin management: which plugins are loaded, their category and fiber lifecycle state. For questions about LeapFlow's own capabilities or whether it supports X, use self_describe instead.\n- **plugin_status**(plugin_id): Get detailed status of a specific plugin: its declared category, runtime dependencies, contributed tools, and fiber lifecycle state.\n- **plugin_versions**(plugin_id): List recorded source versions and active pointer for a profile-scoped plugin.\n- **plugin_propose**(requested_capability, plugin_id, proposed_tools, test_cases, risk_level, evidence): Create a side-effect-free PluginProposal from explicit capability-gap evidence. Use this before plugin_generate when a missing capability should be reviewed. Does not call an LLM, write files, or install anything.\n- **assess_compatibility**(manifest, source_path) [capability_expand category: plugin_management]: Assess whether a foreign plugin manifest or real DSH source bundle is compatible with LeapFlow. A source bundle assessment is static: runtime_ready stays false until restricted Node discovery during plugin_install. Returns component-level verdicts and limitations.\n- **plugin_generate**(plugin_id, description, proposal_id) [capability_expand category: system]: Generate a new ToolPlugin from a natural-language capability description. The LLM produces code that conforms to the ToolPlugin Protocol; it is then rigorously validated (syntax, structure, import, protocol conformance). The generated source is stored in CAS and receives explicit content approval. It DOES NOT install the plugin — installation requires a second, mutation-specific approval via plugin_install.\n- **plugin_install**(plugin_id, code, marketplace_name, source_path, proposal_id, version_label) [capability_expand category: system]: Install a plugin from exactly one source: validated Python code, the configured Python marketplace, or a real DSH source bundle. DSH bundles run restricted Node discovery before registration; only public host tools are installed and client UI limitations are reported. Writes profile-scoped state and mutates the process-global registry. REQUIRES APPROVAL.\n- **plugin_rollback**(plugin_id, version) [capability_expand category: system]: Rollback a profile-scoped plugin to a recorded source version and reload it. REQUIRES APPROVAL.\n- **plugin_reload**(plugin_id, version_label) [capability_expand category: system]: Hot-reload a plugin at runtime. Disposes the old plugin fiber, re-imports its module, and registers a fresh instance. Existing in-flight turns are unaffected (snapshot isolation). REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_disable**(plugin_id) [capability_expand category: system]: Disable a plugin by disposing its fiber, removing its tools from the runtime registry. Cannot disable self_management itself. REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_remove**(plugin_id, delete_source) [capability_expand category: system]: Terminally remove a plugin: dispose its fiber, unregister its tools, remove reload metadata, and optionally delete its profile-scoped source file. Cannot remove self_management itself. REQUIRES APPROVAL.\n- **plugin_unquarantine**(plugin_id) [capability_expand category: plugin_management]: Restore a quarantined plugin to probation status for re-evaluation. Unfreezes the trust ledger, transitions the proposal back to PROBATION, and reloads the plugin. REQUIRES APPROVAL.\n- **plugin_enable**(plugin_id) [capability_expand category: system]: Re-enable a previously disabled plugin by reloading its module and registering a fresh instance. REQUIRES APPROVAL.\n- **self_describe**(facet): Answer questions about LeapFlow ITSELF from live runtime evidence — its identity/version/model, what tools and plugins it has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of self-awareness. Prefer this over reading LeapFlow's own source code. Pick facet=capabilities for 'what can you do / do you support X', identity for version/model, runtime for context/posture, or all for a full self-check when explicitly requested.\n- **runtime_snapshot**(): Lightweight (~150 token) flat snapshot of current runtime state: model, context budget, posture, disclosure level, turn count, cache hit rate, uptime, tool count, and pending approvals.\n- **schedule_create**(trigger_expression, instruction, execution_mode, max_retries, delivery_target) [capability_expand category: scheduler]: Create a new scheduled task. Specify a trigger expression (e.g. '30m', 'every 2h', '0 9 * * *') and the instruction to execute on each trigger. Optionally provide an execution mode and a delivery target to receive notifications.\n- **schedule_list**() [capability_expand category: scheduler]: List all scheduled tasks with their current state, trigger, and next due time.\n- **schedule_status**(task_id) [capability_expand category: scheduler]: Get detailed status and recent execution history for a scheduled task.\n- **schedule_pause**(task_id) [capability_expand category: scheduler]: Pause a scheduled task so it stops firing without being cancelled.\n- **schedule_resume**(task_id) [capability_expand category: scheduler]: Resume a paused scheduled task — re-arms it and recalculates next due time.\n- **schedule_cancel**(task_id) [capability_expand category: scheduler]: Cancel a scheduled task permanently.\n- **tool_search**(query, max_results): Search all registered tools by keyword relevance. Returns a ranked list of matching tools with name, category, and summary. Use this when you need a tool but are unsure of its exact name or category.\n- **tool_describe**(tool_name): Get the full callable schema of a single tool by exact name. Returns the tool's description, parameters, and metadata. Use after tool_search to inspect a specific tool before calling it.\n- **session_search**(query, limit) [capability_expand category: unclassified]: Search past conversation sessions in the current workspace for relevant context.\n- **session_list**(limit): List recent conversation sessions in the current workspace with stable ids, titles, dates, and summaries. Use for browsing past tasks or when user asks to see history without specific search terms. No keywords needed — returns chronological list.\n- **session_detail**(session_id, limit, offset, include_inactive) [capability_expand category: unclassified]: Read a paginated persisted transcript for one past conversation session in the current workspace. Use after session_list or session_search returns a session_id.\n- **click**(element_index) [capability_expand category: desktop]: Click a UI element by its element_index (from the latest observe_ui snapshot)\n- **get_clipboard**() [capability_expand category: desktop]: Read current clipboard text content\n- **list_apps**(filter, running_only) [capability_expand category: desktop]: List available applications on this system. Use to discover correct bundle_id before switch_app.\n- **list_windows**() [capability_expand category: desktop]: List all top-level windows with pid, window_id, title, and per-window state (minimized, on-screen). Call this first to pick the pid and window_id that observe_ui and other window tools require.\n- **observe_ui**(pid, window_id, query) [capability_expand category: desktop]: Snapshot one window's actionable UI elements, each tagged with an element_index for click/right_click/read_text. Re-observe after actions — indices belong to one snapshot. Requires the window's pid and window_id from list_windows.\n- **open_url**(url, app_id) [capability_expand category: desktop]: Open a URL in the default or specified browser\n- **read_text**(element_index) [capability_expand category: desktop]: Read the text content of a specific UI element from the latest snapshot\n- **right_click**(element_index) [capability_expand category: desktop]: Right-click a UI element to open its context menu. Returns visible menu items.\n- **screenshot**(pid, window_id) [capability_expand category: desktop]: Capture a screenshot for visual verification. With pid + window_id captures that window (works across all displays); defaults to the last observed window, or the full desktop when no window has been observed.\n- **scroll**(element_index, direction, amount, pid, window_id) [capability_expand category: desktop]: Scroll a scrollable area of a window. Omit element_index to scroll the window's focused/page scroller; pass one to scroll an exact element from the latest snapshot.\n- **select_text**(element_index) [capability_expand category: desktop]: Select all text in a UI element (focus + select-all, for subsequent copy)\n- **set_clipboard**(text) [capability_expand category: desktop]: Write text to the clipboard\n- **shortcut**(keys) [capability_expand category: desktop]: Execute a keyboard shortcut\n- **switch_app**(app_id) [capability_expand category: desktop]: Switch to an app (launch if needed, activate, verify)\n- **type_text**(text) [capability_expand category: desktop]: Type text into the currently focused element\n- **wait**(seconds) [capability_expand category: desktop]: Wait for a specified duration before continuing\n- **wait_until**(condition, pid, window_id, timeout, poll_interval) [capability_expand category: desktop]: Wait until a UI condition is met (polls UI tree). Returns elements when found or on timeout.\n- **wait_until_stable**(timeout, poll_interval, pid, window_id) [capability_expand category: desktop]: Wait until the UI stops changing (element set stabilizes across polls).\n\n## App Connector Capability Index\nLeapFlow can onboard and manage external apps through `platform_connect` and execute exact registered business actions through `platform_action`.\nFor requests about connecting, setting up, configuring, enabling, or managing a supported app, use `platform_connect` first instead of generating SDK/Webhook sample code.\n`platform_connect.action` is the management namespace: list, guide, preflight, connect, disconnect, remove, status, events_start, events_stop, events_status.\n`platform_action.action` is only for exact registered platform business actions listed below, such as `im.send_message`; never use management actions like `list` or `guide` there.\nAll business fields MUST go inside `payload`; top-level keys are only `platform`, `action`, and `payload`.\nDo not invent platform IDs or platform action names. If the needed action is not listed, ask for discovery/clarification instead of guessing.\nUse `platform_connect` with `action='guide'` and the matching `platform` to start onboarding; use `action='list'` when the app is unclear.\nWhen a pending onboarding state is present, continue from that state with `platform_connect` instead of asking the user to restate the app.\nSupported apps:\n- `api_server`: API Server (OpenAI Compatible) (category=api; backend=adapter; platform_action actions=none registered)\n- `dingtalk`: 钉钉 (DingTalk) (category=im; backend=adapter; platform_action actions=none registered)\n- `feishu`: 飞书 (Feishu/Lark) (category=im; backend=cli)\n - `calendar.create_event` payload={summary*, start_time*, end_time*, attendees} [write/high]\n - `docs.create_markdown` payload={title*, markdown*, folder_token} [write/medium]\n - `drive.search` payload={query*, limit} [read/medium]\n - `im.add_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.download_resource` payload={message_id*, file_key*, type*} [read/low]\n - `im.get_messages` payload={message_ids*} [read/low]\n - `im.list_chats` payload={page_size} [read/low]\n - `im.list_messages` payload={chat_id*} [read/low]\n - `im.list_thread_messages` payload={thread*} [read/low]\n - `im.remove_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.reply_message` payload={message_id*, text*} [send/high]\n - `im.search_chats` payload={query*, page_size} [read/low]\n - `im.search_messages` payload={query*} [read/medium]\n - `im.send_message` payload={chat_id*, text*, thread_id} [send/high]\n - `im.update_card` payload={token*, card*} [write/medium]\n - `im.update_message` payload={message_id*, text*} [write/medium]\n - `mail.search_unread` payload={query, limit} [read/high]\n - `sheets.append_row` payload={spreadsheet_token*, sheet_id*, values*} [write/medium]\n - `task.create` payload={title*, description, due_time} [write/medium]\n- `telegram`: Telegram (category=social; backend=adapter; platform_action actions=none registered)\n- `webhook`: Webhook (Generic) (category=webhook; backend=adapter; platform_action actions=none registered)\n\n## Tool Usage\nTools are normally invoked through the native function-calling mechanism, not by writing JSON in your reply\ntext. Only if the provider signals that native function calling is unavailable for this turn, fall back to a\nsingle JSON code block: `{\"name\": \"tool_name\", \"arguments\": {\"key\": \"value\"}}` — use this fallback format\nonly, never both. Only call a tool whose exact name appears in the tool index above and is currently callable\n(or reachable via `capability_expand`). Never invent, rename, alias, or guess a tool name, platform ID, or\nplatform action from argument shape or wording — if the index does not list it, it does not exist.\n`platform_connect.action` (list/guide/preflight/connect/disconnect/remove/status/events_start/events_stop/\nevents_status) is the App Connector management namespace; `platform_action.action` only accepts exact\nregistered business actions such as `im.send_message` shown in the App Connector Capability Index — never mix\nthe two namespaces. If a tool call returns an unknown/unavailable result, use the returned suggestions or\navailable names for a single retry instead of trying further variations of the same guess.\n\n**Side-effect action rule** (`platform_action` with effect=send/write/execute):\n- Call each unique action+payload **exactly once**. Never include duplicates in the same turn.\n- Once the result returns `\"completed\": true`, that action is DONE for this task. Do NOT call it again\n in any subsequent turn — immediately summarize the result for the user instead.\n- The system enforces idempotency: duplicate calls are blocked and will not execute.\n- If the user explicitly requests sending/writing multiple times, use distinct payloads per call.\n\n**Resource identifier provenance rule**:\n- NEVER fabricate, guess, or infer resource identifiers (chat_id, message_id, file_key, user_id, etc.).\n Every resource ID used in a side-effect action MUST come from a successful API response in this session.\n- If a read/list action fails (e.g. authorization error), you do NOT have valid resource IDs.\n Report the failure to the user — do NOT attempt the dependent write/send action with a guessed ID.\n- When a tool result contains `\"llm_instruction\"`, follow it exactly.\n\n## Guidelines\n1. **Direct answers first**: If you already know the answer, respond directly without tools.\n2. **Avoid redundant tool calls**: Do not call the same tool with the same arguments more than once in the same user turn. When an existing tool result already answers the user's request, stop calling tools and answer directly.\n3. **Use tools proactively**: When the user asks about files, time, system state, or needs actions performed, use the appropriate tool.\n4. **Chain tools when needed**: You can call multiple tools in sequence (e.g., list files → read file → summarize).\n5. **Handle failures gracefully**: If a tool fails, explain what went wrong and suggest alternatives. If it failed because of an unknown tool/platform/action name, retry once with an exact name from the returned suggestions, then explain rather than keep guessing.\n6. **Summarize results naturally**: After tool execution, synthesize the results into a helpful answer rather than dumping raw output.\n7. **Stay conversational**: Maintain a natural, helpful tone. Acknowledge context from earlier in the conversation.\n8. **Recall past work**:\n - Broad queries (\"之前做了什么\", \"列出任务\"): answer from the \"Recent Task History\" section already in your context. If insufficient, call session_list.\n - Specific lookups (\"上次那个配置怎么改的\"): call session_search with relevant phrases (NOT single characters).\n - Do NOT call search tools repeatedly with keyword fragments. One well-phrased call is better than ten fragmented ones.\n\n## Coding & Verification\nWhen working with code, prefer the precise built-in tools over ad-hoc shell: use `repo_map` to orient in an\nunfamiliar project, `code_search`/`file_find` to locate and `code_intel` for symbols, `edit_file` (anchored\nsearch-replace, or a unified `diff`) to change files — never rewrite a whole file to change a few lines — and\n`git_query` to inspect diffs/log. After edits, check `syntax_ok` in the result and run `test_run`/`lint_check`\nbefore declaring the work done. Batch independent read-only calls (search/read/query) into a single turn — they\nrun in parallel.\n\n## Presentation Style\n1. **Polished Markdown only**: Format user-facing answers with clean Markdown headings, short paragraphs, and concise bullets. Use tables only when they improve comparison or scanning.\n2. **Terminal-friendly layout**: Keep lines readable in a TUI; avoid dense walls of text, deeply nested lists, oversized ASCII art, or heavy visual blocks.\n3. **Elegant emphasis**: Use bold text sparingly for key terms and conclusions. Avoid excessive emojis, decorative symbols, repeated separators, or visual noise.\n4. **Theme-safe colors**: Do not emit ANSI escape codes, HTML color tags, Rich markup, or hardcoded color names. Rely on the TUI theme to render Markdown professionally.\n5. **No leaked tool protocol**: Never show tool-call JSON, internal schemas, raw observations, tool result payloads, or hidden reasoning in the final answer unless the user explicitly asks for raw/debug output. Treat any prior `{\"name\": ..., \"arguments\": ...}` blocks and `Tool result (...)` messages as internal execution context only.\n6. **Professional closure**: End with a concise conclusion or next step when helpful; avoid rambling after the useful answer is complete.\n\nWhen finished with all tool calls, respond normally without a JSON block, tool-call transcript, or process log.\n\n## Task Contract\n- Task ID: turn-2\n- Original user request: Use the file_read tool on invoice.txt and report the total.\n- Workspace root: \n- Allowed roots: \n- Treat relative project paths as relative to the workspace root; never infer `.` as the project root when a workspace root is provided.\n- Workspace boundary is enforced by tools: do not read, search, edit, or run commands against paths outside the allowed roots unless the user explicitly requests an external path and the tool/approval policy permits it.\n- LeapFlow workspace config is optional at `/.leapflow/config.yaml`; runtime config is loaded from `~/.leapflow/config/user.yaml` and `~/.leapflow/profiles//config/*.yaml`.\n- Preserve this task contract across summarization, compression, tool loops, and memory retrieval.\n" + }, + { + "role": "system", + "content": "\n## Recent Session Summary\n- [assistant] Hello from LeapFlow.\n- [user] Use the file_read tool on invoice.txt and report the total.\n" + }, + { + "role": "assistant", + "content": "Hello from LeapFlow." + }, + { + "role": "user", + "content": "Use the file_read tool on invoice.txt and report the total.\nUse the file_read tool on invoice.txt and report the total." + }, + { + "role": "assistant", + "content": "", + "tool_calls": [ + "file_read" + ] + }, + { + "role": "tool", + "content": "{\"ok\": true, \"kind\": \"file_read_evidence\", \"path\": \"\", \"lines\": 2, \"truncated\": false, \"mode\": \"raw\", \"excerpt\": \"Invoice 42\\nTotal: 128.50 USD\", \"start_line\": 1, \"end_line\": 2, \"selected_lines\": 2}", + "tool_result": true + } + ], + "tools": [ + "capability_expand", + "code_intel", + "code_search", + "config_get", + "config_list", + "env_info", + "file_find", + "file_list", + "file_read", + "git_query", + "lint_check", + "memory_search", + "plugin_list", + "plugin_propose", + "plugin_status", + "plugin_versions", + "repo_map", + "research_note", + "runtime_snapshot", + "schedule_reentry", + "self_describe", + "session_list", + "skill_view", + "skills_list", + "terminal_list", + "terminal_read", + "test_run", + "text_search", + "time_get", + "tool_describe", + "tool_search", + "web_fetch" + ] + }, + "responses": [ + { + "status": 200, + "content_type": "application/json", + "body": "{\"id\": \"chatcmpl-cassette\", \"object\": \"chat.completion\", \"model\": \"cassette-model\", \"choices\": [{\"index\": 0, \"message\": {\"role\": \"assistant\", \"content\": \"The invoice total is 128.50 USD.\"}, \"finish_reason\": \"stop\"}], \"usage\": {\"prompt_tokens\": 64, \"completion_tokens\": 16, \"total_tokens\": 80}}" + } + ] +} diff --git a/tests/_fixtures/cassettes/r1_conversation/cassette-model-22b0d11ed24a7f8d.cassette.json b/tests/_fixtures/cassettes/r1_conversation/cassette-model-22b0d11ed24a7f8d.cassette.json new file mode 100644 index 00000000..73b94fec --- /dev/null +++ b/tests/_fixtures/cassettes/r1_conversation/cassette-model-22b0d11ed24a7f8d.cassette.json @@ -0,0 +1,63 @@ +{ + "fingerprint": "22b0d11ed24a7f8d12ff3e1cff702b3ee170ed92daf95046350589f223d0f454", + "note": "captured in seed mode", + "request": { + "model": "cassette-model", + "stream": false, + "messages": [ + { + "role": "system", + "content": "You are LeapFlow, an intelligent assistant that can both converse naturally and take real actions on the user's computer.\n\n## Capabilities\nThe tool index below lists **every** registered tool by name and a one-line summary — this is the complete\ncapability contract; nothing else exists. Only a subset is directly callable this turn (via native tool calling,\nnot a JSON block in your reply). If you need a tool from the index that is not yet callable, call\n`capability_expand` with its category name first — the matching tools become callable immediately after.\nWhen the user asks about LeapFlow **itself** — its identity/version/model, what tools, plugins, or capabilities\nit has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of\nself-awareness — answer from live runtime evidence via `self_describe` (facet=capabilities for \"what can you\ndo / do you support X\", identity for version/model, runtime for context/posture, or `all` for a full self-check),\nnever by reading LeapFlow's own source code, documentation, or memory. Report configuration-dependent or\nunavailable capabilities as limitations rather than inferring them.\n- **text_search**(text, pattern): Search for a regex pattern in text.\n- **text_replace**(text, old, new, count) [capability_expand category: write]: Replace occurrences of a substring in text.\n- **time_get**(): Get current date and time.\n- **env_info**(): Get system environment information (OS, Python version, cwd).\n- **skills_list**(query, category, source): List available learned skills. Use when user asks about capabilities or you need a specific skill.\n- **skill_view**(name): View the full content of a specific skill document.\n- **skill_list_files**(name) [capability_expand category: skills]: List all support files in a skill directory (configs, templates, examples). Use before skill_file_read to discover available files.\n- **skill_file_read**(name, file_path) [capability_expand category: skills]: Read an individual support file from a skill directory. Use skill_list_files first to discover available files.\n- **code_intel**(path, operation): Precise document symbols (outline) for a source file: classes, functions, and methods with line ranges. Python uses an exact AST parse; other languages use a keyword-prefix scan. Prefer over file_read mode=symbols for accurate navigation before editing. Read-only.\n- **repo_map**(path): Compact project orientation for a repository root: languages, detected test/lint commands, top-level structure, entry points, manifest, and VCS branch. Call this first when entering an unfamiliar codebase. Read-only.\n- **scm_sync**(action, cwd, remote, pull_ref, push_ref, timeout) [capability_expand category: scm]: Run a typed git SCM action. Use this instead of shell_run for git pull/push/status. For 'pull origin main then push', set action='pull_then_push', remote='origin', pull_ref='main', and omit push_ref so LeapFlow pushes the current local branch.\n- **git_query**(action, cwd, ref, path, staged, max_count, stat): Read-only structured git inspection: action=diff|log|status|branch|show. Prefer over shell_run for reading repo state — output is clipped, redacted, and log/branch are parsed into structured fields. Use scm_sync for pull/push.\n- **git_write**(action, cwd, message, stage_all, name, ref, create) [capability_expand category: scm]: Mutating git actions: action=commit (message, stage_all), branch (create+switch), checkout (switch; create=true for -b). Approval-gated. Use scm_sync for pull/push and git_query for reads.\n- **test_run**(command, cwd, timeout): Run the project's test suite and return structured results (framework, passed/failed counts, failing tests). Auto-detects the runner (pytest/npm/go/cargo) or uses a configured/explicit command; executes via the governed shell. ok=true means the runner executed — see 'success' for pass/fail.\n- **lint_check**(command, cwd, timeout): Run the project's linter and return a structured clean/issue result. Auto-detects the linter (ruff/eslint/go vet/clippy) or uses a configured/explicit command; executes via the governed shell. ok=true means the linter ran — see 'clean'.\n- **file_list**(path, pattern, depth): List files and directories at a given path. Use depth=1 or depth=2 to get a recursive tree in one call instead of listing each sub-directory separately.\n- **file_read**(path, max_lines, start_line, max_chars, mode): Read text file content with adaptive context governance. For large or unfamiliar files, prefer mode='outline' or mode='symbols' first, then use mode='raw' with start_line/max_lines for the specific range you actually need. For LeapFlow's own settings, use config_list / config_get / config_set — its config files are outside the workspace and not readable here.\n- **file_write**(path, content, mode) [capability_expand category: write]: Write content to a file (overwrite or append).\n- **code_search**(pattern, patterns, path, glob, ignore_case, multiline, max_results, context_lines): Search file CONTENTS by regex pattern across a directory tree (ripgrep-backed). Requires a regex pattern. NOT for listing or browsing directory contents — use file_list for that. Prefer this over shell_run grep: faster, skips VCS/dependency/build dirs, and returns structured path:line:column matches. Batch related lookups into ONE call via `patterns` (OR-combined, single pass) instead of issuing several separate searches. Use file_read for the surrounding context of a hit.\n- **file_find**(glob, path, max_results): Find files by a recursive glob pattern under a base path (e.g. '**/test_*.py' or '*.md'). Prefer this over shell_run find; skips VCS/dependency/build dirs.\n- **edit_file**(path, edits, dry_run, diff) [capability_expand category: write]: Apply targeted, anchored search-replace edits to an EXISTING text file (use file_write to create/overwrite). Each edit is {original_text, new_text, replace_all?}; original_text must match exactly and uniquely (or set replace_all) — a non-unique or missing anchor is rejected so files are never corrupted. Set dry_run to preview. Alternatively pass a unified 'diff' to apply its hunks as anchored edits. Far cheaper and safer than rewriting a whole file.\n- **shell_run**(command, cwd, timeout) [capability_expand category: shell]: Execute a one-shot shell command with timeout protection. Runs in the active workspace; paths resolving outside it are refused. Reach for a structured tool first when one fits — web_fetch for anything over HTTP(S), git_query/scm_sync for git, code_search/file_find/file_read for the repo, config_get/config_set for LeapFlow's own settings — because those report typed results, while a failed shell command can only be diagnosed from its exit code and stderr. Every shell run counts as an external side effect, so a failure stops the rest of the batch and is not retried automatically.\n- **terminal_open**(command, cwd, shell) [capability_expand category: terminal]: Open a PERSISTENT shell session (REPL/dev server/watch), returning a session_id for terminal_send/read/close. Disabled unless tools.terminal_session_enabled is set. For one-shot commands use shell_run instead.\n- **terminal_send**(session_id, input, wait) [capability_expand category: terminal]: Send a line of input to a persistent terminal session and return output captured shortly after.\n- **terminal_read**(session_id, wait): Drain buffered output from a persistent terminal session (optionally waiting briefly first).\n- **terminal_close**(session_id) [capability_expand category: terminal]: Terminate a persistent terminal session and release its process group.\n- **terminal_list**(): List active persistent terminal sessions.\n- **config_list**(category, limit): List LeapFlow's own writable settings (LLM model and endpoint, daemon, memory, perception, gateway, …) with current values. Use this to discover the exact key before changing anything. Optionally narrow by `category`. This is the only correct way to inspect LeapFlow configuration — never read config files from disk.\n- **config_get**(key): Read one LeapFlow setting by key (e.g. 'llm.model', 'llm.base_url', 'daemon.log_level'), returning its current value, type, scopes, and whether a change needs a daemon restart. There is no 'llm.provider' key: provider behavior is inferred from the OpenAI-compatible 'llm.base_url'. Never read LeapFlow config files from disk — use this.\n- **config_set**(key, value, scope) [capability_expand category: config]: Change one LeapFlow setting by key, e.g. switch an OpenAI-compatible LLM with key='llm.model' and key='llm.base_url'. There is no 'llm.provider' key: provider behavior is inferred from the endpoint. Values are validated and coerced; credentials are stored in the vault automatically. Call config_list or config_get first if unsure of the key. The result states whether a `leap daemon restart` is required. Never edit LeapFlow config files directly.\n- **web_fetch**(url, select, timeout, max_bytes): Read a URL over HTTP(S) and get back extracted, context-sized content: parsed JSON for API endpoints, readable text plus links for web pages. Use this for anything on the internet — prices, docs, releases, articles — instead of running curl through shell_run: it reports real HTTP status codes, retries rate limits on its own, and is a plain read so a retry is always safe. For JSON APIs pass `select` with a dotted path (e.g. 'chart.result.0.meta') to return just that part instead of the whole payload.\n- **memory_search**(query, limit): Search agent memory for relevant past experiences, observations, and facts.\n- **memory_add**(content, kind) [capability_expand category: write]: Store a new observation or insight in memory for future reference.\n- **research_note**(kind, text): Record a compact, structured note about the current task's state so it survives context compression on long / multi-step tasks. Use for durable findings, open questions still to resolve, decisions / excluded paths, and the immediate next step. One concise sentence per note.\n- **capability_expand**(category): Fetch the full callable schema for every tool in a capability category (e.g. 'hub', 'gateway', 'desktop', 'delegate', 'file', 'memory', 'skill'). The compact tool index always lists every registered tool by name and a one-line summary, but only a static low-risk subset is directly callable each turn. If you need a tool from the index that is not yet callable, call capability_expand with its category first; the matching tools become callable in this turn. Never invent a tool name — expand the category instead.\n- **delegate_task**(goal, context) [capability_expand category: delegate]: Delegate a complex sub-task to an isolated subagent. The subagent gets a fresh context and restricted tool access. Use when a task is self-contained and can be solved independently.\n- **schedule_reentry**(kind, reason, delay_seconds, event_match, max_reentries, deadline_seconds): Register a re-entry so this task can resume later from its current orientation (findings / open questions / next step). Use when work must pause and continue after a delay (kind=time) or when a matching platform event arrives (kind=event), instead of finishing now. The research-ledger state is carried over automatically.\n- **hub_push**(skill_name, visibility, version) [capability_expand category: hub]: Push a local skill to the ModelScope Hub for sharing or backup.\n- **hub_pull**(repo_id, version) [capability_expand category: hub]: Pull a skill from the ModelScope Hub to install locally.\n- **hub_search**(query) [capability_expand category: hub]: Search for skills on the Hub by keyword or description.\n- **hub_sync**(mode, dry_run) [capability_expand category: hub]: Preview or execute sync between local skills and Hub.\n- **platform_action**(platform, action, payload, backend_kind) [capability_expand category: gateway]: Execute an exact registered business action on an external platform through LeapFlow's App Connector layer. Actions must be copied from the App Connector Capability Index and are addressed as domain.operation, e.g. im.send_message or docs.create_markdown. All business fields (chat_id, text, query, etc.) MUST be placed inside `payload`, never at the top level. Example: {\"platform\":\"feishu\",\"action\":\"im.send_message\",\"payload\":{\"chat_id\":\"oc_xxx\",\"text\":\"hello\"}}. Do not invent action names, do not use management actions such as list/guide/connect/status here.\n- **platform_connect**(action, platform, credentials, options, checkpoint) [capability_expand category: gateway]: List, guide, connect, disconnect, remove, or check status for external platforms using the App Connector management namespace. Supports REST and CLI backends. Use this for management actions such as list/guide/preflight/connect/status; use platform_action only for exact registered business actions.\n- **gateway_send**(platform, chat_id, text, thread_id) [capability_expand category: gateway]: Send a message to a connected external platform (Feishu group, Telegram chat, DingTalk conversation, etc.). Requires the platform to be connected via gateway_connect first. Use gateway_connect with action='list' to see connected platforms and available chat IDs.\n- **gateway_connect**(action, platform, credentials, options) [capability_expand category: gateway]: Connect, configure, or manage external platform integrations (Feishu, DingTalk, Telegram, Slack, Discord, etc.). Conversational flow: 1) call 'guide' to get setup steps + required fields, 2) present the steps to the user and ask for ALL required credentials in a single message, 3) call 'connect' with the credentials. Goal: complete in 1–2 user turns. NEVER include credential values in your text response.\n- **plugin_list**(): List the live plugin registry across Tool/Gateway/LLM subsystems for plugin management: which plugins are loaded, their category and fiber lifecycle state. For questions about LeapFlow's own capabilities or whether it supports X, use self_describe instead.\n- **plugin_status**(plugin_id): Get detailed status of a specific plugin: its declared category, runtime dependencies, contributed tools, and fiber lifecycle state.\n- **plugin_versions**(plugin_id): List recorded source versions and active pointer for a profile-scoped plugin.\n- **plugin_propose**(requested_capability, plugin_id, proposed_tools, test_cases, risk_level, evidence): Create a side-effect-free PluginProposal from explicit capability-gap evidence. Use this before plugin_generate when a missing capability should be reviewed. Does not call an LLM, write files, or install anything.\n- **assess_compatibility**(manifest, source_path) [capability_expand category: plugin_management]: Assess whether a foreign plugin manifest or real DSH source bundle is compatible with LeapFlow. A source bundle assessment is static: runtime_ready stays false until restricted Node discovery during plugin_install. Returns component-level verdicts and limitations.\n- **plugin_generate**(plugin_id, description, proposal_id) [capability_expand category: system]: Generate a new ToolPlugin from a natural-language capability description. The LLM produces code that conforms to the ToolPlugin Protocol; it is then rigorously validated (syntax, structure, import, protocol conformance). The generated source is stored in CAS and receives explicit content approval. It DOES NOT install the plugin — installation requires a second, mutation-specific approval via plugin_install.\n- **plugin_install**(plugin_id, code, marketplace_name, source_path, proposal_id, version_label) [capability_expand category: system]: Install a plugin from exactly one source: validated Python code, the configured Python marketplace, or a real DSH source bundle. DSH bundles run restricted Node discovery before registration; only public host tools are installed and client UI limitations are reported. Writes profile-scoped state and mutates the process-global registry. REQUIRES APPROVAL.\n- **plugin_rollback**(plugin_id, version) [capability_expand category: system]: Rollback a profile-scoped plugin to a recorded source version and reload it. REQUIRES APPROVAL.\n- **plugin_reload**(plugin_id, version_label) [capability_expand category: system]: Hot-reload a plugin at runtime. Disposes the old plugin fiber, re-imports its module, and registers a fresh instance. Existing in-flight turns are unaffected (snapshot isolation). REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_disable**(plugin_id) [capability_expand category: system]: Disable a plugin by disposing its fiber, removing its tools from the runtime registry. Cannot disable self_management itself. REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_remove**(plugin_id, delete_source) [capability_expand category: system]: Terminally remove a plugin: dispose its fiber, unregister its tools, remove reload metadata, and optionally delete its profile-scoped source file. Cannot remove self_management itself. REQUIRES APPROVAL.\n- **plugin_unquarantine**(plugin_id) [capability_expand category: plugin_management]: Restore a quarantined plugin to probation status for re-evaluation. Unfreezes the trust ledger, transitions the proposal back to PROBATION, and reloads the plugin. REQUIRES APPROVAL.\n- **plugin_enable**(plugin_id) [capability_expand category: system]: Re-enable a previously disabled plugin by reloading its module and registering a fresh instance. REQUIRES APPROVAL.\n- **self_describe**(facet): Answer questions about LeapFlow ITSELF from live runtime evidence — its identity/version/model, what tools and plugins it has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of self-awareness. Prefer this over reading LeapFlow's own source code. Pick facet=capabilities for 'what can you do / do you support X', identity for version/model, runtime for context/posture, or all for a full self-check when explicitly requested.\n- **runtime_snapshot**(): Lightweight (~150 token) flat snapshot of current runtime state: model, context budget, posture, disclosure level, turn count, cache hit rate, uptime, tool count, and pending approvals.\n- **schedule_create**(trigger_expression, instruction, execution_mode, max_retries, delivery_target) [capability_expand category: scheduler]: Create a new scheduled task. Specify a trigger expression (e.g. '30m', 'every 2h', '0 9 * * *') and the instruction to execute on each trigger. Optionally provide an execution mode and a delivery target to receive notifications.\n- **schedule_list**() [capability_expand category: scheduler]: List all scheduled tasks with their current state, trigger, and next due time.\n- **schedule_status**(task_id) [capability_expand category: scheduler]: Get detailed status and recent execution history for a scheduled task.\n- **schedule_pause**(task_id) [capability_expand category: scheduler]: Pause a scheduled task so it stops firing without being cancelled.\n- **schedule_resume**(task_id) [capability_expand category: scheduler]: Resume a paused scheduled task — re-arms it and recalculates next due time.\n- **schedule_cancel**(task_id) [capability_expand category: scheduler]: Cancel a scheduled task permanently.\n- **tool_search**(query, max_results): Search all registered tools by keyword relevance. Returns a ranked list of matching tools with name, category, and summary. Use this when you need a tool but are unsure of its exact name or category.\n- **tool_describe**(tool_name): Get the full callable schema of a single tool by exact name. Returns the tool's description, parameters, and metadata. Use after tool_search to inspect a specific tool before calling it.\n- **session_search**(query, limit) [capability_expand category: unclassified]: Search past conversation sessions in the current workspace for relevant context.\n- **session_list**(limit): List recent conversation sessions in the current workspace with stable ids, titles, dates, and summaries. Use for browsing past tasks or when user asks to see history without specific search terms. No keywords needed — returns chronological list.\n- **session_detail**(session_id, limit, offset, include_inactive) [capability_expand category: unclassified]: Read a paginated persisted transcript for one past conversation session in the current workspace. Use after session_list or session_search returns a session_id.\n- **click**(element_index) [capability_expand category: desktop]: Click a UI element by its element_index (from the latest observe_ui snapshot)\n- **get_clipboard**() [capability_expand category: desktop]: Read current clipboard text content\n- **list_apps**(filter, running_only) [capability_expand category: desktop]: List available applications on this system. Use to discover correct bundle_id before switch_app.\n- **list_windows**() [capability_expand category: desktop]: List all top-level windows with pid, window_id, title, and per-window state (minimized, on-screen). Call this first to pick the pid and window_id that observe_ui and other window tools require.\n- **observe_ui**(pid, window_id, query) [capability_expand category: desktop]: Snapshot one window's actionable UI elements, each tagged with an element_index for click/right_click/read_text. Re-observe after actions — indices belong to one snapshot. Requires the window's pid and window_id from list_windows.\n- **open_url**(url, app_id) [capability_expand category: desktop]: Open a URL in the default or specified browser\n- **read_text**(element_index) [capability_expand category: desktop]: Read the text content of a specific UI element from the latest snapshot\n- **right_click**(element_index) [capability_expand category: desktop]: Right-click a UI element to open its context menu. Returns visible menu items.\n- **screenshot**(pid, window_id) [capability_expand category: desktop]: Capture a screenshot for visual verification. With pid + window_id captures that window (works across all displays); defaults to the last observed window, or the full desktop when no window has been observed.\n- **scroll**(element_index, direction, amount, pid, window_id) [capability_expand category: desktop]: Scroll a scrollable area of a window. Omit element_index to scroll the window's focused/page scroller; pass one to scroll an exact element from the latest snapshot.\n- **select_text**(element_index) [capability_expand category: desktop]: Select all text in a UI element (focus + select-all, for subsequent copy)\n- **set_clipboard**(text) [capability_expand category: desktop]: Write text to the clipboard\n- **shortcut**(keys) [capability_expand category: desktop]: Execute a keyboard shortcut\n- **switch_app**(app_id) [capability_expand category: desktop]: Switch to an app (launch if needed, activate, verify)\n- **type_text**(text) [capability_expand category: desktop]: Type text into the currently focused element\n- **wait**(seconds) [capability_expand category: desktop]: Wait for a specified duration before continuing\n- **wait_until**(condition, pid, window_id, timeout, poll_interval) [capability_expand category: desktop]: Wait until a UI condition is met (polls UI tree). Returns elements when found or on timeout.\n- **wait_until_stable**(timeout, poll_interval, pid, window_id) [capability_expand category: desktop]: Wait until the UI stops changing (element set stabilizes across polls).\n\n## App Connector Capability Index\nLeapFlow can onboard and manage external apps through `platform_connect` and execute exact registered business actions through `platform_action`.\nFor requests about connecting, setting up, configuring, enabling, or managing a supported app, use `platform_connect` first instead of generating SDK/Webhook sample code.\n`platform_connect.action` is the management namespace: list, guide, preflight, connect, disconnect, remove, status, events_start, events_stop, events_status.\n`platform_action.action` is only for exact registered platform business actions listed below, such as `im.send_message`; never use management actions like `list` or `guide` there.\nAll business fields MUST go inside `payload`; top-level keys are only `platform`, `action`, and `payload`.\nDo not invent platform IDs or platform action names. If the needed action is not listed, ask for discovery/clarification instead of guessing.\nUse `platform_connect` with `action='guide'` and the matching `platform` to start onboarding; use `action='list'` when the app is unclear.\nWhen a pending onboarding state is present, continue from that state with `platform_connect` instead of asking the user to restate the app.\nSupported apps:\n- `api_server`: API Server (OpenAI Compatible) (category=api; backend=adapter; platform_action actions=none registered)\n- `dingtalk`: 钉钉 (DingTalk) (category=im; backend=adapter; platform_action actions=none registered)\n- `feishu`: 飞书 (Feishu/Lark) (category=im; backend=cli)\n - `calendar.create_event` payload={summary*, start_time*, end_time*, attendees} [write/high]\n - `docs.create_markdown` payload={title*, markdown*, folder_token} [write/medium]\n - `drive.search` payload={query*, limit} [read/medium]\n - `im.add_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.download_resource` payload={message_id*, file_key*, type*} [read/low]\n - `im.get_messages` payload={message_ids*} [read/low]\n - `im.list_chats` payload={page_size} [read/low]\n - `im.list_messages` payload={chat_id*} [read/low]\n - `im.list_thread_messages` payload={thread*} [read/low]\n - `im.remove_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.reply_message` payload={message_id*, text*} [send/high]\n - `im.search_chats` payload={query*, page_size} [read/low]\n - `im.search_messages` payload={query*} [read/medium]\n - `im.send_message` payload={chat_id*, text*, thread_id} [send/high]\n - `im.update_card` payload={token*, card*} [write/medium]\n - `im.update_message` payload={message_id*, text*} [write/medium]\n - `mail.search_unread` payload={query, limit} [read/high]\n - `sheets.append_row` payload={spreadsheet_token*, sheet_id*, values*} [write/medium]\n - `task.create` payload={title*, description, due_time} [write/medium]\n- `telegram`: Telegram (category=social; backend=adapter; platform_action actions=none registered)\n- `webhook`: Webhook (Generic) (category=webhook; backend=adapter; platform_action actions=none registered)\n\n## Tool Usage\nTools are normally invoked through the native function-calling mechanism, not by writing JSON in your reply\ntext. Only if the provider signals that native function calling is unavailable for this turn, fall back to a\nsingle JSON code block: `{\"name\": \"tool_name\", \"arguments\": {\"key\": \"value\"}}` — use this fallback format\nonly, never both. Only call a tool whose exact name appears in the tool index above and is currently callable\n(or reachable via `capability_expand`). Never invent, rename, alias, or guess a tool name, platform ID, or\nplatform action from argument shape or wording — if the index does not list it, it does not exist.\n`platform_connect.action` (list/guide/preflight/connect/disconnect/remove/status/events_start/events_stop/\nevents_status) is the App Connector management namespace; `platform_action.action` only accepts exact\nregistered business actions such as `im.send_message` shown in the App Connector Capability Index — never mix\nthe two namespaces. If a tool call returns an unknown/unavailable result, use the returned suggestions or\navailable names for a single retry instead of trying further variations of the same guess.\n\n**Side-effect action rule** (`platform_action` with effect=send/write/execute):\n- Call each unique action+payload **exactly once**. Never include duplicates in the same turn.\n- Once the result returns `\"completed\": true`, that action is DONE for this task. Do NOT call it again\n in any subsequent turn — immediately summarize the result for the user instead.\n- The system enforces idempotency: duplicate calls are blocked and will not execute.\n- If the user explicitly requests sending/writing multiple times, use distinct payloads per call.\n\n**Resource identifier provenance rule**:\n- NEVER fabricate, guess, or infer resource identifiers (chat_id, message_id, file_key, user_id, etc.).\n Every resource ID used in a side-effect action MUST come from a successful API response in this session.\n- If a read/list action fails (e.g. authorization error), you do NOT have valid resource IDs.\n Report the failure to the user — do NOT attempt the dependent write/send action with a guessed ID.\n- When a tool result contains `\"llm_instruction\"`, follow it exactly.\n\n## Guidelines\n1. **Direct answers first**: If you already know the answer, respond directly without tools.\n2. **Avoid redundant tool calls**: Do not call the same tool with the same arguments more than once in the same user turn. When an existing tool result already answers the user's request, stop calling tools and answer directly.\n3. **Use tools proactively**: When the user asks about files, time, system state, or needs actions performed, use the appropriate tool.\n4. **Chain tools when needed**: You can call multiple tools in sequence (e.g., list files → read file → summarize).\n5. **Handle failures gracefully**: If a tool fails, explain what went wrong and suggest alternatives. If it failed because of an unknown tool/platform/action name, retry once with an exact name from the returned suggestions, then explain rather than keep guessing.\n6. **Summarize results naturally**: After tool execution, synthesize the results into a helpful answer rather than dumping raw output.\n7. **Stay conversational**: Maintain a natural, helpful tone. Acknowledge context from earlier in the conversation.\n8. **Recall past work**:\n - Broad queries (\"之前做了什么\", \"列出任务\"): answer from the \"Recent Task History\" section already in your context. If insufficient, call session_list.\n - Specific lookups (\"上次那个配置怎么改的\"): call session_search with relevant phrases (NOT single characters).\n - Do NOT call search tools repeatedly with keyword fragments. One well-phrased call is better than ten fragmented ones.\n\n## Coding & Verification\nWhen working with code, prefer the precise built-in tools over ad-hoc shell: use `repo_map` to orient in an\nunfamiliar project, `code_search`/`file_find` to locate and `code_intel` for symbols, `edit_file` (anchored\nsearch-replace, or a unified `diff`) to change files — never rewrite a whole file to change a few lines — and\n`git_query` to inspect diffs/log. After edits, check `syntax_ok` in the result and run `test_run`/`lint_check`\nbefore declaring the work done. Batch independent read-only calls (search/read/query) into a single turn — they\nrun in parallel.\n\n## Presentation Style\n1. **Polished Markdown only**: Format user-facing answers with clean Markdown headings, short paragraphs, and concise bullets. Use tables only when they improve comparison or scanning.\n2. **Terminal-friendly layout**: Keep lines readable in a TUI; avoid dense walls of text, deeply nested lists, oversized ASCII art, or heavy visual blocks.\n3. **Elegant emphasis**: Use bold text sparingly for key terms and conclusions. Avoid excessive emojis, decorative symbols, repeated separators, or visual noise.\n4. **Theme-safe colors**: Do not emit ANSI escape codes, HTML color tags, Rich markup, or hardcoded color names. Rely on the TUI theme to render Markdown professionally.\n5. **No leaked tool protocol**: Never show tool-call JSON, internal schemas, raw observations, tool result payloads, or hidden reasoning in the final answer unless the user explicitly asks for raw/debug output. Treat any prior `{\"name\": ..., \"arguments\": ...}` blocks and `Tool result (...)` messages as internal execution context only.\n6. **Professional closure**: End with a concise conclusion or next step when helpful; avoid rambling after the useful answer is complete.\n\nWhen finished with all tool calls, respond normally without a JSON block, tool-call transcript, or process log.\n\n## Task Contract\n- Task ID: turn-1\n- Original user request: Say hello.\n- Workspace root: \n- Allowed roots: \n- Treat relative project paths as relative to the workspace root; never infer `.` as the project root when a workspace root is provided.\n- Workspace boundary is enforced by tools: do not read, search, edit, or run commands against paths outside the allowed roots unless the user explicitly requests an external path and the tool/approval policy permits it.\n- LeapFlow workspace config is optional at `/.leapflow/config.yaml`; runtime config is loaded from `~/.leapflow/config/user.yaml` and `~/.leapflow/profiles//config/*.yaml`.\n- Preserve this task contract across summarization, compression, tool loops, and memory retrieval.\n" + }, + { + "role": "system", + "content": "\n## Recent Session Summary\n- [user] Say hello.\n" + }, + { + "role": "user", + "content": "Say hello.\nSay hello." + } + ], + "tools": [ + "capability_expand", + "code_intel", + "code_search", + "config_get", + "config_list", + "env_info", + "file_find", + "file_list", + "file_read", + "git_query", + "lint_check", + "memory_search", + "plugin_list", + "plugin_propose", + "plugin_status", + "plugin_versions", + "repo_map", + "research_note", + "runtime_snapshot", + "schedule_reentry", + "self_describe", + "session_list", + "skill_view", + "skills_list", + "terminal_list", + "terminal_read", + "test_run", + "text_search", + "time_get", + "tool_describe", + "tool_search", + "web_fetch" + ] + }, + "responses": [ + { + "status": 200, + "content_type": "application/json", + "body": "{\"id\": \"chatcmpl-cassette\", \"object\": \"chat.completion\", \"model\": \"cassette-model\", \"choices\": [{\"index\": 0, \"message\": {\"role\": \"assistant\", \"content\": \"Hello from LeapFlow.\"}, \"finish_reason\": \"stop\"}], \"usage\": {\"prompt_tokens\": 64, \"completion_tokens\": 16, \"total_tokens\": 80}}" + } + ] +} diff --git a/tests/_fixtures/cassettes/r1_conversation/cassette-model-6b1f4760e34b3ba7.cassette.json b/tests/_fixtures/cassettes/r1_conversation/cassette-model-6b1f4760e34b3ba7.cassette.json deleted file mode 100644 index 1f081484..00000000 --- a/tests/_fixtures/cassettes/r1_conversation/cassette-model-6b1f4760e34b3ba7.cassette.json +++ /dev/null @@ -1,67 +0,0 @@ -{ - "fingerprint": "6b1f4760e34b3ba7c180f525fb44269b37ac4352ea444766162056db0f74ac80", - "note": "captured in seed mode", - "request": { - "model": "cassette-model", - "stream": false, - "messages": [ - { - "role": "system", - "content": "You are LeapFlow, an intelligent assistant that can both converse naturally and take real actions on the user's computer.\n\n## Capabilities\nThe tool index below lists **every** registered tool by name and a one-line summary — this is the complete\ncapability contract; nothing else exists. Only a subset is directly callable this turn (via native tool calling,\nnot a JSON block in your reply). If you need a tool from the index that is not yet callable, call\n`capability_expand` with its category name first — the matching tools become callable immediately after.\nWhen the user asks what LeapFlow itself supports, whether it supports plugins, or which runtime capabilities\nare available, use the live capability evidence exposed by `plugin_list` before making capability claims; report\nconfiguration-dependent or unavailable capabilities as limitations instead of inferring from documentation.\n- **text_search**(text, pattern): Search for a regex pattern in text.\n- **text_replace**(text, old, new, count) [capability_expand category: write]: Replace occurrences of a substring in text.\n- **time_get**(): Get current date and time.\n- **env_info**(): Get system environment information (OS, Python version, cwd).\n- **skills_list**(query, category, source): List available learned skills. Use when user asks about capabilities or you need a specific skill.\n- **skill_view**(name): View the full content of a specific skill document.\n- **code_intel**(path, operation): Precise document symbols (outline) for a source file: classes, functions, and methods with line ranges. Python uses an exact AST parse; other languages use a keyword-prefix scan. Prefer over file_read mode=symbols for accurate navigation before editing. Read-only.\n- **repo_map**(path): Compact project orientation for a repository root: languages, detected test/lint commands, top-level structure, entry points, manifest, and VCS branch. Call this first when entering an unfamiliar codebase. Read-only.\n- **scm_sync**(action, cwd, remote, pull_ref, push_ref, timeout) [capability_expand category: scm]: Run a typed git SCM action. Use this instead of shell_run for git pull/push/status. For 'pull origin main then push', set action='pull_then_push', remote='origin', pull_ref='main', and omit push_ref so LeapFlow pushes the current local branch.\n- **git_query**(action, cwd, ref, path, staged, max_count, stat): Read-only structured git inspection: action=diff|log|status|branch|show. Prefer over shell_run for reading repo state — output is clipped, redacted, and log/branch are parsed into structured fields. Use scm_sync for pull/push.\n- **git_write**(action, cwd, message, stage_all, name, ref, create) [capability_expand category: scm]: Mutating git actions: action=commit (message, stage_all), branch (create+switch), checkout (switch; create=true for -b). Approval-gated. Use scm_sync for pull/push and git_query for reads.\n- **test_run**(command, cwd, timeout): Run the project's test suite and return structured results (framework, passed/failed counts, failing tests). Auto-detects the runner (pytest/npm/go/cargo) or uses a configured/explicit command; executes via the governed shell. ok=true means the runner executed — see 'success' for pass/fail.\n- **lint_check**(command, cwd, timeout): Run the project's linter and return a structured clean/issue result. Auto-detects the linter (ruff/eslint/go vet/clippy) or uses a configured/explicit command; executes via the governed shell. ok=true means the linter ran — see 'clean'.\n- **file_list**(path, pattern, depth): List files and directories at a given path. Use depth=1 or depth=2 to get a recursive tree in one call instead of listing each sub-directory separately.\n- **file_read**(path, max_lines, start_line, max_chars, mode): Read text file content with adaptive context governance. For large or unfamiliar files, prefer mode='outline' or mode='symbols' first, then use mode='raw' with start_line/max_lines for the specific range you actually need. For LeapFlow's own settings, use config_list / config_get / config_set — its config files are outside the workspace and not readable here.\n- **file_write**(path, content, mode) [capability_expand category: write]: Write content to a file (overwrite or append).\n- **code_search**(pattern, patterns, path, glob, ignore_case, multiline, max_results, context_lines): Search file CONTENTS by regex pattern across a directory tree (ripgrep-backed). Requires a regex pattern. NOT for listing or browsing directory contents — use file_list for that. Prefer this over shell_run grep: faster, skips VCS/dependency/build dirs, and returns structured path:line:column matches. Batch related lookups into ONE call via `patterns` (OR-combined, single pass) instead of issuing several separate searches. Use file_read for the surrounding context of a hit.\n- **file_find**(glob, path, max_results): Find files by a recursive glob pattern under a base path (e.g. '**/test_*.py' or '*.md'). Prefer this over shell_run find; skips VCS/dependency/build dirs.\n- **edit_file**(path, edits, dry_run, diff) [capability_expand category: write]: Apply targeted, anchored search-replace edits to an EXISTING text file (use file_write to create/overwrite). Each edit is {original_text, new_text, replace_all?}; original_text must match exactly and uniquely (or set replace_all) — a non-unique or missing anchor is rejected so files are never corrupted. Set dry_run to preview. Alternatively pass a unified 'diff' to apply its hunks as anchored edits. Far cheaper and safer than rewriting a whole file.\n- **shell_run**(command, cwd, timeout) [capability_expand category: shell]: Execute a one-shot shell command with timeout protection. Runs in the active workspace; paths resolving outside it are refused. Reach for a structured tool first when one fits — web_fetch for anything over HTTP(S), git_query/scm_sync for git, code_search/file_find/file_read for the repo, config_get/config_set for LeapFlow's own settings — because those report typed results, while a failed shell command can only be diagnosed from its exit code and stderr. Every shell run counts as an external side effect, so a failure stops the rest of the batch and is not retried automatically.\n- **terminal_open**(command, cwd, shell) [capability_expand category: terminal]: Open a PERSISTENT shell session (REPL/dev server/watch), returning a session_id for terminal_send/read/close. Disabled unless tools.terminal_session_enabled is set. For one-shot commands use shell_run instead.\n- **terminal_send**(session_id, input, wait) [capability_expand category: terminal]: Send a line of input to a persistent terminal session and return output captured shortly after.\n- **terminal_read**(session_id, wait): Drain buffered output from a persistent terminal session (optionally waiting briefly first).\n- **terminal_close**(session_id) [capability_expand category: terminal]: Terminate a persistent terminal session and release its process group.\n- **terminal_list**(): List active persistent terminal sessions.\n- **config_list**(category, limit): List LeapFlow's own writable settings (LLM model and endpoint, daemon, memory, perception, gateway, …) with current values. Use this to discover the exact key before changing anything. Optionally narrow by `category`. This is the only correct way to inspect LeapFlow configuration — never read config files from disk.\n- **config_get**(key): Read one LeapFlow setting by key (e.g. 'llm.model', 'llm.base_url', 'daemon.log_level'), returning its current value, type, scopes, and whether a change needs a daemon restart. There is no 'llm.provider' key: provider behavior is inferred from the OpenAI-compatible 'llm.base_url'. Never read LeapFlow config files from disk — use this.\n- **config_set**(key, value, scope) [capability_expand category: config]: Change one LeapFlow setting by key, e.g. switch an OpenAI-compatible LLM with key='llm.model' and key='llm.base_url'. There is no 'llm.provider' key: provider behavior is inferred from the endpoint. Values are validated and coerced; credentials are stored in the vault automatically. Call config_list or config_get first if unsure of the key. The result states whether a `leap daemon restart` is required. Never edit LeapFlow config files directly.\n- **web_fetch**(url, select, timeout, max_bytes): Read a URL over HTTP(S) and get back extracted, context-sized content: parsed JSON for API endpoints, readable text plus links for web pages. Use this for anything on the internet — prices, docs, releases, articles — instead of running curl through shell_run: it reports real HTTP status codes, retries rate limits on its own, and is a plain read so a retry is always safe. For JSON APIs pass `select` with a dotted path (e.g. 'chart.result.0.meta') to return just that part instead of the whole payload.\n- **memory_search**(query, limit): Search agent memory for relevant past experiences, observations, and facts.\n- **memory_add**(content, kind) [capability_expand category: write]: Store a new observation or insight in memory for future reference.\n- **research_note**(kind, text): Record a compact, structured note about the current task's state so it survives context compression on long / multi-step tasks. Use for durable findings, open questions still to resolve, decisions / excluded paths, and the immediate next step. One concise sentence per note.\n- **capability_expand**(category): Fetch the full callable schema for every tool in a capability category (e.g. 'hub', 'gateway', 'desktop', 'delegate', 'file', 'memory', 'skill'). The compact tool index always lists every registered tool by name and a one-line summary, but only a static low-risk subset is directly callable each turn. If you need a tool from the index that is not yet callable, call capability_expand with its category first; the matching tools become callable in this turn. Never invent a tool name — expand the category instead.\n- **delegate_task**(goal, context) [capability_expand category: delegate]: Delegate a complex sub-task to an isolated subagent. The subagent gets a fresh context and restricted tool access. Use when a task is self-contained and can be solved independently.\n- **schedule_reentry**(kind, reason, delay_seconds, event_match, max_reentries, deadline_seconds): Register a re-entry so this task can resume later from its current orientation (findings / open questions / next step). Use when work must pause and continue after a delay (kind=time) or when a matching platform event arrives (kind=event), instead of finishing now. The research-ledger state is carried over automatically.\n- **hub_push**(skill_name, visibility, version) [capability_expand category: hub]: Push a local skill to the ModelScope Hub for sharing or backup.\n- **hub_pull**(repo_id, version) [capability_expand category: hub]: Pull a skill from the ModelScope Hub to install locally.\n- **hub_search**(query) [capability_expand category: hub]: Search for skills on the Hub by keyword or description.\n- **hub_sync**(mode, dry_run) [capability_expand category: hub]: Preview or execute sync between local skills and Hub.\n- **platform_action**(platform, action, payload, backend_kind) [capability_expand category: gateway]: Execute an exact registered business action on an external platform through LeapFlow's App Connector layer. Actions must be copied from the App Connector Capability Index and are addressed as domain.operation, e.g. im.send_message or docs.create_markdown. All business fields (chat_id, text, query, etc.) MUST be placed inside `payload`, never at the top level. Example: {\"platform\":\"feishu\",\"action\":\"im.send_message\",\"payload\":{\"chat_id\":\"oc_xxx\",\"text\":\"hello\"}}. Do not invent action names, do not use management actions such as list/guide/connect/status here.\n- **platform_connect**(action, platform, credentials, options, checkpoint) [capability_expand category: gateway]: List, guide, connect, disconnect, remove, or check status for external platforms using the App Connector management namespace. Supports REST and CLI backends. Use this for management actions such as list/guide/preflight/connect/status; use platform_action only for exact registered business actions.\n- **gateway_send**(platform, chat_id, text, thread_id) [capability_expand category: gateway]: Send a message to a connected external platform (Feishu group, Telegram chat, DingTalk conversation, etc.). Requires the platform to be connected via gateway_connect first. Use gateway_connect with action='list' to see connected platforms and available chat IDs.\n- **gateway_connect**(action, platform, credentials, options) [capability_expand category: gateway]: Connect, configure, or manage external platform integrations (Feishu, DingTalk, Telegram, Slack, Discord, etc.). Conversational flow: 1) call 'guide' to get setup steps + required fields, 2) present the steps to the user and ask for ALL required credentials in a single message, 3) call 'connect' with the credentials. Goal: complete in 1–2 user turns. NEVER include credential values in your text response.\n- **plugin_list**(): List the live plugin registry and cross-subsystem capability evidence. Use this before answering questions about whether LeapFlow supports plugins, self-evolution, plugin installation, hot reload, versioning, or other runtime capabilities.\n- **plugin_status**(plugin_id): Get detailed status of a specific plugin: its declared category, runtime dependencies, contributed tools, and fiber lifecycle state.\n- **plugin_versions**(plugin_id): List recorded source versions and active pointer for a profile-scoped plugin.\n- **plugin_propose**(requested_capability, plugin_id, proposed_tools, test_cases, risk_level, evidence): Create a side-effect-free PluginProposal from explicit capability-gap evidence. Use this before plugin_generate when a missing capability should be reviewed. Does not call an LLM, write files, or install anything.\n- **assess_compatibility**(manifest, source_path) [capability_expand category: plugin_management]: Assess whether a foreign plugin manifest or real DSH source bundle is compatible with LeapFlow. A source bundle assessment is static: runtime_ready stays false until restricted Node discovery during plugin_install. Returns component-level verdicts and limitations.\n- **plugin_generate**(plugin_id, description, proposal_id) [capability_expand category: system]: Generate a new ToolPlugin from a natural-language capability description. The LLM produces code that conforms to the ToolPlugin Protocol; it is then rigorously validated (syntax, structure, import, protocol conformance). The generated source is stored in CAS and receives explicit content approval. It DOES NOT install the plugin — installation requires a second, mutation-specific approval via plugin_install.\n- **plugin_install**(plugin_id, code, marketplace_name, source_path, proposal_id, version_label) [capability_expand category: system]: Install a plugin from exactly one source: validated Python code, the configured Python marketplace, or a real DSH source bundle. DSH bundles run restricted Node discovery before registration; only public host tools are installed and client UI limitations are reported. Writes profile-scoped state and mutates the process-global registry. REQUIRES APPROVAL.\n- **plugin_rollback**(plugin_id, version) [capability_expand category: system]: Rollback a profile-scoped plugin to a recorded source version and reload it. REQUIRES APPROVAL.\n- **plugin_reload**(plugin_id, version_label) [capability_expand category: system]: Hot-reload a plugin at runtime. Disposes the old plugin fiber, re-imports its module, and registers a fresh instance. Existing in-flight turns are unaffected (snapshot isolation). REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_disable**(plugin_id) [capability_expand category: system]: Disable a plugin by disposing its fiber, removing its tools from the runtime registry. Cannot disable self_management itself. REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_remove**(plugin_id, delete_source) [capability_expand category: system]: Terminally remove a plugin: dispose its fiber, unregister its tools, remove reload metadata, and optionally delete its profile-scoped source file. Cannot remove self_management itself. REQUIRES APPROVAL.\n- **plugin_unquarantine**(plugin_id) [capability_expand category: plugin_management]: Restore a quarantined plugin to probation status for re-evaluation. Unfreezes the trust ledger, transitions the proposal back to PROBATION, and reloads the plugin. REQUIRES APPROVAL.\n- **plugin_enable**(plugin_id) [capability_expand category: system]: Re-enable a previously disabled plugin by reloading its module and registering a fresh instance. REQUIRES APPROVAL.\n- **self_describe**(facet): Introspect LeapFlow's own identity, capabilities, runtime state, evolution metrics, or platform connections. Use facet='all' only when a comprehensive self-check is explicitly requested.\n- **runtime_snapshot**(): Lightweight (~150 token) flat snapshot of current runtime state: model, context budget, posture, disclosure level, turn count, cache hit rate, uptime, tool count, and pending approvals.\n- **schedule_create**(trigger_expression, instruction, execution_mode, max_retries, delivery_target) [capability_expand category: scheduler]: Create a new scheduled task. Specify a trigger expression (e.g. '30m', 'every 2h', '0 9 * * *') and the instruction to execute on each trigger. Optionally provide an execution mode and a delivery target to receive notifications.\n- **schedule_list**() [capability_expand category: scheduler]: List all scheduled tasks with their current state, trigger, and next due time.\n- **schedule_status**(task_id) [capability_expand category: scheduler]: Get detailed status and recent execution history for a scheduled task.\n- **schedule_pause**(task_id) [capability_expand category: scheduler]: Pause a scheduled task so it stops firing without being cancelled.\n- **schedule_resume**(task_id) [capability_expand category: scheduler]: Resume a paused scheduled task — re-arms it and recalculates next due time.\n- **schedule_cancel**(task_id) [capability_expand category: scheduler]: Cancel a scheduled task permanently.\n- **tool_search**(query, max_results): Search all registered tools by keyword relevance. Returns a ranked list of matching tools with name, category, and summary. Use this when you need a tool but are unsure of its exact name or category.\n- **tool_describe**(tool_name): Get the full callable schema of a single tool by exact name. Returns the tool's description, parameters, and metadata. Use after tool_search to inspect a specific tool before calling it.\n- **session_search**(query, limit) [capability_expand category: unclassified]: Search past conversation sessions in the current workspace for relevant context.\n- **session_list**(limit): List recent conversation sessions in the current workspace with stable ids, titles, dates, and summaries. Use for browsing past tasks or when user asks to see history without specific search terms. No keywords needed — returns chronological list.\n- **session_detail**(session_id, limit, offset, include_inactive) [capability_expand category: unclassified]: Read a paginated persisted transcript for one past conversation session in the current workspace. Use after session_list or session_search returns a session_id.\n- **click**(element_index) [capability_expand category: desktop]: Click a UI element by its element_index (from the latest observe_ui snapshot)\n- **get_clipboard**() [capability_expand category: desktop]: Read current clipboard text content\n- **list_apps**(filter, running_only) [capability_expand category: desktop]: List available applications on this system. Use to discover correct bundle_id before switch_app.\n- **list_windows**() [capability_expand category: desktop]: List all top-level windows with pid, window_id, title, and per-window state (minimized, on-screen). Call this first to pick the pid and window_id that observe_ui and other window tools require.\n- **observe_ui**(pid, window_id, query) [capability_expand category: desktop]: Snapshot one window's actionable UI elements, each tagged with an element_index for click/right_click/read_text. Re-observe after actions — indices belong to one snapshot. Requires the window's pid and window_id from list_windows.\n- **open_url**(url, app_id) [capability_expand category: desktop]: Open a URL in the default or specified browser\n- **read_text**(element_index) [capability_expand category: desktop]: Read the text content of a specific UI element from the latest snapshot\n- **right_click**(element_index) [capability_expand category: desktop]: Right-click a UI element to open its context menu. Returns visible menu items.\n- **screenshot**(pid, window_id) [capability_expand category: desktop]: Capture a screenshot for visual verification. With pid + window_id captures that window (works across all displays); defaults to the last observed window, or the full desktop when no window has been observed.\n- **scroll**(element_index, direction, amount, pid, window_id) [capability_expand category: desktop]: Scroll a scrollable area of a window. Omit element_index to scroll the window's focused/page scroller; pass one to scroll an exact element from the latest snapshot.\n- **select_text**(element_index) [capability_expand category: desktop]: Select all text in a UI element (focus + select-all, for subsequent copy)\n- **set_clipboard**(text) [capability_expand category: desktop]: Write text to the clipboard\n- **shortcut**(keys) [capability_expand category: desktop]: Execute a keyboard shortcut\n- **switch_app**(app_id) [capability_expand category: desktop]: Switch to an app (launch if needed, activate, verify)\n- **type_text**(text) [capability_expand category: desktop]: Type text into the currently focused element\n- **wait**(seconds) [capability_expand category: desktop]: Wait for a specified duration before continuing\n- **wait_until**(condition, pid, window_id, timeout, poll_interval) [capability_expand category: desktop]: Wait until a UI condition is met (polls UI tree). Returns elements when found or on timeout.\n- **wait_until_stable**(timeout, poll_interval, pid, window_id) [capability_expand category: desktop]: Wait until the UI stops changing (element set stabilizes across polls).\n\n## App Connector Capability Index\nLeapFlow can onboard and manage external apps through `platform_connect` and execute exact registered business actions through `platform_action`.\nFor requests about connecting, setting up, configuring, enabling, or managing a supported app, use `platform_connect` first instead of generating SDK/Webhook sample code.\n`platform_connect.action` is the management namespace: list, guide, preflight, connect, disconnect, remove, status, events_start, events_stop, events_status.\n`platform_action.action` is only for exact registered platform business actions listed below, such as `im.send_message`; never use management actions like `list` or `guide` there.\nAll business fields MUST go inside `payload`; top-level keys are only `platform`, `action`, and `payload`.\nDo not invent platform IDs or platform action names. If the needed action is not listed, ask for discovery/clarification instead of guessing.\nUse `platform_connect` with `action='guide'` and the matching `platform` to start onboarding; use `action='list'` when the app is unclear.\nWhen a pending onboarding state is present, continue from that state with `platform_connect` instead of asking the user to restate the app.\nSupported apps:\n- `api_server`: API Server (OpenAI Compatible) (category=api; backend=adapter; platform_action actions=none registered)\n- `dingtalk`: 钉钉 (DingTalk) (category=im; backend=adapter; platform_action actions=none registered)\n- `feishu`: 飞书 (Feishu/Lark) (category=im; backend=cli)\n - `calendar.create_event` payload={summary*, start_time*, end_time*, attendees} [write/high]\n - `docs.create_markdown` payload={title*, markdown*, folder_token} [write/medium]\n - `drive.search` payload={query*, limit} [read/medium]\n - `im.add_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.download_resource` payload={message_id*, file_key*, type*} [read/low]\n - `im.get_messages` payload={message_ids*} [read/low]\n - `im.list_chats` payload={page_size} [read/low]\n - `im.list_messages` payload={chat_id*} [read/low]\n - `im.list_thread_messages` payload={thread*} [read/low]\n - `im.remove_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.reply_message` payload={message_id*, text*} [send/high]\n - `im.search_chats` payload={query*, page_size} [read/low]\n - `im.search_messages` payload={query*} [read/medium]\n - `im.send_message` payload={chat_id*, text*, thread_id} [send/high]\n - `im.update_card` payload={token*, card*} [write/medium]\n - `im.update_message` payload={message_id*, text*} [write/medium]\n - `mail.search_unread` payload={query, limit} [read/high]\n - `sheets.append_row` payload={spreadsheet_token*, sheet_id*, values*} [write/medium]\n - `task.create` payload={title*, description, due_time} [write/medium]\n- `telegram`: Telegram (category=social; backend=adapter; platform_action actions=none registered)\n- `webhook`: Webhook (Generic) (category=webhook; backend=adapter; platform_action actions=none registered)\n\n## Tool Usage\nTools are normally invoked through the native function-calling mechanism, not by writing JSON in your reply\ntext. Only if the provider signals that native function calling is unavailable for this turn, fall back to a\nsingle JSON code block: `{\"name\": \"tool_name\", \"arguments\": {\"key\": \"value\"}}` — use this fallback format\nonly, never both. Only call a tool whose exact name appears in the tool index above and is currently callable\n(or reachable via `capability_expand`). Never invent, rename, alias, or guess a tool name, platform ID, or\nplatform action from argument shape or wording — if the index does not list it, it does not exist.\n`platform_connect.action` (list/guide/preflight/connect/disconnect/remove/status/events_start/events_stop/\nevents_status) is the App Connector management namespace; `platform_action.action` only accepts exact\nregistered business actions such as `im.send_message` shown in the App Connector Capability Index — never mix\nthe two namespaces. If a tool call returns an unknown/unavailable result, use the returned suggestions or\navailable names for a single retry instead of trying further variations of the same guess.\n\n**Side-effect action rule** (`platform_action` with effect=send/write/execute):\n- Call each unique action+payload **exactly once**. Never include duplicates in the same turn.\n- Once the result returns `\"completed\": true`, that action is DONE for this task. Do NOT call it again\n in any subsequent turn — immediately summarize the result for the user instead.\n- The system enforces idempotency: duplicate calls are blocked and will not execute.\n- If the user explicitly requests sending/writing multiple times, use distinct payloads per call.\n\n**Resource identifier provenance rule**:\n- NEVER fabricate, guess, or infer resource identifiers (chat_id, message_id, file_key, user_id, etc.).\n Every resource ID used in a side-effect action MUST come from a successful API response in this session.\n- If a read/list action fails (e.g. authorization error), you do NOT have valid resource IDs.\n Report the failure to the user — do NOT attempt the dependent write/send action with a guessed ID.\n- When a tool result contains `\"llm_instruction\"`, follow it exactly.\n\n## Guidelines\n1. **Direct answers first**: If you already know the answer, respond directly without tools.\n2. **Avoid redundant tool calls**: Do not call the same tool with the same arguments more than once in the same user turn. When an existing tool result already answers the user's request, stop calling tools and answer directly.\n3. **Use tools proactively**: When the user asks about files, time, system state, or needs actions performed, use the appropriate tool.\n4. **Chain tools when needed**: You can call multiple tools in sequence (e.g., list files → read file → summarize).\n5. **Handle failures gracefully**: If a tool fails, explain what went wrong and suggest alternatives. If it failed because of an unknown tool/platform/action name, retry once with an exact name from the returned suggestions, then explain rather than keep guessing.\n6. **Summarize results naturally**: After tool execution, synthesize the results into a helpful answer rather than dumping raw output.\n7. **Stay conversational**: Maintain a natural, helpful tone. Acknowledge context from earlier in the conversation.\n8. **Recall past work**:\n - Broad queries (\"之前做了什么\", \"列出任务\"): answer from the \"Recent Task History\" section already in your context. If insufficient, call session_list.\n - Specific lookups (\"上次那个配置怎么改的\"): call session_search with relevant phrases (NOT single characters).\n - Do NOT call search tools repeatedly with keyword fragments. One well-phrased call is better than ten fragmented ones.\n\n## Coding & Verification\nWhen working with code, prefer the precise built-in tools over ad-hoc shell: use `repo_map` to orient in an\nunfamiliar project, `code_search`/`file_find` to locate and `code_intel` for symbols, `edit_file` (anchored\nsearch-replace, or a unified `diff`) to change files — never rewrite a whole file to change a few lines — and\n`git_query` to inspect diffs/log. After edits, check `syntax_ok` in the result and run `test_run`/`lint_check`\nbefore declaring the work done. Batch independent read-only calls (search/read/query) into a single turn — they\nrun in parallel.\n\n## Presentation Style\n1. **Polished Markdown only**: Format user-facing answers with clean Markdown headings, short paragraphs, and concise bullets. Use tables only when they improve comparison or scanning.\n2. **Terminal-friendly layout**: Keep lines readable in a TUI; avoid dense walls of text, deeply nested lists, oversized ASCII art, or heavy visual blocks.\n3. **Elegant emphasis**: Use bold text sparingly for key terms and conclusions. Avoid excessive emojis, decorative symbols, repeated separators, or visual noise.\n4. **Theme-safe colors**: Do not emit ANSI escape codes, HTML color tags, Rich markup, or hardcoded color names. Rely on the TUI theme to render Markdown professionally.\n5. **No leaked tool protocol**: Never show tool-call JSON, internal schemas, raw observations, tool result payloads, or hidden reasoning in the final answer unless the user explicitly asks for raw/debug output. Treat any prior `{\"name\": ..., \"arguments\": ...}` blocks and `Tool result (...)` messages as internal execution context only.\n6. **Professional closure**: End with a concise conclusion or next step when helpful; avoid rambling after the useful answer is complete.\n\nWhen finished with all tool calls, respond normally without a JSON block, tool-call transcript, or process log.\n\n## Task Contract\n- Task ID: turn-3\n- Original user request: Is that the same invoice?\n- Workspace root: \n- Allowed roots: \n- Treat relative project paths as relative to the workspace root; never infer `.` as the project root when a workspace root is provided.\n- Workspace boundary is enforced by tools: do not read, search, edit, or run commands against paths outside the allowed roots unless the user explicitly requests an external path and the tool/approval policy permits it.\n- LeapFlow workspace config is optional at `/.leapflow/config.yaml`; runtime config is loaded from `~/.leapflow/config/user.yaml` and `~/.leapflow/profiles//config/*.yaml`.\n- Preserve this task contract across summarization, compression, tool loops, and memory retrieval.\n" - }, - { - "role": "system", - "content": "## Semantic Focus Plane\nCurrent task focus:\n- Target: invoice.txt\n- Type: file\n- Evidence refs: file_read:invoice.txt\nResolved user reference:\n- file -> invoice.txt (task_semantic, confidence=0.92)\nUse Current task focus for deictic task references such as 'the above paper'; use control-plane events only when the user explicitly asks about runtime settings.\n\n\n## Recent Session Summary\n- [assistant] The invoice total is 128.50 USD.\n- [user] Is that the same invoice?\n" - }, - { - "role": "assistant", - "content": "The invoice total is 128.50 USD." - }, - { - "role": "user", - "content": "Is that the same invoice?\nIs that the same invoice?" - } - ], - "tools": [ - "capability_expand", - "code_intel", - "code_search", - "config_get", - "config_list", - "env_info", - "file_find", - "file_list", - "file_read", - "git_query", - "lint_check", - "memory_search", - "plugin_list", - "plugin_propose", - "plugin_status", - "plugin_versions", - "repo_map", - "research_note", - "runtime_snapshot", - "schedule_reentry", - "self_describe", - "session_list", - "skill_view", - "skills_list", - "terminal_list", - "terminal_read", - "test_run", - "text_search", - "time_get", - "tool_describe", - "tool_search", - "web_fetch" - ] - }, - "responses": [ - { - "status": 200, - "content_type": "application/json", - "body": "{\"id\": \"chatcmpl-cassette\", \"object\": \"chat.completion\", \"model\": \"cassette-model\", \"choices\": [{\"index\": 0, \"message\": {\"role\": \"assistant\", \"content\": \"Yes, that is the same invoice.\"}, \"finish_reason\": \"stop\"}], \"usage\": {\"prompt_tokens\": 64, \"completion_tokens\": 16, \"total_tokens\": 80}}" - } - ] -} diff --git a/tests/_fixtures/cassettes/r1_conversation/cassette-model-809a7a1875c59886.cassette.json b/tests/_fixtures/cassettes/r1_conversation/cassette-model-809a7a1875c59886.cassette.json new file mode 100644 index 00000000..07b5063c --- /dev/null +++ b/tests/_fixtures/cassettes/r1_conversation/cassette-model-809a7a1875c59886.cassette.json @@ -0,0 +1,67 @@ +{ + "fingerprint": "809a7a1875c598869b3d03a555e303b686c9d00000732a19db20abb42597feb0", + "note": "captured in seed mode", + "request": { + "model": "cassette-model", + "stream": false, + "messages": [ + { + "role": "system", + "content": "You are LeapFlow, an intelligent assistant that can both converse naturally and take real actions on the user's computer.\n\n## Capabilities\nThe tool index below lists **every** registered tool by name and a one-line summary — this is the complete\ncapability contract; nothing else exists. Only a subset is directly callable this turn (via native tool calling,\nnot a JSON block in your reply). If you need a tool from the index that is not yet callable, call\n`capability_expand` with its category name first — the matching tools become callable immediately after.\nWhen the user asks about LeapFlow **itself** — its identity/version/model, what tools, plugins, or capabilities\nit has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of\nself-awareness — answer from live runtime evidence via `self_describe` (facet=capabilities for \"what can you\ndo / do you support X\", identity for version/model, runtime for context/posture, or `all` for a full self-check),\nnever by reading LeapFlow's own source code, documentation, or memory. Report configuration-dependent or\nunavailable capabilities as limitations rather than inferring them.\n- **text_search**(text, pattern): Search for a regex pattern in text.\n- **text_replace**(text, old, new, count) [capability_expand category: write]: Replace occurrences of a substring in text.\n- **time_get**(): Get current date and time.\n- **env_info**(): Get system environment information (OS, Python version, cwd).\n- **skills_list**(query, category, source): List available learned skills. Use when user asks about capabilities or you need a specific skill.\n- **skill_view**(name): View the full content of a specific skill document.\n- **skill_list_files**(name) [capability_expand category: skills]: List all support files in a skill directory (configs, templates, examples). Use before skill_file_read to discover available files.\n- **skill_file_read**(name, file_path) [capability_expand category: skills]: Read an individual support file from a skill directory. Use skill_list_files first to discover available files.\n- **code_intel**(path, operation): Precise document symbols (outline) for a source file: classes, functions, and methods with line ranges. Python uses an exact AST parse; other languages use a keyword-prefix scan. Prefer over file_read mode=symbols for accurate navigation before editing. Read-only.\n- **repo_map**(path): Compact project orientation for a repository root: languages, detected test/lint commands, top-level structure, entry points, manifest, and VCS branch. Call this first when entering an unfamiliar codebase. Read-only.\n- **scm_sync**(action, cwd, remote, pull_ref, push_ref, timeout) [capability_expand category: scm]: Run a typed git SCM action. Use this instead of shell_run for git pull/push/status. For 'pull origin main then push', set action='pull_then_push', remote='origin', pull_ref='main', and omit push_ref so LeapFlow pushes the current local branch.\n- **git_query**(action, cwd, ref, path, staged, max_count, stat): Read-only structured git inspection: action=diff|log|status|branch|show. Prefer over shell_run for reading repo state — output is clipped, redacted, and log/branch are parsed into structured fields. Use scm_sync for pull/push.\n- **git_write**(action, cwd, message, stage_all, name, ref, create) [capability_expand category: scm]: Mutating git actions: action=commit (message, stage_all), branch (create+switch), checkout (switch; create=true for -b). Approval-gated. Use scm_sync for pull/push and git_query for reads.\n- **test_run**(command, cwd, timeout): Run the project's test suite and return structured results (framework, passed/failed counts, failing tests). Auto-detects the runner (pytest/npm/go/cargo) or uses a configured/explicit command; executes via the governed shell. ok=true means the runner executed — see 'success' for pass/fail.\n- **lint_check**(command, cwd, timeout): Run the project's linter and return a structured clean/issue result. Auto-detects the linter (ruff/eslint/go vet/clippy) or uses a configured/explicit command; executes via the governed shell. ok=true means the linter ran — see 'clean'.\n- **file_list**(path, pattern, depth): List files and directories at a given path. Use depth=1 or depth=2 to get a recursive tree in one call instead of listing each sub-directory separately.\n- **file_read**(path, max_lines, start_line, max_chars, mode): Read text file content with adaptive context governance. For large or unfamiliar files, prefer mode='outline' or mode='symbols' first, then use mode='raw' with start_line/max_lines for the specific range you actually need. For LeapFlow's own settings, use config_list / config_get / config_set — its config files are outside the workspace and not readable here.\n- **file_write**(path, content, mode) [capability_expand category: write]: Write content to a file (overwrite or append).\n- **code_search**(pattern, patterns, path, glob, ignore_case, multiline, max_results, context_lines): Search file CONTENTS by regex pattern across a directory tree (ripgrep-backed). Requires a regex pattern. NOT for listing or browsing directory contents — use file_list for that. Prefer this over shell_run grep: faster, skips VCS/dependency/build dirs, and returns structured path:line:column matches. Batch related lookups into ONE call via `patterns` (OR-combined, single pass) instead of issuing several separate searches. Use file_read for the surrounding context of a hit.\n- **file_find**(glob, path, max_results): Find files by a recursive glob pattern under a base path (e.g. '**/test_*.py' or '*.md'). Prefer this over shell_run find; skips VCS/dependency/build dirs.\n- **edit_file**(path, edits, dry_run, diff) [capability_expand category: write]: Apply targeted, anchored search-replace edits to an EXISTING text file (use file_write to create/overwrite). Each edit is {original_text, new_text, replace_all?}; original_text must match exactly and uniquely (or set replace_all) — a non-unique or missing anchor is rejected so files are never corrupted. Set dry_run to preview. Alternatively pass a unified 'diff' to apply its hunks as anchored edits. Far cheaper and safer than rewriting a whole file.\n- **shell_run**(command, cwd, timeout) [capability_expand category: shell]: Execute a one-shot shell command with timeout protection. Runs in the active workspace; paths resolving outside it are refused. Reach for a structured tool first when one fits — web_fetch for anything over HTTP(S), git_query/scm_sync for git, code_search/file_find/file_read for the repo, config_get/config_set for LeapFlow's own settings — because those report typed results, while a failed shell command can only be diagnosed from its exit code and stderr. Every shell run counts as an external side effect, so a failure stops the rest of the batch and is not retried automatically.\n- **terminal_open**(command, cwd, shell) [capability_expand category: terminal]: Open a PERSISTENT shell session (REPL/dev server/watch), returning a session_id for terminal_send/read/close. Disabled unless tools.terminal_session_enabled is set. For one-shot commands use shell_run instead.\n- **terminal_send**(session_id, input, wait) [capability_expand category: terminal]: Send a line of input to a persistent terminal session and return output captured shortly after.\n- **terminal_read**(session_id, wait): Drain buffered output from a persistent terminal session (optionally waiting briefly first).\n- **terminal_close**(session_id) [capability_expand category: terminal]: Terminate a persistent terminal session and release its process group.\n- **terminal_list**(): List active persistent terminal sessions.\n- **config_list**(category, limit): List LeapFlow's own writable settings (LLM model and endpoint, daemon, memory, perception, gateway, …) with current values. Use this to discover the exact key before changing anything. Optionally narrow by `category`. This is the only correct way to inspect LeapFlow configuration — never read config files from disk.\n- **config_get**(key): Read one LeapFlow setting by key (e.g. 'llm.model', 'llm.base_url', 'daemon.log_level'), returning its current value, type, scopes, and whether a change needs a daemon restart. There is no 'llm.provider' key: provider behavior is inferred from the OpenAI-compatible 'llm.base_url'. Never read LeapFlow config files from disk — use this.\n- **config_set**(key, value, scope) [capability_expand category: config]: Change one LeapFlow setting by key, e.g. switch an OpenAI-compatible LLM with key='llm.model' and key='llm.base_url'. There is no 'llm.provider' key: provider behavior is inferred from the endpoint. Values are validated and coerced; credentials are stored in the vault automatically. Call config_list or config_get first if unsure of the key. The result states whether a `leap daemon restart` is required. Never edit LeapFlow config files directly.\n- **web_fetch**(url, select, timeout, max_bytes): Read a URL over HTTP(S) and get back extracted, context-sized content: parsed JSON for API endpoints, readable text plus links for web pages. Use this for anything on the internet — prices, docs, releases, articles — instead of running curl through shell_run: it reports real HTTP status codes, retries rate limits on its own, and is a plain read so a retry is always safe. For JSON APIs pass `select` with a dotted path (e.g. 'chart.result.0.meta') to return just that part instead of the whole payload.\n- **memory_search**(query, limit): Search agent memory for relevant past experiences, observations, and facts.\n- **memory_add**(content, kind) [capability_expand category: write]: Store a new observation or insight in memory for future reference.\n- **research_note**(kind, text): Record a compact, structured note about the current task's state so it survives context compression on long / multi-step tasks. Use for durable findings, open questions still to resolve, decisions / excluded paths, and the immediate next step. One concise sentence per note.\n- **capability_expand**(category): Fetch the full callable schema for every tool in a capability category (e.g. 'hub', 'gateway', 'desktop', 'delegate', 'file', 'memory', 'skill'). The compact tool index always lists every registered tool by name and a one-line summary, but only a static low-risk subset is directly callable each turn. If you need a tool from the index that is not yet callable, call capability_expand with its category first; the matching tools become callable in this turn. Never invent a tool name — expand the category instead.\n- **delegate_task**(goal, context) [capability_expand category: delegate]: Delegate a complex sub-task to an isolated subagent. The subagent gets a fresh context and restricted tool access. Use when a task is self-contained and can be solved independently.\n- **schedule_reentry**(kind, reason, delay_seconds, event_match, max_reentries, deadline_seconds): Register a re-entry so this task can resume later from its current orientation (findings / open questions / next step). Use when work must pause and continue after a delay (kind=time) or when a matching platform event arrives (kind=event), instead of finishing now. The research-ledger state is carried over automatically.\n- **hub_push**(skill_name, visibility, version) [capability_expand category: hub]: Push a local skill to the ModelScope Hub for sharing or backup.\n- **hub_pull**(repo_id, version) [capability_expand category: hub]: Pull a skill from the ModelScope Hub to install locally.\n- **hub_search**(query) [capability_expand category: hub]: Search for skills on the Hub by keyword or description.\n- **hub_sync**(mode, dry_run) [capability_expand category: hub]: Preview or execute sync between local skills and Hub.\n- **platform_action**(platform, action, payload, backend_kind) [capability_expand category: gateway]: Execute an exact registered business action on an external platform through LeapFlow's App Connector layer. Actions must be copied from the App Connector Capability Index and are addressed as domain.operation, e.g. im.send_message or docs.create_markdown. All business fields (chat_id, text, query, etc.) MUST be placed inside `payload`, never at the top level. Example: {\"platform\":\"feishu\",\"action\":\"im.send_message\",\"payload\":{\"chat_id\":\"oc_xxx\",\"text\":\"hello\"}}. Do not invent action names, do not use management actions such as list/guide/connect/status here.\n- **platform_connect**(action, platform, credentials, options, checkpoint) [capability_expand category: gateway]: List, guide, connect, disconnect, remove, or check status for external platforms using the App Connector management namespace. Supports REST and CLI backends. Use this for management actions such as list/guide/preflight/connect/status; use platform_action only for exact registered business actions.\n- **gateway_send**(platform, chat_id, text, thread_id) [capability_expand category: gateway]: Send a message to a connected external platform (Feishu group, Telegram chat, DingTalk conversation, etc.). Requires the platform to be connected via gateway_connect first. Use gateway_connect with action='list' to see connected platforms and available chat IDs.\n- **gateway_connect**(action, platform, credentials, options) [capability_expand category: gateway]: Connect, configure, or manage external platform integrations (Feishu, DingTalk, Telegram, Slack, Discord, etc.). Conversational flow: 1) call 'guide' to get setup steps + required fields, 2) present the steps to the user and ask for ALL required credentials in a single message, 3) call 'connect' with the credentials. Goal: complete in 1–2 user turns. NEVER include credential values in your text response.\n- **plugin_list**(): List the live plugin registry across Tool/Gateway/LLM subsystems for plugin management: which plugins are loaded, their category and fiber lifecycle state. For questions about LeapFlow's own capabilities or whether it supports X, use self_describe instead.\n- **plugin_status**(plugin_id): Get detailed status of a specific plugin: its declared category, runtime dependencies, contributed tools, and fiber lifecycle state.\n- **plugin_versions**(plugin_id): List recorded source versions and active pointer for a profile-scoped plugin.\n- **plugin_propose**(requested_capability, plugin_id, proposed_tools, test_cases, risk_level, evidence): Create a side-effect-free PluginProposal from explicit capability-gap evidence. Use this before plugin_generate when a missing capability should be reviewed. Does not call an LLM, write files, or install anything.\n- **assess_compatibility**(manifest, source_path) [capability_expand category: plugin_management]: Assess whether a foreign plugin manifest or real DSH source bundle is compatible with LeapFlow. A source bundle assessment is static: runtime_ready stays false until restricted Node discovery during plugin_install. Returns component-level verdicts and limitations.\n- **plugin_generate**(plugin_id, description, proposal_id) [capability_expand category: system]: Generate a new ToolPlugin from a natural-language capability description. The LLM produces code that conforms to the ToolPlugin Protocol; it is then rigorously validated (syntax, structure, import, protocol conformance). The generated source is stored in CAS and receives explicit content approval. It DOES NOT install the plugin — installation requires a second, mutation-specific approval via plugin_install.\n- **plugin_install**(plugin_id, code, marketplace_name, source_path, proposal_id, version_label) [capability_expand category: system]: Install a plugin from exactly one source: validated Python code, the configured Python marketplace, or a real DSH source bundle. DSH bundles run restricted Node discovery before registration; only public host tools are installed and client UI limitations are reported. Writes profile-scoped state and mutates the process-global registry. REQUIRES APPROVAL.\n- **plugin_rollback**(plugin_id, version) [capability_expand category: system]: Rollback a profile-scoped plugin to a recorded source version and reload it. REQUIRES APPROVAL.\n- **plugin_reload**(plugin_id, version_label) [capability_expand category: system]: Hot-reload a plugin at runtime. Disposes the old plugin fiber, re-imports its module, and registers a fresh instance. Existing in-flight turns are unaffected (snapshot isolation). REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_disable**(plugin_id) [capability_expand category: system]: Disable a plugin by disposing its fiber, removing its tools from the runtime registry. Cannot disable self_management itself. REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_remove**(plugin_id, delete_source) [capability_expand category: system]: Terminally remove a plugin: dispose its fiber, unregister its tools, remove reload metadata, and optionally delete its profile-scoped source file. Cannot remove self_management itself. REQUIRES APPROVAL.\n- **plugin_unquarantine**(plugin_id) [capability_expand category: plugin_management]: Restore a quarantined plugin to probation status for re-evaluation. Unfreezes the trust ledger, transitions the proposal back to PROBATION, and reloads the plugin. REQUIRES APPROVAL.\n- **plugin_enable**(plugin_id) [capability_expand category: system]: Re-enable a previously disabled plugin by reloading its module and registering a fresh instance. REQUIRES APPROVAL.\n- **self_describe**(facet): Answer questions about LeapFlow ITSELF from live runtime evidence — its identity/version/model, what tools and plugins it has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of self-awareness. Prefer this over reading LeapFlow's own source code. Pick facet=capabilities for 'what can you do / do you support X', identity for version/model, runtime for context/posture, or all for a full self-check when explicitly requested.\n- **runtime_snapshot**(): Lightweight (~150 token) flat snapshot of current runtime state: model, context budget, posture, disclosure level, turn count, cache hit rate, uptime, tool count, and pending approvals.\n- **schedule_create**(trigger_expression, instruction, execution_mode, max_retries, delivery_target) [capability_expand category: scheduler]: Create a new scheduled task. Specify a trigger expression (e.g. '30m', 'every 2h', '0 9 * * *') and the instruction to execute on each trigger. Optionally provide an execution mode and a delivery target to receive notifications.\n- **schedule_list**() [capability_expand category: scheduler]: List all scheduled tasks with their current state, trigger, and next due time.\n- **schedule_status**(task_id) [capability_expand category: scheduler]: Get detailed status and recent execution history for a scheduled task.\n- **schedule_pause**(task_id) [capability_expand category: scheduler]: Pause a scheduled task so it stops firing without being cancelled.\n- **schedule_resume**(task_id) [capability_expand category: scheduler]: Resume a paused scheduled task — re-arms it and recalculates next due time.\n- **schedule_cancel**(task_id) [capability_expand category: scheduler]: Cancel a scheduled task permanently.\n- **tool_search**(query, max_results): Search all registered tools by keyword relevance. Returns a ranked list of matching tools with name, category, and summary. Use this when you need a tool but are unsure of its exact name or category.\n- **tool_describe**(tool_name): Get the full callable schema of a single tool by exact name. Returns the tool's description, parameters, and metadata. Use after tool_search to inspect a specific tool before calling it.\n- **session_search**(query, limit) [capability_expand category: unclassified]: Search past conversation sessions in the current workspace for relevant context.\n- **session_list**(limit): List recent conversation sessions in the current workspace with stable ids, titles, dates, and summaries. Use for browsing past tasks or when user asks to see history without specific search terms. No keywords needed — returns chronological list.\n- **session_detail**(session_id, limit, offset, include_inactive) [capability_expand category: unclassified]: Read a paginated persisted transcript for one past conversation session in the current workspace. Use after session_list or session_search returns a session_id.\n- **click**(element_index) [capability_expand category: desktop]: Click a UI element by its element_index (from the latest observe_ui snapshot)\n- **get_clipboard**() [capability_expand category: desktop]: Read current clipboard text content\n- **list_apps**(filter, running_only) [capability_expand category: desktop]: List available applications on this system. Use to discover correct bundle_id before switch_app.\n- **list_windows**() [capability_expand category: desktop]: List all top-level windows with pid, window_id, title, and per-window state (minimized, on-screen). Call this first to pick the pid and window_id that observe_ui and other window tools require.\n- **observe_ui**(pid, window_id, query) [capability_expand category: desktop]: Snapshot one window's actionable UI elements, each tagged with an element_index for click/right_click/read_text. Re-observe after actions — indices belong to one snapshot. Requires the window's pid and window_id from list_windows.\n- **open_url**(url, app_id) [capability_expand category: desktop]: Open a URL in the default or specified browser\n- **read_text**(element_index) [capability_expand category: desktop]: Read the text content of a specific UI element from the latest snapshot\n- **right_click**(element_index) [capability_expand category: desktop]: Right-click a UI element to open its context menu. Returns visible menu items.\n- **screenshot**(pid, window_id) [capability_expand category: desktop]: Capture a screenshot for visual verification. With pid + window_id captures that window (works across all displays); defaults to the last observed window, or the full desktop when no window has been observed.\n- **scroll**(element_index, direction, amount, pid, window_id) [capability_expand category: desktop]: Scroll a scrollable area of a window. Omit element_index to scroll the window's focused/page scroller; pass one to scroll an exact element from the latest snapshot.\n- **select_text**(element_index) [capability_expand category: desktop]: Select all text in a UI element (focus + select-all, for subsequent copy)\n- **set_clipboard**(text) [capability_expand category: desktop]: Write text to the clipboard\n- **shortcut**(keys) [capability_expand category: desktop]: Execute a keyboard shortcut\n- **switch_app**(app_id) [capability_expand category: desktop]: Switch to an app (launch if needed, activate, verify)\n- **type_text**(text) [capability_expand category: desktop]: Type text into the currently focused element\n- **wait**(seconds) [capability_expand category: desktop]: Wait for a specified duration before continuing\n- **wait_until**(condition, pid, window_id, timeout, poll_interval) [capability_expand category: desktop]: Wait until a UI condition is met (polls UI tree). Returns elements when found or on timeout.\n- **wait_until_stable**(timeout, poll_interval, pid, window_id) [capability_expand category: desktop]: Wait until the UI stops changing (element set stabilizes across polls).\n\n## App Connector Capability Index\nLeapFlow can onboard and manage external apps through `platform_connect` and execute exact registered business actions through `platform_action`.\nFor requests about connecting, setting up, configuring, enabling, or managing a supported app, use `platform_connect` first instead of generating SDK/Webhook sample code.\n`platform_connect.action` is the management namespace: list, guide, preflight, connect, disconnect, remove, status, events_start, events_stop, events_status.\n`platform_action.action` is only for exact registered platform business actions listed below, such as `im.send_message`; never use management actions like `list` or `guide` there.\nAll business fields MUST go inside `payload`; top-level keys are only `platform`, `action`, and `payload`.\nDo not invent platform IDs or platform action names. If the needed action is not listed, ask for discovery/clarification instead of guessing.\nUse `platform_connect` with `action='guide'` and the matching `platform` to start onboarding; use `action='list'` when the app is unclear.\nWhen a pending onboarding state is present, continue from that state with `platform_connect` instead of asking the user to restate the app.\nSupported apps:\n- `api_server`: API Server (OpenAI Compatible) (category=api; backend=adapter; platform_action actions=none registered)\n- `dingtalk`: 钉钉 (DingTalk) (category=im; backend=adapter; platform_action actions=none registered)\n- `feishu`: 飞书 (Feishu/Lark) (category=im; backend=cli)\n - `calendar.create_event` payload={summary*, start_time*, end_time*, attendees} [write/high]\n - `docs.create_markdown` payload={title*, markdown*, folder_token} [write/medium]\n - `drive.search` payload={query*, limit} [read/medium]\n - `im.add_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.download_resource` payload={message_id*, file_key*, type*} [read/low]\n - `im.get_messages` payload={message_ids*} [read/low]\n - `im.list_chats` payload={page_size} [read/low]\n - `im.list_messages` payload={chat_id*} [read/low]\n - `im.list_thread_messages` payload={thread*} [read/low]\n - `im.remove_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.reply_message` payload={message_id*, text*} [send/high]\n - `im.search_chats` payload={query*, page_size} [read/low]\n - `im.search_messages` payload={query*} [read/medium]\n - `im.send_message` payload={chat_id*, text*, thread_id} [send/high]\n - `im.update_card` payload={token*, card*} [write/medium]\n - `im.update_message` payload={message_id*, text*} [write/medium]\n - `mail.search_unread` payload={query, limit} [read/high]\n - `sheets.append_row` payload={spreadsheet_token*, sheet_id*, values*} [write/medium]\n - `task.create` payload={title*, description, due_time} [write/medium]\n- `telegram`: Telegram (category=social; backend=adapter; platform_action actions=none registered)\n- `webhook`: Webhook (Generic) (category=webhook; backend=adapter; platform_action actions=none registered)\n\n## Tool Usage\nTools are normally invoked through the native function-calling mechanism, not by writing JSON in your reply\ntext. Only if the provider signals that native function calling is unavailable for this turn, fall back to a\nsingle JSON code block: `{\"name\": \"tool_name\", \"arguments\": {\"key\": \"value\"}}` — use this fallback format\nonly, never both. Only call a tool whose exact name appears in the tool index above and is currently callable\n(or reachable via `capability_expand`). Never invent, rename, alias, or guess a tool name, platform ID, or\nplatform action from argument shape or wording — if the index does not list it, it does not exist.\n`platform_connect.action` (list/guide/preflight/connect/disconnect/remove/status/events_start/events_stop/\nevents_status) is the App Connector management namespace; `platform_action.action` only accepts exact\nregistered business actions such as `im.send_message` shown in the App Connector Capability Index — never mix\nthe two namespaces. If a tool call returns an unknown/unavailable result, use the returned suggestions or\navailable names for a single retry instead of trying further variations of the same guess.\n\n**Side-effect action rule** (`platform_action` with effect=send/write/execute):\n- Call each unique action+payload **exactly once**. Never include duplicates in the same turn.\n- Once the result returns `\"completed\": true`, that action is DONE for this task. Do NOT call it again\n in any subsequent turn — immediately summarize the result for the user instead.\n- The system enforces idempotency: duplicate calls are blocked and will not execute.\n- If the user explicitly requests sending/writing multiple times, use distinct payloads per call.\n\n**Resource identifier provenance rule**:\n- NEVER fabricate, guess, or infer resource identifiers (chat_id, message_id, file_key, user_id, etc.).\n Every resource ID used in a side-effect action MUST come from a successful API response in this session.\n- If a read/list action fails (e.g. authorization error), you do NOT have valid resource IDs.\n Report the failure to the user — do NOT attempt the dependent write/send action with a guessed ID.\n- When a tool result contains `\"llm_instruction\"`, follow it exactly.\n\n## Guidelines\n1. **Direct answers first**: If you already know the answer, respond directly without tools.\n2. **Avoid redundant tool calls**: Do not call the same tool with the same arguments more than once in the same user turn. When an existing tool result already answers the user's request, stop calling tools and answer directly.\n3. **Use tools proactively**: When the user asks about files, time, system state, or needs actions performed, use the appropriate tool.\n4. **Chain tools when needed**: You can call multiple tools in sequence (e.g., list files → read file → summarize).\n5. **Handle failures gracefully**: If a tool fails, explain what went wrong and suggest alternatives. If it failed because of an unknown tool/platform/action name, retry once with an exact name from the returned suggestions, then explain rather than keep guessing.\n6. **Summarize results naturally**: After tool execution, synthesize the results into a helpful answer rather than dumping raw output.\n7. **Stay conversational**: Maintain a natural, helpful tone. Acknowledge context from earlier in the conversation.\n8. **Recall past work**:\n - Broad queries (\"之前做了什么\", \"列出任务\"): answer from the \"Recent Task History\" section already in your context. If insufficient, call session_list.\n - Specific lookups (\"上次那个配置怎么改的\"): call session_search with relevant phrases (NOT single characters).\n - Do NOT call search tools repeatedly with keyword fragments. One well-phrased call is better than ten fragmented ones.\n\n## Coding & Verification\nWhen working with code, prefer the precise built-in tools over ad-hoc shell: use `repo_map` to orient in an\nunfamiliar project, `code_search`/`file_find` to locate and `code_intel` for symbols, `edit_file` (anchored\nsearch-replace, or a unified `diff`) to change files — never rewrite a whole file to change a few lines — and\n`git_query` to inspect diffs/log. After edits, check `syntax_ok` in the result and run `test_run`/`lint_check`\nbefore declaring the work done. Batch independent read-only calls (search/read/query) into a single turn — they\nrun in parallel.\n\n## Presentation Style\n1. **Polished Markdown only**: Format user-facing answers with clean Markdown headings, short paragraphs, and concise bullets. Use tables only when they improve comparison or scanning.\n2. **Terminal-friendly layout**: Keep lines readable in a TUI; avoid dense walls of text, deeply nested lists, oversized ASCII art, or heavy visual blocks.\n3. **Elegant emphasis**: Use bold text sparingly for key terms and conclusions. Avoid excessive emojis, decorative symbols, repeated separators, or visual noise.\n4. **Theme-safe colors**: Do not emit ANSI escape codes, HTML color tags, Rich markup, or hardcoded color names. Rely on the TUI theme to render Markdown professionally.\n5. **No leaked tool protocol**: Never show tool-call JSON, internal schemas, raw observations, tool result payloads, or hidden reasoning in the final answer unless the user explicitly asks for raw/debug output. Treat any prior `{\"name\": ..., \"arguments\": ...}` blocks and `Tool result (...)` messages as internal execution context only.\n6. **Professional closure**: End with a concise conclusion or next step when helpful; avoid rambling after the useful answer is complete.\n\nWhen finished with all tool calls, respond normally without a JSON block, tool-call transcript, or process log.\n\n## Task Contract\n- Task ID: turn-2\n- Original user request: Use the file_read tool on invoice.txt and report the total.\n- Workspace root: \n- Allowed roots: \n- Treat relative project paths as relative to the workspace root; never infer `.` as the project root when a workspace root is provided.\n- Workspace boundary is enforced by tools: do not read, search, edit, or run commands against paths outside the allowed roots unless the user explicitly requests an external path and the tool/approval policy permits it.\n- LeapFlow workspace config is optional at `/.leapflow/config.yaml`; runtime config is loaded from `~/.leapflow/config/user.yaml` and `~/.leapflow/profiles//config/*.yaml`.\n- Preserve this task contract across summarization, compression, tool loops, and memory retrieval.\n" + }, + { + "role": "system", + "content": "\n## Recent Session Summary\n- [assistant] Hello from LeapFlow.\n- [user] Use the file_read tool on invoice.txt and report the total.\n" + }, + { + "role": "assistant", + "content": "Hello from LeapFlow." + }, + { + "role": "user", + "content": "Use the file_read tool on invoice.txt and report the total.\nUse the file_read tool on invoice.txt and report the total." + } + ], + "tools": [ + "capability_expand", + "code_intel", + "code_search", + "config_get", + "config_list", + "env_info", + "file_find", + "file_list", + "file_read", + "git_query", + "lint_check", + "memory_search", + "plugin_list", + "plugin_propose", + "plugin_status", + "plugin_versions", + "repo_map", + "research_note", + "runtime_snapshot", + "schedule_reentry", + "self_describe", + "session_list", + "skill_view", + "skills_list", + "terminal_list", + "terminal_read", + "test_run", + "text_search", + "time_get", + "tool_describe", + "tool_search", + "web_fetch" + ] + }, + "responses": [ + { + "status": 200, + "content_type": "application/json", + "body": "{\"id\": \"chatcmpl-cassette\", \"object\": \"chat.completion\", \"model\": \"cassette-model\", \"choices\": [{\"index\": 0, \"message\": {\"role\": \"assistant\", \"content\": \"\", \"tool_calls\": [{\"id\": \"call_1\", \"type\": \"function\", \"function\": {\"name\": \"file_read\", \"arguments\": \"{\\\"path\\\": \\\"invoice.txt\\\"}\"}}]}, \"finish_reason\": \"tool_calls\"}], \"usage\": {\"prompt_tokens\": 64, \"completion_tokens\": 16, \"total_tokens\": 80}}" + } + ] +} diff --git a/tests/_fixtures/cassettes/r1_conversation/cassette-model-b37c5a15793b65ba.cassette.json b/tests/_fixtures/cassettes/r1_conversation/cassette-model-b37c5a15793b65ba.cassette.json deleted file mode 100644 index 9cfd9f11..00000000 --- a/tests/_fixtures/cassettes/r1_conversation/cassette-model-b37c5a15793b65ba.cassette.json +++ /dev/null @@ -1,67 +0,0 @@ -{ - "fingerprint": "b37c5a15793b65baaabf7fde149814aa850332dc52421c15de01db69662454ab", - "note": "captured in seed mode", - "request": { - "model": "cassette-model", - "stream": false, - "messages": [ - { - "role": "system", - "content": "You are LeapFlow, an intelligent assistant that can both converse naturally and take real actions on the user's computer.\n\n## Capabilities\nThe tool index below lists **every** registered tool by name and a one-line summary — this is the complete\ncapability contract; nothing else exists. Only a subset is directly callable this turn (via native tool calling,\nnot a JSON block in your reply). If you need a tool from the index that is not yet callable, call\n`capability_expand` with its category name first — the matching tools become callable immediately after.\nWhen the user asks what LeapFlow itself supports, whether it supports plugins, or which runtime capabilities\nare available, use the live capability evidence exposed by `plugin_list` before making capability claims; report\nconfiguration-dependent or unavailable capabilities as limitations instead of inferring from documentation.\n- **text_search**(text, pattern): Search for a regex pattern in text.\n- **text_replace**(text, old, new, count) [capability_expand category: write]: Replace occurrences of a substring in text.\n- **time_get**(): Get current date and time.\n- **env_info**(): Get system environment information (OS, Python version, cwd).\n- **skills_list**(query, category, source): List available learned skills. Use when user asks about capabilities or you need a specific skill.\n- **skill_view**(name): View the full content of a specific skill document.\n- **code_intel**(path, operation): Precise document symbols (outline) for a source file: classes, functions, and methods with line ranges. Python uses an exact AST parse; other languages use a keyword-prefix scan. Prefer over file_read mode=symbols for accurate navigation before editing. Read-only.\n- **repo_map**(path): Compact project orientation for a repository root: languages, detected test/lint commands, top-level structure, entry points, manifest, and VCS branch. Call this first when entering an unfamiliar codebase. Read-only.\n- **scm_sync**(action, cwd, remote, pull_ref, push_ref, timeout) [capability_expand category: scm]: Run a typed git SCM action. Use this instead of shell_run for git pull/push/status. For 'pull origin main then push', set action='pull_then_push', remote='origin', pull_ref='main', and omit push_ref so LeapFlow pushes the current local branch.\n- **git_query**(action, cwd, ref, path, staged, max_count, stat): Read-only structured git inspection: action=diff|log|status|branch|show. Prefer over shell_run for reading repo state — output is clipped, redacted, and log/branch are parsed into structured fields. Use scm_sync for pull/push.\n- **git_write**(action, cwd, message, stage_all, name, ref, create) [capability_expand category: scm]: Mutating git actions: action=commit (message, stage_all), branch (create+switch), checkout (switch; create=true for -b). Approval-gated. Use scm_sync for pull/push and git_query for reads.\n- **test_run**(command, cwd, timeout): Run the project's test suite and return structured results (framework, passed/failed counts, failing tests). Auto-detects the runner (pytest/npm/go/cargo) or uses a configured/explicit command; executes via the governed shell. ok=true means the runner executed — see 'success' for pass/fail.\n- **lint_check**(command, cwd, timeout): Run the project's linter and return a structured clean/issue result. Auto-detects the linter (ruff/eslint/go vet/clippy) or uses a configured/explicit command; executes via the governed shell. ok=true means the linter ran — see 'clean'.\n- **file_list**(path, pattern, depth): List files and directories at a given path. Use depth=1 or depth=2 to get a recursive tree in one call instead of listing each sub-directory separately.\n- **file_read**(path, max_lines, start_line, max_chars, mode): Read text file content with adaptive context governance. For large or unfamiliar files, prefer mode='outline' or mode='symbols' first, then use mode='raw' with start_line/max_lines for the specific range you actually need. For LeapFlow's own settings, use config_list / config_get / config_set — its config files are outside the workspace and not readable here.\n- **file_write**(path, content, mode) [capability_expand category: write]: Write content to a file (overwrite or append).\n- **code_search**(pattern, patterns, path, glob, ignore_case, multiline, max_results, context_lines): Search file CONTENTS by regex pattern across a directory tree (ripgrep-backed). Requires a regex pattern. NOT for listing or browsing directory contents — use file_list for that. Prefer this over shell_run grep: faster, skips VCS/dependency/build dirs, and returns structured path:line:column matches. Batch related lookups into ONE call via `patterns` (OR-combined, single pass) instead of issuing several separate searches. Use file_read for the surrounding context of a hit.\n- **file_find**(glob, path, max_results): Find files by a recursive glob pattern under a base path (e.g. '**/test_*.py' or '*.md'). Prefer this over shell_run find; skips VCS/dependency/build dirs.\n- **edit_file**(path, edits, dry_run, diff) [capability_expand category: write]: Apply targeted, anchored search-replace edits to an EXISTING text file (use file_write to create/overwrite). Each edit is {original_text, new_text, replace_all?}; original_text must match exactly and uniquely (or set replace_all) — a non-unique or missing anchor is rejected so files are never corrupted. Set dry_run to preview. Alternatively pass a unified 'diff' to apply its hunks as anchored edits. Far cheaper and safer than rewriting a whole file.\n- **shell_run**(command, cwd, timeout) [capability_expand category: shell]: Execute a one-shot shell command with timeout protection. Runs in the active workspace; paths resolving outside it are refused. Reach for a structured tool first when one fits — web_fetch for anything over HTTP(S), git_query/scm_sync for git, code_search/file_find/file_read for the repo, config_get/config_set for LeapFlow's own settings — because those report typed results, while a failed shell command can only be diagnosed from its exit code and stderr. Every shell run counts as an external side effect, so a failure stops the rest of the batch and is not retried automatically.\n- **terminal_open**(command, cwd, shell) [capability_expand category: terminal]: Open a PERSISTENT shell session (REPL/dev server/watch), returning a session_id for terminal_send/read/close. Disabled unless tools.terminal_session_enabled is set. For one-shot commands use shell_run instead.\n- **terminal_send**(session_id, input, wait) [capability_expand category: terminal]: Send a line of input to a persistent terminal session and return output captured shortly after.\n- **terminal_read**(session_id, wait): Drain buffered output from a persistent terminal session (optionally waiting briefly first).\n- **terminal_close**(session_id) [capability_expand category: terminal]: Terminate a persistent terminal session and release its process group.\n- **terminal_list**(): List active persistent terminal sessions.\n- **config_list**(category, limit): List LeapFlow's own writable settings (LLM model and endpoint, daemon, memory, perception, gateway, …) with current values. Use this to discover the exact key before changing anything. Optionally narrow by `category`. This is the only correct way to inspect LeapFlow configuration — never read config files from disk.\n- **config_get**(key): Read one LeapFlow setting by key (e.g. 'llm.model', 'llm.base_url', 'daemon.log_level'), returning its current value, type, scopes, and whether a change needs a daemon restart. There is no 'llm.provider' key: provider behavior is inferred from the OpenAI-compatible 'llm.base_url'. Never read LeapFlow config files from disk — use this.\n- **config_set**(key, value, scope) [capability_expand category: config]: Change one LeapFlow setting by key, e.g. switch an OpenAI-compatible LLM with key='llm.model' and key='llm.base_url'. There is no 'llm.provider' key: provider behavior is inferred from the endpoint. Values are validated and coerced; credentials are stored in the vault automatically. Call config_list or config_get first if unsure of the key. The result states whether a `leap daemon restart` is required. Never edit LeapFlow config files directly.\n- **web_fetch**(url, select, timeout, max_bytes): Read a URL over HTTP(S) and get back extracted, context-sized content: parsed JSON for API endpoints, readable text plus links for web pages. Use this for anything on the internet — prices, docs, releases, articles — instead of running curl through shell_run: it reports real HTTP status codes, retries rate limits on its own, and is a plain read so a retry is always safe. For JSON APIs pass `select` with a dotted path (e.g. 'chart.result.0.meta') to return just that part instead of the whole payload.\n- **memory_search**(query, limit): Search agent memory for relevant past experiences, observations, and facts.\n- **memory_add**(content, kind) [capability_expand category: write]: Store a new observation or insight in memory for future reference.\n- **research_note**(kind, text): Record a compact, structured note about the current task's state so it survives context compression on long / multi-step tasks. Use for durable findings, open questions still to resolve, decisions / excluded paths, and the immediate next step. One concise sentence per note.\n- **capability_expand**(category): Fetch the full callable schema for every tool in a capability category (e.g. 'hub', 'gateway', 'desktop', 'delegate', 'file', 'memory', 'skill'). The compact tool index always lists every registered tool by name and a one-line summary, but only a static low-risk subset is directly callable each turn. If you need a tool from the index that is not yet callable, call capability_expand with its category first; the matching tools become callable in this turn. Never invent a tool name — expand the category instead.\n- **delegate_task**(goal, context) [capability_expand category: delegate]: Delegate a complex sub-task to an isolated subagent. The subagent gets a fresh context and restricted tool access. Use when a task is self-contained and can be solved independently.\n- **schedule_reentry**(kind, reason, delay_seconds, event_match, max_reentries, deadline_seconds): Register a re-entry so this task can resume later from its current orientation (findings / open questions / next step). Use when work must pause and continue after a delay (kind=time) or when a matching platform event arrives (kind=event), instead of finishing now. The research-ledger state is carried over automatically.\n- **hub_push**(skill_name, visibility, version) [capability_expand category: hub]: Push a local skill to the ModelScope Hub for sharing or backup.\n- **hub_pull**(repo_id, version) [capability_expand category: hub]: Pull a skill from the ModelScope Hub to install locally.\n- **hub_search**(query) [capability_expand category: hub]: Search for skills on the Hub by keyword or description.\n- **hub_sync**(mode, dry_run) [capability_expand category: hub]: Preview or execute sync between local skills and Hub.\n- **platform_action**(platform, action, payload, backend_kind) [capability_expand category: gateway]: Execute an exact registered business action on an external platform through LeapFlow's App Connector layer. Actions must be copied from the App Connector Capability Index and are addressed as domain.operation, e.g. im.send_message or docs.create_markdown. All business fields (chat_id, text, query, etc.) MUST be placed inside `payload`, never at the top level. Example: {\"platform\":\"feishu\",\"action\":\"im.send_message\",\"payload\":{\"chat_id\":\"oc_xxx\",\"text\":\"hello\"}}. Do not invent action names, do not use management actions such as list/guide/connect/status here.\n- **platform_connect**(action, platform, credentials, options, checkpoint) [capability_expand category: gateway]: List, guide, connect, disconnect, remove, or check status for external platforms using the App Connector management namespace. Supports REST and CLI backends. Use this for management actions such as list/guide/preflight/connect/status; use platform_action only for exact registered business actions.\n- **gateway_send**(platform, chat_id, text, thread_id) [capability_expand category: gateway]: Send a message to a connected external platform (Feishu group, Telegram chat, DingTalk conversation, etc.). Requires the platform to be connected via gateway_connect first. Use gateway_connect with action='list' to see connected platforms and available chat IDs.\n- **gateway_connect**(action, platform, credentials, options) [capability_expand category: gateway]: Connect, configure, or manage external platform integrations (Feishu, DingTalk, Telegram, Slack, Discord, etc.). Conversational flow: 1) call 'guide' to get setup steps + required fields, 2) present the steps to the user and ask for ALL required credentials in a single message, 3) call 'connect' with the credentials. Goal: complete in 1–2 user turns. NEVER include credential values in your text response.\n- **plugin_list**(): List the live plugin registry and cross-subsystem capability evidence. Use this before answering questions about whether LeapFlow supports plugins, self-evolution, plugin installation, hot reload, versioning, or other runtime capabilities.\n- **plugin_status**(plugin_id): Get detailed status of a specific plugin: its declared category, runtime dependencies, contributed tools, and fiber lifecycle state.\n- **plugin_versions**(plugin_id): List recorded source versions and active pointer for a profile-scoped plugin.\n- **plugin_propose**(requested_capability, plugin_id, proposed_tools, test_cases, risk_level, evidence): Create a side-effect-free PluginProposal from explicit capability-gap evidence. Use this before plugin_generate when a missing capability should be reviewed. Does not call an LLM, write files, or install anything.\n- **assess_compatibility**(manifest, source_path) [capability_expand category: plugin_management]: Assess whether a foreign plugin manifest or real DSH source bundle is compatible with LeapFlow. A source bundle assessment is static: runtime_ready stays false until restricted Node discovery during plugin_install. Returns component-level verdicts and limitations.\n- **plugin_generate**(plugin_id, description, proposal_id) [capability_expand category: system]: Generate a new ToolPlugin from a natural-language capability description. The LLM produces code that conforms to the ToolPlugin Protocol; it is then rigorously validated (syntax, structure, import, protocol conformance). The generated source is stored in CAS and receives explicit content approval. It DOES NOT install the plugin — installation requires a second, mutation-specific approval via plugin_install.\n- **plugin_install**(plugin_id, code, marketplace_name, source_path, proposal_id, version_label) [capability_expand category: system]: Install a plugin from exactly one source: validated Python code, the configured Python marketplace, or a real DSH source bundle. DSH bundles run restricted Node discovery before registration; only public host tools are installed and client UI limitations are reported. Writes profile-scoped state and mutates the process-global registry. REQUIRES APPROVAL.\n- **plugin_rollback**(plugin_id, version) [capability_expand category: system]: Rollback a profile-scoped plugin to a recorded source version and reload it. REQUIRES APPROVAL.\n- **plugin_reload**(plugin_id, version_label) [capability_expand category: system]: Hot-reload a plugin at runtime. Disposes the old plugin fiber, re-imports its module, and registers a fresh instance. Existing in-flight turns are unaffected (snapshot isolation). REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_disable**(plugin_id) [capability_expand category: system]: Disable a plugin by disposing its fiber, removing its tools from the runtime registry. Cannot disable self_management itself. REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_remove**(plugin_id, delete_source) [capability_expand category: system]: Terminally remove a plugin: dispose its fiber, unregister its tools, remove reload metadata, and optionally delete its profile-scoped source file. Cannot remove self_management itself. REQUIRES APPROVAL.\n- **plugin_unquarantine**(plugin_id) [capability_expand category: plugin_management]: Restore a quarantined plugin to probation status for re-evaluation. Unfreezes the trust ledger, transitions the proposal back to PROBATION, and reloads the plugin. REQUIRES APPROVAL.\n- **plugin_enable**(plugin_id) [capability_expand category: system]: Re-enable a previously disabled plugin by reloading its module and registering a fresh instance. REQUIRES APPROVAL.\n- **self_describe**(facet): Introspect LeapFlow's own identity, capabilities, runtime state, evolution metrics, or platform connections. Use facet='all' only when a comprehensive self-check is explicitly requested.\n- **runtime_snapshot**(): Lightweight (~150 token) flat snapshot of current runtime state: model, context budget, posture, disclosure level, turn count, cache hit rate, uptime, tool count, and pending approvals.\n- **schedule_create**(trigger_expression, instruction, execution_mode, max_retries, delivery_target) [capability_expand category: scheduler]: Create a new scheduled task. Specify a trigger expression (e.g. '30m', 'every 2h', '0 9 * * *') and the instruction to execute on each trigger. Optionally provide an execution mode and a delivery target to receive notifications.\n- **schedule_list**() [capability_expand category: scheduler]: List all scheduled tasks with their current state, trigger, and next due time.\n- **schedule_status**(task_id) [capability_expand category: scheduler]: Get detailed status and recent execution history for a scheduled task.\n- **schedule_pause**(task_id) [capability_expand category: scheduler]: Pause a scheduled task so it stops firing without being cancelled.\n- **schedule_resume**(task_id) [capability_expand category: scheduler]: Resume a paused scheduled task — re-arms it and recalculates next due time.\n- **schedule_cancel**(task_id) [capability_expand category: scheduler]: Cancel a scheduled task permanently.\n- **tool_search**(query, max_results): Search all registered tools by keyword relevance. Returns a ranked list of matching tools with name, category, and summary. Use this when you need a tool but are unsure of its exact name or category.\n- **tool_describe**(tool_name): Get the full callable schema of a single tool by exact name. Returns the tool's description, parameters, and metadata. Use after tool_search to inspect a specific tool before calling it.\n- **session_search**(query, limit) [capability_expand category: unclassified]: Search past conversation sessions in the current workspace for relevant context.\n- **session_list**(limit): List recent conversation sessions in the current workspace with stable ids, titles, dates, and summaries. Use for browsing past tasks or when user asks to see history without specific search terms. No keywords needed — returns chronological list.\n- **session_detail**(session_id, limit, offset, include_inactive) [capability_expand category: unclassified]: Read a paginated persisted transcript for one past conversation session in the current workspace. Use after session_list or session_search returns a session_id.\n- **click**(element_index) [capability_expand category: desktop]: Click a UI element by its element_index (from the latest observe_ui snapshot)\n- **get_clipboard**() [capability_expand category: desktop]: Read current clipboard text content\n- **list_apps**(filter, running_only) [capability_expand category: desktop]: List available applications on this system. Use to discover correct bundle_id before switch_app.\n- **list_windows**() [capability_expand category: desktop]: List all top-level windows with pid, window_id, title, and per-window state (minimized, on-screen). Call this first to pick the pid and window_id that observe_ui and other window tools require.\n- **observe_ui**(pid, window_id, query) [capability_expand category: desktop]: Snapshot one window's actionable UI elements, each tagged with an element_index for click/right_click/read_text. Re-observe after actions — indices belong to one snapshot. Requires the window's pid and window_id from list_windows.\n- **open_url**(url, app_id) [capability_expand category: desktop]: Open a URL in the default or specified browser\n- **read_text**(element_index) [capability_expand category: desktop]: Read the text content of a specific UI element from the latest snapshot\n- **right_click**(element_index) [capability_expand category: desktop]: Right-click a UI element to open its context menu. Returns visible menu items.\n- **screenshot**(pid, window_id) [capability_expand category: desktop]: Capture a screenshot for visual verification. With pid + window_id captures that window (works across all displays); defaults to the last observed window, or the full desktop when no window has been observed.\n- **scroll**(element_index, direction, amount, pid, window_id) [capability_expand category: desktop]: Scroll a scrollable area of a window. Omit element_index to scroll the window's focused/page scroller; pass one to scroll an exact element from the latest snapshot.\n- **select_text**(element_index) [capability_expand category: desktop]: Select all text in a UI element (focus + select-all, for subsequent copy)\n- **set_clipboard**(text) [capability_expand category: desktop]: Write text to the clipboard\n- **shortcut**(keys) [capability_expand category: desktop]: Execute a keyboard shortcut\n- **switch_app**(app_id) [capability_expand category: desktop]: Switch to an app (launch if needed, activate, verify)\n- **type_text**(text) [capability_expand category: desktop]: Type text into the currently focused element\n- **wait**(seconds) [capability_expand category: desktop]: Wait for a specified duration before continuing\n- **wait_until**(condition, pid, window_id, timeout, poll_interval) [capability_expand category: desktop]: Wait until a UI condition is met (polls UI tree). Returns elements when found or on timeout.\n- **wait_until_stable**(timeout, poll_interval, pid, window_id) [capability_expand category: desktop]: Wait until the UI stops changing (element set stabilizes across polls).\n\n## App Connector Capability Index\nLeapFlow can onboard and manage external apps through `platform_connect` and execute exact registered business actions through `platform_action`.\nFor requests about connecting, setting up, configuring, enabling, or managing a supported app, use `platform_connect` first instead of generating SDK/Webhook sample code.\n`platform_connect.action` is the management namespace: list, guide, preflight, connect, disconnect, remove, status, events_start, events_stop, events_status.\n`platform_action.action` is only for exact registered platform business actions listed below, such as `im.send_message`; never use management actions like `list` or `guide` there.\nAll business fields MUST go inside `payload`; top-level keys are only `platform`, `action`, and `payload`.\nDo not invent platform IDs or platform action names. If the needed action is not listed, ask for discovery/clarification instead of guessing.\nUse `platform_connect` with `action='guide'` and the matching `platform` to start onboarding; use `action='list'` when the app is unclear.\nWhen a pending onboarding state is present, continue from that state with `platform_connect` instead of asking the user to restate the app.\nSupported apps:\n- `api_server`: API Server (OpenAI Compatible) (category=api; backend=adapter; platform_action actions=none registered)\n- `dingtalk`: 钉钉 (DingTalk) (category=im; backend=adapter; platform_action actions=none registered)\n- `feishu`: 飞书 (Feishu/Lark) (category=im; backend=cli)\n - `calendar.create_event` payload={summary*, start_time*, end_time*, attendees} [write/high]\n - `docs.create_markdown` payload={title*, markdown*, folder_token} [write/medium]\n - `drive.search` payload={query*, limit} [read/medium]\n - `im.add_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.download_resource` payload={message_id*, file_key*, type*} [read/low]\n - `im.get_messages` payload={message_ids*} [read/low]\n - `im.list_chats` payload={page_size} [read/low]\n - `im.list_messages` payload={chat_id*} [read/low]\n - `im.list_thread_messages` payload={thread*} [read/low]\n - `im.remove_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.reply_message` payload={message_id*, text*} [send/high]\n - `im.search_chats` payload={query*, page_size} [read/low]\n - `im.search_messages` payload={query*} [read/medium]\n - `im.send_message` payload={chat_id*, text*, thread_id} [send/high]\n - `im.update_card` payload={token*, card*} [write/medium]\n - `im.update_message` payload={message_id*, text*} [write/medium]\n - `mail.search_unread` payload={query, limit} [read/high]\n - `sheets.append_row` payload={spreadsheet_token*, sheet_id*, values*} [write/medium]\n - `task.create` payload={title*, description, due_time} [write/medium]\n- `telegram`: Telegram (category=social; backend=adapter; platform_action actions=none registered)\n- `webhook`: Webhook (Generic) (category=webhook; backend=adapter; platform_action actions=none registered)\n\n## Tool Usage\nTools are normally invoked through the native function-calling mechanism, not by writing JSON in your reply\ntext. Only if the provider signals that native function calling is unavailable for this turn, fall back to a\nsingle JSON code block: `{\"name\": \"tool_name\", \"arguments\": {\"key\": \"value\"}}` — use this fallback format\nonly, never both. Only call a tool whose exact name appears in the tool index above and is currently callable\n(or reachable via `capability_expand`). Never invent, rename, alias, or guess a tool name, platform ID, or\nplatform action from argument shape or wording — if the index does not list it, it does not exist.\n`platform_connect.action` (list/guide/preflight/connect/disconnect/remove/status/events_start/events_stop/\nevents_status) is the App Connector management namespace; `platform_action.action` only accepts exact\nregistered business actions such as `im.send_message` shown in the App Connector Capability Index — never mix\nthe two namespaces. If a tool call returns an unknown/unavailable result, use the returned suggestions or\navailable names for a single retry instead of trying further variations of the same guess.\n\n**Side-effect action rule** (`platform_action` with effect=send/write/execute):\n- Call each unique action+payload **exactly once**. Never include duplicates in the same turn.\n- Once the result returns `\"completed\": true`, that action is DONE for this task. Do NOT call it again\n in any subsequent turn — immediately summarize the result for the user instead.\n- The system enforces idempotency: duplicate calls are blocked and will not execute.\n- If the user explicitly requests sending/writing multiple times, use distinct payloads per call.\n\n**Resource identifier provenance rule**:\n- NEVER fabricate, guess, or infer resource identifiers (chat_id, message_id, file_key, user_id, etc.).\n Every resource ID used in a side-effect action MUST come from a successful API response in this session.\n- If a read/list action fails (e.g. authorization error), you do NOT have valid resource IDs.\n Report the failure to the user — do NOT attempt the dependent write/send action with a guessed ID.\n- When a tool result contains `\"llm_instruction\"`, follow it exactly.\n\n## Guidelines\n1. **Direct answers first**: If you already know the answer, respond directly without tools.\n2. **Avoid redundant tool calls**: Do not call the same tool with the same arguments more than once in the same user turn. When an existing tool result already answers the user's request, stop calling tools and answer directly.\n3. **Use tools proactively**: When the user asks about files, time, system state, or needs actions performed, use the appropriate tool.\n4. **Chain tools when needed**: You can call multiple tools in sequence (e.g., list files → read file → summarize).\n5. **Handle failures gracefully**: If a tool fails, explain what went wrong and suggest alternatives. If it failed because of an unknown tool/platform/action name, retry once with an exact name from the returned suggestions, then explain rather than keep guessing.\n6. **Summarize results naturally**: After tool execution, synthesize the results into a helpful answer rather than dumping raw output.\n7. **Stay conversational**: Maintain a natural, helpful tone. Acknowledge context from earlier in the conversation.\n8. **Recall past work**:\n - Broad queries (\"之前做了什么\", \"列出任务\"): answer from the \"Recent Task History\" section already in your context. If insufficient, call session_list.\n - Specific lookups (\"上次那个配置怎么改的\"): call session_search with relevant phrases (NOT single characters).\n - Do NOT call search tools repeatedly with keyword fragments. One well-phrased call is better than ten fragmented ones.\n\n## Coding & Verification\nWhen working with code, prefer the precise built-in tools over ad-hoc shell: use `repo_map` to orient in an\nunfamiliar project, `code_search`/`file_find` to locate and `code_intel` for symbols, `edit_file` (anchored\nsearch-replace, or a unified `diff`) to change files — never rewrite a whole file to change a few lines — and\n`git_query` to inspect diffs/log. After edits, check `syntax_ok` in the result and run `test_run`/`lint_check`\nbefore declaring the work done. Batch independent read-only calls (search/read/query) into a single turn — they\nrun in parallel.\n\n## Presentation Style\n1. **Polished Markdown only**: Format user-facing answers with clean Markdown headings, short paragraphs, and concise bullets. Use tables only when they improve comparison or scanning.\n2. **Terminal-friendly layout**: Keep lines readable in a TUI; avoid dense walls of text, deeply nested lists, oversized ASCII art, or heavy visual blocks.\n3. **Elegant emphasis**: Use bold text sparingly for key terms and conclusions. Avoid excessive emojis, decorative symbols, repeated separators, or visual noise.\n4. **Theme-safe colors**: Do not emit ANSI escape codes, HTML color tags, Rich markup, or hardcoded color names. Rely on the TUI theme to render Markdown professionally.\n5. **No leaked tool protocol**: Never show tool-call JSON, internal schemas, raw observations, tool result payloads, or hidden reasoning in the final answer unless the user explicitly asks for raw/debug output. Treat any prior `{\"name\": ..., \"arguments\": ...}` blocks and `Tool result (...)` messages as internal execution context only.\n6. **Professional closure**: End with a concise conclusion or next step when helpful; avoid rambling after the useful answer is complete.\n\nWhen finished with all tool calls, respond normally without a JSON block, tool-call transcript, or process log.\n\n## Task Contract\n- Task ID: turn-2\n- Original user request: Use the file_read tool on invoice.txt and report the total.\n- Workspace root: \n- Allowed roots: \n- Treat relative project paths as relative to the workspace root; never infer `.` as the project root when a workspace root is provided.\n- Workspace boundary is enforced by tools: do not read, search, edit, or run commands against paths outside the allowed roots unless the user explicitly requests an external path and the tool/approval policy permits it.\n- LeapFlow workspace config is optional at `/.leapflow/config.yaml`; runtime config is loaded from `~/.leapflow/config/user.yaml` and `~/.leapflow/profiles//config/*.yaml`.\n- Preserve this task contract across summarization, compression, tool loops, and memory retrieval.\n" - }, - { - "role": "system", - "content": "\n## Recent Session Summary\n- [assistant] Hello from LeapFlow.\n- [user] Use the file_read tool on invoice.txt and report the total.\n" - }, - { - "role": "assistant", - "content": "Hello from LeapFlow." - }, - { - "role": "user", - "content": "Use the file_read tool on invoice.txt and report the total.\nUse the file_read tool on invoice.txt and report the total." - } - ], - "tools": [ - "capability_expand", - "code_intel", - "code_search", - "config_get", - "config_list", - "env_info", - "file_find", - "file_list", - "file_read", - "git_query", - "lint_check", - "memory_search", - "plugin_list", - "plugin_propose", - "plugin_status", - "plugin_versions", - "repo_map", - "research_note", - "runtime_snapshot", - "schedule_reentry", - "self_describe", - "session_list", - "skill_view", - "skills_list", - "terminal_list", - "terminal_read", - "test_run", - "text_search", - "time_get", - "tool_describe", - "tool_search", - "web_fetch" - ] - }, - "responses": [ - { - "status": 200, - "content_type": "application/json", - "body": "{\"id\": \"chatcmpl-cassette\", \"object\": \"chat.completion\", \"model\": \"cassette-model\", \"choices\": [{\"index\": 0, \"message\": {\"role\": \"assistant\", \"content\": \"\", \"tool_calls\": [{\"id\": \"call_1\", \"type\": \"function\", \"function\": {\"name\": \"file_read\", \"arguments\": \"{\\\"path\\\": \\\"invoice.txt\\\"}\"}}]}, \"finish_reason\": \"tool_calls\"}], \"usage\": {\"prompt_tokens\": 64, \"completion_tokens\": 16, \"total_tokens\": 80}}" - } - ] -} diff --git a/tests/_fixtures/cassettes/r1_conversation/cassette-model-d8f8fe8c20d702ae.cassette.json b/tests/_fixtures/cassettes/r1_conversation/cassette-model-d8f8fe8c20d702ae.cassette.json deleted file mode 100644 index 7cfc605a..00000000 --- a/tests/_fixtures/cassettes/r1_conversation/cassette-model-d8f8fe8c20d702ae.cassette.json +++ /dev/null @@ -1,63 +0,0 @@ -{ - "fingerprint": "d8f8fe8c20d702ae1da84920137f9c650cf832b61889605dfd4890267677c756", - "note": "captured in seed mode", - "request": { - "model": "cassette-model", - "stream": false, - "messages": [ - { - "role": "system", - "content": "You are LeapFlow, an intelligent assistant that can both converse naturally and take real actions on the user's computer.\n\n## Capabilities\nThe tool index below lists **every** registered tool by name and a one-line summary — this is the complete\ncapability contract; nothing else exists. Only a subset is directly callable this turn (via native tool calling,\nnot a JSON block in your reply). If you need a tool from the index that is not yet callable, call\n`capability_expand` with its category name first — the matching tools become callable immediately after.\nWhen the user asks what LeapFlow itself supports, whether it supports plugins, or which runtime capabilities\nare available, use the live capability evidence exposed by `plugin_list` before making capability claims; report\nconfiguration-dependent or unavailable capabilities as limitations instead of inferring from documentation.\n- **text_search**(text, pattern): Search for a regex pattern in text.\n- **text_replace**(text, old, new, count) [capability_expand category: write]: Replace occurrences of a substring in text.\n- **time_get**(): Get current date and time.\n- **env_info**(): Get system environment information (OS, Python version, cwd).\n- **skills_list**(query, category, source): List available learned skills. Use when user asks about capabilities or you need a specific skill.\n- **skill_view**(name): View the full content of a specific skill document.\n- **code_intel**(path, operation): Precise document symbols (outline) for a source file: classes, functions, and methods with line ranges. Python uses an exact AST parse; other languages use a keyword-prefix scan. Prefer over file_read mode=symbols for accurate navigation before editing. Read-only.\n- **repo_map**(path): Compact project orientation for a repository root: languages, detected test/lint commands, top-level structure, entry points, manifest, and VCS branch. Call this first when entering an unfamiliar codebase. Read-only.\n- **scm_sync**(action, cwd, remote, pull_ref, push_ref, timeout) [capability_expand category: scm]: Run a typed git SCM action. Use this instead of shell_run for git pull/push/status. For 'pull origin main then push', set action='pull_then_push', remote='origin', pull_ref='main', and omit push_ref so LeapFlow pushes the current local branch.\n- **git_query**(action, cwd, ref, path, staged, max_count, stat): Read-only structured git inspection: action=diff|log|status|branch|show. Prefer over shell_run for reading repo state — output is clipped, redacted, and log/branch are parsed into structured fields. Use scm_sync for pull/push.\n- **git_write**(action, cwd, message, stage_all, name, ref, create) [capability_expand category: scm]: Mutating git actions: action=commit (message, stage_all), branch (create+switch), checkout (switch; create=true for -b). Approval-gated. Use scm_sync for pull/push and git_query for reads.\n- **test_run**(command, cwd, timeout): Run the project's test suite and return structured results (framework, passed/failed counts, failing tests). Auto-detects the runner (pytest/npm/go/cargo) or uses a configured/explicit command; executes via the governed shell. ok=true means the runner executed — see 'success' for pass/fail.\n- **lint_check**(command, cwd, timeout): Run the project's linter and return a structured clean/issue result. Auto-detects the linter (ruff/eslint/go vet/clippy) or uses a configured/explicit command; executes via the governed shell. ok=true means the linter ran — see 'clean'.\n- **file_list**(path, pattern, depth): List files and directories at a given path. Use depth=1 or depth=2 to get a recursive tree in one call instead of listing each sub-directory separately.\n- **file_read**(path, max_lines, start_line, max_chars, mode): Read text file content with adaptive context governance. For large or unfamiliar files, prefer mode='outline' or mode='symbols' first, then use mode='raw' with start_line/max_lines for the specific range you actually need. For LeapFlow's own settings, use config_list / config_get / config_set — its config files are outside the workspace and not readable here.\n- **file_write**(path, content, mode) [capability_expand category: write]: Write content to a file (overwrite or append).\n- **code_search**(pattern, patterns, path, glob, ignore_case, multiline, max_results, context_lines): Search file CONTENTS by regex pattern across a directory tree (ripgrep-backed). Requires a regex pattern. NOT for listing or browsing directory contents — use file_list for that. Prefer this over shell_run grep: faster, skips VCS/dependency/build dirs, and returns structured path:line:column matches. Batch related lookups into ONE call via `patterns` (OR-combined, single pass) instead of issuing several separate searches. Use file_read for the surrounding context of a hit.\n- **file_find**(glob, path, max_results): Find files by a recursive glob pattern under a base path (e.g. '**/test_*.py' or '*.md'). Prefer this over shell_run find; skips VCS/dependency/build dirs.\n- **edit_file**(path, edits, dry_run, diff) [capability_expand category: write]: Apply targeted, anchored search-replace edits to an EXISTING text file (use file_write to create/overwrite). Each edit is {original_text, new_text, replace_all?}; original_text must match exactly and uniquely (or set replace_all) — a non-unique or missing anchor is rejected so files are never corrupted. Set dry_run to preview. Alternatively pass a unified 'diff' to apply its hunks as anchored edits. Far cheaper and safer than rewriting a whole file.\n- **shell_run**(command, cwd, timeout) [capability_expand category: shell]: Execute a one-shot shell command with timeout protection. Runs in the active workspace; paths resolving outside it are refused. Reach for a structured tool first when one fits — web_fetch for anything over HTTP(S), git_query/scm_sync for git, code_search/file_find/file_read for the repo, config_get/config_set for LeapFlow's own settings — because those report typed results, while a failed shell command can only be diagnosed from its exit code and stderr. Every shell run counts as an external side effect, so a failure stops the rest of the batch and is not retried automatically.\n- **terminal_open**(command, cwd, shell) [capability_expand category: terminal]: Open a PERSISTENT shell session (REPL/dev server/watch), returning a session_id for terminal_send/read/close. Disabled unless tools.terminal_session_enabled is set. For one-shot commands use shell_run instead.\n- **terminal_send**(session_id, input, wait) [capability_expand category: terminal]: Send a line of input to a persistent terminal session and return output captured shortly after.\n- **terminal_read**(session_id, wait): Drain buffered output from a persistent terminal session (optionally waiting briefly first).\n- **terminal_close**(session_id) [capability_expand category: terminal]: Terminate a persistent terminal session and release its process group.\n- **terminal_list**(): List active persistent terminal sessions.\n- **config_list**(category, limit): List LeapFlow's own writable settings (LLM model and endpoint, daemon, memory, perception, gateway, …) with current values. Use this to discover the exact key before changing anything. Optionally narrow by `category`. This is the only correct way to inspect LeapFlow configuration — never read config files from disk.\n- **config_get**(key): Read one LeapFlow setting by key (e.g. 'llm.model', 'llm.base_url', 'daemon.log_level'), returning its current value, type, scopes, and whether a change needs a daemon restart. There is no 'llm.provider' key: provider behavior is inferred from the OpenAI-compatible 'llm.base_url'. Never read LeapFlow config files from disk — use this.\n- **config_set**(key, value, scope) [capability_expand category: config]: Change one LeapFlow setting by key, e.g. switch an OpenAI-compatible LLM with key='llm.model' and key='llm.base_url'. There is no 'llm.provider' key: provider behavior is inferred from the endpoint. Values are validated and coerced; credentials are stored in the vault automatically. Call config_list or config_get first if unsure of the key. The result states whether a `leap daemon restart` is required. Never edit LeapFlow config files directly.\n- **web_fetch**(url, select, timeout, max_bytes): Read a URL over HTTP(S) and get back extracted, context-sized content: parsed JSON for API endpoints, readable text plus links for web pages. Use this for anything on the internet — prices, docs, releases, articles — instead of running curl through shell_run: it reports real HTTP status codes, retries rate limits on its own, and is a plain read so a retry is always safe. For JSON APIs pass `select` with a dotted path (e.g. 'chart.result.0.meta') to return just that part instead of the whole payload.\n- **memory_search**(query, limit): Search agent memory for relevant past experiences, observations, and facts.\n- **memory_add**(content, kind) [capability_expand category: write]: Store a new observation or insight in memory for future reference.\n- **research_note**(kind, text): Record a compact, structured note about the current task's state so it survives context compression on long / multi-step tasks. Use for durable findings, open questions still to resolve, decisions / excluded paths, and the immediate next step. One concise sentence per note.\n- **capability_expand**(category): Fetch the full callable schema for every tool in a capability category (e.g. 'hub', 'gateway', 'desktop', 'delegate', 'file', 'memory', 'skill'). The compact tool index always lists every registered tool by name and a one-line summary, but only a static low-risk subset is directly callable each turn. If you need a tool from the index that is not yet callable, call capability_expand with its category first; the matching tools become callable in this turn. Never invent a tool name — expand the category instead.\n- **delegate_task**(goal, context) [capability_expand category: delegate]: Delegate a complex sub-task to an isolated subagent. The subagent gets a fresh context and restricted tool access. Use when a task is self-contained and can be solved independently.\n- **schedule_reentry**(kind, reason, delay_seconds, event_match, max_reentries, deadline_seconds): Register a re-entry so this task can resume later from its current orientation (findings / open questions / next step). Use when work must pause and continue after a delay (kind=time) or when a matching platform event arrives (kind=event), instead of finishing now. The research-ledger state is carried over automatically.\n- **hub_push**(skill_name, visibility, version) [capability_expand category: hub]: Push a local skill to the ModelScope Hub for sharing or backup.\n- **hub_pull**(repo_id, version) [capability_expand category: hub]: Pull a skill from the ModelScope Hub to install locally.\n- **hub_search**(query) [capability_expand category: hub]: Search for skills on the Hub by keyword or description.\n- **hub_sync**(mode, dry_run) [capability_expand category: hub]: Preview or execute sync between local skills and Hub.\n- **platform_action**(platform, action, payload, backend_kind) [capability_expand category: gateway]: Execute an exact registered business action on an external platform through LeapFlow's App Connector layer. Actions must be copied from the App Connector Capability Index and are addressed as domain.operation, e.g. im.send_message or docs.create_markdown. All business fields (chat_id, text, query, etc.) MUST be placed inside `payload`, never at the top level. Example: {\"platform\":\"feishu\",\"action\":\"im.send_message\",\"payload\":{\"chat_id\":\"oc_xxx\",\"text\":\"hello\"}}. Do not invent action names, do not use management actions such as list/guide/connect/status here.\n- **platform_connect**(action, platform, credentials, options, checkpoint) [capability_expand category: gateway]: List, guide, connect, disconnect, remove, or check status for external platforms using the App Connector management namespace. Supports REST and CLI backends. Use this for management actions such as list/guide/preflight/connect/status; use platform_action only for exact registered business actions.\n- **gateway_send**(platform, chat_id, text, thread_id) [capability_expand category: gateway]: Send a message to a connected external platform (Feishu group, Telegram chat, DingTalk conversation, etc.). Requires the platform to be connected via gateway_connect first. Use gateway_connect with action='list' to see connected platforms and available chat IDs.\n- **gateway_connect**(action, platform, credentials, options) [capability_expand category: gateway]: Connect, configure, or manage external platform integrations (Feishu, DingTalk, Telegram, Slack, Discord, etc.). Conversational flow: 1) call 'guide' to get setup steps + required fields, 2) present the steps to the user and ask for ALL required credentials in a single message, 3) call 'connect' with the credentials. Goal: complete in 1–2 user turns. NEVER include credential values in your text response.\n- **plugin_list**(): List the live plugin registry and cross-subsystem capability evidence. Use this before answering questions about whether LeapFlow supports plugins, self-evolution, plugin installation, hot reload, versioning, or other runtime capabilities.\n- **plugin_status**(plugin_id): Get detailed status of a specific plugin: its declared category, runtime dependencies, contributed tools, and fiber lifecycle state.\n- **plugin_versions**(plugin_id): List recorded source versions and active pointer for a profile-scoped plugin.\n- **plugin_propose**(requested_capability, plugin_id, proposed_tools, test_cases, risk_level, evidence): Create a side-effect-free PluginProposal from explicit capability-gap evidence. Use this before plugin_generate when a missing capability should be reviewed. Does not call an LLM, write files, or install anything.\n- **assess_compatibility**(manifest, source_path) [capability_expand category: plugin_management]: Assess whether a foreign plugin manifest or real DSH source bundle is compatible with LeapFlow. A source bundle assessment is static: runtime_ready stays false until restricted Node discovery during plugin_install. Returns component-level verdicts and limitations.\n- **plugin_generate**(plugin_id, description, proposal_id) [capability_expand category: system]: Generate a new ToolPlugin from a natural-language capability description. The LLM produces code that conforms to the ToolPlugin Protocol; it is then rigorously validated (syntax, structure, import, protocol conformance). The generated source is stored in CAS and receives explicit content approval. It DOES NOT install the plugin — installation requires a second, mutation-specific approval via plugin_install.\n- **plugin_install**(plugin_id, code, marketplace_name, source_path, proposal_id, version_label) [capability_expand category: system]: Install a plugin from exactly one source: validated Python code, the configured Python marketplace, or a real DSH source bundle. DSH bundles run restricted Node discovery before registration; only public host tools are installed and client UI limitations are reported. Writes profile-scoped state and mutates the process-global registry. REQUIRES APPROVAL.\n- **plugin_rollback**(plugin_id, version) [capability_expand category: system]: Rollback a profile-scoped plugin to a recorded source version and reload it. REQUIRES APPROVAL.\n- **plugin_reload**(plugin_id, version_label) [capability_expand category: system]: Hot-reload a plugin at runtime. Disposes the old plugin fiber, re-imports its module, and registers a fresh instance. Existing in-flight turns are unaffected (snapshot isolation). REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_disable**(plugin_id) [capability_expand category: system]: Disable a plugin by disposing its fiber, removing its tools from the runtime registry. Cannot disable self_management itself. REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_remove**(plugin_id, delete_source) [capability_expand category: system]: Terminally remove a plugin: dispose its fiber, unregister its tools, remove reload metadata, and optionally delete its profile-scoped source file. Cannot remove self_management itself. REQUIRES APPROVAL.\n- **plugin_unquarantine**(plugin_id) [capability_expand category: plugin_management]: Restore a quarantined plugin to probation status for re-evaluation. Unfreezes the trust ledger, transitions the proposal back to PROBATION, and reloads the plugin. REQUIRES APPROVAL.\n- **plugin_enable**(plugin_id) [capability_expand category: system]: Re-enable a previously disabled plugin by reloading its module and registering a fresh instance. REQUIRES APPROVAL.\n- **self_describe**(facet): Introspect LeapFlow's own identity, capabilities, runtime state, evolution metrics, or platform connections. Use facet='all' only when a comprehensive self-check is explicitly requested.\n- **runtime_snapshot**(): Lightweight (~150 token) flat snapshot of current runtime state: model, context budget, posture, disclosure level, turn count, cache hit rate, uptime, tool count, and pending approvals.\n- **schedule_create**(trigger_expression, instruction, execution_mode, max_retries, delivery_target) [capability_expand category: scheduler]: Create a new scheduled task. Specify a trigger expression (e.g. '30m', 'every 2h', '0 9 * * *') and the instruction to execute on each trigger. Optionally provide an execution mode and a delivery target to receive notifications.\n- **schedule_list**() [capability_expand category: scheduler]: List all scheduled tasks with their current state, trigger, and next due time.\n- **schedule_status**(task_id) [capability_expand category: scheduler]: Get detailed status and recent execution history for a scheduled task.\n- **schedule_pause**(task_id) [capability_expand category: scheduler]: Pause a scheduled task so it stops firing without being cancelled.\n- **schedule_resume**(task_id) [capability_expand category: scheduler]: Resume a paused scheduled task — re-arms it and recalculates next due time.\n- **schedule_cancel**(task_id) [capability_expand category: scheduler]: Cancel a scheduled task permanently.\n- **tool_search**(query, max_results): Search all registered tools by keyword relevance. Returns a ranked list of matching tools with name, category, and summary. Use this when you need a tool but are unsure of its exact name or category.\n- **tool_describe**(tool_name): Get the full callable schema of a single tool by exact name. Returns the tool's description, parameters, and metadata. Use after tool_search to inspect a specific tool before calling it.\n- **session_search**(query, limit) [capability_expand category: unclassified]: Search past conversation sessions in the current workspace for relevant context.\n- **session_list**(limit): List recent conversation sessions in the current workspace with stable ids, titles, dates, and summaries. Use for browsing past tasks or when user asks to see history without specific search terms. No keywords needed — returns chronological list.\n- **session_detail**(session_id, limit, offset, include_inactive) [capability_expand category: unclassified]: Read a paginated persisted transcript for one past conversation session in the current workspace. Use after session_list or session_search returns a session_id.\n- **click**(element_index) [capability_expand category: desktop]: Click a UI element by its element_index (from the latest observe_ui snapshot)\n- **get_clipboard**() [capability_expand category: desktop]: Read current clipboard text content\n- **list_apps**(filter, running_only) [capability_expand category: desktop]: List available applications on this system. Use to discover correct bundle_id before switch_app.\n- **list_windows**() [capability_expand category: desktop]: List all top-level windows with pid, window_id, title, and per-window state (minimized, on-screen). Call this first to pick the pid and window_id that observe_ui and other window tools require.\n- **observe_ui**(pid, window_id, query) [capability_expand category: desktop]: Snapshot one window's actionable UI elements, each tagged with an element_index for click/right_click/read_text. Re-observe after actions — indices belong to one snapshot. Requires the window's pid and window_id from list_windows.\n- **open_url**(url, app_id) [capability_expand category: desktop]: Open a URL in the default or specified browser\n- **read_text**(element_index) [capability_expand category: desktop]: Read the text content of a specific UI element from the latest snapshot\n- **right_click**(element_index) [capability_expand category: desktop]: Right-click a UI element to open its context menu. Returns visible menu items.\n- **screenshot**(pid, window_id) [capability_expand category: desktop]: Capture a screenshot for visual verification. With pid + window_id captures that window (works across all displays); defaults to the last observed window, or the full desktop when no window has been observed.\n- **scroll**(element_index, direction, amount, pid, window_id) [capability_expand category: desktop]: Scroll a scrollable area of a window. Omit element_index to scroll the window's focused/page scroller; pass one to scroll an exact element from the latest snapshot.\n- **select_text**(element_index) [capability_expand category: desktop]: Select all text in a UI element (focus + select-all, for subsequent copy)\n- **set_clipboard**(text) [capability_expand category: desktop]: Write text to the clipboard\n- **shortcut**(keys) [capability_expand category: desktop]: Execute a keyboard shortcut\n- **switch_app**(app_id) [capability_expand category: desktop]: Switch to an app (launch if needed, activate, verify)\n- **type_text**(text) [capability_expand category: desktop]: Type text into the currently focused element\n- **wait**(seconds) [capability_expand category: desktop]: Wait for a specified duration before continuing\n- **wait_until**(condition, pid, window_id, timeout, poll_interval) [capability_expand category: desktop]: Wait until a UI condition is met (polls UI tree). Returns elements when found or on timeout.\n- **wait_until_stable**(timeout, poll_interval, pid, window_id) [capability_expand category: desktop]: Wait until the UI stops changing (element set stabilizes across polls).\n\n## App Connector Capability Index\nLeapFlow can onboard and manage external apps through `platform_connect` and execute exact registered business actions through `platform_action`.\nFor requests about connecting, setting up, configuring, enabling, or managing a supported app, use `platform_connect` first instead of generating SDK/Webhook sample code.\n`platform_connect.action` is the management namespace: list, guide, preflight, connect, disconnect, remove, status, events_start, events_stop, events_status.\n`platform_action.action` is only for exact registered platform business actions listed below, such as `im.send_message`; never use management actions like `list` or `guide` there.\nAll business fields MUST go inside `payload`; top-level keys are only `platform`, `action`, and `payload`.\nDo not invent platform IDs or platform action names. If the needed action is not listed, ask for discovery/clarification instead of guessing.\nUse `platform_connect` with `action='guide'` and the matching `platform` to start onboarding; use `action='list'` when the app is unclear.\nWhen a pending onboarding state is present, continue from that state with `platform_connect` instead of asking the user to restate the app.\nSupported apps:\n- `api_server`: API Server (OpenAI Compatible) (category=api; backend=adapter; platform_action actions=none registered)\n- `dingtalk`: 钉钉 (DingTalk) (category=im; backend=adapter; platform_action actions=none registered)\n- `feishu`: 飞书 (Feishu/Lark) (category=im; backend=cli)\n - `calendar.create_event` payload={summary*, start_time*, end_time*, attendees} [write/high]\n - `docs.create_markdown` payload={title*, markdown*, folder_token} [write/medium]\n - `drive.search` payload={query*, limit} [read/medium]\n - `im.add_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.download_resource` payload={message_id*, file_key*, type*} [read/low]\n - `im.get_messages` payload={message_ids*} [read/low]\n - `im.list_chats` payload={page_size} [read/low]\n - `im.list_messages` payload={chat_id*} [read/low]\n - `im.list_thread_messages` payload={thread*} [read/low]\n - `im.remove_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.reply_message` payload={message_id*, text*} [send/high]\n - `im.search_chats` payload={query*, page_size} [read/low]\n - `im.search_messages` payload={query*} [read/medium]\n - `im.send_message` payload={chat_id*, text*, thread_id} [send/high]\n - `im.update_card` payload={token*, card*} [write/medium]\n - `im.update_message` payload={message_id*, text*} [write/medium]\n - `mail.search_unread` payload={query, limit} [read/high]\n - `sheets.append_row` payload={spreadsheet_token*, sheet_id*, values*} [write/medium]\n - `task.create` payload={title*, description, due_time} [write/medium]\n- `telegram`: Telegram (category=social; backend=adapter; platform_action actions=none registered)\n- `webhook`: Webhook (Generic) (category=webhook; backend=adapter; platform_action actions=none registered)\n\n## Tool Usage\nTools are normally invoked through the native function-calling mechanism, not by writing JSON in your reply\ntext. Only if the provider signals that native function calling is unavailable for this turn, fall back to a\nsingle JSON code block: `{\"name\": \"tool_name\", \"arguments\": {\"key\": \"value\"}}` — use this fallback format\nonly, never both. Only call a tool whose exact name appears in the tool index above and is currently callable\n(or reachable via `capability_expand`). Never invent, rename, alias, or guess a tool name, platform ID, or\nplatform action from argument shape or wording — if the index does not list it, it does not exist.\n`platform_connect.action` (list/guide/preflight/connect/disconnect/remove/status/events_start/events_stop/\nevents_status) is the App Connector management namespace; `platform_action.action` only accepts exact\nregistered business actions such as `im.send_message` shown in the App Connector Capability Index — never mix\nthe two namespaces. If a tool call returns an unknown/unavailable result, use the returned suggestions or\navailable names for a single retry instead of trying further variations of the same guess.\n\n**Side-effect action rule** (`platform_action` with effect=send/write/execute):\n- Call each unique action+payload **exactly once**. Never include duplicates in the same turn.\n- Once the result returns `\"completed\": true`, that action is DONE for this task. Do NOT call it again\n in any subsequent turn — immediately summarize the result for the user instead.\n- The system enforces idempotency: duplicate calls are blocked and will not execute.\n- If the user explicitly requests sending/writing multiple times, use distinct payloads per call.\n\n**Resource identifier provenance rule**:\n- NEVER fabricate, guess, or infer resource identifiers (chat_id, message_id, file_key, user_id, etc.).\n Every resource ID used in a side-effect action MUST come from a successful API response in this session.\n- If a read/list action fails (e.g. authorization error), you do NOT have valid resource IDs.\n Report the failure to the user — do NOT attempt the dependent write/send action with a guessed ID.\n- When a tool result contains `\"llm_instruction\"`, follow it exactly.\n\n## Guidelines\n1. **Direct answers first**: If you already know the answer, respond directly without tools.\n2. **Avoid redundant tool calls**: Do not call the same tool with the same arguments more than once in the same user turn. When an existing tool result already answers the user's request, stop calling tools and answer directly.\n3. **Use tools proactively**: When the user asks about files, time, system state, or needs actions performed, use the appropriate tool.\n4. **Chain tools when needed**: You can call multiple tools in sequence (e.g., list files → read file → summarize).\n5. **Handle failures gracefully**: If a tool fails, explain what went wrong and suggest alternatives. If it failed because of an unknown tool/platform/action name, retry once with an exact name from the returned suggestions, then explain rather than keep guessing.\n6. **Summarize results naturally**: After tool execution, synthesize the results into a helpful answer rather than dumping raw output.\n7. **Stay conversational**: Maintain a natural, helpful tone. Acknowledge context from earlier in the conversation.\n8. **Recall past work**:\n - Broad queries (\"之前做了什么\", \"列出任务\"): answer from the \"Recent Task History\" section already in your context. If insufficient, call session_list.\n - Specific lookups (\"上次那个配置怎么改的\"): call session_search with relevant phrases (NOT single characters).\n - Do NOT call search tools repeatedly with keyword fragments. One well-phrased call is better than ten fragmented ones.\n\n## Coding & Verification\nWhen working with code, prefer the precise built-in tools over ad-hoc shell: use `repo_map` to orient in an\nunfamiliar project, `code_search`/`file_find` to locate and `code_intel` for symbols, `edit_file` (anchored\nsearch-replace, or a unified `diff`) to change files — never rewrite a whole file to change a few lines — and\n`git_query` to inspect diffs/log. After edits, check `syntax_ok` in the result and run `test_run`/`lint_check`\nbefore declaring the work done. Batch independent read-only calls (search/read/query) into a single turn — they\nrun in parallel.\n\n## Presentation Style\n1. **Polished Markdown only**: Format user-facing answers with clean Markdown headings, short paragraphs, and concise bullets. Use tables only when they improve comparison or scanning.\n2. **Terminal-friendly layout**: Keep lines readable in a TUI; avoid dense walls of text, deeply nested lists, oversized ASCII art, or heavy visual blocks.\n3. **Elegant emphasis**: Use bold text sparingly for key terms and conclusions. Avoid excessive emojis, decorative symbols, repeated separators, or visual noise.\n4. **Theme-safe colors**: Do not emit ANSI escape codes, HTML color tags, Rich markup, or hardcoded color names. Rely on the TUI theme to render Markdown professionally.\n5. **No leaked tool protocol**: Never show tool-call JSON, internal schemas, raw observations, tool result payloads, or hidden reasoning in the final answer unless the user explicitly asks for raw/debug output. Treat any prior `{\"name\": ..., \"arguments\": ...}` blocks and `Tool result (...)` messages as internal execution context only.\n6. **Professional closure**: End with a concise conclusion or next step when helpful; avoid rambling after the useful answer is complete.\n\nWhen finished with all tool calls, respond normally without a JSON block, tool-call transcript, or process log.\n\n## Task Contract\n- Task ID: turn-1\n- Original user request: Say hello.\n- Workspace root: \n- Allowed roots: \n- Treat relative project paths as relative to the workspace root; never infer `.` as the project root when a workspace root is provided.\n- Workspace boundary is enforced by tools: do not read, search, edit, or run commands against paths outside the allowed roots unless the user explicitly requests an external path and the tool/approval policy permits it.\n- LeapFlow workspace config is optional at `/.leapflow/config.yaml`; runtime config is loaded from `~/.leapflow/config/user.yaml` and `~/.leapflow/profiles//config/*.yaml`.\n- Preserve this task contract across summarization, compression, tool loops, and memory retrieval.\n" - }, - { - "role": "system", - "content": "\n## Recent Session Summary\n- [user] Say hello.\n" - }, - { - "role": "user", - "content": "Say hello.\nSay hello." - } - ], - "tools": [ - "capability_expand", - "code_intel", - "code_search", - "config_get", - "config_list", - "env_info", - "file_find", - "file_list", - "file_read", - "git_query", - "lint_check", - "memory_search", - "plugin_list", - "plugin_propose", - "plugin_status", - "plugin_versions", - "repo_map", - "research_note", - "runtime_snapshot", - "schedule_reentry", - "self_describe", - "session_list", - "skill_view", - "skills_list", - "terminal_list", - "terminal_read", - "test_run", - "text_search", - "time_get", - "tool_describe", - "tool_search", - "web_fetch" - ] - }, - "responses": [ - { - "status": 200, - "content_type": "application/json", - "body": "{\"id\": \"chatcmpl-cassette\", \"object\": \"chat.completion\", \"model\": \"cassette-model\", \"choices\": [{\"index\": 0, \"message\": {\"role\": \"assistant\", \"content\": \"Hello from LeapFlow.\"}, \"finish_reason\": \"stop\"}], \"usage\": {\"prompt_tokens\": 64, \"completion_tokens\": 16, \"total_tokens\": 80}}" - } - ] -} diff --git a/tests/_fixtures/cassettes/r1_conversation/cassette-model-edd842ba40e1cf99.cassette.json b/tests/_fixtures/cassettes/r1_conversation/cassette-model-edd842ba40e1cf99.cassette.json deleted file mode 100644 index 371d206e..00000000 --- a/tests/_fixtures/cassettes/r1_conversation/cassette-model-edd842ba40e1cf99.cassette.json +++ /dev/null @@ -1,79 +0,0 @@ -{ - "fingerprint": "edd842ba40e1cf99a4e5d1807e608c65049496cd281110b217a33fd9c62e6942", - "note": "captured in seed mode", - "request": { - "model": "cassette-model", - "stream": false, - "messages": [ - { - "role": "system", - "content": "You are LeapFlow, an intelligent assistant that can both converse naturally and take real actions on the user's computer.\n\n## Capabilities\nThe tool index below lists **every** registered tool by name and a one-line summary — this is the complete\ncapability contract; nothing else exists. Only a subset is directly callable this turn (via native tool calling,\nnot a JSON block in your reply). If you need a tool from the index that is not yet callable, call\n`capability_expand` with its category name first — the matching tools become callable immediately after.\nWhen the user asks what LeapFlow itself supports, whether it supports plugins, or which runtime capabilities\nare available, use the live capability evidence exposed by `plugin_list` before making capability claims; report\nconfiguration-dependent or unavailable capabilities as limitations instead of inferring from documentation.\n- **text_search**(text, pattern): Search for a regex pattern in text.\n- **text_replace**(text, old, new, count) [capability_expand category: write]: Replace occurrences of a substring in text.\n- **time_get**(): Get current date and time.\n- **env_info**(): Get system environment information (OS, Python version, cwd).\n- **skills_list**(query, category, source): List available learned skills. Use when user asks about capabilities or you need a specific skill.\n- **skill_view**(name): View the full content of a specific skill document.\n- **code_intel**(path, operation): Precise document symbols (outline) for a source file: classes, functions, and methods with line ranges. Python uses an exact AST parse; other languages use a keyword-prefix scan. Prefer over file_read mode=symbols for accurate navigation before editing. Read-only.\n- **repo_map**(path): Compact project orientation for a repository root: languages, detected test/lint commands, top-level structure, entry points, manifest, and VCS branch. Call this first when entering an unfamiliar codebase. Read-only.\n- **scm_sync**(action, cwd, remote, pull_ref, push_ref, timeout) [capability_expand category: scm]: Run a typed git SCM action. Use this instead of shell_run for git pull/push/status. For 'pull origin main then push', set action='pull_then_push', remote='origin', pull_ref='main', and omit push_ref so LeapFlow pushes the current local branch.\n- **git_query**(action, cwd, ref, path, staged, max_count, stat): Read-only structured git inspection: action=diff|log|status|branch|show. Prefer over shell_run for reading repo state — output is clipped, redacted, and log/branch are parsed into structured fields. Use scm_sync for pull/push.\n- **git_write**(action, cwd, message, stage_all, name, ref, create) [capability_expand category: scm]: Mutating git actions: action=commit (message, stage_all), branch (create+switch), checkout (switch; create=true for -b). Approval-gated. Use scm_sync for pull/push and git_query for reads.\n- **test_run**(command, cwd, timeout): Run the project's test suite and return structured results (framework, passed/failed counts, failing tests). Auto-detects the runner (pytest/npm/go/cargo) or uses a configured/explicit command; executes via the governed shell. ok=true means the runner executed — see 'success' for pass/fail.\n- **lint_check**(command, cwd, timeout): Run the project's linter and return a structured clean/issue result. Auto-detects the linter (ruff/eslint/go vet/clippy) or uses a configured/explicit command; executes via the governed shell. ok=true means the linter ran — see 'clean'.\n- **file_list**(path, pattern, depth): List files and directories at a given path. Use depth=1 or depth=2 to get a recursive tree in one call instead of listing each sub-directory separately.\n- **file_read**(path, max_lines, start_line, max_chars, mode): Read text file content with adaptive context governance. For large or unfamiliar files, prefer mode='outline' or mode='symbols' first, then use mode='raw' with start_line/max_lines for the specific range you actually need. For LeapFlow's own settings, use config_list / config_get / config_set — its config files are outside the workspace and not readable here.\n- **file_write**(path, content, mode) [capability_expand category: write]: Write content to a file (overwrite or append).\n- **code_search**(pattern, patterns, path, glob, ignore_case, multiline, max_results, context_lines): Search file CONTENTS by regex pattern across a directory tree (ripgrep-backed). Requires a regex pattern. NOT for listing or browsing directory contents — use file_list for that. Prefer this over shell_run grep: faster, skips VCS/dependency/build dirs, and returns structured path:line:column matches. Batch related lookups into ONE call via `patterns` (OR-combined, single pass) instead of issuing several separate searches. Use file_read for the surrounding context of a hit.\n- **file_find**(glob, path, max_results): Find files by a recursive glob pattern under a base path (e.g. '**/test_*.py' or '*.md'). Prefer this over shell_run find; skips VCS/dependency/build dirs.\n- **edit_file**(path, edits, dry_run, diff) [capability_expand category: write]: Apply targeted, anchored search-replace edits to an EXISTING text file (use file_write to create/overwrite). Each edit is {original_text, new_text, replace_all?}; original_text must match exactly and uniquely (or set replace_all) — a non-unique or missing anchor is rejected so files are never corrupted. Set dry_run to preview. Alternatively pass a unified 'diff' to apply its hunks as anchored edits. Far cheaper and safer than rewriting a whole file.\n- **shell_run**(command, cwd, timeout) [capability_expand category: shell]: Execute a one-shot shell command with timeout protection. Runs in the active workspace; paths resolving outside it are refused. Reach for a structured tool first when one fits — web_fetch for anything over HTTP(S), git_query/scm_sync for git, code_search/file_find/file_read for the repo, config_get/config_set for LeapFlow's own settings — because those report typed results, while a failed shell command can only be diagnosed from its exit code and stderr. Every shell run counts as an external side effect, so a failure stops the rest of the batch and is not retried automatically.\n- **terminal_open**(command, cwd, shell) [capability_expand category: terminal]: Open a PERSISTENT shell session (REPL/dev server/watch), returning a session_id for terminal_send/read/close. Disabled unless tools.terminal_session_enabled is set. For one-shot commands use shell_run instead.\n- **terminal_send**(session_id, input, wait) [capability_expand category: terminal]: Send a line of input to a persistent terminal session and return output captured shortly after.\n- **terminal_read**(session_id, wait): Drain buffered output from a persistent terminal session (optionally waiting briefly first).\n- **terminal_close**(session_id) [capability_expand category: terminal]: Terminate a persistent terminal session and release its process group.\n- **terminal_list**(): List active persistent terminal sessions.\n- **config_list**(category, limit): List LeapFlow's own writable settings (LLM model and endpoint, daemon, memory, perception, gateway, …) with current values. Use this to discover the exact key before changing anything. Optionally narrow by `category`. This is the only correct way to inspect LeapFlow configuration — never read config files from disk.\n- **config_get**(key): Read one LeapFlow setting by key (e.g. 'llm.model', 'llm.base_url', 'daemon.log_level'), returning its current value, type, scopes, and whether a change needs a daemon restart. There is no 'llm.provider' key: provider behavior is inferred from the OpenAI-compatible 'llm.base_url'. Never read LeapFlow config files from disk — use this.\n- **config_set**(key, value, scope) [capability_expand category: config]: Change one LeapFlow setting by key, e.g. switch an OpenAI-compatible LLM with key='llm.model' and key='llm.base_url'. There is no 'llm.provider' key: provider behavior is inferred from the endpoint. Values are validated and coerced; credentials are stored in the vault automatically. Call config_list or config_get first if unsure of the key. The result states whether a `leap daemon restart` is required. Never edit LeapFlow config files directly.\n- **web_fetch**(url, select, timeout, max_bytes): Read a URL over HTTP(S) and get back extracted, context-sized content: parsed JSON for API endpoints, readable text plus links for web pages. Use this for anything on the internet — prices, docs, releases, articles — instead of running curl through shell_run: it reports real HTTP status codes, retries rate limits on its own, and is a plain read so a retry is always safe. For JSON APIs pass `select` with a dotted path (e.g. 'chart.result.0.meta') to return just that part instead of the whole payload.\n- **memory_search**(query, limit): Search agent memory for relevant past experiences, observations, and facts.\n- **memory_add**(content, kind) [capability_expand category: write]: Store a new observation or insight in memory for future reference.\n- **research_note**(kind, text): Record a compact, structured note about the current task's state so it survives context compression on long / multi-step tasks. Use for durable findings, open questions still to resolve, decisions / excluded paths, and the immediate next step. One concise sentence per note.\n- **capability_expand**(category): Fetch the full callable schema for every tool in a capability category (e.g. 'hub', 'gateway', 'desktop', 'delegate', 'file', 'memory', 'skill'). The compact tool index always lists every registered tool by name and a one-line summary, but only a static low-risk subset is directly callable each turn. If you need a tool from the index that is not yet callable, call capability_expand with its category first; the matching tools become callable in this turn. Never invent a tool name — expand the category instead.\n- **delegate_task**(goal, context) [capability_expand category: delegate]: Delegate a complex sub-task to an isolated subagent. The subagent gets a fresh context and restricted tool access. Use when a task is self-contained and can be solved independently.\n- **schedule_reentry**(kind, reason, delay_seconds, event_match, max_reentries, deadline_seconds): Register a re-entry so this task can resume later from its current orientation (findings / open questions / next step). Use when work must pause and continue after a delay (kind=time) or when a matching platform event arrives (kind=event), instead of finishing now. The research-ledger state is carried over automatically.\n- **hub_push**(skill_name, visibility, version) [capability_expand category: hub]: Push a local skill to the ModelScope Hub for sharing or backup.\n- **hub_pull**(repo_id, version) [capability_expand category: hub]: Pull a skill from the ModelScope Hub to install locally.\n- **hub_search**(query) [capability_expand category: hub]: Search for skills on the Hub by keyword or description.\n- **hub_sync**(mode, dry_run) [capability_expand category: hub]: Preview or execute sync between local skills and Hub.\n- **platform_action**(platform, action, payload, backend_kind) [capability_expand category: gateway]: Execute an exact registered business action on an external platform through LeapFlow's App Connector layer. Actions must be copied from the App Connector Capability Index and are addressed as domain.operation, e.g. im.send_message or docs.create_markdown. All business fields (chat_id, text, query, etc.) MUST be placed inside `payload`, never at the top level. Example: {\"platform\":\"feishu\",\"action\":\"im.send_message\",\"payload\":{\"chat_id\":\"oc_xxx\",\"text\":\"hello\"}}. Do not invent action names, do not use management actions such as list/guide/connect/status here.\n- **platform_connect**(action, platform, credentials, options, checkpoint) [capability_expand category: gateway]: List, guide, connect, disconnect, remove, or check status for external platforms using the App Connector management namespace. Supports REST and CLI backends. Use this for management actions such as list/guide/preflight/connect/status; use platform_action only for exact registered business actions.\n- **gateway_send**(platform, chat_id, text, thread_id) [capability_expand category: gateway]: Send a message to a connected external platform (Feishu group, Telegram chat, DingTalk conversation, etc.). Requires the platform to be connected via gateway_connect first. Use gateway_connect with action='list' to see connected platforms and available chat IDs.\n- **gateway_connect**(action, platform, credentials, options) [capability_expand category: gateway]: Connect, configure, or manage external platform integrations (Feishu, DingTalk, Telegram, Slack, Discord, etc.). Conversational flow: 1) call 'guide' to get setup steps + required fields, 2) present the steps to the user and ask for ALL required credentials in a single message, 3) call 'connect' with the credentials. Goal: complete in 1–2 user turns. NEVER include credential values in your text response.\n- **plugin_list**(): List the live plugin registry and cross-subsystem capability evidence. Use this before answering questions about whether LeapFlow supports plugins, self-evolution, plugin installation, hot reload, versioning, or other runtime capabilities.\n- **plugin_status**(plugin_id): Get detailed status of a specific plugin: its declared category, runtime dependencies, contributed tools, and fiber lifecycle state.\n- **plugin_versions**(plugin_id): List recorded source versions and active pointer for a profile-scoped plugin.\n- **plugin_propose**(requested_capability, plugin_id, proposed_tools, test_cases, risk_level, evidence): Create a side-effect-free PluginProposal from explicit capability-gap evidence. Use this before plugin_generate when a missing capability should be reviewed. Does not call an LLM, write files, or install anything.\n- **assess_compatibility**(manifest, source_path) [capability_expand category: plugin_management]: Assess whether a foreign plugin manifest or real DSH source bundle is compatible with LeapFlow. A source bundle assessment is static: runtime_ready stays false until restricted Node discovery during plugin_install. Returns component-level verdicts and limitations.\n- **plugin_generate**(plugin_id, description, proposal_id) [capability_expand category: system]: Generate a new ToolPlugin from a natural-language capability description. The LLM produces code that conforms to the ToolPlugin Protocol; it is then rigorously validated (syntax, structure, import, protocol conformance). The generated source is stored in CAS and receives explicit content approval. It DOES NOT install the plugin — installation requires a second, mutation-specific approval via plugin_install.\n- **plugin_install**(plugin_id, code, marketplace_name, source_path, proposal_id, version_label) [capability_expand category: system]: Install a plugin from exactly one source: validated Python code, the configured Python marketplace, or a real DSH source bundle. DSH bundles run restricted Node discovery before registration; only public host tools are installed and client UI limitations are reported. Writes profile-scoped state and mutates the process-global registry. REQUIRES APPROVAL.\n- **plugin_rollback**(plugin_id, version) [capability_expand category: system]: Rollback a profile-scoped plugin to a recorded source version and reload it. REQUIRES APPROVAL.\n- **plugin_reload**(plugin_id, version_label) [capability_expand category: system]: Hot-reload a plugin at runtime. Disposes the old plugin fiber, re-imports its module, and registers a fresh instance. Existing in-flight turns are unaffected (snapshot isolation). REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_disable**(plugin_id) [capability_expand category: system]: Disable a plugin by disposing its fiber, removing its tools from the runtime registry. Cannot disable self_management itself. REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_remove**(plugin_id, delete_source) [capability_expand category: system]: Terminally remove a plugin: dispose its fiber, unregister its tools, remove reload metadata, and optionally delete its profile-scoped source file. Cannot remove self_management itself. REQUIRES APPROVAL.\n- **plugin_unquarantine**(plugin_id) [capability_expand category: plugin_management]: Restore a quarantined plugin to probation status for re-evaluation. Unfreezes the trust ledger, transitions the proposal back to PROBATION, and reloads the plugin. REQUIRES APPROVAL.\n- **plugin_enable**(plugin_id) [capability_expand category: system]: Re-enable a previously disabled plugin by reloading its module and registering a fresh instance. REQUIRES APPROVAL.\n- **self_describe**(facet): Introspect LeapFlow's own identity, capabilities, runtime state, evolution metrics, or platform connections. Use facet='all' only when a comprehensive self-check is explicitly requested.\n- **runtime_snapshot**(): Lightweight (~150 token) flat snapshot of current runtime state: model, context budget, posture, disclosure level, turn count, cache hit rate, uptime, tool count, and pending approvals.\n- **schedule_create**(trigger_expression, instruction, execution_mode, max_retries, delivery_target) [capability_expand category: scheduler]: Create a new scheduled task. Specify a trigger expression (e.g. '30m', 'every 2h', '0 9 * * *') and the instruction to execute on each trigger. Optionally provide an execution mode and a delivery target to receive notifications.\n- **schedule_list**() [capability_expand category: scheduler]: List all scheduled tasks with their current state, trigger, and next due time.\n- **schedule_status**(task_id) [capability_expand category: scheduler]: Get detailed status and recent execution history for a scheduled task.\n- **schedule_pause**(task_id) [capability_expand category: scheduler]: Pause a scheduled task so it stops firing without being cancelled.\n- **schedule_resume**(task_id) [capability_expand category: scheduler]: Resume a paused scheduled task — re-arms it and recalculates next due time.\n- **schedule_cancel**(task_id) [capability_expand category: scheduler]: Cancel a scheduled task permanently.\n- **tool_search**(query, max_results): Search all registered tools by keyword relevance. Returns a ranked list of matching tools with name, category, and summary. Use this when you need a tool but are unsure of its exact name or category.\n- **tool_describe**(tool_name): Get the full callable schema of a single tool by exact name. Returns the tool's description, parameters, and metadata. Use after tool_search to inspect a specific tool before calling it.\n- **session_search**(query, limit) [capability_expand category: unclassified]: Search past conversation sessions in the current workspace for relevant context.\n- **session_list**(limit): List recent conversation sessions in the current workspace with stable ids, titles, dates, and summaries. Use for browsing past tasks or when user asks to see history without specific search terms. No keywords needed — returns chronological list.\n- **session_detail**(session_id, limit, offset, include_inactive) [capability_expand category: unclassified]: Read a paginated persisted transcript for one past conversation session in the current workspace. Use after session_list or session_search returns a session_id.\n- **click**(element_index) [capability_expand category: desktop]: Click a UI element by its element_index (from the latest observe_ui snapshot)\n- **get_clipboard**() [capability_expand category: desktop]: Read current clipboard text content\n- **list_apps**(filter, running_only) [capability_expand category: desktop]: List available applications on this system. Use to discover correct bundle_id before switch_app.\n- **list_windows**() [capability_expand category: desktop]: List all top-level windows with pid, window_id, title, and per-window state (minimized, on-screen). Call this first to pick the pid and window_id that observe_ui and other window tools require.\n- **observe_ui**(pid, window_id, query) [capability_expand category: desktop]: Snapshot one window's actionable UI elements, each tagged with an element_index for click/right_click/read_text. Re-observe after actions — indices belong to one snapshot. Requires the window's pid and window_id from list_windows.\n- **open_url**(url, app_id) [capability_expand category: desktop]: Open a URL in the default or specified browser\n- **read_text**(element_index) [capability_expand category: desktop]: Read the text content of a specific UI element from the latest snapshot\n- **right_click**(element_index) [capability_expand category: desktop]: Right-click a UI element to open its context menu. Returns visible menu items.\n- **screenshot**(pid, window_id) [capability_expand category: desktop]: Capture a screenshot for visual verification. With pid + window_id captures that window (works across all displays); defaults to the last observed window, or the full desktop when no window has been observed.\n- **scroll**(element_index, direction, amount, pid, window_id) [capability_expand category: desktop]: Scroll a scrollable area of a window. Omit element_index to scroll the window's focused/page scroller; pass one to scroll an exact element from the latest snapshot.\n- **select_text**(element_index) [capability_expand category: desktop]: Select all text in a UI element (focus + select-all, for subsequent copy)\n- **set_clipboard**(text) [capability_expand category: desktop]: Write text to the clipboard\n- **shortcut**(keys) [capability_expand category: desktop]: Execute a keyboard shortcut\n- **switch_app**(app_id) [capability_expand category: desktop]: Switch to an app (launch if needed, activate, verify)\n- **type_text**(text) [capability_expand category: desktop]: Type text into the currently focused element\n- **wait**(seconds) [capability_expand category: desktop]: Wait for a specified duration before continuing\n- **wait_until**(condition, pid, window_id, timeout, poll_interval) [capability_expand category: desktop]: Wait until a UI condition is met (polls UI tree). Returns elements when found or on timeout.\n- **wait_until_stable**(timeout, poll_interval, pid, window_id) [capability_expand category: desktop]: Wait until the UI stops changing (element set stabilizes across polls).\n\n## App Connector Capability Index\nLeapFlow can onboard and manage external apps through `platform_connect` and execute exact registered business actions through `platform_action`.\nFor requests about connecting, setting up, configuring, enabling, or managing a supported app, use `platform_connect` first instead of generating SDK/Webhook sample code.\n`platform_connect.action` is the management namespace: list, guide, preflight, connect, disconnect, remove, status, events_start, events_stop, events_status.\n`platform_action.action` is only for exact registered platform business actions listed below, such as `im.send_message`; never use management actions like `list` or `guide` there.\nAll business fields MUST go inside `payload`; top-level keys are only `platform`, `action`, and `payload`.\nDo not invent platform IDs or platform action names. If the needed action is not listed, ask for discovery/clarification instead of guessing.\nUse `platform_connect` with `action='guide'` and the matching `platform` to start onboarding; use `action='list'` when the app is unclear.\nWhen a pending onboarding state is present, continue from that state with `platform_connect` instead of asking the user to restate the app.\nSupported apps:\n- `api_server`: API Server (OpenAI Compatible) (category=api; backend=adapter; platform_action actions=none registered)\n- `dingtalk`: 钉钉 (DingTalk) (category=im; backend=adapter; platform_action actions=none registered)\n- `feishu`: 飞书 (Feishu/Lark) (category=im; backend=cli)\n - `calendar.create_event` payload={summary*, start_time*, end_time*, attendees} [write/high]\n - `docs.create_markdown` payload={title*, markdown*, folder_token} [write/medium]\n - `drive.search` payload={query*, limit} [read/medium]\n - `im.add_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.download_resource` payload={message_id*, file_key*, type*} [read/low]\n - `im.get_messages` payload={message_ids*} [read/low]\n - `im.list_chats` payload={page_size} [read/low]\n - `im.list_messages` payload={chat_id*} [read/low]\n - `im.list_thread_messages` payload={thread*} [read/low]\n - `im.remove_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.reply_message` payload={message_id*, text*} [send/high]\n - `im.search_chats` payload={query*, page_size} [read/low]\n - `im.search_messages` payload={query*} [read/medium]\n - `im.send_message` payload={chat_id*, text*, thread_id} [send/high]\n - `im.update_card` payload={token*, card*} [write/medium]\n - `im.update_message` payload={message_id*, text*} [write/medium]\n - `mail.search_unread` payload={query, limit} [read/high]\n - `sheets.append_row` payload={spreadsheet_token*, sheet_id*, values*} [write/medium]\n - `task.create` payload={title*, description, due_time} [write/medium]\n- `telegram`: Telegram (category=social; backend=adapter; platform_action actions=none registered)\n- `webhook`: Webhook (Generic) (category=webhook; backend=adapter; platform_action actions=none registered)\n\n## Tool Usage\nTools are normally invoked through the native function-calling mechanism, not by writing JSON in your reply\ntext. Only if the provider signals that native function calling is unavailable for this turn, fall back to a\nsingle JSON code block: `{\"name\": \"tool_name\", \"arguments\": {\"key\": \"value\"}}` — use this fallback format\nonly, never both. Only call a tool whose exact name appears in the tool index above and is currently callable\n(or reachable via `capability_expand`). Never invent, rename, alias, or guess a tool name, platform ID, or\nplatform action from argument shape or wording — if the index does not list it, it does not exist.\n`platform_connect.action` (list/guide/preflight/connect/disconnect/remove/status/events_start/events_stop/\nevents_status) is the App Connector management namespace; `platform_action.action` only accepts exact\nregistered business actions such as `im.send_message` shown in the App Connector Capability Index — never mix\nthe two namespaces. If a tool call returns an unknown/unavailable result, use the returned suggestions or\navailable names for a single retry instead of trying further variations of the same guess.\n\n**Side-effect action rule** (`platform_action` with effect=send/write/execute):\n- Call each unique action+payload **exactly once**. Never include duplicates in the same turn.\n- Once the result returns `\"completed\": true`, that action is DONE for this task. Do NOT call it again\n in any subsequent turn — immediately summarize the result for the user instead.\n- The system enforces idempotency: duplicate calls are blocked and will not execute.\n- If the user explicitly requests sending/writing multiple times, use distinct payloads per call.\n\n**Resource identifier provenance rule**:\n- NEVER fabricate, guess, or infer resource identifiers (chat_id, message_id, file_key, user_id, etc.).\n Every resource ID used in a side-effect action MUST come from a successful API response in this session.\n- If a read/list action fails (e.g. authorization error), you do NOT have valid resource IDs.\n Report the failure to the user — do NOT attempt the dependent write/send action with a guessed ID.\n- When a tool result contains `\"llm_instruction\"`, follow it exactly.\n\n## Guidelines\n1. **Direct answers first**: If you already know the answer, respond directly without tools.\n2. **Avoid redundant tool calls**: Do not call the same tool with the same arguments more than once in the same user turn. When an existing tool result already answers the user's request, stop calling tools and answer directly.\n3. **Use tools proactively**: When the user asks about files, time, system state, or needs actions performed, use the appropriate tool.\n4. **Chain tools when needed**: You can call multiple tools in sequence (e.g., list files → read file → summarize).\n5. **Handle failures gracefully**: If a tool fails, explain what went wrong and suggest alternatives. If it failed because of an unknown tool/platform/action name, retry once with an exact name from the returned suggestions, then explain rather than keep guessing.\n6. **Summarize results naturally**: After tool execution, synthesize the results into a helpful answer rather than dumping raw output.\n7. **Stay conversational**: Maintain a natural, helpful tone. Acknowledge context from earlier in the conversation.\n8. **Recall past work**:\n - Broad queries (\"之前做了什么\", \"列出任务\"): answer from the \"Recent Task History\" section already in your context. If insufficient, call session_list.\n - Specific lookups (\"上次那个配置怎么改的\"): call session_search with relevant phrases (NOT single characters).\n - Do NOT call search tools repeatedly with keyword fragments. One well-phrased call is better than ten fragmented ones.\n\n## Coding & Verification\nWhen working with code, prefer the precise built-in tools over ad-hoc shell: use `repo_map` to orient in an\nunfamiliar project, `code_search`/`file_find` to locate and `code_intel` for symbols, `edit_file` (anchored\nsearch-replace, or a unified `diff`) to change files — never rewrite a whole file to change a few lines — and\n`git_query` to inspect diffs/log. After edits, check `syntax_ok` in the result and run `test_run`/`lint_check`\nbefore declaring the work done. Batch independent read-only calls (search/read/query) into a single turn — they\nrun in parallel.\n\n## Presentation Style\n1. **Polished Markdown only**: Format user-facing answers with clean Markdown headings, short paragraphs, and concise bullets. Use tables only when they improve comparison or scanning.\n2. **Terminal-friendly layout**: Keep lines readable in a TUI; avoid dense walls of text, deeply nested lists, oversized ASCII art, or heavy visual blocks.\n3. **Elegant emphasis**: Use bold text sparingly for key terms and conclusions. Avoid excessive emojis, decorative symbols, repeated separators, or visual noise.\n4. **Theme-safe colors**: Do not emit ANSI escape codes, HTML color tags, Rich markup, or hardcoded color names. Rely on the TUI theme to render Markdown professionally.\n5. **No leaked tool protocol**: Never show tool-call JSON, internal schemas, raw observations, tool result payloads, or hidden reasoning in the final answer unless the user explicitly asks for raw/debug output. Treat any prior `{\"name\": ..., \"arguments\": ...}` blocks and `Tool result (...)` messages as internal execution context only.\n6. **Professional closure**: End with a concise conclusion or next step when helpful; avoid rambling after the useful answer is complete.\n\nWhen finished with all tool calls, respond normally without a JSON block, tool-call transcript, or process log.\n\n## Task Contract\n- Task ID: turn-2\n- Original user request: Use the file_read tool on invoice.txt and report the total.\n- Workspace root: \n- Allowed roots: \n- Treat relative project paths as relative to the workspace root; never infer `.` as the project root when a workspace root is provided.\n- Workspace boundary is enforced by tools: do not read, search, edit, or run commands against paths outside the allowed roots unless the user explicitly requests an external path and the tool/approval policy permits it.\n- LeapFlow workspace config is optional at `/.leapflow/config.yaml`; runtime config is loaded from `~/.leapflow/config/user.yaml` and `~/.leapflow/profiles//config/*.yaml`.\n- Preserve this task contract across summarization, compression, tool loops, and memory retrieval.\n" - }, - { - "role": "system", - "content": "\n## Recent Session Summary\n- [assistant] Hello from LeapFlow.\n- [user] Use the file_read tool on invoice.txt and report the total.\n" - }, - { - "role": "assistant", - "content": "Hello from LeapFlow." - }, - { - "role": "user", - "content": "Use the file_read tool on invoice.txt and report the total.\nUse the file_read tool on invoice.txt and report the total." - }, - { - "role": "assistant", - "content": "", - "tool_calls": [ - "file_read" - ] - }, - { - "role": "tool", - "content": "{\"ok\": true, \"kind\": \"file_read_evidence\", \"path\": \"\", \"lines\": 2, \"truncated\": false, \"mode\": \"raw\", \"excerpt\": \"Invoice 42\\nTotal: 128.50 USD\", \"start_line\": 1, \"end_line\": 2, \"selected_lines\": 2}", - "tool_result": true - } - ], - "tools": [ - "capability_expand", - "code_intel", - "code_search", - "config_get", - "config_list", - "env_info", - "file_find", - "file_list", - "file_read", - "git_query", - "lint_check", - "memory_search", - "plugin_list", - "plugin_propose", - "plugin_status", - "plugin_versions", - "repo_map", - "research_note", - "runtime_snapshot", - "schedule_reentry", - "self_describe", - "session_list", - "skill_view", - "skills_list", - "terminal_list", - "terminal_read", - "test_run", - "text_search", - "time_get", - "tool_describe", - "tool_search", - "web_fetch" - ] - }, - "responses": [ - { - "status": 200, - "content_type": "application/json", - "body": "{\"id\": \"chatcmpl-cassette\", \"object\": \"chat.completion\", \"model\": \"cassette-model\", \"choices\": [{\"index\": 0, \"message\": {\"role\": \"assistant\", \"content\": \"The invoice total is 128.50 USD.\"}, \"finish_reason\": \"stop\"}], \"usage\": {\"prompt_tokens\": 64, \"completion_tokens\": 16, \"total_tokens\": 80}}" - } - ] -} diff --git a/tests/_fixtures/cassettes/r1_conversation/cassette-model-f8a8a8ec2f575965.cassette.json b/tests/_fixtures/cassettes/r1_conversation/cassette-model-f8a8a8ec2f575965.cassette.json new file mode 100644 index 00000000..9a17f939 --- /dev/null +++ b/tests/_fixtures/cassettes/r1_conversation/cassette-model-f8a8a8ec2f575965.cassette.json @@ -0,0 +1,67 @@ +{ + "fingerprint": "f8a8a8ec2f575965e402e34c109fb7cb4db9b4f7efc63556e471eecca9941294", + "note": "captured in seed mode", + "request": { + "model": "cassette-model", + "stream": false, + "messages": [ + { + "role": "system", + "content": "You are LeapFlow, an intelligent assistant that can both converse naturally and take real actions on the user's computer.\n\n## Capabilities\nThe tool index below lists **every** registered tool by name and a one-line summary — this is the complete\ncapability contract; nothing else exists. Only a subset is directly callable this turn (via native tool calling,\nnot a JSON block in your reply). If you need a tool from the index that is not yet callable, call\n`capability_expand` with its category name first — the matching tools become callable immediately after.\nWhen the user asks about LeapFlow **itself** — its identity/version/model, what tools, plugins, or capabilities\nit has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of\nself-awareness — answer from live runtime evidence via `self_describe` (facet=capabilities for \"what can you\ndo / do you support X\", identity for version/model, runtime for context/posture, or `all` for a full self-check),\nnever by reading LeapFlow's own source code, documentation, or memory. Report configuration-dependent or\nunavailable capabilities as limitations rather than inferring them.\n- **text_search**(text, pattern): Search for a regex pattern in text.\n- **text_replace**(text, old, new, count) [capability_expand category: write]: Replace occurrences of a substring in text.\n- **time_get**(): Get current date and time.\n- **env_info**(): Get system environment information (OS, Python version, cwd).\n- **skills_list**(query, category, source): List available learned skills. Use when user asks about capabilities or you need a specific skill.\n- **skill_view**(name): View the full content of a specific skill document.\n- **skill_list_files**(name) [capability_expand category: skills]: List all support files in a skill directory (configs, templates, examples). Use before skill_file_read to discover available files.\n- **skill_file_read**(name, file_path) [capability_expand category: skills]: Read an individual support file from a skill directory. Use skill_list_files first to discover available files.\n- **code_intel**(path, operation): Precise document symbols (outline) for a source file: classes, functions, and methods with line ranges. Python uses an exact AST parse; other languages use a keyword-prefix scan. Prefer over file_read mode=symbols for accurate navigation before editing. Read-only.\n- **repo_map**(path): Compact project orientation for a repository root: languages, detected test/lint commands, top-level structure, entry points, manifest, and VCS branch. Call this first when entering an unfamiliar codebase. Read-only.\n- **scm_sync**(action, cwd, remote, pull_ref, push_ref, timeout) [capability_expand category: scm]: Run a typed git SCM action. Use this instead of shell_run for git pull/push/status. For 'pull origin main then push', set action='pull_then_push', remote='origin', pull_ref='main', and omit push_ref so LeapFlow pushes the current local branch.\n- **git_query**(action, cwd, ref, path, staged, max_count, stat): Read-only structured git inspection: action=diff|log|status|branch|show. Prefer over shell_run for reading repo state — output is clipped, redacted, and log/branch are parsed into structured fields. Use scm_sync for pull/push.\n- **git_write**(action, cwd, message, stage_all, name, ref, create) [capability_expand category: scm]: Mutating git actions: action=commit (message, stage_all), branch (create+switch), checkout (switch; create=true for -b). Approval-gated. Use scm_sync for pull/push and git_query for reads.\n- **test_run**(command, cwd, timeout): Run the project's test suite and return structured results (framework, passed/failed counts, failing tests). Auto-detects the runner (pytest/npm/go/cargo) or uses a configured/explicit command; executes via the governed shell. ok=true means the runner executed — see 'success' for pass/fail.\n- **lint_check**(command, cwd, timeout): Run the project's linter and return a structured clean/issue result. Auto-detects the linter (ruff/eslint/go vet/clippy) or uses a configured/explicit command; executes via the governed shell. ok=true means the linter ran — see 'clean'.\n- **file_list**(path, pattern, depth): List files and directories at a given path. Use depth=1 or depth=2 to get a recursive tree in one call instead of listing each sub-directory separately.\n- **file_read**(path, max_lines, start_line, max_chars, mode): Read text file content with adaptive context governance. For large or unfamiliar files, prefer mode='outline' or mode='symbols' first, then use mode='raw' with start_line/max_lines for the specific range you actually need. For LeapFlow's own settings, use config_list / config_get / config_set — its config files are outside the workspace and not readable here.\n- **file_write**(path, content, mode) [capability_expand category: write]: Write content to a file (overwrite or append).\n- **code_search**(pattern, patterns, path, glob, ignore_case, multiline, max_results, context_lines): Search file CONTENTS by regex pattern across a directory tree (ripgrep-backed). Requires a regex pattern. NOT for listing or browsing directory contents — use file_list for that. Prefer this over shell_run grep: faster, skips VCS/dependency/build dirs, and returns structured path:line:column matches. Batch related lookups into ONE call via `patterns` (OR-combined, single pass) instead of issuing several separate searches. Use file_read for the surrounding context of a hit.\n- **file_find**(glob, path, max_results): Find files by a recursive glob pattern under a base path (e.g. '**/test_*.py' or '*.md'). Prefer this over shell_run find; skips VCS/dependency/build dirs.\n- **edit_file**(path, edits, dry_run, diff) [capability_expand category: write]: Apply targeted, anchored search-replace edits to an EXISTING text file (use file_write to create/overwrite). Each edit is {original_text, new_text, replace_all?}; original_text must match exactly and uniquely (or set replace_all) — a non-unique or missing anchor is rejected so files are never corrupted. Set dry_run to preview. Alternatively pass a unified 'diff' to apply its hunks as anchored edits. Far cheaper and safer than rewriting a whole file.\n- **shell_run**(command, cwd, timeout) [capability_expand category: shell]: Execute a one-shot shell command with timeout protection. Runs in the active workspace; paths resolving outside it are refused. Reach for a structured tool first when one fits — web_fetch for anything over HTTP(S), git_query/scm_sync for git, code_search/file_find/file_read for the repo, config_get/config_set for LeapFlow's own settings — because those report typed results, while a failed shell command can only be diagnosed from its exit code and stderr. Every shell run counts as an external side effect, so a failure stops the rest of the batch and is not retried automatically.\n- **terminal_open**(command, cwd, shell) [capability_expand category: terminal]: Open a PERSISTENT shell session (REPL/dev server/watch), returning a session_id for terminal_send/read/close. Disabled unless tools.terminal_session_enabled is set. For one-shot commands use shell_run instead.\n- **terminal_send**(session_id, input, wait) [capability_expand category: terminal]: Send a line of input to a persistent terminal session and return output captured shortly after.\n- **terminal_read**(session_id, wait): Drain buffered output from a persistent terminal session (optionally waiting briefly first).\n- **terminal_close**(session_id) [capability_expand category: terminal]: Terminate a persistent terminal session and release its process group.\n- **terminal_list**(): List active persistent terminal sessions.\n- **config_list**(category, limit): List LeapFlow's own writable settings (LLM model and endpoint, daemon, memory, perception, gateway, …) with current values. Use this to discover the exact key before changing anything. Optionally narrow by `category`. This is the only correct way to inspect LeapFlow configuration — never read config files from disk.\n- **config_get**(key): Read one LeapFlow setting by key (e.g. 'llm.model', 'llm.base_url', 'daemon.log_level'), returning its current value, type, scopes, and whether a change needs a daemon restart. There is no 'llm.provider' key: provider behavior is inferred from the OpenAI-compatible 'llm.base_url'. Never read LeapFlow config files from disk — use this.\n- **config_set**(key, value, scope) [capability_expand category: config]: Change one LeapFlow setting by key, e.g. switch an OpenAI-compatible LLM with key='llm.model' and key='llm.base_url'. There is no 'llm.provider' key: provider behavior is inferred from the endpoint. Values are validated and coerced; credentials are stored in the vault automatically. Call config_list or config_get first if unsure of the key. The result states whether a `leap daemon restart` is required. Never edit LeapFlow config files directly.\n- **web_fetch**(url, select, timeout, max_bytes): Read a URL over HTTP(S) and get back extracted, context-sized content: parsed JSON for API endpoints, readable text plus links for web pages. Use this for anything on the internet — prices, docs, releases, articles — instead of running curl through shell_run: it reports real HTTP status codes, retries rate limits on its own, and is a plain read so a retry is always safe. For JSON APIs pass `select` with a dotted path (e.g. 'chart.result.0.meta') to return just that part instead of the whole payload.\n- **memory_search**(query, limit): Search agent memory for relevant past experiences, observations, and facts.\n- **memory_add**(content, kind) [capability_expand category: write]: Store a new observation or insight in memory for future reference.\n- **research_note**(kind, text): Record a compact, structured note about the current task's state so it survives context compression on long / multi-step tasks. Use for durable findings, open questions still to resolve, decisions / excluded paths, and the immediate next step. One concise sentence per note.\n- **capability_expand**(category): Fetch the full callable schema for every tool in a capability category (e.g. 'hub', 'gateway', 'desktop', 'delegate', 'file', 'memory', 'skill'). The compact tool index always lists every registered tool by name and a one-line summary, but only a static low-risk subset is directly callable each turn. If you need a tool from the index that is not yet callable, call capability_expand with its category first; the matching tools become callable in this turn. Never invent a tool name — expand the category instead.\n- **delegate_task**(goal, context) [capability_expand category: delegate]: Delegate a complex sub-task to an isolated subagent. The subagent gets a fresh context and restricted tool access. Use when a task is self-contained and can be solved independently.\n- **schedule_reentry**(kind, reason, delay_seconds, event_match, max_reentries, deadline_seconds): Register a re-entry so this task can resume later from its current orientation (findings / open questions / next step). Use when work must pause and continue after a delay (kind=time) or when a matching platform event arrives (kind=event), instead of finishing now. The research-ledger state is carried over automatically.\n- **hub_push**(skill_name, visibility, version) [capability_expand category: hub]: Push a local skill to the ModelScope Hub for sharing or backup.\n- **hub_pull**(repo_id, version) [capability_expand category: hub]: Pull a skill from the ModelScope Hub to install locally.\n- **hub_search**(query) [capability_expand category: hub]: Search for skills on the Hub by keyword or description.\n- **hub_sync**(mode, dry_run) [capability_expand category: hub]: Preview or execute sync between local skills and Hub.\n- **platform_action**(platform, action, payload, backend_kind) [capability_expand category: gateway]: Execute an exact registered business action on an external platform through LeapFlow's App Connector layer. Actions must be copied from the App Connector Capability Index and are addressed as domain.operation, e.g. im.send_message or docs.create_markdown. All business fields (chat_id, text, query, etc.) MUST be placed inside `payload`, never at the top level. Example: {\"platform\":\"feishu\",\"action\":\"im.send_message\",\"payload\":{\"chat_id\":\"oc_xxx\",\"text\":\"hello\"}}. Do not invent action names, do not use management actions such as list/guide/connect/status here.\n- **platform_connect**(action, platform, credentials, options, checkpoint) [capability_expand category: gateway]: List, guide, connect, disconnect, remove, or check status for external platforms using the App Connector management namespace. Supports REST and CLI backends. Use this for management actions such as list/guide/preflight/connect/status; use platform_action only for exact registered business actions.\n- **gateway_send**(platform, chat_id, text, thread_id) [capability_expand category: gateway]: Send a message to a connected external platform (Feishu group, Telegram chat, DingTalk conversation, etc.). Requires the platform to be connected via gateway_connect first. Use gateway_connect with action='list' to see connected platforms and available chat IDs.\n- **gateway_connect**(action, platform, credentials, options) [capability_expand category: gateway]: Connect, configure, or manage external platform integrations (Feishu, DingTalk, Telegram, Slack, Discord, etc.). Conversational flow: 1) call 'guide' to get setup steps + required fields, 2) present the steps to the user and ask for ALL required credentials in a single message, 3) call 'connect' with the credentials. Goal: complete in 1–2 user turns. NEVER include credential values in your text response.\n- **plugin_list**(): List the live plugin registry across Tool/Gateway/LLM subsystems for plugin management: which plugins are loaded, their category and fiber lifecycle state. For questions about LeapFlow's own capabilities or whether it supports X, use self_describe instead.\n- **plugin_status**(plugin_id): Get detailed status of a specific plugin: its declared category, runtime dependencies, contributed tools, and fiber lifecycle state.\n- **plugin_versions**(plugin_id): List recorded source versions and active pointer for a profile-scoped plugin.\n- **plugin_propose**(requested_capability, plugin_id, proposed_tools, test_cases, risk_level, evidence): Create a side-effect-free PluginProposal from explicit capability-gap evidence. Use this before plugin_generate when a missing capability should be reviewed. Does not call an LLM, write files, or install anything.\n- **assess_compatibility**(manifest, source_path) [capability_expand category: plugin_management]: Assess whether a foreign plugin manifest or real DSH source bundle is compatible with LeapFlow. A source bundle assessment is static: runtime_ready stays false until restricted Node discovery during plugin_install. Returns component-level verdicts and limitations.\n- **plugin_generate**(plugin_id, description, proposal_id) [capability_expand category: system]: Generate a new ToolPlugin from a natural-language capability description. The LLM produces code that conforms to the ToolPlugin Protocol; it is then rigorously validated (syntax, structure, import, protocol conformance). The generated source is stored in CAS and receives explicit content approval. It DOES NOT install the plugin — installation requires a second, mutation-specific approval via plugin_install.\n- **plugin_install**(plugin_id, code, marketplace_name, source_path, proposal_id, version_label) [capability_expand category: system]: Install a plugin from exactly one source: validated Python code, the configured Python marketplace, or a real DSH source bundle. DSH bundles run restricted Node discovery before registration; only public host tools are installed and client UI limitations are reported. Writes profile-scoped state and mutates the process-global registry. REQUIRES APPROVAL.\n- **plugin_rollback**(plugin_id, version) [capability_expand category: system]: Rollback a profile-scoped plugin to a recorded source version and reload it. REQUIRES APPROVAL.\n- **plugin_reload**(plugin_id, version_label) [capability_expand category: system]: Hot-reload a plugin at runtime. Disposes the old plugin fiber, re-imports its module, and registers a fresh instance. Existing in-flight turns are unaffected (snapshot isolation). REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_disable**(plugin_id) [capability_expand category: system]: Disable a plugin by disposing its fiber, removing its tools from the runtime registry. Cannot disable self_management itself. REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_remove**(plugin_id, delete_source) [capability_expand category: system]: Terminally remove a plugin: dispose its fiber, unregister its tools, remove reload metadata, and optionally delete its profile-scoped source file. Cannot remove self_management itself. REQUIRES APPROVAL.\n- **plugin_unquarantine**(plugin_id) [capability_expand category: plugin_management]: Restore a quarantined plugin to probation status for re-evaluation. Unfreezes the trust ledger, transitions the proposal back to PROBATION, and reloads the plugin. REQUIRES APPROVAL.\n- **plugin_enable**(plugin_id) [capability_expand category: system]: Re-enable a previously disabled plugin by reloading its module and registering a fresh instance. REQUIRES APPROVAL.\n- **self_describe**(facet): Answer questions about LeapFlow ITSELF from live runtime evidence — its identity/version/model, what tools and plugins it has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of self-awareness. Prefer this over reading LeapFlow's own source code. Pick facet=capabilities for 'what can you do / do you support X', identity for version/model, runtime for context/posture, or all for a full self-check when explicitly requested.\n- **runtime_snapshot**(): Lightweight (~150 token) flat snapshot of current runtime state: model, context budget, posture, disclosure level, turn count, cache hit rate, uptime, tool count, and pending approvals.\n- **schedule_create**(trigger_expression, instruction, execution_mode, max_retries, delivery_target) [capability_expand category: scheduler]: Create a new scheduled task. Specify a trigger expression (e.g. '30m', 'every 2h', '0 9 * * *') and the instruction to execute on each trigger. Optionally provide an execution mode and a delivery target to receive notifications.\n- **schedule_list**() [capability_expand category: scheduler]: List all scheduled tasks with their current state, trigger, and next due time.\n- **schedule_status**(task_id) [capability_expand category: scheduler]: Get detailed status and recent execution history for a scheduled task.\n- **schedule_pause**(task_id) [capability_expand category: scheduler]: Pause a scheduled task so it stops firing without being cancelled.\n- **schedule_resume**(task_id) [capability_expand category: scheduler]: Resume a paused scheduled task — re-arms it and recalculates next due time.\n- **schedule_cancel**(task_id) [capability_expand category: scheduler]: Cancel a scheduled task permanently.\n- **tool_search**(query, max_results): Search all registered tools by keyword relevance. Returns a ranked list of matching tools with name, category, and summary. Use this when you need a tool but are unsure of its exact name or category.\n- **tool_describe**(tool_name): Get the full callable schema of a single tool by exact name. Returns the tool's description, parameters, and metadata. Use after tool_search to inspect a specific tool before calling it.\n- **session_search**(query, limit) [capability_expand category: unclassified]: Search past conversation sessions in the current workspace for relevant context.\n- **session_list**(limit): List recent conversation sessions in the current workspace with stable ids, titles, dates, and summaries. Use for browsing past tasks or when user asks to see history without specific search terms. No keywords needed — returns chronological list.\n- **session_detail**(session_id, limit, offset, include_inactive) [capability_expand category: unclassified]: Read a paginated persisted transcript for one past conversation session in the current workspace. Use after session_list or session_search returns a session_id.\n- **click**(element_index) [capability_expand category: desktop]: Click a UI element by its element_index (from the latest observe_ui snapshot)\n- **get_clipboard**() [capability_expand category: desktop]: Read current clipboard text content\n- **list_apps**(filter, running_only) [capability_expand category: desktop]: List available applications on this system. Use to discover correct bundle_id before switch_app.\n- **list_windows**() [capability_expand category: desktop]: List all top-level windows with pid, window_id, title, and per-window state (minimized, on-screen). Call this first to pick the pid and window_id that observe_ui and other window tools require.\n- **observe_ui**(pid, window_id, query) [capability_expand category: desktop]: Snapshot one window's actionable UI elements, each tagged with an element_index for click/right_click/read_text. Re-observe after actions — indices belong to one snapshot. Requires the window's pid and window_id from list_windows.\n- **open_url**(url, app_id) [capability_expand category: desktop]: Open a URL in the default or specified browser\n- **read_text**(element_index) [capability_expand category: desktop]: Read the text content of a specific UI element from the latest snapshot\n- **right_click**(element_index) [capability_expand category: desktop]: Right-click a UI element to open its context menu. Returns visible menu items.\n- **screenshot**(pid, window_id) [capability_expand category: desktop]: Capture a screenshot for visual verification. With pid + window_id captures that window (works across all displays); defaults to the last observed window, or the full desktop when no window has been observed.\n- **scroll**(element_index, direction, amount, pid, window_id) [capability_expand category: desktop]: Scroll a scrollable area of a window. Omit element_index to scroll the window's focused/page scroller; pass one to scroll an exact element from the latest snapshot.\n- **select_text**(element_index) [capability_expand category: desktop]: Select all text in a UI element (focus + select-all, for subsequent copy)\n- **set_clipboard**(text) [capability_expand category: desktop]: Write text to the clipboard\n- **shortcut**(keys) [capability_expand category: desktop]: Execute a keyboard shortcut\n- **switch_app**(app_id) [capability_expand category: desktop]: Switch to an app (launch if needed, activate, verify)\n- **type_text**(text) [capability_expand category: desktop]: Type text into the currently focused element\n- **wait**(seconds) [capability_expand category: desktop]: Wait for a specified duration before continuing\n- **wait_until**(condition, pid, window_id, timeout, poll_interval) [capability_expand category: desktop]: Wait until a UI condition is met (polls UI tree). Returns elements when found or on timeout.\n- **wait_until_stable**(timeout, poll_interval, pid, window_id) [capability_expand category: desktop]: Wait until the UI stops changing (element set stabilizes across polls).\n\n## App Connector Capability Index\nLeapFlow can onboard and manage external apps through `platform_connect` and execute exact registered business actions through `platform_action`.\nFor requests about connecting, setting up, configuring, enabling, or managing a supported app, use `platform_connect` first instead of generating SDK/Webhook sample code.\n`platform_connect.action` is the management namespace: list, guide, preflight, connect, disconnect, remove, status, events_start, events_stop, events_status.\n`platform_action.action` is only for exact registered platform business actions listed below, such as `im.send_message`; never use management actions like `list` or `guide` there.\nAll business fields MUST go inside `payload`; top-level keys are only `platform`, `action`, and `payload`.\nDo not invent platform IDs or platform action names. If the needed action is not listed, ask for discovery/clarification instead of guessing.\nUse `platform_connect` with `action='guide'` and the matching `platform` to start onboarding; use `action='list'` when the app is unclear.\nWhen a pending onboarding state is present, continue from that state with `platform_connect` instead of asking the user to restate the app.\nSupported apps:\n- `api_server`: API Server (OpenAI Compatible) (category=api; backend=adapter; platform_action actions=none registered)\n- `dingtalk`: 钉钉 (DingTalk) (category=im; backend=adapter; platform_action actions=none registered)\n- `feishu`: 飞书 (Feishu/Lark) (category=im; backend=cli)\n - `calendar.create_event` payload={summary*, start_time*, end_time*, attendees} [write/high]\n - `docs.create_markdown` payload={title*, markdown*, folder_token} [write/medium]\n - `drive.search` payload={query*, limit} [read/medium]\n - `im.add_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.download_resource` payload={message_id*, file_key*, type*} [read/low]\n - `im.get_messages` payload={message_ids*} [read/low]\n - `im.list_chats` payload={page_size} [read/low]\n - `im.list_messages` payload={chat_id*} [read/low]\n - `im.list_thread_messages` payload={thread*} [read/low]\n - `im.remove_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.reply_message` payload={message_id*, text*} [send/high]\n - `im.search_chats` payload={query*, page_size} [read/low]\n - `im.search_messages` payload={query*} [read/medium]\n - `im.send_message` payload={chat_id*, text*, thread_id} [send/high]\n - `im.update_card` payload={token*, card*} [write/medium]\n - `im.update_message` payload={message_id*, text*} [write/medium]\n - `mail.search_unread` payload={query, limit} [read/high]\n - `sheets.append_row` payload={spreadsheet_token*, sheet_id*, values*} [write/medium]\n - `task.create` payload={title*, description, due_time} [write/medium]\n- `telegram`: Telegram (category=social; backend=adapter; platform_action actions=none registered)\n- `webhook`: Webhook (Generic) (category=webhook; backend=adapter; platform_action actions=none registered)\n\n## Tool Usage\nTools are normally invoked through the native function-calling mechanism, not by writing JSON in your reply\ntext. Only if the provider signals that native function calling is unavailable for this turn, fall back to a\nsingle JSON code block: `{\"name\": \"tool_name\", \"arguments\": {\"key\": \"value\"}}` — use this fallback format\nonly, never both. Only call a tool whose exact name appears in the tool index above and is currently callable\n(or reachable via `capability_expand`). Never invent, rename, alias, or guess a tool name, platform ID, or\nplatform action from argument shape or wording — if the index does not list it, it does not exist.\n`platform_connect.action` (list/guide/preflight/connect/disconnect/remove/status/events_start/events_stop/\nevents_status) is the App Connector management namespace; `platform_action.action` only accepts exact\nregistered business actions such as `im.send_message` shown in the App Connector Capability Index — never mix\nthe two namespaces. If a tool call returns an unknown/unavailable result, use the returned suggestions or\navailable names for a single retry instead of trying further variations of the same guess.\n\n**Side-effect action rule** (`platform_action` with effect=send/write/execute):\n- Call each unique action+payload **exactly once**. Never include duplicates in the same turn.\n- Once the result returns `\"completed\": true`, that action is DONE for this task. Do NOT call it again\n in any subsequent turn — immediately summarize the result for the user instead.\n- The system enforces idempotency: duplicate calls are blocked and will not execute.\n- If the user explicitly requests sending/writing multiple times, use distinct payloads per call.\n\n**Resource identifier provenance rule**:\n- NEVER fabricate, guess, or infer resource identifiers (chat_id, message_id, file_key, user_id, etc.).\n Every resource ID used in a side-effect action MUST come from a successful API response in this session.\n- If a read/list action fails (e.g. authorization error), you do NOT have valid resource IDs.\n Report the failure to the user — do NOT attempt the dependent write/send action with a guessed ID.\n- When a tool result contains `\"llm_instruction\"`, follow it exactly.\n\n## Guidelines\n1. **Direct answers first**: If you already know the answer, respond directly without tools.\n2. **Avoid redundant tool calls**: Do not call the same tool with the same arguments more than once in the same user turn. When an existing tool result already answers the user's request, stop calling tools and answer directly.\n3. **Use tools proactively**: When the user asks about files, time, system state, or needs actions performed, use the appropriate tool.\n4. **Chain tools when needed**: You can call multiple tools in sequence (e.g., list files → read file → summarize).\n5. **Handle failures gracefully**: If a tool fails, explain what went wrong and suggest alternatives. If it failed because of an unknown tool/platform/action name, retry once with an exact name from the returned suggestions, then explain rather than keep guessing.\n6. **Summarize results naturally**: After tool execution, synthesize the results into a helpful answer rather than dumping raw output.\n7. **Stay conversational**: Maintain a natural, helpful tone. Acknowledge context from earlier in the conversation.\n8. **Recall past work**:\n - Broad queries (\"之前做了什么\", \"列出任务\"): answer from the \"Recent Task History\" section already in your context. If insufficient, call session_list.\n - Specific lookups (\"上次那个配置怎么改的\"): call session_search with relevant phrases (NOT single characters).\n - Do NOT call search tools repeatedly with keyword fragments. One well-phrased call is better than ten fragmented ones.\n\n## Coding & Verification\nWhen working with code, prefer the precise built-in tools over ad-hoc shell: use `repo_map` to orient in an\nunfamiliar project, `code_search`/`file_find` to locate and `code_intel` for symbols, `edit_file` (anchored\nsearch-replace, or a unified `diff`) to change files — never rewrite a whole file to change a few lines — and\n`git_query` to inspect diffs/log. After edits, check `syntax_ok` in the result and run `test_run`/`lint_check`\nbefore declaring the work done. Batch independent read-only calls (search/read/query) into a single turn — they\nrun in parallel.\n\n## Presentation Style\n1. **Polished Markdown only**: Format user-facing answers with clean Markdown headings, short paragraphs, and concise bullets. Use tables only when they improve comparison or scanning.\n2. **Terminal-friendly layout**: Keep lines readable in a TUI; avoid dense walls of text, deeply nested lists, oversized ASCII art, or heavy visual blocks.\n3. **Elegant emphasis**: Use bold text sparingly for key terms and conclusions. Avoid excessive emojis, decorative symbols, repeated separators, or visual noise.\n4. **Theme-safe colors**: Do not emit ANSI escape codes, HTML color tags, Rich markup, or hardcoded color names. Rely on the TUI theme to render Markdown professionally.\n5. **No leaked tool protocol**: Never show tool-call JSON, internal schemas, raw observations, tool result payloads, or hidden reasoning in the final answer unless the user explicitly asks for raw/debug output. Treat any prior `{\"name\": ..., \"arguments\": ...}` blocks and `Tool result (...)` messages as internal execution context only.\n6. **Professional closure**: End with a concise conclusion or next step when helpful; avoid rambling after the useful answer is complete.\n\nWhen finished with all tool calls, respond normally without a JSON block, tool-call transcript, or process log.\n\n## Task Contract\n- Task ID: turn-3\n- Original user request: Is that the same invoice?\n- Workspace root: \n- Allowed roots: \n- Treat relative project paths as relative to the workspace root; never infer `.` as the project root when a workspace root is provided.\n- Workspace boundary is enforced by tools: do not read, search, edit, or run commands against paths outside the allowed roots unless the user explicitly requests an external path and the tool/approval policy permits it.\n- LeapFlow workspace config is optional at `/.leapflow/config.yaml`; runtime config is loaded from `~/.leapflow/config/user.yaml` and `~/.leapflow/profiles//config/*.yaml`.\n- Preserve this task contract across summarization, compression, tool loops, and memory retrieval.\n" + }, + { + "role": "system", + "content": "## Semantic Focus Plane\nCurrent task focus:\n- Target: invoice.txt\n- Type: file\n- Evidence refs: file_read:invoice.txt\nResolved user reference:\n- file -> invoice.txt (task_semantic, confidence=0.92)\nUse Current task focus for deictic task references such as 'the above paper'; use control-plane events only when the user explicitly asks about runtime settings.\n\n\n## Recent Session Summary\n- [assistant] The invoice total is 128.50 USD.\n- [user] Is that the same invoice?\n" + }, + { + "role": "assistant", + "content": "The invoice total is 128.50 USD." + }, + { + "role": "user", + "content": "Is that the same invoice?\nIs that the same invoice?" + } + ], + "tools": [ + "capability_expand", + "code_intel", + "code_search", + "config_get", + "config_list", + "env_info", + "file_find", + "file_list", + "file_read", + "git_query", + "lint_check", + "memory_search", + "plugin_list", + "plugin_propose", + "plugin_status", + "plugin_versions", + "repo_map", + "research_note", + "runtime_snapshot", + "schedule_reentry", + "self_describe", + "session_list", + "skill_view", + "skills_list", + "terminal_list", + "terminal_read", + "test_run", + "text_search", + "time_get", + "tool_describe", + "tool_search", + "web_fetch" + ] + }, + "responses": [ + { + "status": 200, + "content_type": "application/json", + "body": "{\"id\": \"chatcmpl-cassette\", \"object\": \"chat.completion\", \"model\": \"cassette-model\", \"choices\": [{\"index\": 0, \"message\": {\"role\": \"assistant\", \"content\": \"Yes, that is the same invoice.\"}, \"finish_reason\": \"stop\"}], \"usage\": {\"prompt_tokens\": 64, \"completion_tokens\": 16, \"total_tokens\": 80}}" + } + ] +} diff --git a/tests/_fixtures/cassettes/r2_isolation/cassette-model-203cbc712bed9109.cassette.json b/tests/_fixtures/cassettes/r2_isolation/cassette-model-203cbc712bed9109.cassette.json new file mode 100644 index 00000000..cf27fb3a --- /dev/null +++ b/tests/_fixtures/cassettes/r2_isolation/cassette-model-203cbc712bed9109.cassette.json @@ -0,0 +1,67 @@ +{ + "fingerprint": "203cbc712bed91090be47929945c4c97866b7ee941a222dc44a271e7955b6018", + "note": "captured in seed mode", + "request": { + "model": "cassette-model", + "stream": false, + "messages": [ + { + "role": "system", + "content": "You are LeapFlow, an intelligent assistant that can both converse naturally and take real actions on the user's computer.\n\n## Capabilities\nThe tool index below lists **every** registered tool by name and a one-line summary — this is the complete\ncapability contract; nothing else exists. Only a subset is directly callable this turn (via native tool calling,\nnot a JSON block in your reply). If you need a tool from the index that is not yet callable, call\n`capability_expand` with its category name first — the matching tools become callable immediately after.\nWhen the user asks about LeapFlow **itself** — its identity/version/model, what tools, plugins, or capabilities\nit has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of\nself-awareness — answer from live runtime evidence via `self_describe` (facet=capabilities for \"what can you\ndo / do you support X\", identity for version/model, runtime for context/posture, or `all` for a full self-check),\nnever by reading LeapFlow's own source code, documentation, or memory. Report configuration-dependent or\nunavailable capabilities as limitations rather than inferring them.\n- **text_search**(text, pattern): Search for a regex pattern in text.\n- **text_replace**(text, old, new, count) [capability_expand category: write]: Replace occurrences of a substring in text.\n- **time_get**(): Get current date and time.\n- **env_info**(): Get system environment information (OS, Python version, cwd).\n- **skills_list**(query, category, source): List available learned skills. Use when user asks about capabilities or you need a specific skill.\n- **skill_view**(name): View the full content of a specific skill document.\n- **skill_list_files**(name) [capability_expand category: skills]: List all support files in a skill directory (configs, templates, examples). Use before skill_file_read to discover available files.\n- **skill_file_read**(name, file_path) [capability_expand category: skills]: Read an individual support file from a skill directory. Use skill_list_files first to discover available files.\n- **code_intel**(path, operation): Precise document symbols (outline) for a source file: classes, functions, and methods with line ranges. Python uses an exact AST parse; other languages use a keyword-prefix scan. Prefer over file_read mode=symbols for accurate navigation before editing. Read-only.\n- **repo_map**(path): Compact project orientation for a repository root: languages, detected test/lint commands, top-level structure, entry points, manifest, and VCS branch. Call this first when entering an unfamiliar codebase. Read-only.\n- **scm_sync**(action, cwd, remote, pull_ref, push_ref, timeout) [capability_expand category: scm]: Run a typed git SCM action. Use this instead of shell_run for git pull/push/status. For 'pull origin main then push', set action='pull_then_push', remote='origin', pull_ref='main', and omit push_ref so LeapFlow pushes the current local branch.\n- **git_query**(action, cwd, ref, path, staged, max_count, stat): Read-only structured git inspection: action=diff|log|status|branch|show. Prefer over shell_run for reading repo state — output is clipped, redacted, and log/branch are parsed into structured fields. Use scm_sync for pull/push.\n- **git_write**(action, cwd, message, stage_all, name, ref, create) [capability_expand category: scm]: Mutating git actions: action=commit (message, stage_all), branch (create+switch), checkout (switch; create=true for -b). Approval-gated. Use scm_sync for pull/push and git_query for reads.\n- **test_run**(command, cwd, timeout): Run the project's test suite and return structured results (framework, passed/failed counts, failing tests). Auto-detects the runner (pytest/npm/go/cargo) or uses a configured/explicit command; executes via the governed shell. ok=true means the runner executed — see 'success' for pass/fail.\n- **lint_check**(command, cwd, timeout): Run the project's linter and return a structured clean/issue result. Auto-detects the linter (ruff/eslint/go vet/clippy) or uses a configured/explicit command; executes via the governed shell. ok=true means the linter ran — see 'clean'.\n- **file_list**(path, pattern, depth): List files and directories at a given path. Use depth=1 or depth=2 to get a recursive tree in one call instead of listing each sub-directory separately.\n- **file_read**(path, max_lines, start_line, max_chars, mode): Read text file content with adaptive context governance. For large or unfamiliar files, prefer mode='outline' or mode='symbols' first, then use mode='raw' with start_line/max_lines for the specific range you actually need. For LeapFlow's own settings, use config_list / config_get / config_set — its config files are outside the workspace and not readable here.\n- **file_write**(path, content, mode) [capability_expand category: write]: Write content to a file (overwrite or append).\n- **code_search**(pattern, patterns, path, glob, ignore_case, multiline, max_results, context_lines): Search file CONTENTS by regex pattern across a directory tree (ripgrep-backed). Requires a regex pattern. NOT for listing or browsing directory contents — use file_list for that. Prefer this over shell_run grep: faster, skips VCS/dependency/build dirs, and returns structured path:line:column matches. Batch related lookups into ONE call via `patterns` (OR-combined, single pass) instead of issuing several separate searches. Use file_read for the surrounding context of a hit.\n- **file_find**(glob, path, max_results): Find files by a recursive glob pattern under a base path (e.g. '**/test_*.py' or '*.md'). Prefer this over shell_run find; skips VCS/dependency/build dirs.\n- **edit_file**(path, edits, dry_run, diff) [capability_expand category: write]: Apply targeted, anchored search-replace edits to an EXISTING text file (use file_write to create/overwrite). Each edit is {original_text, new_text, replace_all?}; original_text must match exactly and uniquely (or set replace_all) — a non-unique or missing anchor is rejected so files are never corrupted. Set dry_run to preview. Alternatively pass a unified 'diff' to apply its hunks as anchored edits. Far cheaper and safer than rewriting a whole file.\n- **shell_run**(command, cwd, timeout) [capability_expand category: shell]: Execute a one-shot shell command with timeout protection. Runs in the active workspace; paths resolving outside it are refused. Reach for a structured tool first when one fits — web_fetch for anything over HTTP(S), git_query/scm_sync for git, code_search/file_find/file_read for the repo, config_get/config_set for LeapFlow's own settings — because those report typed results, while a failed shell command can only be diagnosed from its exit code and stderr. Every shell run counts as an external side effect, so a failure stops the rest of the batch and is not retried automatically.\n- **terminal_open**(command, cwd, shell) [capability_expand category: terminal]: Open a PERSISTENT shell session (REPL/dev server/watch), returning a session_id for terminal_send/read/close. Disabled unless tools.terminal_session_enabled is set. For one-shot commands use shell_run instead.\n- **terminal_send**(session_id, input, wait) [capability_expand category: terminal]: Send a line of input to a persistent terminal session and return output captured shortly after.\n- **terminal_read**(session_id, wait): Drain buffered output from a persistent terminal session (optionally waiting briefly first).\n- **terminal_close**(session_id) [capability_expand category: terminal]: Terminate a persistent terminal session and release its process group.\n- **terminal_list**(): List active persistent terminal sessions.\n- **config_list**(category, limit): List LeapFlow's own writable settings (LLM model and endpoint, daemon, memory, perception, gateway, …) with current values. Use this to discover the exact key before changing anything. Optionally narrow by `category`. This is the only correct way to inspect LeapFlow configuration — never read config files from disk.\n- **config_get**(key): Read one LeapFlow setting by key (e.g. 'llm.model', 'llm.base_url', 'daemon.log_level'), returning its current value, type, scopes, and whether a change needs a daemon restart. There is no 'llm.provider' key: provider behavior is inferred from the OpenAI-compatible 'llm.base_url'. Never read LeapFlow config files from disk — use this.\n- **config_set**(key, value, scope) [capability_expand category: config]: Change one LeapFlow setting by key, e.g. switch an OpenAI-compatible LLM with key='llm.model' and key='llm.base_url'. There is no 'llm.provider' key: provider behavior is inferred from the endpoint. Values are validated and coerced; credentials are stored in the vault automatically. Call config_list or config_get first if unsure of the key. The result states whether a `leap daemon restart` is required. Never edit LeapFlow config files directly.\n- **web_fetch**(url, select, timeout, max_bytes): Read a URL over HTTP(S) and get back extracted, context-sized content: parsed JSON for API endpoints, readable text plus links for web pages. Use this for anything on the internet — prices, docs, releases, articles — instead of running curl through shell_run: it reports real HTTP status codes, retries rate limits on its own, and is a plain read so a retry is always safe. For JSON APIs pass `select` with a dotted path (e.g. 'chart.result.0.meta') to return just that part instead of the whole payload.\n- **memory_search**(query, limit): Search agent memory for relevant past experiences, observations, and facts.\n- **memory_add**(content, kind) [capability_expand category: write]: Store a new observation or insight in memory for future reference.\n- **research_note**(kind, text): Record a compact, structured note about the current task's state so it survives context compression on long / multi-step tasks. Use for durable findings, open questions still to resolve, decisions / excluded paths, and the immediate next step. One concise sentence per note.\n- **capability_expand**(category): Fetch the full callable schema for every tool in a capability category (e.g. 'hub', 'gateway', 'desktop', 'delegate', 'file', 'memory', 'skill'). The compact tool index always lists every registered tool by name and a one-line summary, but only a static low-risk subset is directly callable each turn. If you need a tool from the index that is not yet callable, call capability_expand with its category first; the matching tools become callable in this turn. Never invent a tool name — expand the category instead.\n- **delegate_task**(goal, context) [capability_expand category: delegate]: Delegate a complex sub-task to an isolated subagent. The subagent gets a fresh context and restricted tool access. Use when a task is self-contained and can be solved independently.\n- **schedule_reentry**(kind, reason, delay_seconds, event_match, max_reentries, deadline_seconds): Register a re-entry so this task can resume later from its current orientation (findings / open questions / next step). Use when work must pause and continue after a delay (kind=time) or when a matching platform event arrives (kind=event), instead of finishing now. The research-ledger state is carried over automatically.\n- **hub_push**(skill_name, visibility, version) [capability_expand category: hub]: Push a local skill to the ModelScope Hub for sharing or backup.\n- **hub_pull**(repo_id, version) [capability_expand category: hub]: Pull a skill from the ModelScope Hub to install locally.\n- **hub_search**(query) [capability_expand category: hub]: Search for skills on the Hub by keyword or description.\n- **hub_sync**(mode, dry_run) [capability_expand category: hub]: Preview or execute sync between local skills and Hub.\n- **platform_action**(platform, action, payload, backend_kind) [capability_expand category: gateway]: Execute an exact registered business action on an external platform through LeapFlow's App Connector layer. Actions must be copied from the App Connector Capability Index and are addressed as domain.operation, e.g. im.send_message or docs.create_markdown. All business fields (chat_id, text, query, etc.) MUST be placed inside `payload`, never at the top level. Example: {\"platform\":\"feishu\",\"action\":\"im.send_message\",\"payload\":{\"chat_id\":\"oc_xxx\",\"text\":\"hello\"}}. Do not invent action names, do not use management actions such as list/guide/connect/status here.\n- **platform_connect**(action, platform, credentials, options, checkpoint) [capability_expand category: gateway]: List, guide, connect, disconnect, remove, or check status for external platforms using the App Connector management namespace. Supports REST and CLI backends. Use this for management actions such as list/guide/preflight/connect/status; use platform_action only for exact registered business actions.\n- **gateway_send**(platform, chat_id, text, thread_id) [capability_expand category: gateway]: Send a message to a connected external platform (Feishu group, Telegram chat, DingTalk conversation, etc.). Requires the platform to be connected via gateway_connect first. Use gateway_connect with action='list' to see connected platforms and available chat IDs.\n- **gateway_connect**(action, platform, credentials, options) [capability_expand category: gateway]: Connect, configure, or manage external platform integrations (Feishu, DingTalk, Telegram, Slack, Discord, etc.). Conversational flow: 1) call 'guide' to get setup steps + required fields, 2) present the steps to the user and ask for ALL required credentials in a single message, 3) call 'connect' with the credentials. Goal: complete in 1–2 user turns. NEVER include credential values in your text response.\n- **plugin_list**(): List the live plugin registry across Tool/Gateway/LLM subsystems for plugin management: which plugins are loaded, their category and fiber lifecycle state. For questions about LeapFlow's own capabilities or whether it supports X, use self_describe instead.\n- **plugin_status**(plugin_id): Get detailed status of a specific plugin: its declared category, runtime dependencies, contributed tools, and fiber lifecycle state.\n- **plugin_versions**(plugin_id): List recorded source versions and active pointer for a profile-scoped plugin.\n- **plugin_propose**(requested_capability, plugin_id, proposed_tools, test_cases, risk_level, evidence): Create a side-effect-free PluginProposal from explicit capability-gap evidence. Use this before plugin_generate when a missing capability should be reviewed. Does not call an LLM, write files, or install anything.\n- **assess_compatibility**(manifest, source_path) [capability_expand category: plugin_management]: Assess whether a foreign plugin manifest or real DSH source bundle is compatible with LeapFlow. A source bundle assessment is static: runtime_ready stays false until restricted Node discovery during plugin_install. Returns component-level verdicts and limitations.\n- **plugin_generate**(plugin_id, description, proposal_id) [capability_expand category: system]: Generate a new ToolPlugin from a natural-language capability description. The LLM produces code that conforms to the ToolPlugin Protocol; it is then rigorously validated (syntax, structure, import, protocol conformance). The generated source is stored in CAS and receives explicit content approval. It DOES NOT install the plugin — installation requires a second, mutation-specific approval via plugin_install.\n- **plugin_install**(plugin_id, code, marketplace_name, source_path, proposal_id, version_label) [capability_expand category: system]: Install a plugin from exactly one source: validated Python code, the configured Python marketplace, or a real DSH source bundle. DSH bundles run restricted Node discovery before registration; only public host tools are installed and client UI limitations are reported. Writes profile-scoped state and mutates the process-global registry. REQUIRES APPROVAL.\n- **plugin_rollback**(plugin_id, version) [capability_expand category: system]: Rollback a profile-scoped plugin to a recorded source version and reload it. REQUIRES APPROVAL.\n- **plugin_reload**(plugin_id, version_label) [capability_expand category: system]: Hot-reload a plugin at runtime. Disposes the old plugin fiber, re-imports its module, and registers a fresh instance. Existing in-flight turns are unaffected (snapshot isolation). REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_disable**(plugin_id) [capability_expand category: system]: Disable a plugin by disposing its fiber, removing its tools from the runtime registry. Cannot disable self_management itself. REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_remove**(plugin_id, delete_source) [capability_expand category: system]: Terminally remove a plugin: dispose its fiber, unregister its tools, remove reload metadata, and optionally delete its profile-scoped source file. Cannot remove self_management itself. REQUIRES APPROVAL.\n- **plugin_unquarantine**(plugin_id) [capability_expand category: plugin_management]: Restore a quarantined plugin to probation status for re-evaluation. Unfreezes the trust ledger, transitions the proposal back to PROBATION, and reloads the plugin. REQUIRES APPROVAL.\n- **plugin_enable**(plugin_id) [capability_expand category: system]: Re-enable a previously disabled plugin by reloading its module and registering a fresh instance. REQUIRES APPROVAL.\n- **self_describe**(facet): Answer questions about LeapFlow ITSELF from live runtime evidence — its identity/version/model, what tools and plugins it has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of self-awareness. Prefer this over reading LeapFlow's own source code. Pick facet=capabilities for 'what can you do / do you support X', identity for version/model, runtime for context/posture, or all for a full self-check when explicitly requested.\n- **runtime_snapshot**(): Lightweight (~150 token) flat snapshot of current runtime state: model, context budget, posture, disclosure level, turn count, cache hit rate, uptime, tool count, and pending approvals.\n- **schedule_create**(trigger_expression, instruction, execution_mode, max_retries, delivery_target) [capability_expand category: scheduler]: Create a new scheduled task. Specify a trigger expression (e.g. '30m', 'every 2h', '0 9 * * *') and the instruction to execute on each trigger. Optionally provide an execution mode and a delivery target to receive notifications.\n- **schedule_list**() [capability_expand category: scheduler]: List all scheduled tasks with their current state, trigger, and next due time.\n- **schedule_status**(task_id) [capability_expand category: scheduler]: Get detailed status and recent execution history for a scheduled task.\n- **schedule_pause**(task_id) [capability_expand category: scheduler]: Pause a scheduled task so it stops firing without being cancelled.\n- **schedule_resume**(task_id) [capability_expand category: scheduler]: Resume a paused scheduled task — re-arms it and recalculates next due time.\n- **schedule_cancel**(task_id) [capability_expand category: scheduler]: Cancel a scheduled task permanently.\n- **tool_search**(query, max_results): Search all registered tools by keyword relevance. Returns a ranked list of matching tools with name, category, and summary. Use this when you need a tool but are unsure of its exact name or category.\n- **tool_describe**(tool_name): Get the full callable schema of a single tool by exact name. Returns the tool's description, parameters, and metadata. Use after tool_search to inspect a specific tool before calling it.\n- **session_search**(query, limit) [capability_expand category: unclassified]: Search past conversation sessions in the current workspace for relevant context.\n- **session_list**(limit): List recent conversation sessions in the current workspace with stable ids, titles, dates, and summaries. Use for browsing past tasks or when user asks to see history without specific search terms. No keywords needed — returns chronological list.\n- **session_detail**(session_id, limit, offset, include_inactive) [capability_expand category: unclassified]: Read a paginated persisted transcript for one past conversation session in the current workspace. Use after session_list or session_search returns a session_id.\n- **click**(element_index) [capability_expand category: desktop]: Click a UI element by its element_index (from the latest observe_ui snapshot)\n- **get_clipboard**() [capability_expand category: desktop]: Read current clipboard text content\n- **list_apps**(filter, running_only) [capability_expand category: desktop]: List available applications on this system. Use to discover correct bundle_id before switch_app.\n- **list_windows**() [capability_expand category: desktop]: List all top-level windows with pid, window_id, title, and per-window state (minimized, on-screen). Call this first to pick the pid and window_id that observe_ui and other window tools require.\n- **observe_ui**(pid, window_id, query) [capability_expand category: desktop]: Snapshot one window's actionable UI elements, each tagged with an element_index for click/right_click/read_text. Re-observe after actions — indices belong to one snapshot. Requires the window's pid and window_id from list_windows.\n- **open_url**(url, app_id) [capability_expand category: desktop]: Open a URL in the default or specified browser\n- **read_text**(element_index) [capability_expand category: desktop]: Read the text content of a specific UI element from the latest snapshot\n- **right_click**(element_index) [capability_expand category: desktop]: Right-click a UI element to open its context menu. Returns visible menu items.\n- **screenshot**(pid, window_id) [capability_expand category: desktop]: Capture a screenshot for visual verification. With pid + window_id captures that window (works across all displays); defaults to the last observed window, or the full desktop when no window has been observed.\n- **scroll**(element_index, direction, amount, pid, window_id) [capability_expand category: desktop]: Scroll a scrollable area of a window. Omit element_index to scroll the window's focused/page scroller; pass one to scroll an exact element from the latest snapshot.\n- **select_text**(element_index) [capability_expand category: desktop]: Select all text in a UI element (focus + select-all, for subsequent copy)\n- **set_clipboard**(text) [capability_expand category: desktop]: Write text to the clipboard\n- **shortcut**(keys) [capability_expand category: desktop]: Execute a keyboard shortcut\n- **switch_app**(app_id) [capability_expand category: desktop]: Switch to an app (launch if needed, activate, verify)\n- **type_text**(text) [capability_expand category: desktop]: Type text into the currently focused element\n- **wait**(seconds) [capability_expand category: desktop]: Wait for a specified duration before continuing\n- **wait_until**(condition, pid, window_id, timeout, poll_interval) [capability_expand category: desktop]: Wait until a UI condition is met (polls UI tree). Returns elements when found or on timeout.\n- **wait_until_stable**(timeout, poll_interval, pid, window_id) [capability_expand category: desktop]: Wait until the UI stops changing (element set stabilizes across polls).\n\n## App Connector Capability Index\nLeapFlow can onboard and manage external apps through `platform_connect` and execute exact registered business actions through `platform_action`.\nFor requests about connecting, setting up, configuring, enabling, or managing a supported app, use `platform_connect` first instead of generating SDK/Webhook sample code.\n`platform_connect.action` is the management namespace: list, guide, preflight, connect, disconnect, remove, status, events_start, events_stop, events_status.\n`platform_action.action` is only for exact registered platform business actions listed below, such as `im.send_message`; never use management actions like `list` or `guide` there.\nAll business fields MUST go inside `payload`; top-level keys are only `platform`, `action`, and `payload`.\nDo not invent platform IDs or platform action names. If the needed action is not listed, ask for discovery/clarification instead of guessing.\nUse `platform_connect` with `action='guide'` and the matching `platform` to start onboarding; use `action='list'` when the app is unclear.\nWhen a pending onboarding state is present, continue from that state with `platform_connect` instead of asking the user to restate the app.\nSupported apps:\n- `api_server`: API Server (OpenAI Compatible) (category=api; backend=adapter; platform_action actions=none registered)\n- `dingtalk`: 钉钉 (DingTalk) (category=im; backend=adapter; platform_action actions=none registered)\n- `feishu`: 飞书 (Feishu/Lark) (category=im; backend=cli)\n - `calendar.create_event` payload={summary*, start_time*, end_time*, attendees} [write/high]\n - `docs.create_markdown` payload={title*, markdown*, folder_token} [write/medium]\n - `drive.search` payload={query*, limit} [read/medium]\n - `im.add_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.download_resource` payload={message_id*, file_key*, type*} [read/low]\n - `im.get_messages` payload={message_ids*} [read/low]\n - `im.list_chats` payload={page_size} [read/low]\n - `im.list_messages` payload={chat_id*} [read/low]\n - `im.list_thread_messages` payload={thread*} [read/low]\n - `im.remove_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.reply_message` payload={message_id*, text*} [send/high]\n - `im.search_chats` payload={query*, page_size} [read/low]\n - `im.search_messages` payload={query*} [read/medium]\n - `im.send_message` payload={chat_id*, text*, thread_id} [send/high]\n - `im.update_card` payload={token*, card*} [write/medium]\n - `im.update_message` payload={message_id*, text*} [write/medium]\n - `mail.search_unread` payload={query, limit} [read/high]\n - `sheets.append_row` payload={spreadsheet_token*, sheet_id*, values*} [write/medium]\n - `task.create` payload={title*, description, due_time} [write/medium]\n- `telegram`: Telegram (category=social; backend=adapter; platform_action actions=none registered)\n- `webhook`: Webhook (Generic) (category=webhook; backend=adapter; platform_action actions=none registered)\n\n## Tool Usage\nTools are normally invoked through the native function-calling mechanism, not by writing JSON in your reply\ntext. Only if the provider signals that native function calling is unavailable for this turn, fall back to a\nsingle JSON code block: `{\"name\": \"tool_name\", \"arguments\": {\"key\": \"value\"}}` — use this fallback format\nonly, never both. Only call a tool whose exact name appears in the tool index above and is currently callable\n(or reachable via `capability_expand`). Never invent, rename, alias, or guess a tool name, platform ID, or\nplatform action from argument shape or wording — if the index does not list it, it does not exist.\n`platform_connect.action` (list/guide/preflight/connect/disconnect/remove/status/events_start/events_stop/\nevents_status) is the App Connector management namespace; `platform_action.action` only accepts exact\nregistered business actions such as `im.send_message` shown in the App Connector Capability Index — never mix\nthe two namespaces. If a tool call returns an unknown/unavailable result, use the returned suggestions or\navailable names for a single retry instead of trying further variations of the same guess.\n\n**Side-effect action rule** (`platform_action` with effect=send/write/execute):\n- Call each unique action+payload **exactly once**. Never include duplicates in the same turn.\n- Once the result returns `\"completed\": true`, that action is DONE for this task. Do NOT call it again\n in any subsequent turn — immediately summarize the result for the user instead.\n- The system enforces idempotency: duplicate calls are blocked and will not execute.\n- If the user explicitly requests sending/writing multiple times, use distinct payloads per call.\n\n**Resource identifier provenance rule**:\n- NEVER fabricate, guess, or infer resource identifiers (chat_id, message_id, file_key, user_id, etc.).\n Every resource ID used in a side-effect action MUST come from a successful API response in this session.\n- If a read/list action fails (e.g. authorization error), you do NOT have valid resource IDs.\n Report the failure to the user — do NOT attempt the dependent write/send action with a guessed ID.\n- When a tool result contains `\"llm_instruction\"`, follow it exactly.\n\n## Guidelines\n1. **Direct answers first**: If you already know the answer, respond directly without tools.\n2. **Avoid redundant tool calls**: Do not call the same tool with the same arguments more than once in the same user turn. When an existing tool result already answers the user's request, stop calling tools and answer directly.\n3. **Use tools proactively**: When the user asks about files, time, system state, or needs actions performed, use the appropriate tool.\n4. **Chain tools when needed**: You can call multiple tools in sequence (e.g., list files → read file → summarize).\n5. **Handle failures gracefully**: If a tool fails, explain what went wrong and suggest alternatives. If it failed because of an unknown tool/platform/action name, retry once with an exact name from the returned suggestions, then explain rather than keep guessing.\n6. **Summarize results naturally**: After tool execution, synthesize the results into a helpful answer rather than dumping raw output.\n7. **Stay conversational**: Maintain a natural, helpful tone. Acknowledge context from earlier in the conversation.\n8. **Recall past work**:\n - Broad queries (\"之前做了什么\", \"列出任务\"): answer from the \"Recent Task History\" section already in your context. If insufficient, call session_list.\n - Specific lookups (\"上次那个配置怎么改的\"): call session_search with relevant phrases (NOT single characters).\n - Do NOT call search tools repeatedly with keyword fragments. One well-phrased call is better than ten fragmented ones.\n\n## Coding & Verification\nWhen working with code, prefer the precise built-in tools over ad-hoc shell: use `repo_map` to orient in an\nunfamiliar project, `code_search`/`file_find` to locate and `code_intel` for symbols, `edit_file` (anchored\nsearch-replace, or a unified `diff`) to change files — never rewrite a whole file to change a few lines — and\n`git_query` to inspect diffs/log. After edits, check `syntax_ok` in the result and run `test_run`/`lint_check`\nbefore declaring the work done. Batch independent read-only calls (search/read/query) into a single turn — they\nrun in parallel.\n\n## Presentation Style\n1. **Polished Markdown only**: Format user-facing answers with clean Markdown headings, short paragraphs, and concise bullets. Use tables only when they improve comparison or scanning.\n2. **Terminal-friendly layout**: Keep lines readable in a TUI; avoid dense walls of text, deeply nested lists, oversized ASCII art, or heavy visual blocks.\n3. **Elegant emphasis**: Use bold text sparingly for key terms and conclusions. Avoid excessive emojis, decorative symbols, repeated separators, or visual noise.\n4. **Theme-safe colors**: Do not emit ANSI escape codes, HTML color tags, Rich markup, or hardcoded color names. Rely on the TUI theme to render Markdown professionally.\n5. **No leaked tool protocol**: Never show tool-call JSON, internal schemas, raw observations, tool result payloads, or hidden reasoning in the final answer unless the user explicitly asks for raw/debug output. Treat any prior `{\"name\": ..., \"arguments\": ...}` blocks and `Tool result (...)` messages as internal execution context only.\n6. **Professional closure**: End with a concise conclusion or next step when helpful; avoid rambling after the useful answer is complete.\n\nWhen finished with all tool calls, respond normally without a JSON block, tool-call transcript, or process log.\n\n## Task Contract\n- Task ID: turn-2\n- Original user request: Second A turn.\n- Workspace root: \n- Allowed roots: \n- Treat relative project paths as relative to the workspace root; never infer `.` as the project root when a workspace root is provided.\n- Workspace boundary is enforced by tools: do not read, search, edit, or run commands against paths outside the allowed roots unless the user explicitly requests an external path and the tool/approval policy permits it.\n- LeapFlow workspace config is optional at `/.leapflow/config.yaml`; runtime config is loaded from `~/.leapflow/config/user.yaml` and `~/.leapflow/profiles//config/*.yaml`.\n- Preserve this task contract across summarization, compression, tool loops, and memory retrieval.\n" + }, + { + "role": "system", + "content": "\n## Recent Session Summary\n- [assistant] Workspace A acknowledged.\n- [user] Second A turn.\n" + }, + { + "role": "assistant", + "content": "Workspace A acknowledged." + }, + { + "role": "user", + "content": "Second A turn.\nSecond A turn." + } + ], + "tools": [ + "capability_expand", + "code_intel", + "code_search", + "config_get", + "config_list", + "env_info", + "file_find", + "file_list", + "file_read", + "git_query", + "lint_check", + "memory_search", + "plugin_list", + "plugin_propose", + "plugin_status", + "plugin_versions", + "repo_map", + "research_note", + "runtime_snapshot", + "schedule_reentry", + "self_describe", + "session_list", + "skill_view", + "skills_list", + "terminal_list", + "terminal_read", + "test_run", + "text_search", + "time_get", + "tool_describe", + "tool_search", + "web_fetch" + ] + }, + "responses": [ + { + "status": 200, + "content_type": "application/json", + "body": "{\"id\": \"chatcmpl-cassette\", \"object\": \"chat.completion\", \"model\": \"cassette-model\", \"choices\": [{\"index\": 0, \"message\": {\"role\": \"assistant\", \"content\": \"Still workspace A.\"}, \"finish_reason\": \"stop\"}], \"usage\": {\"prompt_tokens\": 64, \"completion_tokens\": 16, \"total_tokens\": 80}}" + } + ] +} diff --git a/tests/_fixtures/cassettes/r2_isolation/cassette-model-30e1383d84511916.cassette.json b/tests/_fixtures/cassettes/r2_isolation/cassette-model-30e1383d84511916.cassette.json deleted file mode 100644 index 928804eb..00000000 --- a/tests/_fixtures/cassettes/r2_isolation/cassette-model-30e1383d84511916.cassette.json +++ /dev/null @@ -1,63 +0,0 @@ -{ - "fingerprint": "30e1383d84511916851127d57619abdf23913e4f7d28138fd7492512889aae21", - "note": "captured in seed mode", - "request": { - "model": "cassette-model", - "stream": false, - "messages": [ - { - "role": "system", - "content": "You are LeapFlow, an intelligent assistant that can both converse naturally and take real actions on the user's computer.\n\n## Capabilities\nThe tool index below lists **every** registered tool by name and a one-line summary — this is the complete\ncapability contract; nothing else exists. Only a subset is directly callable this turn (via native tool calling,\nnot a JSON block in your reply). If you need a tool from the index that is not yet callable, call\n`capability_expand` with its category name first — the matching tools become callable immediately after.\nWhen the user asks what LeapFlow itself supports, whether it supports plugins, or which runtime capabilities\nare available, use the live capability evidence exposed by `plugin_list` before making capability claims; report\nconfiguration-dependent or unavailable capabilities as limitations instead of inferring from documentation.\n- **text_search**(text, pattern): Search for a regex pattern in text.\n- **text_replace**(text, old, new, count) [capability_expand category: write]: Replace occurrences of a substring in text.\n- **time_get**(): Get current date and time.\n- **env_info**(): Get system environment information (OS, Python version, cwd).\n- **skills_list**(query, category, source): List available learned skills. Use when user asks about capabilities or you need a specific skill.\n- **skill_view**(name): View the full content of a specific skill document.\n- **code_intel**(path, operation): Precise document symbols (outline) for a source file: classes, functions, and methods with line ranges. Python uses an exact AST parse; other languages use a keyword-prefix scan. Prefer over file_read mode=symbols for accurate navigation before editing. Read-only.\n- **repo_map**(path): Compact project orientation for a repository root: languages, detected test/lint commands, top-level structure, entry points, manifest, and VCS branch. Call this first when entering an unfamiliar codebase. Read-only.\n- **scm_sync**(action, cwd, remote, pull_ref, push_ref, timeout) [capability_expand category: scm]: Run a typed git SCM action. Use this instead of shell_run for git pull/push/status. For 'pull origin main then push', set action='pull_then_push', remote='origin', pull_ref='main', and omit push_ref so LeapFlow pushes the current local branch.\n- **git_query**(action, cwd, ref, path, staged, max_count, stat): Read-only structured git inspection: action=diff|log|status|branch|show. Prefer over shell_run for reading repo state — output is clipped, redacted, and log/branch are parsed into structured fields. Use scm_sync for pull/push.\n- **git_write**(action, cwd, message, stage_all, name, ref, create) [capability_expand category: scm]: Mutating git actions: action=commit (message, stage_all), branch (create+switch), checkout (switch; create=true for -b). Approval-gated. Use scm_sync for pull/push and git_query for reads.\n- **test_run**(command, cwd, timeout): Run the project's test suite and return structured results (framework, passed/failed counts, failing tests). Auto-detects the runner (pytest/npm/go/cargo) or uses a configured/explicit command; executes via the governed shell. ok=true means the runner executed — see 'success' for pass/fail.\n- **lint_check**(command, cwd, timeout): Run the project's linter and return a structured clean/issue result. Auto-detects the linter (ruff/eslint/go vet/clippy) or uses a configured/explicit command; executes via the governed shell. ok=true means the linter ran — see 'clean'.\n- **file_list**(path, pattern, depth): List files and directories at a given path. Use depth=1 or depth=2 to get a recursive tree in one call instead of listing each sub-directory separately.\n- **file_read**(path, max_lines, start_line, max_chars, mode): Read text file content with adaptive context governance. For large or unfamiliar files, prefer mode='outline' or mode='symbols' first, then use mode='raw' with start_line/max_lines for the specific range you actually need. For LeapFlow's own settings, use config_list / config_get / config_set — its config files are outside the workspace and not readable here.\n- **file_write**(path, content, mode) [capability_expand category: write]: Write content to a file (overwrite or append).\n- **code_search**(pattern, patterns, path, glob, ignore_case, multiline, max_results, context_lines): Search file CONTENTS by regex pattern across a directory tree (ripgrep-backed). Requires a regex pattern. NOT for listing or browsing directory contents — use file_list for that. Prefer this over shell_run grep: faster, skips VCS/dependency/build dirs, and returns structured path:line:column matches. Batch related lookups into ONE call via `patterns` (OR-combined, single pass) instead of issuing several separate searches. Use file_read for the surrounding context of a hit.\n- **file_find**(glob, path, max_results): Find files by a recursive glob pattern under a base path (e.g. '**/test_*.py' or '*.md'). Prefer this over shell_run find; skips VCS/dependency/build dirs.\n- **edit_file**(path, edits, dry_run, diff) [capability_expand category: write]: Apply targeted, anchored search-replace edits to an EXISTING text file (use file_write to create/overwrite). Each edit is {original_text, new_text, replace_all?}; original_text must match exactly and uniquely (or set replace_all) — a non-unique or missing anchor is rejected so files are never corrupted. Set dry_run to preview. Alternatively pass a unified 'diff' to apply its hunks as anchored edits. Far cheaper and safer than rewriting a whole file.\n- **shell_run**(command, cwd, timeout) [capability_expand category: shell]: Execute a one-shot shell command with timeout protection. Runs in the active workspace; paths resolving outside it are refused. Reach for a structured tool first when one fits — web_fetch for anything over HTTP(S), git_query/scm_sync for git, code_search/file_find/file_read for the repo, config_get/config_set for LeapFlow's own settings — because those report typed results, while a failed shell command can only be diagnosed from its exit code and stderr. Every shell run counts as an external side effect, so a failure stops the rest of the batch and is not retried automatically.\n- **terminal_open**(command, cwd, shell) [capability_expand category: terminal]: Open a PERSISTENT shell session (REPL/dev server/watch), returning a session_id for terminal_send/read/close. Disabled unless tools.terminal_session_enabled is set. For one-shot commands use shell_run instead.\n- **terminal_send**(session_id, input, wait) [capability_expand category: terminal]: Send a line of input to a persistent terminal session and return output captured shortly after.\n- **terminal_read**(session_id, wait): Drain buffered output from a persistent terminal session (optionally waiting briefly first).\n- **terminal_close**(session_id) [capability_expand category: terminal]: Terminate a persistent terminal session and release its process group.\n- **terminal_list**(): List active persistent terminal sessions.\n- **config_list**(category, limit): List LeapFlow's own writable settings (LLM model and endpoint, daemon, memory, perception, gateway, …) with current values. Use this to discover the exact key before changing anything. Optionally narrow by `category`. This is the only correct way to inspect LeapFlow configuration — never read config files from disk.\n- **config_get**(key): Read one LeapFlow setting by key (e.g. 'llm.model', 'llm.base_url', 'daemon.log_level'), returning its current value, type, scopes, and whether a change needs a daemon restart. There is no 'llm.provider' key: provider behavior is inferred from the OpenAI-compatible 'llm.base_url'. Never read LeapFlow config files from disk — use this.\n- **config_set**(key, value, scope) [capability_expand category: config]: Change one LeapFlow setting by key, e.g. switch an OpenAI-compatible LLM with key='llm.model' and key='llm.base_url'. There is no 'llm.provider' key: provider behavior is inferred from the endpoint. Values are validated and coerced; credentials are stored in the vault automatically. Call config_list or config_get first if unsure of the key. The result states whether a `leap daemon restart` is required. Never edit LeapFlow config files directly.\n- **web_fetch**(url, select, timeout, max_bytes): Read a URL over HTTP(S) and get back extracted, context-sized content: parsed JSON for API endpoints, readable text plus links for web pages. Use this for anything on the internet — prices, docs, releases, articles — instead of running curl through shell_run: it reports real HTTP status codes, retries rate limits on its own, and is a plain read so a retry is always safe. For JSON APIs pass `select` with a dotted path (e.g. 'chart.result.0.meta') to return just that part instead of the whole payload.\n- **memory_search**(query, limit): Search agent memory for relevant past experiences, observations, and facts.\n- **memory_add**(content, kind) [capability_expand category: write]: Store a new observation or insight in memory for future reference.\n- **research_note**(kind, text): Record a compact, structured note about the current task's state so it survives context compression on long / multi-step tasks. Use for durable findings, open questions still to resolve, decisions / excluded paths, and the immediate next step. One concise sentence per note.\n- **capability_expand**(category): Fetch the full callable schema for every tool in a capability category (e.g. 'hub', 'gateway', 'desktop', 'delegate', 'file', 'memory', 'skill'). The compact tool index always lists every registered tool by name and a one-line summary, but only a static low-risk subset is directly callable each turn. If you need a tool from the index that is not yet callable, call capability_expand with its category first; the matching tools become callable in this turn. Never invent a tool name — expand the category instead.\n- **delegate_task**(goal, context) [capability_expand category: delegate]: Delegate a complex sub-task to an isolated subagent. The subagent gets a fresh context and restricted tool access. Use when a task is self-contained and can be solved independently.\n- **schedule_reentry**(kind, reason, delay_seconds, event_match, max_reentries, deadline_seconds): Register a re-entry so this task can resume later from its current orientation (findings / open questions / next step). Use when work must pause and continue after a delay (kind=time) or when a matching platform event arrives (kind=event), instead of finishing now. The research-ledger state is carried over automatically.\n- **hub_push**(skill_name, visibility, version) [capability_expand category: hub]: Push a local skill to the ModelScope Hub for sharing or backup.\n- **hub_pull**(repo_id, version) [capability_expand category: hub]: Pull a skill from the ModelScope Hub to install locally.\n- **hub_search**(query) [capability_expand category: hub]: Search for skills on the Hub by keyword or description.\n- **hub_sync**(mode, dry_run) [capability_expand category: hub]: Preview or execute sync between local skills and Hub.\n- **platform_action**(platform, action, payload, backend_kind) [capability_expand category: gateway]: Execute an exact registered business action on an external platform through LeapFlow's App Connector layer. Actions must be copied from the App Connector Capability Index and are addressed as domain.operation, e.g. im.send_message or docs.create_markdown. All business fields (chat_id, text, query, etc.) MUST be placed inside `payload`, never at the top level. Example: {\"platform\":\"feishu\",\"action\":\"im.send_message\",\"payload\":{\"chat_id\":\"oc_xxx\",\"text\":\"hello\"}}. Do not invent action names, do not use management actions such as list/guide/connect/status here.\n- **platform_connect**(action, platform, credentials, options, checkpoint) [capability_expand category: gateway]: List, guide, connect, disconnect, remove, or check status for external platforms using the App Connector management namespace. Supports REST and CLI backends. Use this for management actions such as list/guide/preflight/connect/status; use platform_action only for exact registered business actions.\n- **gateway_send**(platform, chat_id, text, thread_id) [capability_expand category: gateway]: Send a message to a connected external platform (Feishu group, Telegram chat, DingTalk conversation, etc.). Requires the platform to be connected via gateway_connect first. Use gateway_connect with action='list' to see connected platforms and available chat IDs.\n- **gateway_connect**(action, platform, credentials, options) [capability_expand category: gateway]: Connect, configure, or manage external platform integrations (Feishu, DingTalk, Telegram, Slack, Discord, etc.). Conversational flow: 1) call 'guide' to get setup steps + required fields, 2) present the steps to the user and ask for ALL required credentials in a single message, 3) call 'connect' with the credentials. Goal: complete in 1–2 user turns. NEVER include credential values in your text response.\n- **plugin_list**(): List the live plugin registry and cross-subsystem capability evidence. Use this before answering questions about whether LeapFlow supports plugins, self-evolution, plugin installation, hot reload, versioning, or other runtime capabilities.\n- **plugin_status**(plugin_id): Get detailed status of a specific plugin: its declared category, runtime dependencies, contributed tools, and fiber lifecycle state.\n- **plugin_versions**(plugin_id): List recorded source versions and active pointer for a profile-scoped plugin.\n- **plugin_propose**(requested_capability, plugin_id, proposed_tools, test_cases, risk_level, evidence): Create a side-effect-free PluginProposal from explicit capability-gap evidence. Use this before plugin_generate when a missing capability should be reviewed. Does not call an LLM, write files, or install anything.\n- **assess_compatibility**(manifest, source_path) [capability_expand category: plugin_management]: Assess whether a foreign plugin manifest or real DSH source bundle is compatible with LeapFlow. A source bundle assessment is static: runtime_ready stays false until restricted Node discovery during plugin_install. Returns component-level verdicts and limitations.\n- **plugin_generate**(plugin_id, description, proposal_id) [capability_expand category: system]: Generate a new ToolPlugin from a natural-language capability description. The LLM produces code that conforms to the ToolPlugin Protocol; it is then rigorously validated (syntax, structure, import, protocol conformance). The generated source is stored in CAS and receives explicit content approval. It DOES NOT install the plugin — installation requires a second, mutation-specific approval via plugin_install.\n- **plugin_install**(plugin_id, code, marketplace_name, source_path, proposal_id, version_label) [capability_expand category: system]: Install a plugin from exactly one source: validated Python code, the configured Python marketplace, or a real DSH source bundle. DSH bundles run restricted Node discovery before registration; only public host tools are installed and client UI limitations are reported. Writes profile-scoped state and mutates the process-global registry. REQUIRES APPROVAL.\n- **plugin_rollback**(plugin_id, version) [capability_expand category: system]: Rollback a profile-scoped plugin to a recorded source version and reload it. REQUIRES APPROVAL.\n- **plugin_reload**(plugin_id, version_label) [capability_expand category: system]: Hot-reload a plugin at runtime. Disposes the old plugin fiber, re-imports its module, and registers a fresh instance. Existing in-flight turns are unaffected (snapshot isolation). REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_disable**(plugin_id) [capability_expand category: system]: Disable a plugin by disposing its fiber, removing its tools from the runtime registry. Cannot disable self_management itself. REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_remove**(plugin_id, delete_source) [capability_expand category: system]: Terminally remove a plugin: dispose its fiber, unregister its tools, remove reload metadata, and optionally delete its profile-scoped source file. Cannot remove self_management itself. REQUIRES APPROVAL.\n- **plugin_unquarantine**(plugin_id) [capability_expand category: plugin_management]: Restore a quarantined plugin to probation status for re-evaluation. Unfreezes the trust ledger, transitions the proposal back to PROBATION, and reloads the plugin. REQUIRES APPROVAL.\n- **plugin_enable**(plugin_id) [capability_expand category: system]: Re-enable a previously disabled plugin by reloading its module and registering a fresh instance. REQUIRES APPROVAL.\n- **self_describe**(facet): Introspect LeapFlow's own identity, capabilities, runtime state, evolution metrics, or platform connections. Use facet='all' only when a comprehensive self-check is explicitly requested.\n- **runtime_snapshot**(): Lightweight (~150 token) flat snapshot of current runtime state: model, context budget, posture, disclosure level, turn count, cache hit rate, uptime, tool count, and pending approvals.\n- **schedule_create**(trigger_expression, instruction, execution_mode, max_retries, delivery_target) [capability_expand category: scheduler]: Create a new scheduled task. Specify a trigger expression (e.g. '30m', 'every 2h', '0 9 * * *') and the instruction to execute on each trigger. Optionally provide an execution mode and a delivery target to receive notifications.\n- **schedule_list**() [capability_expand category: scheduler]: List all scheduled tasks with their current state, trigger, and next due time.\n- **schedule_status**(task_id) [capability_expand category: scheduler]: Get detailed status and recent execution history for a scheduled task.\n- **schedule_pause**(task_id) [capability_expand category: scheduler]: Pause a scheduled task so it stops firing without being cancelled.\n- **schedule_resume**(task_id) [capability_expand category: scheduler]: Resume a paused scheduled task — re-arms it and recalculates next due time.\n- **schedule_cancel**(task_id) [capability_expand category: scheduler]: Cancel a scheduled task permanently.\n- **tool_search**(query, max_results): Search all registered tools by keyword relevance. Returns a ranked list of matching tools with name, category, and summary. Use this when you need a tool but are unsure of its exact name or category.\n- **tool_describe**(tool_name): Get the full callable schema of a single tool by exact name. Returns the tool's description, parameters, and metadata. Use after tool_search to inspect a specific tool before calling it.\n- **session_search**(query, limit) [capability_expand category: unclassified]: Search past conversation sessions in the current workspace for relevant context.\n- **session_list**(limit): List recent conversation sessions in the current workspace with stable ids, titles, dates, and summaries. Use for browsing past tasks or when user asks to see history without specific search terms. No keywords needed — returns chronological list.\n- **session_detail**(session_id, limit, offset, include_inactive) [capability_expand category: unclassified]: Read a paginated persisted transcript for one past conversation session in the current workspace. Use after session_list or session_search returns a session_id.\n- **click**(element_index) [capability_expand category: desktop]: Click a UI element by its element_index (from the latest observe_ui snapshot)\n- **get_clipboard**() [capability_expand category: desktop]: Read current clipboard text content\n- **list_apps**(filter, running_only) [capability_expand category: desktop]: List available applications on this system. Use to discover correct bundle_id before switch_app.\n- **list_windows**() [capability_expand category: desktop]: List all top-level windows with pid, window_id, title, and per-window state (minimized, on-screen). Call this first to pick the pid and window_id that observe_ui and other window tools require.\n- **observe_ui**(pid, window_id, query) [capability_expand category: desktop]: Snapshot one window's actionable UI elements, each tagged with an element_index for click/right_click/read_text. Re-observe after actions — indices belong to one snapshot. Requires the window's pid and window_id from list_windows.\n- **open_url**(url, app_id) [capability_expand category: desktop]: Open a URL in the default or specified browser\n- **read_text**(element_index) [capability_expand category: desktop]: Read the text content of a specific UI element from the latest snapshot\n- **right_click**(element_index) [capability_expand category: desktop]: Right-click a UI element to open its context menu. Returns visible menu items.\n- **screenshot**(pid, window_id) [capability_expand category: desktop]: Capture a screenshot for visual verification. With pid + window_id captures that window (works across all displays); defaults to the last observed window, or the full desktop when no window has been observed.\n- **scroll**(element_index, direction, amount, pid, window_id) [capability_expand category: desktop]: Scroll a scrollable area of a window. Omit element_index to scroll the window's focused/page scroller; pass one to scroll an exact element from the latest snapshot.\n- **select_text**(element_index) [capability_expand category: desktop]: Select all text in a UI element (focus + select-all, for subsequent copy)\n- **set_clipboard**(text) [capability_expand category: desktop]: Write text to the clipboard\n- **shortcut**(keys) [capability_expand category: desktop]: Execute a keyboard shortcut\n- **switch_app**(app_id) [capability_expand category: desktop]: Switch to an app (launch if needed, activate, verify)\n- **type_text**(text) [capability_expand category: desktop]: Type text into the currently focused element\n- **wait**(seconds) [capability_expand category: desktop]: Wait for a specified duration before continuing\n- **wait_until**(condition, pid, window_id, timeout, poll_interval) [capability_expand category: desktop]: Wait until a UI condition is met (polls UI tree). Returns elements when found or on timeout.\n- **wait_until_stable**(timeout, poll_interval, pid, window_id) [capability_expand category: desktop]: Wait until the UI stops changing (element set stabilizes across polls).\n\n## App Connector Capability Index\nLeapFlow can onboard and manage external apps through `platform_connect` and execute exact registered business actions through `platform_action`.\nFor requests about connecting, setting up, configuring, enabling, or managing a supported app, use `platform_connect` first instead of generating SDK/Webhook sample code.\n`platform_connect.action` is the management namespace: list, guide, preflight, connect, disconnect, remove, status, events_start, events_stop, events_status.\n`platform_action.action` is only for exact registered platform business actions listed below, such as `im.send_message`; never use management actions like `list` or `guide` there.\nAll business fields MUST go inside `payload`; top-level keys are only `platform`, `action`, and `payload`.\nDo not invent platform IDs or platform action names. If the needed action is not listed, ask for discovery/clarification instead of guessing.\nUse `platform_connect` with `action='guide'` and the matching `platform` to start onboarding; use `action='list'` when the app is unclear.\nWhen a pending onboarding state is present, continue from that state with `platform_connect` instead of asking the user to restate the app.\nSupported apps:\n- `api_server`: API Server (OpenAI Compatible) (category=api; backend=adapter; platform_action actions=none registered)\n- `dingtalk`: 钉钉 (DingTalk) (category=im; backend=adapter; platform_action actions=none registered)\n- `feishu`: 飞书 (Feishu/Lark) (category=im; backend=cli)\n - `calendar.create_event` payload={summary*, start_time*, end_time*, attendees} [write/high]\n - `docs.create_markdown` payload={title*, markdown*, folder_token} [write/medium]\n - `drive.search` payload={query*, limit} [read/medium]\n - `im.add_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.download_resource` payload={message_id*, file_key*, type*} [read/low]\n - `im.get_messages` payload={message_ids*} [read/low]\n - `im.list_chats` payload={page_size} [read/low]\n - `im.list_messages` payload={chat_id*} [read/low]\n - `im.list_thread_messages` payload={thread*} [read/low]\n - `im.remove_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.reply_message` payload={message_id*, text*} [send/high]\n - `im.search_chats` payload={query*, page_size} [read/low]\n - `im.search_messages` payload={query*} [read/medium]\n - `im.send_message` payload={chat_id*, text*, thread_id} [send/high]\n - `im.update_card` payload={token*, card*} [write/medium]\n - `im.update_message` payload={message_id*, text*} [write/medium]\n - `mail.search_unread` payload={query, limit} [read/high]\n - `sheets.append_row` payload={spreadsheet_token*, sheet_id*, values*} [write/medium]\n - `task.create` payload={title*, description, due_time} [write/medium]\n- `telegram`: Telegram (category=social; backend=adapter; platform_action actions=none registered)\n- `webhook`: Webhook (Generic) (category=webhook; backend=adapter; platform_action actions=none registered)\n\n## Tool Usage\nTools are normally invoked through the native function-calling mechanism, not by writing JSON in your reply\ntext. Only if the provider signals that native function calling is unavailable for this turn, fall back to a\nsingle JSON code block: `{\"name\": \"tool_name\", \"arguments\": {\"key\": \"value\"}}` — use this fallback format\nonly, never both. Only call a tool whose exact name appears in the tool index above and is currently callable\n(or reachable via `capability_expand`). Never invent, rename, alias, or guess a tool name, platform ID, or\nplatform action from argument shape or wording — if the index does not list it, it does not exist.\n`platform_connect.action` (list/guide/preflight/connect/disconnect/remove/status/events_start/events_stop/\nevents_status) is the App Connector management namespace; `platform_action.action` only accepts exact\nregistered business actions such as `im.send_message` shown in the App Connector Capability Index — never mix\nthe two namespaces. If a tool call returns an unknown/unavailable result, use the returned suggestions or\navailable names for a single retry instead of trying further variations of the same guess.\n\n**Side-effect action rule** (`platform_action` with effect=send/write/execute):\n- Call each unique action+payload **exactly once**. Never include duplicates in the same turn.\n- Once the result returns `\"completed\": true`, that action is DONE for this task. Do NOT call it again\n in any subsequent turn — immediately summarize the result for the user instead.\n- The system enforces idempotency: duplicate calls are blocked and will not execute.\n- If the user explicitly requests sending/writing multiple times, use distinct payloads per call.\n\n**Resource identifier provenance rule**:\n- NEVER fabricate, guess, or infer resource identifiers (chat_id, message_id, file_key, user_id, etc.).\n Every resource ID used in a side-effect action MUST come from a successful API response in this session.\n- If a read/list action fails (e.g. authorization error), you do NOT have valid resource IDs.\n Report the failure to the user — do NOT attempt the dependent write/send action with a guessed ID.\n- When a tool result contains `\"llm_instruction\"`, follow it exactly.\n\n## Guidelines\n1. **Direct answers first**: If you already know the answer, respond directly without tools.\n2. **Avoid redundant tool calls**: Do not call the same tool with the same arguments more than once in the same user turn. When an existing tool result already answers the user's request, stop calling tools and answer directly.\n3. **Use tools proactively**: When the user asks about files, time, system state, or needs actions performed, use the appropriate tool.\n4. **Chain tools when needed**: You can call multiple tools in sequence (e.g., list files → read file → summarize).\n5. **Handle failures gracefully**: If a tool fails, explain what went wrong and suggest alternatives. If it failed because of an unknown tool/platform/action name, retry once with an exact name from the returned suggestions, then explain rather than keep guessing.\n6. **Summarize results naturally**: After tool execution, synthesize the results into a helpful answer rather than dumping raw output.\n7. **Stay conversational**: Maintain a natural, helpful tone. Acknowledge context from earlier in the conversation.\n8. **Recall past work**:\n - Broad queries (\"之前做了什么\", \"列出任务\"): answer from the \"Recent Task History\" section already in your context. If insufficient, call session_list.\n - Specific lookups (\"上次那个配置怎么改的\"): call session_search with relevant phrases (NOT single characters).\n - Do NOT call search tools repeatedly with keyword fragments. One well-phrased call is better than ten fragmented ones.\n\n## Coding & Verification\nWhen working with code, prefer the precise built-in tools over ad-hoc shell: use `repo_map` to orient in an\nunfamiliar project, `code_search`/`file_find` to locate and `code_intel` for symbols, `edit_file` (anchored\nsearch-replace, or a unified `diff`) to change files — never rewrite a whole file to change a few lines — and\n`git_query` to inspect diffs/log. After edits, check `syntax_ok` in the result and run `test_run`/`lint_check`\nbefore declaring the work done. Batch independent read-only calls (search/read/query) into a single turn — they\nrun in parallel.\n\n## Presentation Style\n1. **Polished Markdown only**: Format user-facing answers with clean Markdown headings, short paragraphs, and concise bullets. Use tables only when they improve comparison or scanning.\n2. **Terminal-friendly layout**: Keep lines readable in a TUI; avoid dense walls of text, deeply nested lists, oversized ASCII art, or heavy visual blocks.\n3. **Elegant emphasis**: Use bold text sparingly for key terms and conclusions. Avoid excessive emojis, decorative symbols, repeated separators, or visual noise.\n4. **Theme-safe colors**: Do not emit ANSI escape codes, HTML color tags, Rich markup, or hardcoded color names. Rely on the TUI theme to render Markdown professionally.\n5. **No leaked tool protocol**: Never show tool-call JSON, internal schemas, raw observations, tool result payloads, or hidden reasoning in the final answer unless the user explicitly asks for raw/debug output. Treat any prior `{\"name\": ..., \"arguments\": ...}` blocks and `Tool result (...)` messages as internal execution context only.\n6. **Professional closure**: End with a concise conclusion or next step when helpful; avoid rambling after the useful answer is complete.\n\nWhen finished with all tool calls, respond normally without a JSON block, tool-call transcript, or process log.\n\n## Task Contract\n- Task ID: turn-1\n- Original user request: Hello from A.\n- Workspace root: \n- Allowed roots: \n- Treat relative project paths as relative to the workspace root; never infer `.` as the project root when a workspace root is provided.\n- Workspace boundary is enforced by tools: do not read, search, edit, or run commands against paths outside the allowed roots unless the user explicitly requests an external path and the tool/approval policy permits it.\n- LeapFlow workspace config is optional at `/.leapflow/config.yaml`; runtime config is loaded from `~/.leapflow/config/user.yaml` and `~/.leapflow/profiles//config/*.yaml`.\n- Preserve this task contract across summarization, compression, tool loops, and memory retrieval.\n" - }, - { - "role": "system", - "content": "\n## Recent Session Summary\n- [user] Hello from A.\n" - }, - { - "role": "user", - "content": "Hello from A.\nHello from A." - } - ], - "tools": [ - "capability_expand", - "code_intel", - "code_search", - "config_get", - "config_list", - "env_info", - "file_find", - "file_list", - "file_read", - "git_query", - "lint_check", - "memory_search", - "plugin_list", - "plugin_propose", - "plugin_status", - "plugin_versions", - "repo_map", - "research_note", - "runtime_snapshot", - "schedule_reentry", - "self_describe", - "session_list", - "skill_view", - "skills_list", - "terminal_list", - "terminal_read", - "test_run", - "text_search", - "time_get", - "tool_describe", - "tool_search", - "web_fetch" - ] - }, - "responses": [ - { - "status": 200, - "content_type": "application/json", - "body": "{\"id\": \"chatcmpl-cassette\", \"object\": \"chat.completion\", \"model\": \"cassette-model\", \"choices\": [{\"index\": 0, \"message\": {\"role\": \"assistant\", \"content\": \"Workspace A acknowledged.\"}, \"finish_reason\": \"stop\"}], \"usage\": {\"prompt_tokens\": 64, \"completion_tokens\": 16, \"total_tokens\": 80}}" - } - ] -} diff --git a/tests/_fixtures/cassettes/r2_isolation/cassette-model-381119793b34c8d1.cassette.json b/tests/_fixtures/cassettes/r2_isolation/cassette-model-381119793b34c8d1.cassette.json deleted file mode 100644 index 683dd0db..00000000 --- a/tests/_fixtures/cassettes/r2_isolation/cassette-model-381119793b34c8d1.cassette.json +++ /dev/null @@ -1,67 +0,0 @@ -{ - "fingerprint": "381119793b34c8d1513093d1e2746fbbac0bc39ab5db7d4a02d9f39be2a3b29d", - "note": "captured in seed mode", - "request": { - "model": "cassette-model", - "stream": false, - "messages": [ - { - "role": "system", - "content": "You are LeapFlow, an intelligent assistant that can both converse naturally and take real actions on the user's computer.\n\n## Capabilities\nThe tool index below lists **every** registered tool by name and a one-line summary — this is the complete\ncapability contract; nothing else exists. Only a subset is directly callable this turn (via native tool calling,\nnot a JSON block in your reply). If you need a tool from the index that is not yet callable, call\n`capability_expand` with its category name first — the matching tools become callable immediately after.\nWhen the user asks what LeapFlow itself supports, whether it supports plugins, or which runtime capabilities\nare available, use the live capability evidence exposed by `plugin_list` before making capability claims; report\nconfiguration-dependent or unavailable capabilities as limitations instead of inferring from documentation.\n- **text_search**(text, pattern): Search for a regex pattern in text.\n- **text_replace**(text, old, new, count) [capability_expand category: write]: Replace occurrences of a substring in text.\n- **time_get**(): Get current date and time.\n- **env_info**(): Get system environment information (OS, Python version, cwd).\n- **skills_list**(query, category, source): List available learned skills. Use when user asks about capabilities or you need a specific skill.\n- **skill_view**(name): View the full content of a specific skill document.\n- **code_intel**(path, operation): Precise document symbols (outline) for a source file: classes, functions, and methods with line ranges. Python uses an exact AST parse; other languages use a keyword-prefix scan. Prefer over file_read mode=symbols for accurate navigation before editing. Read-only.\n- **repo_map**(path): Compact project orientation for a repository root: languages, detected test/lint commands, top-level structure, entry points, manifest, and VCS branch. Call this first when entering an unfamiliar codebase. Read-only.\n- **scm_sync**(action, cwd, remote, pull_ref, push_ref, timeout) [capability_expand category: scm]: Run a typed git SCM action. Use this instead of shell_run for git pull/push/status. For 'pull origin main then push', set action='pull_then_push', remote='origin', pull_ref='main', and omit push_ref so LeapFlow pushes the current local branch.\n- **git_query**(action, cwd, ref, path, staged, max_count, stat): Read-only structured git inspection: action=diff|log|status|branch|show. Prefer over shell_run for reading repo state — output is clipped, redacted, and log/branch are parsed into structured fields. Use scm_sync for pull/push.\n- **git_write**(action, cwd, message, stage_all, name, ref, create) [capability_expand category: scm]: Mutating git actions: action=commit (message, stage_all), branch (create+switch), checkout (switch; create=true for -b). Approval-gated. Use scm_sync for pull/push and git_query for reads.\n- **test_run**(command, cwd, timeout): Run the project's test suite and return structured results (framework, passed/failed counts, failing tests). Auto-detects the runner (pytest/npm/go/cargo) or uses a configured/explicit command; executes via the governed shell. ok=true means the runner executed — see 'success' for pass/fail.\n- **lint_check**(command, cwd, timeout): Run the project's linter and return a structured clean/issue result. Auto-detects the linter (ruff/eslint/go vet/clippy) or uses a configured/explicit command; executes via the governed shell. ok=true means the linter ran — see 'clean'.\n- **file_list**(path, pattern, depth): List files and directories at a given path. Use depth=1 or depth=2 to get a recursive tree in one call instead of listing each sub-directory separately.\n- **file_read**(path, max_lines, start_line, max_chars, mode): Read text file content with adaptive context governance. For large or unfamiliar files, prefer mode='outline' or mode='symbols' first, then use mode='raw' with start_line/max_lines for the specific range you actually need. For LeapFlow's own settings, use config_list / config_get / config_set — its config files are outside the workspace and not readable here.\n- **file_write**(path, content, mode) [capability_expand category: write]: Write content to a file (overwrite or append).\n- **code_search**(pattern, patterns, path, glob, ignore_case, multiline, max_results, context_lines): Search file CONTENTS by regex pattern across a directory tree (ripgrep-backed). Requires a regex pattern. NOT for listing or browsing directory contents — use file_list for that. Prefer this over shell_run grep: faster, skips VCS/dependency/build dirs, and returns structured path:line:column matches. Batch related lookups into ONE call via `patterns` (OR-combined, single pass) instead of issuing several separate searches. Use file_read for the surrounding context of a hit.\n- **file_find**(glob, path, max_results): Find files by a recursive glob pattern under a base path (e.g. '**/test_*.py' or '*.md'). Prefer this over shell_run find; skips VCS/dependency/build dirs.\n- **edit_file**(path, edits, dry_run, diff) [capability_expand category: write]: Apply targeted, anchored search-replace edits to an EXISTING text file (use file_write to create/overwrite). Each edit is {original_text, new_text, replace_all?}; original_text must match exactly and uniquely (or set replace_all) — a non-unique or missing anchor is rejected so files are never corrupted. Set dry_run to preview. Alternatively pass a unified 'diff' to apply its hunks as anchored edits. Far cheaper and safer than rewriting a whole file.\n- **shell_run**(command, cwd, timeout) [capability_expand category: shell]: Execute a one-shot shell command with timeout protection. Runs in the active workspace; paths resolving outside it are refused. Reach for a structured tool first when one fits — web_fetch for anything over HTTP(S), git_query/scm_sync for git, code_search/file_find/file_read for the repo, config_get/config_set for LeapFlow's own settings — because those report typed results, while a failed shell command can only be diagnosed from its exit code and stderr. Every shell run counts as an external side effect, so a failure stops the rest of the batch and is not retried automatically.\n- **terminal_open**(command, cwd, shell) [capability_expand category: terminal]: Open a PERSISTENT shell session (REPL/dev server/watch), returning a session_id for terminal_send/read/close. Disabled unless tools.terminal_session_enabled is set. For one-shot commands use shell_run instead.\n- **terminal_send**(session_id, input, wait) [capability_expand category: terminal]: Send a line of input to a persistent terminal session and return output captured shortly after.\n- **terminal_read**(session_id, wait): Drain buffered output from a persistent terminal session (optionally waiting briefly first).\n- **terminal_close**(session_id) [capability_expand category: terminal]: Terminate a persistent terminal session and release its process group.\n- **terminal_list**(): List active persistent terminal sessions.\n- **config_list**(category, limit): List LeapFlow's own writable settings (LLM model and endpoint, daemon, memory, perception, gateway, …) with current values. Use this to discover the exact key before changing anything. Optionally narrow by `category`. This is the only correct way to inspect LeapFlow configuration — never read config files from disk.\n- **config_get**(key): Read one LeapFlow setting by key (e.g. 'llm.model', 'llm.base_url', 'daemon.log_level'), returning its current value, type, scopes, and whether a change needs a daemon restart. There is no 'llm.provider' key: provider behavior is inferred from the OpenAI-compatible 'llm.base_url'. Never read LeapFlow config files from disk — use this.\n- **config_set**(key, value, scope) [capability_expand category: config]: Change one LeapFlow setting by key, e.g. switch an OpenAI-compatible LLM with key='llm.model' and key='llm.base_url'. There is no 'llm.provider' key: provider behavior is inferred from the endpoint. Values are validated and coerced; credentials are stored in the vault automatically. Call config_list or config_get first if unsure of the key. The result states whether a `leap daemon restart` is required. Never edit LeapFlow config files directly.\n- **web_fetch**(url, select, timeout, max_bytes): Read a URL over HTTP(S) and get back extracted, context-sized content: parsed JSON for API endpoints, readable text plus links for web pages. Use this for anything on the internet — prices, docs, releases, articles — instead of running curl through shell_run: it reports real HTTP status codes, retries rate limits on its own, and is a plain read so a retry is always safe. For JSON APIs pass `select` with a dotted path (e.g. 'chart.result.0.meta') to return just that part instead of the whole payload.\n- **memory_search**(query, limit): Search agent memory for relevant past experiences, observations, and facts.\n- **memory_add**(content, kind) [capability_expand category: write]: Store a new observation or insight in memory for future reference.\n- **research_note**(kind, text): Record a compact, structured note about the current task's state so it survives context compression on long / multi-step tasks. Use for durable findings, open questions still to resolve, decisions / excluded paths, and the immediate next step. One concise sentence per note.\n- **capability_expand**(category): Fetch the full callable schema for every tool in a capability category (e.g. 'hub', 'gateway', 'desktop', 'delegate', 'file', 'memory', 'skill'). The compact tool index always lists every registered tool by name and a one-line summary, but only a static low-risk subset is directly callable each turn. If you need a tool from the index that is not yet callable, call capability_expand with its category first; the matching tools become callable in this turn. Never invent a tool name — expand the category instead.\n- **delegate_task**(goal, context) [capability_expand category: delegate]: Delegate a complex sub-task to an isolated subagent. The subagent gets a fresh context and restricted tool access. Use when a task is self-contained and can be solved independently.\n- **schedule_reentry**(kind, reason, delay_seconds, event_match, max_reentries, deadline_seconds): Register a re-entry so this task can resume later from its current orientation (findings / open questions / next step). Use when work must pause and continue after a delay (kind=time) or when a matching platform event arrives (kind=event), instead of finishing now. The research-ledger state is carried over automatically.\n- **hub_push**(skill_name, visibility, version) [capability_expand category: hub]: Push a local skill to the ModelScope Hub for sharing or backup.\n- **hub_pull**(repo_id, version) [capability_expand category: hub]: Pull a skill from the ModelScope Hub to install locally.\n- **hub_search**(query) [capability_expand category: hub]: Search for skills on the Hub by keyword or description.\n- **hub_sync**(mode, dry_run) [capability_expand category: hub]: Preview or execute sync between local skills and Hub.\n- **platform_action**(platform, action, payload, backend_kind) [capability_expand category: gateway]: Execute an exact registered business action on an external platform through LeapFlow's App Connector layer. Actions must be copied from the App Connector Capability Index and are addressed as domain.operation, e.g. im.send_message or docs.create_markdown. All business fields (chat_id, text, query, etc.) MUST be placed inside `payload`, never at the top level. Example: {\"platform\":\"feishu\",\"action\":\"im.send_message\",\"payload\":{\"chat_id\":\"oc_xxx\",\"text\":\"hello\"}}. Do not invent action names, do not use management actions such as list/guide/connect/status here.\n- **platform_connect**(action, platform, credentials, options, checkpoint) [capability_expand category: gateway]: List, guide, connect, disconnect, remove, or check status for external platforms using the App Connector management namespace. Supports REST and CLI backends. Use this for management actions such as list/guide/preflight/connect/status; use platform_action only for exact registered business actions.\n- **gateway_send**(platform, chat_id, text, thread_id) [capability_expand category: gateway]: Send a message to a connected external platform (Feishu group, Telegram chat, DingTalk conversation, etc.). Requires the platform to be connected via gateway_connect first. Use gateway_connect with action='list' to see connected platforms and available chat IDs.\n- **gateway_connect**(action, platform, credentials, options) [capability_expand category: gateway]: Connect, configure, or manage external platform integrations (Feishu, DingTalk, Telegram, Slack, Discord, etc.). Conversational flow: 1) call 'guide' to get setup steps + required fields, 2) present the steps to the user and ask for ALL required credentials in a single message, 3) call 'connect' with the credentials. Goal: complete in 1–2 user turns. NEVER include credential values in your text response.\n- **plugin_list**(): List the live plugin registry and cross-subsystem capability evidence. Use this before answering questions about whether LeapFlow supports plugins, self-evolution, plugin installation, hot reload, versioning, or other runtime capabilities.\n- **plugin_status**(plugin_id): Get detailed status of a specific plugin: its declared category, runtime dependencies, contributed tools, and fiber lifecycle state.\n- **plugin_versions**(plugin_id): List recorded source versions and active pointer for a profile-scoped plugin.\n- **plugin_propose**(requested_capability, plugin_id, proposed_tools, test_cases, risk_level, evidence): Create a side-effect-free PluginProposal from explicit capability-gap evidence. Use this before plugin_generate when a missing capability should be reviewed. Does not call an LLM, write files, or install anything.\n- **assess_compatibility**(manifest, source_path) [capability_expand category: plugin_management]: Assess whether a foreign plugin manifest or real DSH source bundle is compatible with LeapFlow. A source bundle assessment is static: runtime_ready stays false until restricted Node discovery during plugin_install. Returns component-level verdicts and limitations.\n- **plugin_generate**(plugin_id, description, proposal_id) [capability_expand category: system]: Generate a new ToolPlugin from a natural-language capability description. The LLM produces code that conforms to the ToolPlugin Protocol; it is then rigorously validated (syntax, structure, import, protocol conformance). The generated source is stored in CAS and receives explicit content approval. It DOES NOT install the plugin — installation requires a second, mutation-specific approval via plugin_install.\n- **plugin_install**(plugin_id, code, marketplace_name, source_path, proposal_id, version_label) [capability_expand category: system]: Install a plugin from exactly one source: validated Python code, the configured Python marketplace, or a real DSH source bundle. DSH bundles run restricted Node discovery before registration; only public host tools are installed and client UI limitations are reported. Writes profile-scoped state and mutates the process-global registry. REQUIRES APPROVAL.\n- **plugin_rollback**(plugin_id, version) [capability_expand category: system]: Rollback a profile-scoped plugin to a recorded source version and reload it. REQUIRES APPROVAL.\n- **plugin_reload**(plugin_id, version_label) [capability_expand category: system]: Hot-reload a plugin at runtime. Disposes the old plugin fiber, re-imports its module, and registers a fresh instance. Existing in-flight turns are unaffected (snapshot isolation). REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_disable**(plugin_id) [capability_expand category: system]: Disable a plugin by disposing its fiber, removing its tools from the runtime registry. Cannot disable self_management itself. REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_remove**(plugin_id, delete_source) [capability_expand category: system]: Terminally remove a plugin: dispose its fiber, unregister its tools, remove reload metadata, and optionally delete its profile-scoped source file. Cannot remove self_management itself. REQUIRES APPROVAL.\n- **plugin_unquarantine**(plugin_id) [capability_expand category: plugin_management]: Restore a quarantined plugin to probation status for re-evaluation. Unfreezes the trust ledger, transitions the proposal back to PROBATION, and reloads the plugin. REQUIRES APPROVAL.\n- **plugin_enable**(plugin_id) [capability_expand category: system]: Re-enable a previously disabled plugin by reloading its module and registering a fresh instance. REQUIRES APPROVAL.\n- **self_describe**(facet): Introspect LeapFlow's own identity, capabilities, runtime state, evolution metrics, or platform connections. Use facet='all' only when a comprehensive self-check is explicitly requested.\n- **runtime_snapshot**(): Lightweight (~150 token) flat snapshot of current runtime state: model, context budget, posture, disclosure level, turn count, cache hit rate, uptime, tool count, and pending approvals.\n- **schedule_create**(trigger_expression, instruction, execution_mode, max_retries, delivery_target) [capability_expand category: scheduler]: Create a new scheduled task. Specify a trigger expression (e.g. '30m', 'every 2h', '0 9 * * *') and the instruction to execute on each trigger. Optionally provide an execution mode and a delivery target to receive notifications.\n- **schedule_list**() [capability_expand category: scheduler]: List all scheduled tasks with their current state, trigger, and next due time.\n- **schedule_status**(task_id) [capability_expand category: scheduler]: Get detailed status and recent execution history for a scheduled task.\n- **schedule_pause**(task_id) [capability_expand category: scheduler]: Pause a scheduled task so it stops firing without being cancelled.\n- **schedule_resume**(task_id) [capability_expand category: scheduler]: Resume a paused scheduled task — re-arms it and recalculates next due time.\n- **schedule_cancel**(task_id) [capability_expand category: scheduler]: Cancel a scheduled task permanently.\n- **tool_search**(query, max_results): Search all registered tools by keyword relevance. Returns a ranked list of matching tools with name, category, and summary. Use this when you need a tool but are unsure of its exact name or category.\n- **tool_describe**(tool_name): Get the full callable schema of a single tool by exact name. Returns the tool's description, parameters, and metadata. Use after tool_search to inspect a specific tool before calling it.\n- **session_search**(query, limit) [capability_expand category: unclassified]: Search past conversation sessions in the current workspace for relevant context.\n- **session_list**(limit): List recent conversation sessions in the current workspace with stable ids, titles, dates, and summaries. Use for browsing past tasks or when user asks to see history without specific search terms. No keywords needed — returns chronological list.\n- **session_detail**(session_id, limit, offset, include_inactive) [capability_expand category: unclassified]: Read a paginated persisted transcript for one past conversation session in the current workspace. Use after session_list or session_search returns a session_id.\n- **click**(element_index) [capability_expand category: desktop]: Click a UI element by its element_index (from the latest observe_ui snapshot)\n- **get_clipboard**() [capability_expand category: desktop]: Read current clipboard text content\n- **list_apps**(filter, running_only) [capability_expand category: desktop]: List available applications on this system. Use to discover correct bundle_id before switch_app.\n- **list_windows**() [capability_expand category: desktop]: List all top-level windows with pid, window_id, title, and per-window state (minimized, on-screen). Call this first to pick the pid and window_id that observe_ui and other window tools require.\n- **observe_ui**(pid, window_id, query) [capability_expand category: desktop]: Snapshot one window's actionable UI elements, each tagged with an element_index for click/right_click/read_text. Re-observe after actions — indices belong to one snapshot. Requires the window's pid and window_id from list_windows.\n- **open_url**(url, app_id) [capability_expand category: desktop]: Open a URL in the default or specified browser\n- **read_text**(element_index) [capability_expand category: desktop]: Read the text content of a specific UI element from the latest snapshot\n- **right_click**(element_index) [capability_expand category: desktop]: Right-click a UI element to open its context menu. Returns visible menu items.\n- **screenshot**(pid, window_id) [capability_expand category: desktop]: Capture a screenshot for visual verification. With pid + window_id captures that window (works across all displays); defaults to the last observed window, or the full desktop when no window has been observed.\n- **scroll**(element_index, direction, amount, pid, window_id) [capability_expand category: desktop]: Scroll a scrollable area of a window. Omit element_index to scroll the window's focused/page scroller; pass one to scroll an exact element from the latest snapshot.\n- **select_text**(element_index) [capability_expand category: desktop]: Select all text in a UI element (focus + select-all, for subsequent copy)\n- **set_clipboard**(text) [capability_expand category: desktop]: Write text to the clipboard\n- **shortcut**(keys) [capability_expand category: desktop]: Execute a keyboard shortcut\n- **switch_app**(app_id) [capability_expand category: desktop]: Switch to an app (launch if needed, activate, verify)\n- **type_text**(text) [capability_expand category: desktop]: Type text into the currently focused element\n- **wait**(seconds) [capability_expand category: desktop]: Wait for a specified duration before continuing\n- **wait_until**(condition, pid, window_id, timeout, poll_interval) [capability_expand category: desktop]: Wait until a UI condition is met (polls UI tree). Returns elements when found or on timeout.\n- **wait_until_stable**(timeout, poll_interval, pid, window_id) [capability_expand category: desktop]: Wait until the UI stops changing (element set stabilizes across polls).\n\n## App Connector Capability Index\nLeapFlow can onboard and manage external apps through `platform_connect` and execute exact registered business actions through `platform_action`.\nFor requests about connecting, setting up, configuring, enabling, or managing a supported app, use `platform_connect` first instead of generating SDK/Webhook sample code.\n`platform_connect.action` is the management namespace: list, guide, preflight, connect, disconnect, remove, status, events_start, events_stop, events_status.\n`platform_action.action` is only for exact registered platform business actions listed below, such as `im.send_message`; never use management actions like `list` or `guide` there.\nAll business fields MUST go inside `payload`; top-level keys are only `platform`, `action`, and `payload`.\nDo not invent platform IDs or platform action names. If the needed action is not listed, ask for discovery/clarification instead of guessing.\nUse `platform_connect` with `action='guide'` and the matching `platform` to start onboarding; use `action='list'` when the app is unclear.\nWhen a pending onboarding state is present, continue from that state with `platform_connect` instead of asking the user to restate the app.\nSupported apps:\n- `api_server`: API Server (OpenAI Compatible) (category=api; backend=adapter; platform_action actions=none registered)\n- `dingtalk`: 钉钉 (DingTalk) (category=im; backend=adapter; platform_action actions=none registered)\n- `feishu`: 飞书 (Feishu/Lark) (category=im; backend=cli)\n - `calendar.create_event` payload={summary*, start_time*, end_time*, attendees} [write/high]\n - `docs.create_markdown` payload={title*, markdown*, folder_token} [write/medium]\n - `drive.search` payload={query*, limit} [read/medium]\n - `im.add_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.download_resource` payload={message_id*, file_key*, type*} [read/low]\n - `im.get_messages` payload={message_ids*} [read/low]\n - `im.list_chats` payload={page_size} [read/low]\n - `im.list_messages` payload={chat_id*} [read/low]\n - `im.list_thread_messages` payload={thread*} [read/low]\n - `im.remove_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.reply_message` payload={message_id*, text*} [send/high]\n - `im.search_chats` payload={query*, page_size} [read/low]\n - `im.search_messages` payload={query*} [read/medium]\n - `im.send_message` payload={chat_id*, text*, thread_id} [send/high]\n - `im.update_card` payload={token*, card*} [write/medium]\n - `im.update_message` payload={message_id*, text*} [write/medium]\n - `mail.search_unread` payload={query, limit} [read/high]\n - `sheets.append_row` payload={spreadsheet_token*, sheet_id*, values*} [write/medium]\n - `task.create` payload={title*, description, due_time} [write/medium]\n- `telegram`: Telegram (category=social; backend=adapter; platform_action actions=none registered)\n- `webhook`: Webhook (Generic) (category=webhook; backend=adapter; platform_action actions=none registered)\n\n## Tool Usage\nTools are normally invoked through the native function-calling mechanism, not by writing JSON in your reply\ntext. Only if the provider signals that native function calling is unavailable for this turn, fall back to a\nsingle JSON code block: `{\"name\": \"tool_name\", \"arguments\": {\"key\": \"value\"}}` — use this fallback format\nonly, never both. Only call a tool whose exact name appears in the tool index above and is currently callable\n(or reachable via `capability_expand`). Never invent, rename, alias, or guess a tool name, platform ID, or\nplatform action from argument shape or wording — if the index does not list it, it does not exist.\n`platform_connect.action` (list/guide/preflight/connect/disconnect/remove/status/events_start/events_stop/\nevents_status) is the App Connector management namespace; `platform_action.action` only accepts exact\nregistered business actions such as `im.send_message` shown in the App Connector Capability Index — never mix\nthe two namespaces. If a tool call returns an unknown/unavailable result, use the returned suggestions or\navailable names for a single retry instead of trying further variations of the same guess.\n\n**Side-effect action rule** (`platform_action` with effect=send/write/execute):\n- Call each unique action+payload **exactly once**. Never include duplicates in the same turn.\n- Once the result returns `\"completed\": true`, that action is DONE for this task. Do NOT call it again\n in any subsequent turn — immediately summarize the result for the user instead.\n- The system enforces idempotency: duplicate calls are blocked and will not execute.\n- If the user explicitly requests sending/writing multiple times, use distinct payloads per call.\n\n**Resource identifier provenance rule**:\n- NEVER fabricate, guess, or infer resource identifiers (chat_id, message_id, file_key, user_id, etc.).\n Every resource ID used in a side-effect action MUST come from a successful API response in this session.\n- If a read/list action fails (e.g. authorization error), you do NOT have valid resource IDs.\n Report the failure to the user — do NOT attempt the dependent write/send action with a guessed ID.\n- When a tool result contains `\"llm_instruction\"`, follow it exactly.\n\n## Guidelines\n1. **Direct answers first**: If you already know the answer, respond directly without tools.\n2. **Avoid redundant tool calls**: Do not call the same tool with the same arguments more than once in the same user turn. When an existing tool result already answers the user's request, stop calling tools and answer directly.\n3. **Use tools proactively**: When the user asks about files, time, system state, or needs actions performed, use the appropriate tool.\n4. **Chain tools when needed**: You can call multiple tools in sequence (e.g., list files → read file → summarize).\n5. **Handle failures gracefully**: If a tool fails, explain what went wrong and suggest alternatives. If it failed because of an unknown tool/platform/action name, retry once with an exact name from the returned suggestions, then explain rather than keep guessing.\n6. **Summarize results naturally**: After tool execution, synthesize the results into a helpful answer rather than dumping raw output.\n7. **Stay conversational**: Maintain a natural, helpful tone. Acknowledge context from earlier in the conversation.\n8. **Recall past work**:\n - Broad queries (\"之前做了什么\", \"列出任务\"): answer from the \"Recent Task History\" section already in your context. If insufficient, call session_list.\n - Specific lookups (\"上次那个配置怎么改的\"): call session_search with relevant phrases (NOT single characters).\n - Do NOT call search tools repeatedly with keyword fragments. One well-phrased call is better than ten fragmented ones.\n\n## Coding & Verification\nWhen working with code, prefer the precise built-in tools over ad-hoc shell: use `repo_map` to orient in an\nunfamiliar project, `code_search`/`file_find` to locate and `code_intel` for symbols, `edit_file` (anchored\nsearch-replace, or a unified `diff`) to change files — never rewrite a whole file to change a few lines — and\n`git_query` to inspect diffs/log. After edits, check `syntax_ok` in the result and run `test_run`/`lint_check`\nbefore declaring the work done. Batch independent read-only calls (search/read/query) into a single turn — they\nrun in parallel.\n\n## Presentation Style\n1. **Polished Markdown only**: Format user-facing answers with clean Markdown headings, short paragraphs, and concise bullets. Use tables only when they improve comparison or scanning.\n2. **Terminal-friendly layout**: Keep lines readable in a TUI; avoid dense walls of text, deeply nested lists, oversized ASCII art, or heavy visual blocks.\n3. **Elegant emphasis**: Use bold text sparingly for key terms and conclusions. Avoid excessive emojis, decorative symbols, repeated separators, or visual noise.\n4. **Theme-safe colors**: Do not emit ANSI escape codes, HTML color tags, Rich markup, or hardcoded color names. Rely on the TUI theme to render Markdown professionally.\n5. **No leaked tool protocol**: Never show tool-call JSON, internal schemas, raw observations, tool result payloads, or hidden reasoning in the final answer unless the user explicitly asks for raw/debug output. Treat any prior `{\"name\": ..., \"arguments\": ...}` blocks and `Tool result (...)` messages as internal execution context only.\n6. **Professional closure**: End with a concise conclusion or next step when helpful; avoid rambling after the useful answer is complete.\n\nWhen finished with all tool calls, respond normally without a JSON block, tool-call transcript, or process log.\n\n## Task Contract\n- Task ID: turn-2\n- Original user request: Second B turn.\n- Workspace root: \n- Allowed roots: \n- Treat relative project paths as relative to the workspace root; never infer `.` as the project root when a workspace root is provided.\n- Workspace boundary is enforced by tools: do not read, search, edit, or run commands against paths outside the allowed roots unless the user explicitly requests an external path and the tool/approval policy permits it.\n- LeapFlow workspace config is optional at `/.leapflow/config.yaml`; runtime config is loaded from `~/.leapflow/config/user.yaml` and `~/.leapflow/profiles//config/*.yaml`.\n- Preserve this task contract across summarization, compression, tool loops, and memory retrieval.\n" - }, - { - "role": "system", - "content": "\n## Recent Session Summary\n- [assistant] Workspace B acknowledged.\n- [user] Second B turn.\n" - }, - { - "role": "assistant", - "content": "Workspace B acknowledged." - }, - { - "role": "user", - "content": "Second B turn.\nSecond B turn." - } - ], - "tools": [ - "capability_expand", - "code_intel", - "code_search", - "config_get", - "config_list", - "env_info", - "file_find", - "file_list", - "file_read", - "git_query", - "lint_check", - "memory_search", - "plugin_list", - "plugin_propose", - "plugin_status", - "plugin_versions", - "repo_map", - "research_note", - "runtime_snapshot", - "schedule_reentry", - "self_describe", - "session_list", - "skill_view", - "skills_list", - "terminal_list", - "terminal_read", - "test_run", - "text_search", - "time_get", - "tool_describe", - "tool_search", - "web_fetch" - ] - }, - "responses": [ - { - "status": 200, - "content_type": "application/json", - "body": "{\"id\": \"chatcmpl-cassette\", \"object\": \"chat.completion\", \"model\": \"cassette-model\", \"choices\": [{\"index\": 0, \"message\": {\"role\": \"assistant\", \"content\": \"Still workspace B.\"}, \"finish_reason\": \"stop\"}], \"usage\": {\"prompt_tokens\": 64, \"completion_tokens\": 16, \"total_tokens\": 80}}" - } - ] -} diff --git a/tests/_fixtures/cassettes/r2_isolation/cassette-model-49cfe7e29183008c.cassette.json b/tests/_fixtures/cassettes/r2_isolation/cassette-model-49cfe7e29183008c.cassette.json new file mode 100644 index 00000000..109cc2cb --- /dev/null +++ b/tests/_fixtures/cassettes/r2_isolation/cassette-model-49cfe7e29183008c.cassette.json @@ -0,0 +1,63 @@ +{ + "fingerprint": "49cfe7e29183008c88598cd0900fba3ff71237908d8ed81fb18d888459447b74", + "note": "captured in seed mode", + "request": { + "model": "cassette-model", + "stream": false, + "messages": [ + { + "role": "system", + "content": "You are LeapFlow, an intelligent assistant that can both converse naturally and take real actions on the user's computer.\n\n## Capabilities\nThe tool index below lists **every** registered tool by name and a one-line summary — this is the complete\ncapability contract; nothing else exists. Only a subset is directly callable this turn (via native tool calling,\nnot a JSON block in your reply). If you need a tool from the index that is not yet callable, call\n`capability_expand` with its category name first — the matching tools become callable immediately after.\nWhen the user asks about LeapFlow **itself** — its identity/version/model, what tools, plugins, or capabilities\nit has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of\nself-awareness — answer from live runtime evidence via `self_describe` (facet=capabilities for \"what can you\ndo / do you support X\", identity for version/model, runtime for context/posture, or `all` for a full self-check),\nnever by reading LeapFlow's own source code, documentation, or memory. Report configuration-dependent or\nunavailable capabilities as limitations rather than inferring them.\n- **text_search**(text, pattern): Search for a regex pattern in text.\n- **text_replace**(text, old, new, count) [capability_expand category: write]: Replace occurrences of a substring in text.\n- **time_get**(): Get current date and time.\n- **env_info**(): Get system environment information (OS, Python version, cwd).\n- **skills_list**(query, category, source): List available learned skills. Use when user asks about capabilities or you need a specific skill.\n- **skill_view**(name): View the full content of a specific skill document.\n- **skill_list_files**(name) [capability_expand category: skills]: List all support files in a skill directory (configs, templates, examples). Use before skill_file_read to discover available files.\n- **skill_file_read**(name, file_path) [capability_expand category: skills]: Read an individual support file from a skill directory. Use skill_list_files first to discover available files.\n- **code_intel**(path, operation): Precise document symbols (outline) for a source file: classes, functions, and methods with line ranges. Python uses an exact AST parse; other languages use a keyword-prefix scan. Prefer over file_read mode=symbols for accurate navigation before editing. Read-only.\n- **repo_map**(path): Compact project orientation for a repository root: languages, detected test/lint commands, top-level structure, entry points, manifest, and VCS branch. Call this first when entering an unfamiliar codebase. Read-only.\n- **scm_sync**(action, cwd, remote, pull_ref, push_ref, timeout) [capability_expand category: scm]: Run a typed git SCM action. Use this instead of shell_run for git pull/push/status. For 'pull origin main then push', set action='pull_then_push', remote='origin', pull_ref='main', and omit push_ref so LeapFlow pushes the current local branch.\n- **git_query**(action, cwd, ref, path, staged, max_count, stat): Read-only structured git inspection: action=diff|log|status|branch|show. Prefer over shell_run for reading repo state — output is clipped, redacted, and log/branch are parsed into structured fields. Use scm_sync for pull/push.\n- **git_write**(action, cwd, message, stage_all, name, ref, create) [capability_expand category: scm]: Mutating git actions: action=commit (message, stage_all), branch (create+switch), checkout (switch; create=true for -b). Approval-gated. Use scm_sync for pull/push and git_query for reads.\n- **test_run**(command, cwd, timeout): Run the project's test suite and return structured results (framework, passed/failed counts, failing tests). Auto-detects the runner (pytest/npm/go/cargo) or uses a configured/explicit command; executes via the governed shell. ok=true means the runner executed — see 'success' for pass/fail.\n- **lint_check**(command, cwd, timeout): Run the project's linter and return a structured clean/issue result. Auto-detects the linter (ruff/eslint/go vet/clippy) or uses a configured/explicit command; executes via the governed shell. ok=true means the linter ran — see 'clean'.\n- **file_list**(path, pattern, depth): List files and directories at a given path. Use depth=1 or depth=2 to get a recursive tree in one call instead of listing each sub-directory separately.\n- **file_read**(path, max_lines, start_line, max_chars, mode): Read text file content with adaptive context governance. For large or unfamiliar files, prefer mode='outline' or mode='symbols' first, then use mode='raw' with start_line/max_lines for the specific range you actually need. For LeapFlow's own settings, use config_list / config_get / config_set — its config files are outside the workspace and not readable here.\n- **file_write**(path, content, mode) [capability_expand category: write]: Write content to a file (overwrite or append).\n- **code_search**(pattern, patterns, path, glob, ignore_case, multiline, max_results, context_lines): Search file CONTENTS by regex pattern across a directory tree (ripgrep-backed). Requires a regex pattern. NOT for listing or browsing directory contents — use file_list for that. Prefer this over shell_run grep: faster, skips VCS/dependency/build dirs, and returns structured path:line:column matches. Batch related lookups into ONE call via `patterns` (OR-combined, single pass) instead of issuing several separate searches. Use file_read for the surrounding context of a hit.\n- **file_find**(glob, path, max_results): Find files by a recursive glob pattern under a base path (e.g. '**/test_*.py' or '*.md'). Prefer this over shell_run find; skips VCS/dependency/build dirs.\n- **edit_file**(path, edits, dry_run, diff) [capability_expand category: write]: Apply targeted, anchored search-replace edits to an EXISTING text file (use file_write to create/overwrite). Each edit is {original_text, new_text, replace_all?}; original_text must match exactly and uniquely (or set replace_all) — a non-unique or missing anchor is rejected so files are never corrupted. Set dry_run to preview. Alternatively pass a unified 'diff' to apply its hunks as anchored edits. Far cheaper and safer than rewriting a whole file.\n- **shell_run**(command, cwd, timeout) [capability_expand category: shell]: Execute a one-shot shell command with timeout protection. Runs in the active workspace; paths resolving outside it are refused. Reach for a structured tool first when one fits — web_fetch for anything over HTTP(S), git_query/scm_sync for git, code_search/file_find/file_read for the repo, config_get/config_set for LeapFlow's own settings — because those report typed results, while a failed shell command can only be diagnosed from its exit code and stderr. Every shell run counts as an external side effect, so a failure stops the rest of the batch and is not retried automatically.\n- **terminal_open**(command, cwd, shell) [capability_expand category: terminal]: Open a PERSISTENT shell session (REPL/dev server/watch), returning a session_id for terminal_send/read/close. Disabled unless tools.terminal_session_enabled is set. For one-shot commands use shell_run instead.\n- **terminal_send**(session_id, input, wait) [capability_expand category: terminal]: Send a line of input to a persistent terminal session and return output captured shortly after.\n- **terminal_read**(session_id, wait): Drain buffered output from a persistent terminal session (optionally waiting briefly first).\n- **terminal_close**(session_id) [capability_expand category: terminal]: Terminate a persistent terminal session and release its process group.\n- **terminal_list**(): List active persistent terminal sessions.\n- **config_list**(category, limit): List LeapFlow's own writable settings (LLM model and endpoint, daemon, memory, perception, gateway, …) with current values. Use this to discover the exact key before changing anything. Optionally narrow by `category`. This is the only correct way to inspect LeapFlow configuration — never read config files from disk.\n- **config_get**(key): Read one LeapFlow setting by key (e.g. 'llm.model', 'llm.base_url', 'daemon.log_level'), returning its current value, type, scopes, and whether a change needs a daemon restart. There is no 'llm.provider' key: provider behavior is inferred from the OpenAI-compatible 'llm.base_url'. Never read LeapFlow config files from disk — use this.\n- **config_set**(key, value, scope) [capability_expand category: config]: Change one LeapFlow setting by key, e.g. switch an OpenAI-compatible LLM with key='llm.model' and key='llm.base_url'. There is no 'llm.provider' key: provider behavior is inferred from the endpoint. Values are validated and coerced; credentials are stored in the vault automatically. Call config_list or config_get first if unsure of the key. The result states whether a `leap daemon restart` is required. Never edit LeapFlow config files directly.\n- **web_fetch**(url, select, timeout, max_bytes): Read a URL over HTTP(S) and get back extracted, context-sized content: parsed JSON for API endpoints, readable text plus links for web pages. Use this for anything on the internet — prices, docs, releases, articles — instead of running curl through shell_run: it reports real HTTP status codes, retries rate limits on its own, and is a plain read so a retry is always safe. For JSON APIs pass `select` with a dotted path (e.g. 'chart.result.0.meta') to return just that part instead of the whole payload.\n- **memory_search**(query, limit): Search agent memory for relevant past experiences, observations, and facts.\n- **memory_add**(content, kind) [capability_expand category: write]: Store a new observation or insight in memory for future reference.\n- **research_note**(kind, text): Record a compact, structured note about the current task's state so it survives context compression on long / multi-step tasks. Use for durable findings, open questions still to resolve, decisions / excluded paths, and the immediate next step. One concise sentence per note.\n- **capability_expand**(category): Fetch the full callable schema for every tool in a capability category (e.g. 'hub', 'gateway', 'desktop', 'delegate', 'file', 'memory', 'skill'). The compact tool index always lists every registered tool by name and a one-line summary, but only a static low-risk subset is directly callable each turn. If you need a tool from the index that is not yet callable, call capability_expand with its category first; the matching tools become callable in this turn. Never invent a tool name — expand the category instead.\n- **delegate_task**(goal, context) [capability_expand category: delegate]: Delegate a complex sub-task to an isolated subagent. The subagent gets a fresh context and restricted tool access. Use when a task is self-contained and can be solved independently.\n- **schedule_reentry**(kind, reason, delay_seconds, event_match, max_reentries, deadline_seconds): Register a re-entry so this task can resume later from its current orientation (findings / open questions / next step). Use when work must pause and continue after a delay (kind=time) or when a matching platform event arrives (kind=event), instead of finishing now. The research-ledger state is carried over automatically.\n- **hub_push**(skill_name, visibility, version) [capability_expand category: hub]: Push a local skill to the ModelScope Hub for sharing or backup.\n- **hub_pull**(repo_id, version) [capability_expand category: hub]: Pull a skill from the ModelScope Hub to install locally.\n- **hub_search**(query) [capability_expand category: hub]: Search for skills on the Hub by keyword or description.\n- **hub_sync**(mode, dry_run) [capability_expand category: hub]: Preview or execute sync between local skills and Hub.\n- **platform_action**(platform, action, payload, backend_kind) [capability_expand category: gateway]: Execute an exact registered business action on an external platform through LeapFlow's App Connector layer. Actions must be copied from the App Connector Capability Index and are addressed as domain.operation, e.g. im.send_message or docs.create_markdown. All business fields (chat_id, text, query, etc.) MUST be placed inside `payload`, never at the top level. Example: {\"platform\":\"feishu\",\"action\":\"im.send_message\",\"payload\":{\"chat_id\":\"oc_xxx\",\"text\":\"hello\"}}. Do not invent action names, do not use management actions such as list/guide/connect/status here.\n- **platform_connect**(action, platform, credentials, options, checkpoint) [capability_expand category: gateway]: List, guide, connect, disconnect, remove, or check status for external platforms using the App Connector management namespace. Supports REST and CLI backends. Use this for management actions such as list/guide/preflight/connect/status; use platform_action only for exact registered business actions.\n- **gateway_send**(platform, chat_id, text, thread_id) [capability_expand category: gateway]: Send a message to a connected external platform (Feishu group, Telegram chat, DingTalk conversation, etc.). Requires the platform to be connected via gateway_connect first. Use gateway_connect with action='list' to see connected platforms and available chat IDs.\n- **gateway_connect**(action, platform, credentials, options) [capability_expand category: gateway]: Connect, configure, or manage external platform integrations (Feishu, DingTalk, Telegram, Slack, Discord, etc.). Conversational flow: 1) call 'guide' to get setup steps + required fields, 2) present the steps to the user and ask for ALL required credentials in a single message, 3) call 'connect' with the credentials. Goal: complete in 1–2 user turns. NEVER include credential values in your text response.\n- **plugin_list**(): List the live plugin registry across Tool/Gateway/LLM subsystems for plugin management: which plugins are loaded, their category and fiber lifecycle state. For questions about LeapFlow's own capabilities or whether it supports X, use self_describe instead.\n- **plugin_status**(plugin_id): Get detailed status of a specific plugin: its declared category, runtime dependencies, contributed tools, and fiber lifecycle state.\n- **plugin_versions**(plugin_id): List recorded source versions and active pointer for a profile-scoped plugin.\n- **plugin_propose**(requested_capability, plugin_id, proposed_tools, test_cases, risk_level, evidence): Create a side-effect-free PluginProposal from explicit capability-gap evidence. Use this before plugin_generate when a missing capability should be reviewed. Does not call an LLM, write files, or install anything.\n- **assess_compatibility**(manifest, source_path) [capability_expand category: plugin_management]: Assess whether a foreign plugin manifest or real DSH source bundle is compatible with LeapFlow. A source bundle assessment is static: runtime_ready stays false until restricted Node discovery during plugin_install. Returns component-level verdicts and limitations.\n- **plugin_generate**(plugin_id, description, proposal_id) [capability_expand category: system]: Generate a new ToolPlugin from a natural-language capability description. The LLM produces code that conforms to the ToolPlugin Protocol; it is then rigorously validated (syntax, structure, import, protocol conformance). The generated source is stored in CAS and receives explicit content approval. It DOES NOT install the plugin — installation requires a second, mutation-specific approval via plugin_install.\n- **plugin_install**(plugin_id, code, marketplace_name, source_path, proposal_id, version_label) [capability_expand category: system]: Install a plugin from exactly one source: validated Python code, the configured Python marketplace, or a real DSH source bundle. DSH bundles run restricted Node discovery before registration; only public host tools are installed and client UI limitations are reported. Writes profile-scoped state and mutates the process-global registry. REQUIRES APPROVAL.\n- **plugin_rollback**(plugin_id, version) [capability_expand category: system]: Rollback a profile-scoped plugin to a recorded source version and reload it. REQUIRES APPROVAL.\n- **plugin_reload**(plugin_id, version_label) [capability_expand category: system]: Hot-reload a plugin at runtime. Disposes the old plugin fiber, re-imports its module, and registers a fresh instance. Existing in-flight turns are unaffected (snapshot isolation). REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_disable**(plugin_id) [capability_expand category: system]: Disable a plugin by disposing its fiber, removing its tools from the runtime registry. Cannot disable self_management itself. REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_remove**(plugin_id, delete_source) [capability_expand category: system]: Terminally remove a plugin: dispose its fiber, unregister its tools, remove reload metadata, and optionally delete its profile-scoped source file. Cannot remove self_management itself. REQUIRES APPROVAL.\n- **plugin_unquarantine**(plugin_id) [capability_expand category: plugin_management]: Restore a quarantined plugin to probation status for re-evaluation. Unfreezes the trust ledger, transitions the proposal back to PROBATION, and reloads the plugin. REQUIRES APPROVAL.\n- **plugin_enable**(plugin_id) [capability_expand category: system]: Re-enable a previously disabled plugin by reloading its module and registering a fresh instance. REQUIRES APPROVAL.\n- **self_describe**(facet): Answer questions about LeapFlow ITSELF from live runtime evidence — its identity/version/model, what tools and plugins it has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of self-awareness. Prefer this over reading LeapFlow's own source code. Pick facet=capabilities for 'what can you do / do you support X', identity for version/model, runtime for context/posture, or all for a full self-check when explicitly requested.\n- **runtime_snapshot**(): Lightweight (~150 token) flat snapshot of current runtime state: model, context budget, posture, disclosure level, turn count, cache hit rate, uptime, tool count, and pending approvals.\n- **schedule_create**(trigger_expression, instruction, execution_mode, max_retries, delivery_target) [capability_expand category: scheduler]: Create a new scheduled task. Specify a trigger expression (e.g. '30m', 'every 2h', '0 9 * * *') and the instruction to execute on each trigger. Optionally provide an execution mode and a delivery target to receive notifications.\n- **schedule_list**() [capability_expand category: scheduler]: List all scheduled tasks with their current state, trigger, and next due time.\n- **schedule_status**(task_id) [capability_expand category: scheduler]: Get detailed status and recent execution history for a scheduled task.\n- **schedule_pause**(task_id) [capability_expand category: scheduler]: Pause a scheduled task so it stops firing without being cancelled.\n- **schedule_resume**(task_id) [capability_expand category: scheduler]: Resume a paused scheduled task — re-arms it and recalculates next due time.\n- **schedule_cancel**(task_id) [capability_expand category: scheduler]: Cancel a scheduled task permanently.\n- **tool_search**(query, max_results): Search all registered tools by keyword relevance. Returns a ranked list of matching tools with name, category, and summary. Use this when you need a tool but are unsure of its exact name or category.\n- **tool_describe**(tool_name): Get the full callable schema of a single tool by exact name. Returns the tool's description, parameters, and metadata. Use after tool_search to inspect a specific tool before calling it.\n- **session_search**(query, limit) [capability_expand category: unclassified]: Search past conversation sessions in the current workspace for relevant context.\n- **session_list**(limit): List recent conversation sessions in the current workspace with stable ids, titles, dates, and summaries. Use for browsing past tasks or when user asks to see history without specific search terms. No keywords needed — returns chronological list.\n- **session_detail**(session_id, limit, offset, include_inactive) [capability_expand category: unclassified]: Read a paginated persisted transcript for one past conversation session in the current workspace. Use after session_list or session_search returns a session_id.\n- **click**(element_index) [capability_expand category: desktop]: Click a UI element by its element_index (from the latest observe_ui snapshot)\n- **get_clipboard**() [capability_expand category: desktop]: Read current clipboard text content\n- **list_apps**(filter, running_only) [capability_expand category: desktop]: List available applications on this system. Use to discover correct bundle_id before switch_app.\n- **list_windows**() [capability_expand category: desktop]: List all top-level windows with pid, window_id, title, and per-window state (minimized, on-screen). Call this first to pick the pid and window_id that observe_ui and other window tools require.\n- **observe_ui**(pid, window_id, query) [capability_expand category: desktop]: Snapshot one window's actionable UI elements, each tagged with an element_index for click/right_click/read_text. Re-observe after actions — indices belong to one snapshot. Requires the window's pid and window_id from list_windows.\n- **open_url**(url, app_id) [capability_expand category: desktop]: Open a URL in the default or specified browser\n- **read_text**(element_index) [capability_expand category: desktop]: Read the text content of a specific UI element from the latest snapshot\n- **right_click**(element_index) [capability_expand category: desktop]: Right-click a UI element to open its context menu. Returns visible menu items.\n- **screenshot**(pid, window_id) [capability_expand category: desktop]: Capture a screenshot for visual verification. With pid + window_id captures that window (works across all displays); defaults to the last observed window, or the full desktop when no window has been observed.\n- **scroll**(element_index, direction, amount, pid, window_id) [capability_expand category: desktop]: Scroll a scrollable area of a window. Omit element_index to scroll the window's focused/page scroller; pass one to scroll an exact element from the latest snapshot.\n- **select_text**(element_index) [capability_expand category: desktop]: Select all text in a UI element (focus + select-all, for subsequent copy)\n- **set_clipboard**(text) [capability_expand category: desktop]: Write text to the clipboard\n- **shortcut**(keys) [capability_expand category: desktop]: Execute a keyboard shortcut\n- **switch_app**(app_id) [capability_expand category: desktop]: Switch to an app (launch if needed, activate, verify)\n- **type_text**(text) [capability_expand category: desktop]: Type text into the currently focused element\n- **wait**(seconds) [capability_expand category: desktop]: Wait for a specified duration before continuing\n- **wait_until**(condition, pid, window_id, timeout, poll_interval) [capability_expand category: desktop]: Wait until a UI condition is met (polls UI tree). Returns elements when found or on timeout.\n- **wait_until_stable**(timeout, poll_interval, pid, window_id) [capability_expand category: desktop]: Wait until the UI stops changing (element set stabilizes across polls).\n\n## App Connector Capability Index\nLeapFlow can onboard and manage external apps through `platform_connect` and execute exact registered business actions through `platform_action`.\nFor requests about connecting, setting up, configuring, enabling, or managing a supported app, use `platform_connect` first instead of generating SDK/Webhook sample code.\n`platform_connect.action` is the management namespace: list, guide, preflight, connect, disconnect, remove, status, events_start, events_stop, events_status.\n`platform_action.action` is only for exact registered platform business actions listed below, such as `im.send_message`; never use management actions like `list` or `guide` there.\nAll business fields MUST go inside `payload`; top-level keys are only `platform`, `action`, and `payload`.\nDo not invent platform IDs or platform action names. If the needed action is not listed, ask for discovery/clarification instead of guessing.\nUse `platform_connect` with `action='guide'` and the matching `platform` to start onboarding; use `action='list'` when the app is unclear.\nWhen a pending onboarding state is present, continue from that state with `platform_connect` instead of asking the user to restate the app.\nSupported apps:\n- `api_server`: API Server (OpenAI Compatible) (category=api; backend=adapter; platform_action actions=none registered)\n- `dingtalk`: 钉钉 (DingTalk) (category=im; backend=adapter; platform_action actions=none registered)\n- `feishu`: 飞书 (Feishu/Lark) (category=im; backend=cli)\n - `calendar.create_event` payload={summary*, start_time*, end_time*, attendees} [write/high]\n - `docs.create_markdown` payload={title*, markdown*, folder_token} [write/medium]\n - `drive.search` payload={query*, limit} [read/medium]\n - `im.add_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.download_resource` payload={message_id*, file_key*, type*} [read/low]\n - `im.get_messages` payload={message_ids*} [read/low]\n - `im.list_chats` payload={page_size} [read/low]\n - `im.list_messages` payload={chat_id*} [read/low]\n - `im.list_thread_messages` payload={thread*} [read/low]\n - `im.remove_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.reply_message` payload={message_id*, text*} [send/high]\n - `im.search_chats` payload={query*, page_size} [read/low]\n - `im.search_messages` payload={query*} [read/medium]\n - `im.send_message` payload={chat_id*, text*, thread_id} [send/high]\n - `im.update_card` payload={token*, card*} [write/medium]\n - `im.update_message` payload={message_id*, text*} [write/medium]\n - `mail.search_unread` payload={query, limit} [read/high]\n - `sheets.append_row` payload={spreadsheet_token*, sheet_id*, values*} [write/medium]\n - `task.create` payload={title*, description, due_time} [write/medium]\n- `telegram`: Telegram (category=social; backend=adapter; platform_action actions=none registered)\n- `webhook`: Webhook (Generic) (category=webhook; backend=adapter; platform_action actions=none registered)\n\n## Tool Usage\nTools are normally invoked through the native function-calling mechanism, not by writing JSON in your reply\ntext. Only if the provider signals that native function calling is unavailable for this turn, fall back to a\nsingle JSON code block: `{\"name\": \"tool_name\", \"arguments\": {\"key\": \"value\"}}` — use this fallback format\nonly, never both. Only call a tool whose exact name appears in the tool index above and is currently callable\n(or reachable via `capability_expand`). Never invent, rename, alias, or guess a tool name, platform ID, or\nplatform action from argument shape or wording — if the index does not list it, it does not exist.\n`platform_connect.action` (list/guide/preflight/connect/disconnect/remove/status/events_start/events_stop/\nevents_status) is the App Connector management namespace; `platform_action.action` only accepts exact\nregistered business actions such as `im.send_message` shown in the App Connector Capability Index — never mix\nthe two namespaces. If a tool call returns an unknown/unavailable result, use the returned suggestions or\navailable names for a single retry instead of trying further variations of the same guess.\n\n**Side-effect action rule** (`platform_action` with effect=send/write/execute):\n- Call each unique action+payload **exactly once**. Never include duplicates in the same turn.\n- Once the result returns `\"completed\": true`, that action is DONE for this task. Do NOT call it again\n in any subsequent turn — immediately summarize the result for the user instead.\n- The system enforces idempotency: duplicate calls are blocked and will not execute.\n- If the user explicitly requests sending/writing multiple times, use distinct payloads per call.\n\n**Resource identifier provenance rule**:\n- NEVER fabricate, guess, or infer resource identifiers (chat_id, message_id, file_key, user_id, etc.).\n Every resource ID used in a side-effect action MUST come from a successful API response in this session.\n- If a read/list action fails (e.g. authorization error), you do NOT have valid resource IDs.\n Report the failure to the user — do NOT attempt the dependent write/send action with a guessed ID.\n- When a tool result contains `\"llm_instruction\"`, follow it exactly.\n\n## Guidelines\n1. **Direct answers first**: If you already know the answer, respond directly without tools.\n2. **Avoid redundant tool calls**: Do not call the same tool with the same arguments more than once in the same user turn. When an existing tool result already answers the user's request, stop calling tools and answer directly.\n3. **Use tools proactively**: When the user asks about files, time, system state, or needs actions performed, use the appropriate tool.\n4. **Chain tools when needed**: You can call multiple tools in sequence (e.g., list files → read file → summarize).\n5. **Handle failures gracefully**: If a tool fails, explain what went wrong and suggest alternatives. If it failed because of an unknown tool/platform/action name, retry once with an exact name from the returned suggestions, then explain rather than keep guessing.\n6. **Summarize results naturally**: After tool execution, synthesize the results into a helpful answer rather than dumping raw output.\n7. **Stay conversational**: Maintain a natural, helpful tone. Acknowledge context from earlier in the conversation.\n8. **Recall past work**:\n - Broad queries (\"之前做了什么\", \"列出任务\"): answer from the \"Recent Task History\" section already in your context. If insufficient, call session_list.\n - Specific lookups (\"上次那个配置怎么改的\"): call session_search with relevant phrases (NOT single characters).\n - Do NOT call search tools repeatedly with keyword fragments. One well-phrased call is better than ten fragmented ones.\n\n## Coding & Verification\nWhen working with code, prefer the precise built-in tools over ad-hoc shell: use `repo_map` to orient in an\nunfamiliar project, `code_search`/`file_find` to locate and `code_intel` for symbols, `edit_file` (anchored\nsearch-replace, or a unified `diff`) to change files — never rewrite a whole file to change a few lines — and\n`git_query` to inspect diffs/log. After edits, check `syntax_ok` in the result and run `test_run`/`lint_check`\nbefore declaring the work done. Batch independent read-only calls (search/read/query) into a single turn — they\nrun in parallel.\n\n## Presentation Style\n1. **Polished Markdown only**: Format user-facing answers with clean Markdown headings, short paragraphs, and concise bullets. Use tables only when they improve comparison or scanning.\n2. **Terminal-friendly layout**: Keep lines readable in a TUI; avoid dense walls of text, deeply nested lists, oversized ASCII art, or heavy visual blocks.\n3. **Elegant emphasis**: Use bold text sparingly for key terms and conclusions. Avoid excessive emojis, decorative symbols, repeated separators, or visual noise.\n4. **Theme-safe colors**: Do not emit ANSI escape codes, HTML color tags, Rich markup, or hardcoded color names. Rely on the TUI theme to render Markdown professionally.\n5. **No leaked tool protocol**: Never show tool-call JSON, internal schemas, raw observations, tool result payloads, or hidden reasoning in the final answer unless the user explicitly asks for raw/debug output. Treat any prior `{\"name\": ..., \"arguments\": ...}` blocks and `Tool result (...)` messages as internal execution context only.\n6. **Professional closure**: End with a concise conclusion or next step when helpful; avoid rambling after the useful answer is complete.\n\nWhen finished with all tool calls, respond normally without a JSON block, tool-call transcript, or process log.\n\n## Task Contract\n- Task ID: turn-1\n- Original user request: Hello from A.\n- Workspace root: \n- Allowed roots: \n- Treat relative project paths as relative to the workspace root; never infer `.` as the project root when a workspace root is provided.\n- Workspace boundary is enforced by tools: do not read, search, edit, or run commands against paths outside the allowed roots unless the user explicitly requests an external path and the tool/approval policy permits it.\n- LeapFlow workspace config is optional at `/.leapflow/config.yaml`; runtime config is loaded from `~/.leapflow/config/user.yaml` and `~/.leapflow/profiles//config/*.yaml`.\n- Preserve this task contract across summarization, compression, tool loops, and memory retrieval.\n" + }, + { + "role": "system", + "content": "\n## Recent Session Summary\n- [user] Hello from A.\n" + }, + { + "role": "user", + "content": "Hello from A.\nHello from A." + } + ], + "tools": [ + "capability_expand", + "code_intel", + "code_search", + "config_get", + "config_list", + "env_info", + "file_find", + "file_list", + "file_read", + "git_query", + "lint_check", + "memory_search", + "plugin_list", + "plugin_propose", + "plugin_status", + "plugin_versions", + "repo_map", + "research_note", + "runtime_snapshot", + "schedule_reentry", + "self_describe", + "session_list", + "skill_view", + "skills_list", + "terminal_list", + "terminal_read", + "test_run", + "text_search", + "time_get", + "tool_describe", + "tool_search", + "web_fetch" + ] + }, + "responses": [ + { + "status": 200, + "content_type": "application/json", + "body": "{\"id\": \"chatcmpl-cassette\", \"object\": \"chat.completion\", \"model\": \"cassette-model\", \"choices\": [{\"index\": 0, \"message\": {\"role\": \"assistant\", \"content\": \"Workspace A acknowledged.\"}, \"finish_reason\": \"stop\"}], \"usage\": {\"prompt_tokens\": 64, \"completion_tokens\": 16, \"total_tokens\": 80}}" + } + ] +} diff --git a/tests/_fixtures/cassettes/r2_isolation/cassette-model-81fbf16923453579.cassette.json b/tests/_fixtures/cassettes/r2_isolation/cassette-model-81fbf16923453579.cassette.json new file mode 100644 index 00000000..31bfbf34 --- /dev/null +++ b/tests/_fixtures/cassettes/r2_isolation/cassette-model-81fbf16923453579.cassette.json @@ -0,0 +1,67 @@ +{ + "fingerprint": "81fbf16923453579cc80f71537627cac4ea70a944321ce8d4aeb48e2b531fd2d", + "note": "captured in seed mode", + "request": { + "model": "cassette-model", + "stream": false, + "messages": [ + { + "role": "system", + "content": "You are LeapFlow, an intelligent assistant that can both converse naturally and take real actions on the user's computer.\n\n## Capabilities\nThe tool index below lists **every** registered tool by name and a one-line summary — this is the complete\ncapability contract; nothing else exists. Only a subset is directly callable this turn (via native tool calling,\nnot a JSON block in your reply). If you need a tool from the index that is not yet callable, call\n`capability_expand` with its category name first — the matching tools become callable immediately after.\nWhen the user asks about LeapFlow **itself** — its identity/version/model, what tools, plugins, or capabilities\nit has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of\nself-awareness — answer from live runtime evidence via `self_describe` (facet=capabilities for \"what can you\ndo / do you support X\", identity for version/model, runtime for context/posture, or `all` for a full self-check),\nnever by reading LeapFlow's own source code, documentation, or memory. Report configuration-dependent or\nunavailable capabilities as limitations rather than inferring them.\n- **text_search**(text, pattern): Search for a regex pattern in text.\n- **text_replace**(text, old, new, count) [capability_expand category: write]: Replace occurrences of a substring in text.\n- **time_get**(): Get current date and time.\n- **env_info**(): Get system environment information (OS, Python version, cwd).\n- **skills_list**(query, category, source): List available learned skills. Use when user asks about capabilities or you need a specific skill.\n- **skill_view**(name): View the full content of a specific skill document.\n- **skill_list_files**(name) [capability_expand category: skills]: List all support files in a skill directory (configs, templates, examples). Use before skill_file_read to discover available files.\n- **skill_file_read**(name, file_path) [capability_expand category: skills]: Read an individual support file from a skill directory. Use skill_list_files first to discover available files.\n- **code_intel**(path, operation): Precise document symbols (outline) for a source file: classes, functions, and methods with line ranges. Python uses an exact AST parse; other languages use a keyword-prefix scan. Prefer over file_read mode=symbols for accurate navigation before editing. Read-only.\n- **repo_map**(path): Compact project orientation for a repository root: languages, detected test/lint commands, top-level structure, entry points, manifest, and VCS branch. Call this first when entering an unfamiliar codebase. Read-only.\n- **scm_sync**(action, cwd, remote, pull_ref, push_ref, timeout) [capability_expand category: scm]: Run a typed git SCM action. Use this instead of shell_run for git pull/push/status. For 'pull origin main then push', set action='pull_then_push', remote='origin', pull_ref='main', and omit push_ref so LeapFlow pushes the current local branch.\n- **git_query**(action, cwd, ref, path, staged, max_count, stat): Read-only structured git inspection: action=diff|log|status|branch|show. Prefer over shell_run for reading repo state — output is clipped, redacted, and log/branch are parsed into structured fields. Use scm_sync for pull/push.\n- **git_write**(action, cwd, message, stage_all, name, ref, create) [capability_expand category: scm]: Mutating git actions: action=commit (message, stage_all), branch (create+switch), checkout (switch; create=true for -b). Approval-gated. Use scm_sync for pull/push and git_query for reads.\n- **test_run**(command, cwd, timeout): Run the project's test suite and return structured results (framework, passed/failed counts, failing tests). Auto-detects the runner (pytest/npm/go/cargo) or uses a configured/explicit command; executes via the governed shell. ok=true means the runner executed — see 'success' for pass/fail.\n- **lint_check**(command, cwd, timeout): Run the project's linter and return a structured clean/issue result. Auto-detects the linter (ruff/eslint/go vet/clippy) or uses a configured/explicit command; executes via the governed shell. ok=true means the linter ran — see 'clean'.\n- **file_list**(path, pattern, depth): List files and directories at a given path. Use depth=1 or depth=2 to get a recursive tree in one call instead of listing each sub-directory separately.\n- **file_read**(path, max_lines, start_line, max_chars, mode): Read text file content with adaptive context governance. For large or unfamiliar files, prefer mode='outline' or mode='symbols' first, then use mode='raw' with start_line/max_lines for the specific range you actually need. For LeapFlow's own settings, use config_list / config_get / config_set — its config files are outside the workspace and not readable here.\n- **file_write**(path, content, mode) [capability_expand category: write]: Write content to a file (overwrite or append).\n- **code_search**(pattern, patterns, path, glob, ignore_case, multiline, max_results, context_lines): Search file CONTENTS by regex pattern across a directory tree (ripgrep-backed). Requires a regex pattern. NOT for listing or browsing directory contents — use file_list for that. Prefer this over shell_run grep: faster, skips VCS/dependency/build dirs, and returns structured path:line:column matches. Batch related lookups into ONE call via `patterns` (OR-combined, single pass) instead of issuing several separate searches. Use file_read for the surrounding context of a hit.\n- **file_find**(glob, path, max_results): Find files by a recursive glob pattern under a base path (e.g. '**/test_*.py' or '*.md'). Prefer this over shell_run find; skips VCS/dependency/build dirs.\n- **edit_file**(path, edits, dry_run, diff) [capability_expand category: write]: Apply targeted, anchored search-replace edits to an EXISTING text file (use file_write to create/overwrite). Each edit is {original_text, new_text, replace_all?}; original_text must match exactly and uniquely (or set replace_all) — a non-unique or missing anchor is rejected so files are never corrupted. Set dry_run to preview. Alternatively pass a unified 'diff' to apply its hunks as anchored edits. Far cheaper and safer than rewriting a whole file.\n- **shell_run**(command, cwd, timeout) [capability_expand category: shell]: Execute a one-shot shell command with timeout protection. Runs in the active workspace; paths resolving outside it are refused. Reach for a structured tool first when one fits — web_fetch for anything over HTTP(S), git_query/scm_sync for git, code_search/file_find/file_read for the repo, config_get/config_set for LeapFlow's own settings — because those report typed results, while a failed shell command can only be diagnosed from its exit code and stderr. Every shell run counts as an external side effect, so a failure stops the rest of the batch and is not retried automatically.\n- **terminal_open**(command, cwd, shell) [capability_expand category: terminal]: Open a PERSISTENT shell session (REPL/dev server/watch), returning a session_id for terminal_send/read/close. Disabled unless tools.terminal_session_enabled is set. For one-shot commands use shell_run instead.\n- **terminal_send**(session_id, input, wait) [capability_expand category: terminal]: Send a line of input to a persistent terminal session and return output captured shortly after.\n- **terminal_read**(session_id, wait): Drain buffered output from a persistent terminal session (optionally waiting briefly first).\n- **terminal_close**(session_id) [capability_expand category: terminal]: Terminate a persistent terminal session and release its process group.\n- **terminal_list**(): List active persistent terminal sessions.\n- **config_list**(category, limit): List LeapFlow's own writable settings (LLM model and endpoint, daemon, memory, perception, gateway, …) with current values. Use this to discover the exact key before changing anything. Optionally narrow by `category`. This is the only correct way to inspect LeapFlow configuration — never read config files from disk.\n- **config_get**(key): Read one LeapFlow setting by key (e.g. 'llm.model', 'llm.base_url', 'daemon.log_level'), returning its current value, type, scopes, and whether a change needs a daemon restart. There is no 'llm.provider' key: provider behavior is inferred from the OpenAI-compatible 'llm.base_url'. Never read LeapFlow config files from disk — use this.\n- **config_set**(key, value, scope) [capability_expand category: config]: Change one LeapFlow setting by key, e.g. switch an OpenAI-compatible LLM with key='llm.model' and key='llm.base_url'. There is no 'llm.provider' key: provider behavior is inferred from the endpoint. Values are validated and coerced; credentials are stored in the vault automatically. Call config_list or config_get first if unsure of the key. The result states whether a `leap daemon restart` is required. Never edit LeapFlow config files directly.\n- **web_fetch**(url, select, timeout, max_bytes): Read a URL over HTTP(S) and get back extracted, context-sized content: parsed JSON for API endpoints, readable text plus links for web pages. Use this for anything on the internet — prices, docs, releases, articles — instead of running curl through shell_run: it reports real HTTP status codes, retries rate limits on its own, and is a plain read so a retry is always safe. For JSON APIs pass `select` with a dotted path (e.g. 'chart.result.0.meta') to return just that part instead of the whole payload.\n- **memory_search**(query, limit): Search agent memory for relevant past experiences, observations, and facts.\n- **memory_add**(content, kind) [capability_expand category: write]: Store a new observation or insight in memory for future reference.\n- **research_note**(kind, text): Record a compact, structured note about the current task's state so it survives context compression on long / multi-step tasks. Use for durable findings, open questions still to resolve, decisions / excluded paths, and the immediate next step. One concise sentence per note.\n- **capability_expand**(category): Fetch the full callable schema for every tool in a capability category (e.g. 'hub', 'gateway', 'desktop', 'delegate', 'file', 'memory', 'skill'). The compact tool index always lists every registered tool by name and a one-line summary, but only a static low-risk subset is directly callable each turn. If you need a tool from the index that is not yet callable, call capability_expand with its category first; the matching tools become callable in this turn. Never invent a tool name — expand the category instead.\n- **delegate_task**(goal, context) [capability_expand category: delegate]: Delegate a complex sub-task to an isolated subagent. The subagent gets a fresh context and restricted tool access. Use when a task is self-contained and can be solved independently.\n- **schedule_reentry**(kind, reason, delay_seconds, event_match, max_reentries, deadline_seconds): Register a re-entry so this task can resume later from its current orientation (findings / open questions / next step). Use when work must pause and continue after a delay (kind=time) or when a matching platform event arrives (kind=event), instead of finishing now. The research-ledger state is carried over automatically.\n- **hub_push**(skill_name, visibility, version) [capability_expand category: hub]: Push a local skill to the ModelScope Hub for sharing or backup.\n- **hub_pull**(repo_id, version) [capability_expand category: hub]: Pull a skill from the ModelScope Hub to install locally.\n- **hub_search**(query) [capability_expand category: hub]: Search for skills on the Hub by keyword or description.\n- **hub_sync**(mode, dry_run) [capability_expand category: hub]: Preview or execute sync between local skills and Hub.\n- **platform_action**(platform, action, payload, backend_kind) [capability_expand category: gateway]: Execute an exact registered business action on an external platform through LeapFlow's App Connector layer. Actions must be copied from the App Connector Capability Index and are addressed as domain.operation, e.g. im.send_message or docs.create_markdown. All business fields (chat_id, text, query, etc.) MUST be placed inside `payload`, never at the top level. Example: {\"platform\":\"feishu\",\"action\":\"im.send_message\",\"payload\":{\"chat_id\":\"oc_xxx\",\"text\":\"hello\"}}. Do not invent action names, do not use management actions such as list/guide/connect/status here.\n- **platform_connect**(action, platform, credentials, options, checkpoint) [capability_expand category: gateway]: List, guide, connect, disconnect, remove, or check status for external platforms using the App Connector management namespace. Supports REST and CLI backends. Use this for management actions such as list/guide/preflight/connect/status; use platform_action only for exact registered business actions.\n- **gateway_send**(platform, chat_id, text, thread_id) [capability_expand category: gateway]: Send a message to a connected external platform (Feishu group, Telegram chat, DingTalk conversation, etc.). Requires the platform to be connected via gateway_connect first. Use gateway_connect with action='list' to see connected platforms and available chat IDs.\n- **gateway_connect**(action, platform, credentials, options) [capability_expand category: gateway]: Connect, configure, or manage external platform integrations (Feishu, DingTalk, Telegram, Slack, Discord, etc.). Conversational flow: 1) call 'guide' to get setup steps + required fields, 2) present the steps to the user and ask for ALL required credentials in a single message, 3) call 'connect' with the credentials. Goal: complete in 1–2 user turns. NEVER include credential values in your text response.\n- **plugin_list**(): List the live plugin registry across Tool/Gateway/LLM subsystems for plugin management: which plugins are loaded, their category and fiber lifecycle state. For questions about LeapFlow's own capabilities or whether it supports X, use self_describe instead.\n- **plugin_status**(plugin_id): Get detailed status of a specific plugin: its declared category, runtime dependencies, contributed tools, and fiber lifecycle state.\n- **plugin_versions**(plugin_id): List recorded source versions and active pointer for a profile-scoped plugin.\n- **plugin_propose**(requested_capability, plugin_id, proposed_tools, test_cases, risk_level, evidence): Create a side-effect-free PluginProposal from explicit capability-gap evidence. Use this before plugin_generate when a missing capability should be reviewed. Does not call an LLM, write files, or install anything.\n- **assess_compatibility**(manifest, source_path) [capability_expand category: plugin_management]: Assess whether a foreign plugin manifest or real DSH source bundle is compatible with LeapFlow. A source bundle assessment is static: runtime_ready stays false until restricted Node discovery during plugin_install. Returns component-level verdicts and limitations.\n- **plugin_generate**(plugin_id, description, proposal_id) [capability_expand category: system]: Generate a new ToolPlugin from a natural-language capability description. The LLM produces code that conforms to the ToolPlugin Protocol; it is then rigorously validated (syntax, structure, import, protocol conformance). The generated source is stored in CAS and receives explicit content approval. It DOES NOT install the plugin — installation requires a second, mutation-specific approval via plugin_install.\n- **plugin_install**(plugin_id, code, marketplace_name, source_path, proposal_id, version_label) [capability_expand category: system]: Install a plugin from exactly one source: validated Python code, the configured Python marketplace, or a real DSH source bundle. DSH bundles run restricted Node discovery before registration; only public host tools are installed and client UI limitations are reported. Writes profile-scoped state and mutates the process-global registry. REQUIRES APPROVAL.\n- **plugin_rollback**(plugin_id, version) [capability_expand category: system]: Rollback a profile-scoped plugin to a recorded source version and reload it. REQUIRES APPROVAL.\n- **plugin_reload**(plugin_id, version_label) [capability_expand category: system]: Hot-reload a plugin at runtime. Disposes the old plugin fiber, re-imports its module, and registers a fresh instance. Existing in-flight turns are unaffected (snapshot isolation). REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_disable**(plugin_id) [capability_expand category: system]: Disable a plugin by disposing its fiber, removing its tools from the runtime registry. Cannot disable self_management itself. REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_remove**(plugin_id, delete_source) [capability_expand category: system]: Terminally remove a plugin: dispose its fiber, unregister its tools, remove reload metadata, and optionally delete its profile-scoped source file. Cannot remove self_management itself. REQUIRES APPROVAL.\n- **plugin_unquarantine**(plugin_id) [capability_expand category: plugin_management]: Restore a quarantined plugin to probation status for re-evaluation. Unfreezes the trust ledger, transitions the proposal back to PROBATION, and reloads the plugin. REQUIRES APPROVAL.\n- **plugin_enable**(plugin_id) [capability_expand category: system]: Re-enable a previously disabled plugin by reloading its module and registering a fresh instance. REQUIRES APPROVAL.\n- **self_describe**(facet): Answer questions about LeapFlow ITSELF from live runtime evidence — its identity/version/model, what tools and plugins it has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of self-awareness. Prefer this over reading LeapFlow's own source code. Pick facet=capabilities for 'what can you do / do you support X', identity for version/model, runtime for context/posture, or all for a full self-check when explicitly requested.\n- **runtime_snapshot**(): Lightweight (~150 token) flat snapshot of current runtime state: model, context budget, posture, disclosure level, turn count, cache hit rate, uptime, tool count, and pending approvals.\n- **schedule_create**(trigger_expression, instruction, execution_mode, max_retries, delivery_target) [capability_expand category: scheduler]: Create a new scheduled task. Specify a trigger expression (e.g. '30m', 'every 2h', '0 9 * * *') and the instruction to execute on each trigger. Optionally provide an execution mode and a delivery target to receive notifications.\n- **schedule_list**() [capability_expand category: scheduler]: List all scheduled tasks with their current state, trigger, and next due time.\n- **schedule_status**(task_id) [capability_expand category: scheduler]: Get detailed status and recent execution history for a scheduled task.\n- **schedule_pause**(task_id) [capability_expand category: scheduler]: Pause a scheduled task so it stops firing without being cancelled.\n- **schedule_resume**(task_id) [capability_expand category: scheduler]: Resume a paused scheduled task — re-arms it and recalculates next due time.\n- **schedule_cancel**(task_id) [capability_expand category: scheduler]: Cancel a scheduled task permanently.\n- **tool_search**(query, max_results): Search all registered tools by keyword relevance. Returns a ranked list of matching tools with name, category, and summary. Use this when you need a tool but are unsure of its exact name or category.\n- **tool_describe**(tool_name): Get the full callable schema of a single tool by exact name. Returns the tool's description, parameters, and metadata. Use after tool_search to inspect a specific tool before calling it.\n- **session_search**(query, limit) [capability_expand category: unclassified]: Search past conversation sessions in the current workspace for relevant context.\n- **session_list**(limit): List recent conversation sessions in the current workspace with stable ids, titles, dates, and summaries. Use for browsing past tasks or when user asks to see history without specific search terms. No keywords needed — returns chronological list.\n- **session_detail**(session_id, limit, offset, include_inactive) [capability_expand category: unclassified]: Read a paginated persisted transcript for one past conversation session in the current workspace. Use after session_list or session_search returns a session_id.\n- **click**(element_index) [capability_expand category: desktop]: Click a UI element by its element_index (from the latest observe_ui snapshot)\n- **get_clipboard**() [capability_expand category: desktop]: Read current clipboard text content\n- **list_apps**(filter, running_only) [capability_expand category: desktop]: List available applications on this system. Use to discover correct bundle_id before switch_app.\n- **list_windows**() [capability_expand category: desktop]: List all top-level windows with pid, window_id, title, and per-window state (minimized, on-screen). Call this first to pick the pid and window_id that observe_ui and other window tools require.\n- **observe_ui**(pid, window_id, query) [capability_expand category: desktop]: Snapshot one window's actionable UI elements, each tagged with an element_index for click/right_click/read_text. Re-observe after actions — indices belong to one snapshot. Requires the window's pid and window_id from list_windows.\n- **open_url**(url, app_id) [capability_expand category: desktop]: Open a URL in the default or specified browser\n- **read_text**(element_index) [capability_expand category: desktop]: Read the text content of a specific UI element from the latest snapshot\n- **right_click**(element_index) [capability_expand category: desktop]: Right-click a UI element to open its context menu. Returns visible menu items.\n- **screenshot**(pid, window_id) [capability_expand category: desktop]: Capture a screenshot for visual verification. With pid + window_id captures that window (works across all displays); defaults to the last observed window, or the full desktop when no window has been observed.\n- **scroll**(element_index, direction, amount, pid, window_id) [capability_expand category: desktop]: Scroll a scrollable area of a window. Omit element_index to scroll the window's focused/page scroller; pass one to scroll an exact element from the latest snapshot.\n- **select_text**(element_index) [capability_expand category: desktop]: Select all text in a UI element (focus + select-all, for subsequent copy)\n- **set_clipboard**(text) [capability_expand category: desktop]: Write text to the clipboard\n- **shortcut**(keys) [capability_expand category: desktop]: Execute a keyboard shortcut\n- **switch_app**(app_id) [capability_expand category: desktop]: Switch to an app (launch if needed, activate, verify)\n- **type_text**(text) [capability_expand category: desktop]: Type text into the currently focused element\n- **wait**(seconds) [capability_expand category: desktop]: Wait for a specified duration before continuing\n- **wait_until**(condition, pid, window_id, timeout, poll_interval) [capability_expand category: desktop]: Wait until a UI condition is met (polls UI tree). Returns elements when found or on timeout.\n- **wait_until_stable**(timeout, poll_interval, pid, window_id) [capability_expand category: desktop]: Wait until the UI stops changing (element set stabilizes across polls).\n\n## App Connector Capability Index\nLeapFlow can onboard and manage external apps through `platform_connect` and execute exact registered business actions through `platform_action`.\nFor requests about connecting, setting up, configuring, enabling, or managing a supported app, use `platform_connect` first instead of generating SDK/Webhook sample code.\n`platform_connect.action` is the management namespace: list, guide, preflight, connect, disconnect, remove, status, events_start, events_stop, events_status.\n`platform_action.action` is only for exact registered platform business actions listed below, such as `im.send_message`; never use management actions like `list` or `guide` there.\nAll business fields MUST go inside `payload`; top-level keys are only `platform`, `action`, and `payload`.\nDo not invent platform IDs or platform action names. If the needed action is not listed, ask for discovery/clarification instead of guessing.\nUse `platform_connect` with `action='guide'` and the matching `platform` to start onboarding; use `action='list'` when the app is unclear.\nWhen a pending onboarding state is present, continue from that state with `platform_connect` instead of asking the user to restate the app.\nSupported apps:\n- `api_server`: API Server (OpenAI Compatible) (category=api; backend=adapter; platform_action actions=none registered)\n- `dingtalk`: 钉钉 (DingTalk) (category=im; backend=adapter; platform_action actions=none registered)\n- `feishu`: 飞书 (Feishu/Lark) (category=im; backend=cli)\n - `calendar.create_event` payload={summary*, start_time*, end_time*, attendees} [write/high]\n - `docs.create_markdown` payload={title*, markdown*, folder_token} [write/medium]\n - `drive.search` payload={query*, limit} [read/medium]\n - `im.add_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.download_resource` payload={message_id*, file_key*, type*} [read/low]\n - `im.get_messages` payload={message_ids*} [read/low]\n - `im.list_chats` payload={page_size} [read/low]\n - `im.list_messages` payload={chat_id*} [read/low]\n - `im.list_thread_messages` payload={thread*} [read/low]\n - `im.remove_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.reply_message` payload={message_id*, text*} [send/high]\n - `im.search_chats` payload={query*, page_size} [read/low]\n - `im.search_messages` payload={query*} [read/medium]\n - `im.send_message` payload={chat_id*, text*, thread_id} [send/high]\n - `im.update_card` payload={token*, card*} [write/medium]\n - `im.update_message` payload={message_id*, text*} [write/medium]\n - `mail.search_unread` payload={query, limit} [read/high]\n - `sheets.append_row` payload={spreadsheet_token*, sheet_id*, values*} [write/medium]\n - `task.create` payload={title*, description, due_time} [write/medium]\n- `telegram`: Telegram (category=social; backend=adapter; platform_action actions=none registered)\n- `webhook`: Webhook (Generic) (category=webhook; backend=adapter; platform_action actions=none registered)\n\n## Tool Usage\nTools are normally invoked through the native function-calling mechanism, not by writing JSON in your reply\ntext. Only if the provider signals that native function calling is unavailable for this turn, fall back to a\nsingle JSON code block: `{\"name\": \"tool_name\", \"arguments\": {\"key\": \"value\"}}` — use this fallback format\nonly, never both. Only call a tool whose exact name appears in the tool index above and is currently callable\n(or reachable via `capability_expand`). Never invent, rename, alias, or guess a tool name, platform ID, or\nplatform action from argument shape or wording — if the index does not list it, it does not exist.\n`platform_connect.action` (list/guide/preflight/connect/disconnect/remove/status/events_start/events_stop/\nevents_status) is the App Connector management namespace; `platform_action.action` only accepts exact\nregistered business actions such as `im.send_message` shown in the App Connector Capability Index — never mix\nthe two namespaces. If a tool call returns an unknown/unavailable result, use the returned suggestions or\navailable names for a single retry instead of trying further variations of the same guess.\n\n**Side-effect action rule** (`platform_action` with effect=send/write/execute):\n- Call each unique action+payload **exactly once**. Never include duplicates in the same turn.\n- Once the result returns `\"completed\": true`, that action is DONE for this task. Do NOT call it again\n in any subsequent turn — immediately summarize the result for the user instead.\n- The system enforces idempotency: duplicate calls are blocked and will not execute.\n- If the user explicitly requests sending/writing multiple times, use distinct payloads per call.\n\n**Resource identifier provenance rule**:\n- NEVER fabricate, guess, or infer resource identifiers (chat_id, message_id, file_key, user_id, etc.).\n Every resource ID used in a side-effect action MUST come from a successful API response in this session.\n- If a read/list action fails (e.g. authorization error), you do NOT have valid resource IDs.\n Report the failure to the user — do NOT attempt the dependent write/send action with a guessed ID.\n- When a tool result contains `\"llm_instruction\"`, follow it exactly.\n\n## Guidelines\n1. **Direct answers first**: If you already know the answer, respond directly without tools.\n2. **Avoid redundant tool calls**: Do not call the same tool with the same arguments more than once in the same user turn. When an existing tool result already answers the user's request, stop calling tools and answer directly.\n3. **Use tools proactively**: When the user asks about files, time, system state, or needs actions performed, use the appropriate tool.\n4. **Chain tools when needed**: You can call multiple tools in sequence (e.g., list files → read file → summarize).\n5. **Handle failures gracefully**: If a tool fails, explain what went wrong and suggest alternatives. If it failed because of an unknown tool/platform/action name, retry once with an exact name from the returned suggestions, then explain rather than keep guessing.\n6. **Summarize results naturally**: After tool execution, synthesize the results into a helpful answer rather than dumping raw output.\n7. **Stay conversational**: Maintain a natural, helpful tone. Acknowledge context from earlier in the conversation.\n8. **Recall past work**:\n - Broad queries (\"之前做了什么\", \"列出任务\"): answer from the \"Recent Task History\" section already in your context. If insufficient, call session_list.\n - Specific lookups (\"上次那个配置怎么改的\"): call session_search with relevant phrases (NOT single characters).\n - Do NOT call search tools repeatedly with keyword fragments. One well-phrased call is better than ten fragmented ones.\n\n## Coding & Verification\nWhen working with code, prefer the precise built-in tools over ad-hoc shell: use `repo_map` to orient in an\nunfamiliar project, `code_search`/`file_find` to locate and `code_intel` for symbols, `edit_file` (anchored\nsearch-replace, or a unified `diff`) to change files — never rewrite a whole file to change a few lines — and\n`git_query` to inspect diffs/log. After edits, check `syntax_ok` in the result and run `test_run`/`lint_check`\nbefore declaring the work done. Batch independent read-only calls (search/read/query) into a single turn — they\nrun in parallel.\n\n## Presentation Style\n1. **Polished Markdown only**: Format user-facing answers with clean Markdown headings, short paragraphs, and concise bullets. Use tables only when they improve comparison or scanning.\n2. **Terminal-friendly layout**: Keep lines readable in a TUI; avoid dense walls of text, deeply nested lists, oversized ASCII art, or heavy visual blocks.\n3. **Elegant emphasis**: Use bold text sparingly for key terms and conclusions. Avoid excessive emojis, decorative symbols, repeated separators, or visual noise.\n4. **Theme-safe colors**: Do not emit ANSI escape codes, HTML color tags, Rich markup, or hardcoded color names. Rely on the TUI theme to render Markdown professionally.\n5. **No leaked tool protocol**: Never show tool-call JSON, internal schemas, raw observations, tool result payloads, or hidden reasoning in the final answer unless the user explicitly asks for raw/debug output. Treat any prior `{\"name\": ..., \"arguments\": ...}` blocks and `Tool result (...)` messages as internal execution context only.\n6. **Professional closure**: End with a concise conclusion or next step when helpful; avoid rambling after the useful answer is complete.\n\nWhen finished with all tool calls, respond normally without a JSON block, tool-call transcript, or process log.\n\n## Task Contract\n- Task ID: turn-2\n- Original user request: Second B turn.\n- Workspace root: \n- Allowed roots: \n- Treat relative project paths as relative to the workspace root; never infer `.` as the project root when a workspace root is provided.\n- Workspace boundary is enforced by tools: do not read, search, edit, or run commands against paths outside the allowed roots unless the user explicitly requests an external path and the tool/approval policy permits it.\n- LeapFlow workspace config is optional at `/.leapflow/config.yaml`; runtime config is loaded from `~/.leapflow/config/user.yaml` and `~/.leapflow/profiles//config/*.yaml`.\n- Preserve this task contract across summarization, compression, tool loops, and memory retrieval.\n" + }, + { + "role": "system", + "content": "\n## Recent Session Summary\n- [assistant] Workspace B acknowledged.\n- [user] Second B turn.\n" + }, + { + "role": "assistant", + "content": "Workspace B acknowledged." + }, + { + "role": "user", + "content": "Second B turn.\nSecond B turn." + } + ], + "tools": [ + "capability_expand", + "code_intel", + "code_search", + "config_get", + "config_list", + "env_info", + "file_find", + "file_list", + "file_read", + "git_query", + "lint_check", + "memory_search", + "plugin_list", + "plugin_propose", + "plugin_status", + "plugin_versions", + "repo_map", + "research_note", + "runtime_snapshot", + "schedule_reentry", + "self_describe", + "session_list", + "skill_view", + "skills_list", + "terminal_list", + "terminal_read", + "test_run", + "text_search", + "time_get", + "tool_describe", + "tool_search", + "web_fetch" + ] + }, + "responses": [ + { + "status": 200, + "content_type": "application/json", + "body": "{\"id\": \"chatcmpl-cassette\", \"object\": \"chat.completion\", \"model\": \"cassette-model\", \"choices\": [{\"index\": 0, \"message\": {\"role\": \"assistant\", \"content\": \"Still workspace B.\"}, \"finish_reason\": \"stop\"}], \"usage\": {\"prompt_tokens\": 64, \"completion_tokens\": 16, \"total_tokens\": 80}}" + } + ] +} diff --git a/tests/_fixtures/cassettes/r2_isolation/cassette-model-a6c1581f67fa97e9.cassette.json b/tests/_fixtures/cassettes/r2_isolation/cassette-model-a6c1581f67fa97e9.cassette.json new file mode 100644 index 00000000..ce3541d1 --- /dev/null +++ b/tests/_fixtures/cassettes/r2_isolation/cassette-model-a6c1581f67fa97e9.cassette.json @@ -0,0 +1,63 @@ +{ + "fingerprint": "a6c1581f67fa97e918855677be28c4cf40f0186873f5e11788acd6568f0974b8", + "note": "captured in seed mode", + "request": { + "model": "cassette-model", + "stream": false, + "messages": [ + { + "role": "system", + "content": "You are LeapFlow, an intelligent assistant that can both converse naturally and take real actions on the user's computer.\n\n## Capabilities\nThe tool index below lists **every** registered tool by name and a one-line summary — this is the complete\ncapability contract; nothing else exists. Only a subset is directly callable this turn (via native tool calling,\nnot a JSON block in your reply). If you need a tool from the index that is not yet callable, call\n`capability_expand` with its category name first — the matching tools become callable immediately after.\nWhen the user asks about LeapFlow **itself** — its identity/version/model, what tools, plugins, or capabilities\nit has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of\nself-awareness — answer from live runtime evidence via `self_describe` (facet=capabilities for \"what can you\ndo / do you support X\", identity for version/model, runtime for context/posture, or `all` for a full self-check),\nnever by reading LeapFlow's own source code, documentation, or memory. Report configuration-dependent or\nunavailable capabilities as limitations rather than inferring them.\n- **text_search**(text, pattern): Search for a regex pattern in text.\n- **text_replace**(text, old, new, count) [capability_expand category: write]: Replace occurrences of a substring in text.\n- **time_get**(): Get current date and time.\n- **env_info**(): Get system environment information (OS, Python version, cwd).\n- **skills_list**(query, category, source): List available learned skills. Use when user asks about capabilities or you need a specific skill.\n- **skill_view**(name): View the full content of a specific skill document.\n- **skill_list_files**(name) [capability_expand category: skills]: List all support files in a skill directory (configs, templates, examples). Use before skill_file_read to discover available files.\n- **skill_file_read**(name, file_path) [capability_expand category: skills]: Read an individual support file from a skill directory. Use skill_list_files first to discover available files.\n- **code_intel**(path, operation): Precise document symbols (outline) for a source file: classes, functions, and methods with line ranges. Python uses an exact AST parse; other languages use a keyword-prefix scan. Prefer over file_read mode=symbols for accurate navigation before editing. Read-only.\n- **repo_map**(path): Compact project orientation for a repository root: languages, detected test/lint commands, top-level structure, entry points, manifest, and VCS branch. Call this first when entering an unfamiliar codebase. Read-only.\n- **scm_sync**(action, cwd, remote, pull_ref, push_ref, timeout) [capability_expand category: scm]: Run a typed git SCM action. Use this instead of shell_run for git pull/push/status. For 'pull origin main then push', set action='pull_then_push', remote='origin', pull_ref='main', and omit push_ref so LeapFlow pushes the current local branch.\n- **git_query**(action, cwd, ref, path, staged, max_count, stat): Read-only structured git inspection: action=diff|log|status|branch|show. Prefer over shell_run for reading repo state — output is clipped, redacted, and log/branch are parsed into structured fields. Use scm_sync for pull/push.\n- **git_write**(action, cwd, message, stage_all, name, ref, create) [capability_expand category: scm]: Mutating git actions: action=commit (message, stage_all), branch (create+switch), checkout (switch; create=true for -b). Approval-gated. Use scm_sync for pull/push and git_query for reads.\n- **test_run**(command, cwd, timeout): Run the project's test suite and return structured results (framework, passed/failed counts, failing tests). Auto-detects the runner (pytest/npm/go/cargo) or uses a configured/explicit command; executes via the governed shell. ok=true means the runner executed — see 'success' for pass/fail.\n- **lint_check**(command, cwd, timeout): Run the project's linter and return a structured clean/issue result. Auto-detects the linter (ruff/eslint/go vet/clippy) or uses a configured/explicit command; executes via the governed shell. ok=true means the linter ran — see 'clean'.\n- **file_list**(path, pattern, depth): List files and directories at a given path. Use depth=1 or depth=2 to get a recursive tree in one call instead of listing each sub-directory separately.\n- **file_read**(path, max_lines, start_line, max_chars, mode): Read text file content with adaptive context governance. For large or unfamiliar files, prefer mode='outline' or mode='symbols' first, then use mode='raw' with start_line/max_lines for the specific range you actually need. For LeapFlow's own settings, use config_list / config_get / config_set — its config files are outside the workspace and not readable here.\n- **file_write**(path, content, mode) [capability_expand category: write]: Write content to a file (overwrite or append).\n- **code_search**(pattern, patterns, path, glob, ignore_case, multiline, max_results, context_lines): Search file CONTENTS by regex pattern across a directory tree (ripgrep-backed). Requires a regex pattern. NOT for listing or browsing directory contents — use file_list for that. Prefer this over shell_run grep: faster, skips VCS/dependency/build dirs, and returns structured path:line:column matches. Batch related lookups into ONE call via `patterns` (OR-combined, single pass) instead of issuing several separate searches. Use file_read for the surrounding context of a hit.\n- **file_find**(glob, path, max_results): Find files by a recursive glob pattern under a base path (e.g. '**/test_*.py' or '*.md'). Prefer this over shell_run find; skips VCS/dependency/build dirs.\n- **edit_file**(path, edits, dry_run, diff) [capability_expand category: write]: Apply targeted, anchored search-replace edits to an EXISTING text file (use file_write to create/overwrite). Each edit is {original_text, new_text, replace_all?}; original_text must match exactly and uniquely (or set replace_all) — a non-unique or missing anchor is rejected so files are never corrupted. Set dry_run to preview. Alternatively pass a unified 'diff' to apply its hunks as anchored edits. Far cheaper and safer than rewriting a whole file.\n- **shell_run**(command, cwd, timeout) [capability_expand category: shell]: Execute a one-shot shell command with timeout protection. Runs in the active workspace; paths resolving outside it are refused. Reach for a structured tool first when one fits — web_fetch for anything over HTTP(S), git_query/scm_sync for git, code_search/file_find/file_read for the repo, config_get/config_set for LeapFlow's own settings — because those report typed results, while a failed shell command can only be diagnosed from its exit code and stderr. Every shell run counts as an external side effect, so a failure stops the rest of the batch and is not retried automatically.\n- **terminal_open**(command, cwd, shell) [capability_expand category: terminal]: Open a PERSISTENT shell session (REPL/dev server/watch), returning a session_id for terminal_send/read/close. Disabled unless tools.terminal_session_enabled is set. For one-shot commands use shell_run instead.\n- **terminal_send**(session_id, input, wait) [capability_expand category: terminal]: Send a line of input to a persistent terminal session and return output captured shortly after.\n- **terminal_read**(session_id, wait): Drain buffered output from a persistent terminal session (optionally waiting briefly first).\n- **terminal_close**(session_id) [capability_expand category: terminal]: Terminate a persistent terminal session and release its process group.\n- **terminal_list**(): List active persistent terminal sessions.\n- **config_list**(category, limit): List LeapFlow's own writable settings (LLM model and endpoint, daemon, memory, perception, gateway, …) with current values. Use this to discover the exact key before changing anything. Optionally narrow by `category`. This is the only correct way to inspect LeapFlow configuration — never read config files from disk.\n- **config_get**(key): Read one LeapFlow setting by key (e.g. 'llm.model', 'llm.base_url', 'daemon.log_level'), returning its current value, type, scopes, and whether a change needs a daemon restart. There is no 'llm.provider' key: provider behavior is inferred from the OpenAI-compatible 'llm.base_url'. Never read LeapFlow config files from disk — use this.\n- **config_set**(key, value, scope) [capability_expand category: config]: Change one LeapFlow setting by key, e.g. switch an OpenAI-compatible LLM with key='llm.model' and key='llm.base_url'. There is no 'llm.provider' key: provider behavior is inferred from the endpoint. Values are validated and coerced; credentials are stored in the vault automatically. Call config_list or config_get first if unsure of the key. The result states whether a `leap daemon restart` is required. Never edit LeapFlow config files directly.\n- **web_fetch**(url, select, timeout, max_bytes): Read a URL over HTTP(S) and get back extracted, context-sized content: parsed JSON for API endpoints, readable text plus links for web pages. Use this for anything on the internet — prices, docs, releases, articles — instead of running curl through shell_run: it reports real HTTP status codes, retries rate limits on its own, and is a plain read so a retry is always safe. For JSON APIs pass `select` with a dotted path (e.g. 'chart.result.0.meta') to return just that part instead of the whole payload.\n- **memory_search**(query, limit): Search agent memory for relevant past experiences, observations, and facts.\n- **memory_add**(content, kind) [capability_expand category: write]: Store a new observation or insight in memory for future reference.\n- **research_note**(kind, text): Record a compact, structured note about the current task's state so it survives context compression on long / multi-step tasks. Use for durable findings, open questions still to resolve, decisions / excluded paths, and the immediate next step. One concise sentence per note.\n- **capability_expand**(category): Fetch the full callable schema for every tool in a capability category (e.g. 'hub', 'gateway', 'desktop', 'delegate', 'file', 'memory', 'skill'). The compact tool index always lists every registered tool by name and a one-line summary, but only a static low-risk subset is directly callable each turn. If you need a tool from the index that is not yet callable, call capability_expand with its category first; the matching tools become callable in this turn. Never invent a tool name — expand the category instead.\n- **delegate_task**(goal, context) [capability_expand category: delegate]: Delegate a complex sub-task to an isolated subagent. The subagent gets a fresh context and restricted tool access. Use when a task is self-contained and can be solved independently.\n- **schedule_reentry**(kind, reason, delay_seconds, event_match, max_reentries, deadline_seconds): Register a re-entry so this task can resume later from its current orientation (findings / open questions / next step). Use when work must pause and continue after a delay (kind=time) or when a matching platform event arrives (kind=event), instead of finishing now. The research-ledger state is carried over automatically.\n- **hub_push**(skill_name, visibility, version) [capability_expand category: hub]: Push a local skill to the ModelScope Hub for sharing or backup.\n- **hub_pull**(repo_id, version) [capability_expand category: hub]: Pull a skill from the ModelScope Hub to install locally.\n- **hub_search**(query) [capability_expand category: hub]: Search for skills on the Hub by keyword or description.\n- **hub_sync**(mode, dry_run) [capability_expand category: hub]: Preview or execute sync between local skills and Hub.\n- **platform_action**(platform, action, payload, backend_kind) [capability_expand category: gateway]: Execute an exact registered business action on an external platform through LeapFlow's App Connector layer. Actions must be copied from the App Connector Capability Index and are addressed as domain.operation, e.g. im.send_message or docs.create_markdown. All business fields (chat_id, text, query, etc.) MUST be placed inside `payload`, never at the top level. Example: {\"platform\":\"feishu\",\"action\":\"im.send_message\",\"payload\":{\"chat_id\":\"oc_xxx\",\"text\":\"hello\"}}. Do not invent action names, do not use management actions such as list/guide/connect/status here.\n- **platform_connect**(action, platform, credentials, options, checkpoint) [capability_expand category: gateway]: List, guide, connect, disconnect, remove, or check status for external platforms using the App Connector management namespace. Supports REST and CLI backends. Use this for management actions such as list/guide/preflight/connect/status; use platform_action only for exact registered business actions.\n- **gateway_send**(platform, chat_id, text, thread_id) [capability_expand category: gateway]: Send a message to a connected external platform (Feishu group, Telegram chat, DingTalk conversation, etc.). Requires the platform to be connected via gateway_connect first. Use gateway_connect with action='list' to see connected platforms and available chat IDs.\n- **gateway_connect**(action, platform, credentials, options) [capability_expand category: gateway]: Connect, configure, or manage external platform integrations (Feishu, DingTalk, Telegram, Slack, Discord, etc.). Conversational flow: 1) call 'guide' to get setup steps + required fields, 2) present the steps to the user and ask for ALL required credentials in a single message, 3) call 'connect' with the credentials. Goal: complete in 1–2 user turns. NEVER include credential values in your text response.\n- **plugin_list**(): List the live plugin registry across Tool/Gateway/LLM subsystems for plugin management: which plugins are loaded, their category and fiber lifecycle state. For questions about LeapFlow's own capabilities or whether it supports X, use self_describe instead.\n- **plugin_status**(plugin_id): Get detailed status of a specific plugin: its declared category, runtime dependencies, contributed tools, and fiber lifecycle state.\n- **plugin_versions**(plugin_id): List recorded source versions and active pointer for a profile-scoped plugin.\n- **plugin_propose**(requested_capability, plugin_id, proposed_tools, test_cases, risk_level, evidence): Create a side-effect-free PluginProposal from explicit capability-gap evidence. Use this before plugin_generate when a missing capability should be reviewed. Does not call an LLM, write files, or install anything.\n- **assess_compatibility**(manifest, source_path) [capability_expand category: plugin_management]: Assess whether a foreign plugin manifest or real DSH source bundle is compatible with LeapFlow. A source bundle assessment is static: runtime_ready stays false until restricted Node discovery during plugin_install. Returns component-level verdicts and limitations.\n- **plugin_generate**(plugin_id, description, proposal_id) [capability_expand category: system]: Generate a new ToolPlugin from a natural-language capability description. The LLM produces code that conforms to the ToolPlugin Protocol; it is then rigorously validated (syntax, structure, import, protocol conformance). The generated source is stored in CAS and receives explicit content approval. It DOES NOT install the plugin — installation requires a second, mutation-specific approval via plugin_install.\n- **plugin_install**(plugin_id, code, marketplace_name, source_path, proposal_id, version_label) [capability_expand category: system]: Install a plugin from exactly one source: validated Python code, the configured Python marketplace, or a real DSH source bundle. DSH bundles run restricted Node discovery before registration; only public host tools are installed and client UI limitations are reported. Writes profile-scoped state and mutates the process-global registry. REQUIRES APPROVAL.\n- **plugin_rollback**(plugin_id, version) [capability_expand category: system]: Rollback a profile-scoped plugin to a recorded source version and reload it. REQUIRES APPROVAL.\n- **plugin_reload**(plugin_id, version_label) [capability_expand category: system]: Hot-reload a plugin at runtime. Disposes the old plugin fiber, re-imports its module, and registers a fresh instance. Existing in-flight turns are unaffected (snapshot isolation). REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_disable**(plugin_id) [capability_expand category: system]: Disable a plugin by disposing its fiber, removing its tools from the runtime registry. Cannot disable self_management itself. REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_remove**(plugin_id, delete_source) [capability_expand category: system]: Terminally remove a plugin: dispose its fiber, unregister its tools, remove reload metadata, and optionally delete its profile-scoped source file. Cannot remove self_management itself. REQUIRES APPROVAL.\n- **plugin_unquarantine**(plugin_id) [capability_expand category: plugin_management]: Restore a quarantined plugin to probation status for re-evaluation. Unfreezes the trust ledger, transitions the proposal back to PROBATION, and reloads the plugin. REQUIRES APPROVAL.\n- **plugin_enable**(plugin_id) [capability_expand category: system]: Re-enable a previously disabled plugin by reloading its module and registering a fresh instance. REQUIRES APPROVAL.\n- **self_describe**(facet): Answer questions about LeapFlow ITSELF from live runtime evidence — its identity/version/model, what tools and plugins it has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of self-awareness. Prefer this over reading LeapFlow's own source code. Pick facet=capabilities for 'what can you do / do you support X', identity for version/model, runtime for context/posture, or all for a full self-check when explicitly requested.\n- **runtime_snapshot**(): Lightweight (~150 token) flat snapshot of current runtime state: model, context budget, posture, disclosure level, turn count, cache hit rate, uptime, tool count, and pending approvals.\n- **schedule_create**(trigger_expression, instruction, execution_mode, max_retries, delivery_target) [capability_expand category: scheduler]: Create a new scheduled task. Specify a trigger expression (e.g. '30m', 'every 2h', '0 9 * * *') and the instruction to execute on each trigger. Optionally provide an execution mode and a delivery target to receive notifications.\n- **schedule_list**() [capability_expand category: scheduler]: List all scheduled tasks with their current state, trigger, and next due time.\n- **schedule_status**(task_id) [capability_expand category: scheduler]: Get detailed status and recent execution history for a scheduled task.\n- **schedule_pause**(task_id) [capability_expand category: scheduler]: Pause a scheduled task so it stops firing without being cancelled.\n- **schedule_resume**(task_id) [capability_expand category: scheduler]: Resume a paused scheduled task — re-arms it and recalculates next due time.\n- **schedule_cancel**(task_id) [capability_expand category: scheduler]: Cancel a scheduled task permanently.\n- **tool_search**(query, max_results): Search all registered tools by keyword relevance. Returns a ranked list of matching tools with name, category, and summary. Use this when you need a tool but are unsure of its exact name or category.\n- **tool_describe**(tool_name): Get the full callable schema of a single tool by exact name. Returns the tool's description, parameters, and metadata. Use after tool_search to inspect a specific tool before calling it.\n- **session_search**(query, limit) [capability_expand category: unclassified]: Search past conversation sessions in the current workspace for relevant context.\n- **session_list**(limit): List recent conversation sessions in the current workspace with stable ids, titles, dates, and summaries. Use for browsing past tasks or when user asks to see history without specific search terms. No keywords needed — returns chronological list.\n- **session_detail**(session_id, limit, offset, include_inactive) [capability_expand category: unclassified]: Read a paginated persisted transcript for one past conversation session in the current workspace. Use after session_list or session_search returns a session_id.\n- **click**(element_index) [capability_expand category: desktop]: Click a UI element by its element_index (from the latest observe_ui snapshot)\n- **get_clipboard**() [capability_expand category: desktop]: Read current clipboard text content\n- **list_apps**(filter, running_only) [capability_expand category: desktop]: List available applications on this system. Use to discover correct bundle_id before switch_app.\n- **list_windows**() [capability_expand category: desktop]: List all top-level windows with pid, window_id, title, and per-window state (minimized, on-screen). Call this first to pick the pid and window_id that observe_ui and other window tools require.\n- **observe_ui**(pid, window_id, query) [capability_expand category: desktop]: Snapshot one window's actionable UI elements, each tagged with an element_index for click/right_click/read_text. Re-observe after actions — indices belong to one snapshot. Requires the window's pid and window_id from list_windows.\n- **open_url**(url, app_id) [capability_expand category: desktop]: Open a URL in the default or specified browser\n- **read_text**(element_index) [capability_expand category: desktop]: Read the text content of a specific UI element from the latest snapshot\n- **right_click**(element_index) [capability_expand category: desktop]: Right-click a UI element to open its context menu. Returns visible menu items.\n- **screenshot**(pid, window_id) [capability_expand category: desktop]: Capture a screenshot for visual verification. With pid + window_id captures that window (works across all displays); defaults to the last observed window, or the full desktop when no window has been observed.\n- **scroll**(element_index, direction, amount, pid, window_id) [capability_expand category: desktop]: Scroll a scrollable area of a window. Omit element_index to scroll the window's focused/page scroller; pass one to scroll an exact element from the latest snapshot.\n- **select_text**(element_index) [capability_expand category: desktop]: Select all text in a UI element (focus + select-all, for subsequent copy)\n- **set_clipboard**(text) [capability_expand category: desktop]: Write text to the clipboard\n- **shortcut**(keys) [capability_expand category: desktop]: Execute a keyboard shortcut\n- **switch_app**(app_id) [capability_expand category: desktop]: Switch to an app (launch if needed, activate, verify)\n- **type_text**(text) [capability_expand category: desktop]: Type text into the currently focused element\n- **wait**(seconds) [capability_expand category: desktop]: Wait for a specified duration before continuing\n- **wait_until**(condition, pid, window_id, timeout, poll_interval) [capability_expand category: desktop]: Wait until a UI condition is met (polls UI tree). Returns elements when found or on timeout.\n- **wait_until_stable**(timeout, poll_interval, pid, window_id) [capability_expand category: desktop]: Wait until the UI stops changing (element set stabilizes across polls).\n\n## App Connector Capability Index\nLeapFlow can onboard and manage external apps through `platform_connect` and execute exact registered business actions through `platform_action`.\nFor requests about connecting, setting up, configuring, enabling, or managing a supported app, use `platform_connect` first instead of generating SDK/Webhook sample code.\n`platform_connect.action` is the management namespace: list, guide, preflight, connect, disconnect, remove, status, events_start, events_stop, events_status.\n`platform_action.action` is only for exact registered platform business actions listed below, such as `im.send_message`; never use management actions like `list` or `guide` there.\nAll business fields MUST go inside `payload`; top-level keys are only `platform`, `action`, and `payload`.\nDo not invent platform IDs or platform action names. If the needed action is not listed, ask for discovery/clarification instead of guessing.\nUse `platform_connect` with `action='guide'` and the matching `platform` to start onboarding; use `action='list'` when the app is unclear.\nWhen a pending onboarding state is present, continue from that state with `platform_connect` instead of asking the user to restate the app.\nSupported apps:\n- `api_server`: API Server (OpenAI Compatible) (category=api; backend=adapter; platform_action actions=none registered)\n- `dingtalk`: 钉钉 (DingTalk) (category=im; backend=adapter; platform_action actions=none registered)\n- `feishu`: 飞书 (Feishu/Lark) (category=im; backend=cli)\n - `calendar.create_event` payload={summary*, start_time*, end_time*, attendees} [write/high]\n - `docs.create_markdown` payload={title*, markdown*, folder_token} [write/medium]\n - `drive.search` payload={query*, limit} [read/medium]\n - `im.add_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.download_resource` payload={message_id*, file_key*, type*} [read/low]\n - `im.get_messages` payload={message_ids*} [read/low]\n - `im.list_chats` payload={page_size} [read/low]\n - `im.list_messages` payload={chat_id*} [read/low]\n - `im.list_thread_messages` payload={thread*} [read/low]\n - `im.remove_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.reply_message` payload={message_id*, text*} [send/high]\n - `im.search_chats` payload={query*, page_size} [read/low]\n - `im.search_messages` payload={query*} [read/medium]\n - `im.send_message` payload={chat_id*, text*, thread_id} [send/high]\n - `im.update_card` payload={token*, card*} [write/medium]\n - `im.update_message` payload={message_id*, text*} [write/medium]\n - `mail.search_unread` payload={query, limit} [read/high]\n - `sheets.append_row` payload={spreadsheet_token*, sheet_id*, values*} [write/medium]\n - `task.create` payload={title*, description, due_time} [write/medium]\n- `telegram`: Telegram (category=social; backend=adapter; platform_action actions=none registered)\n- `webhook`: Webhook (Generic) (category=webhook; backend=adapter; platform_action actions=none registered)\n\n## Tool Usage\nTools are normally invoked through the native function-calling mechanism, not by writing JSON in your reply\ntext. Only if the provider signals that native function calling is unavailable for this turn, fall back to a\nsingle JSON code block: `{\"name\": \"tool_name\", \"arguments\": {\"key\": \"value\"}}` — use this fallback format\nonly, never both. Only call a tool whose exact name appears in the tool index above and is currently callable\n(or reachable via `capability_expand`). Never invent, rename, alias, or guess a tool name, platform ID, or\nplatform action from argument shape or wording — if the index does not list it, it does not exist.\n`platform_connect.action` (list/guide/preflight/connect/disconnect/remove/status/events_start/events_stop/\nevents_status) is the App Connector management namespace; `platform_action.action` only accepts exact\nregistered business actions such as `im.send_message` shown in the App Connector Capability Index — never mix\nthe two namespaces. If a tool call returns an unknown/unavailable result, use the returned suggestions or\navailable names for a single retry instead of trying further variations of the same guess.\n\n**Side-effect action rule** (`platform_action` with effect=send/write/execute):\n- Call each unique action+payload **exactly once**. Never include duplicates in the same turn.\n- Once the result returns `\"completed\": true`, that action is DONE for this task. Do NOT call it again\n in any subsequent turn — immediately summarize the result for the user instead.\n- The system enforces idempotency: duplicate calls are blocked and will not execute.\n- If the user explicitly requests sending/writing multiple times, use distinct payloads per call.\n\n**Resource identifier provenance rule**:\n- NEVER fabricate, guess, or infer resource identifiers (chat_id, message_id, file_key, user_id, etc.).\n Every resource ID used in a side-effect action MUST come from a successful API response in this session.\n- If a read/list action fails (e.g. authorization error), you do NOT have valid resource IDs.\n Report the failure to the user — do NOT attempt the dependent write/send action with a guessed ID.\n- When a tool result contains `\"llm_instruction\"`, follow it exactly.\n\n## Guidelines\n1. **Direct answers first**: If you already know the answer, respond directly without tools.\n2. **Avoid redundant tool calls**: Do not call the same tool with the same arguments more than once in the same user turn. When an existing tool result already answers the user's request, stop calling tools and answer directly.\n3. **Use tools proactively**: When the user asks about files, time, system state, or needs actions performed, use the appropriate tool.\n4. **Chain tools when needed**: You can call multiple tools in sequence (e.g., list files → read file → summarize).\n5. **Handle failures gracefully**: If a tool fails, explain what went wrong and suggest alternatives. If it failed because of an unknown tool/platform/action name, retry once with an exact name from the returned suggestions, then explain rather than keep guessing.\n6. **Summarize results naturally**: After tool execution, synthesize the results into a helpful answer rather than dumping raw output.\n7. **Stay conversational**: Maintain a natural, helpful tone. Acknowledge context from earlier in the conversation.\n8. **Recall past work**:\n - Broad queries (\"之前做了什么\", \"列出任务\"): answer from the \"Recent Task History\" section already in your context. If insufficient, call session_list.\n - Specific lookups (\"上次那个配置怎么改的\"): call session_search with relevant phrases (NOT single characters).\n - Do NOT call search tools repeatedly with keyword fragments. One well-phrased call is better than ten fragmented ones.\n\n## Coding & Verification\nWhen working with code, prefer the precise built-in tools over ad-hoc shell: use `repo_map` to orient in an\nunfamiliar project, `code_search`/`file_find` to locate and `code_intel` for symbols, `edit_file` (anchored\nsearch-replace, or a unified `diff`) to change files — never rewrite a whole file to change a few lines — and\n`git_query` to inspect diffs/log. After edits, check `syntax_ok` in the result and run `test_run`/`lint_check`\nbefore declaring the work done. Batch independent read-only calls (search/read/query) into a single turn — they\nrun in parallel.\n\n## Presentation Style\n1. **Polished Markdown only**: Format user-facing answers with clean Markdown headings, short paragraphs, and concise bullets. Use tables only when they improve comparison or scanning.\n2. **Terminal-friendly layout**: Keep lines readable in a TUI; avoid dense walls of text, deeply nested lists, oversized ASCII art, or heavy visual blocks.\n3. **Elegant emphasis**: Use bold text sparingly for key terms and conclusions. Avoid excessive emojis, decorative symbols, repeated separators, or visual noise.\n4. **Theme-safe colors**: Do not emit ANSI escape codes, HTML color tags, Rich markup, or hardcoded color names. Rely on the TUI theme to render Markdown professionally.\n5. **No leaked tool protocol**: Never show tool-call JSON, internal schemas, raw observations, tool result payloads, or hidden reasoning in the final answer unless the user explicitly asks for raw/debug output. Treat any prior `{\"name\": ..., \"arguments\": ...}` blocks and `Tool result (...)` messages as internal execution context only.\n6. **Professional closure**: End with a concise conclusion or next step when helpful; avoid rambling after the useful answer is complete.\n\nWhen finished with all tool calls, respond normally without a JSON block, tool-call transcript, or process log.\n\n## Task Contract\n- Task ID: turn-1\n- Original user request: Hello from B.\n- Workspace root: \n- Allowed roots: \n- Treat relative project paths as relative to the workspace root; never infer `.` as the project root when a workspace root is provided.\n- Workspace boundary is enforced by tools: do not read, search, edit, or run commands against paths outside the allowed roots unless the user explicitly requests an external path and the tool/approval policy permits it.\n- LeapFlow workspace config is optional at `/.leapflow/config.yaml`; runtime config is loaded from `~/.leapflow/config/user.yaml` and `~/.leapflow/profiles//config/*.yaml`.\n- Preserve this task contract across summarization, compression, tool loops, and memory retrieval.\n" + }, + { + "role": "system", + "content": "\n## Recent Session Summary\n- [user] Hello from B.\n" + }, + { + "role": "user", + "content": "Hello from B.\nHello from B." + } + ], + "tools": [ + "capability_expand", + "code_intel", + "code_search", + "config_get", + "config_list", + "env_info", + "file_find", + "file_list", + "file_read", + "git_query", + "lint_check", + "memory_search", + "plugin_list", + "plugin_propose", + "plugin_status", + "plugin_versions", + "repo_map", + "research_note", + "runtime_snapshot", + "schedule_reentry", + "self_describe", + "session_list", + "skill_view", + "skills_list", + "terminal_list", + "terminal_read", + "test_run", + "text_search", + "time_get", + "tool_describe", + "tool_search", + "web_fetch" + ] + }, + "responses": [ + { + "status": 200, + "content_type": "application/json", + "body": "{\"id\": \"chatcmpl-cassette\", \"object\": \"chat.completion\", \"model\": \"cassette-model\", \"choices\": [{\"index\": 0, \"message\": {\"role\": \"assistant\", \"content\": \"Workspace B acknowledged.\"}, \"finish_reason\": \"stop\"}], \"usage\": {\"prompt_tokens\": 64, \"completion_tokens\": 16, \"total_tokens\": 80}}" + } + ] +} diff --git a/tests/_fixtures/cassettes/r2_isolation/cassette-model-cc8c5e1f035ddae3.cassette.json b/tests/_fixtures/cassettes/r2_isolation/cassette-model-cc8c5e1f035ddae3.cassette.json deleted file mode 100644 index 8d33e576..00000000 --- a/tests/_fixtures/cassettes/r2_isolation/cassette-model-cc8c5e1f035ddae3.cassette.json +++ /dev/null @@ -1,67 +0,0 @@ -{ - "fingerprint": "cc8c5e1f035ddae34cbbd13f6bcabd490abda40dda3cbc93c3908d5e8b583526", - "note": "captured in seed mode", - "request": { - "model": "cassette-model", - "stream": false, - "messages": [ - { - "role": "system", - "content": "You are LeapFlow, an intelligent assistant that can both converse naturally and take real actions on the user's computer.\n\n## Capabilities\nThe tool index below lists **every** registered tool by name and a one-line summary — this is the complete\ncapability contract; nothing else exists. Only a subset is directly callable this turn (via native tool calling,\nnot a JSON block in your reply). If you need a tool from the index that is not yet callable, call\n`capability_expand` with its category name first — the matching tools become callable immediately after.\nWhen the user asks what LeapFlow itself supports, whether it supports plugins, or which runtime capabilities\nare available, use the live capability evidence exposed by `plugin_list` before making capability claims; report\nconfiguration-dependent or unavailable capabilities as limitations instead of inferring from documentation.\n- **text_search**(text, pattern): Search for a regex pattern in text.\n- **text_replace**(text, old, new, count) [capability_expand category: write]: Replace occurrences of a substring in text.\n- **time_get**(): Get current date and time.\n- **env_info**(): Get system environment information (OS, Python version, cwd).\n- **skills_list**(query, category, source): List available learned skills. Use when user asks about capabilities or you need a specific skill.\n- **skill_view**(name): View the full content of a specific skill document.\n- **code_intel**(path, operation): Precise document symbols (outline) for a source file: classes, functions, and methods with line ranges. Python uses an exact AST parse; other languages use a keyword-prefix scan. Prefer over file_read mode=symbols for accurate navigation before editing. Read-only.\n- **repo_map**(path): Compact project orientation for a repository root: languages, detected test/lint commands, top-level structure, entry points, manifest, and VCS branch. Call this first when entering an unfamiliar codebase. Read-only.\n- **scm_sync**(action, cwd, remote, pull_ref, push_ref, timeout) [capability_expand category: scm]: Run a typed git SCM action. Use this instead of shell_run for git pull/push/status. For 'pull origin main then push', set action='pull_then_push', remote='origin', pull_ref='main', and omit push_ref so LeapFlow pushes the current local branch.\n- **git_query**(action, cwd, ref, path, staged, max_count, stat): Read-only structured git inspection: action=diff|log|status|branch|show. Prefer over shell_run for reading repo state — output is clipped, redacted, and log/branch are parsed into structured fields. Use scm_sync for pull/push.\n- **git_write**(action, cwd, message, stage_all, name, ref, create) [capability_expand category: scm]: Mutating git actions: action=commit (message, stage_all), branch (create+switch), checkout (switch; create=true for -b). Approval-gated. Use scm_sync for pull/push and git_query for reads.\n- **test_run**(command, cwd, timeout): Run the project's test suite and return structured results (framework, passed/failed counts, failing tests). Auto-detects the runner (pytest/npm/go/cargo) or uses a configured/explicit command; executes via the governed shell. ok=true means the runner executed — see 'success' for pass/fail.\n- **lint_check**(command, cwd, timeout): Run the project's linter and return a structured clean/issue result. Auto-detects the linter (ruff/eslint/go vet/clippy) or uses a configured/explicit command; executes via the governed shell. ok=true means the linter ran — see 'clean'.\n- **file_list**(path, pattern, depth): List files and directories at a given path. Use depth=1 or depth=2 to get a recursive tree in one call instead of listing each sub-directory separately.\n- **file_read**(path, max_lines, start_line, max_chars, mode): Read text file content with adaptive context governance. For large or unfamiliar files, prefer mode='outline' or mode='symbols' first, then use mode='raw' with start_line/max_lines for the specific range you actually need. For LeapFlow's own settings, use config_list / config_get / config_set — its config files are outside the workspace and not readable here.\n- **file_write**(path, content, mode) [capability_expand category: write]: Write content to a file (overwrite or append).\n- **code_search**(pattern, patterns, path, glob, ignore_case, multiline, max_results, context_lines): Search file CONTENTS by regex pattern across a directory tree (ripgrep-backed). Requires a regex pattern. NOT for listing or browsing directory contents — use file_list for that. Prefer this over shell_run grep: faster, skips VCS/dependency/build dirs, and returns structured path:line:column matches. Batch related lookups into ONE call via `patterns` (OR-combined, single pass) instead of issuing several separate searches. Use file_read for the surrounding context of a hit.\n- **file_find**(glob, path, max_results): Find files by a recursive glob pattern under a base path (e.g. '**/test_*.py' or '*.md'). Prefer this over shell_run find; skips VCS/dependency/build dirs.\n- **edit_file**(path, edits, dry_run, diff) [capability_expand category: write]: Apply targeted, anchored search-replace edits to an EXISTING text file (use file_write to create/overwrite). Each edit is {original_text, new_text, replace_all?}; original_text must match exactly and uniquely (or set replace_all) — a non-unique or missing anchor is rejected so files are never corrupted. Set dry_run to preview. Alternatively pass a unified 'diff' to apply its hunks as anchored edits. Far cheaper and safer than rewriting a whole file.\n- **shell_run**(command, cwd, timeout) [capability_expand category: shell]: Execute a one-shot shell command with timeout protection. Runs in the active workspace; paths resolving outside it are refused. Reach for a structured tool first when one fits — web_fetch for anything over HTTP(S), git_query/scm_sync for git, code_search/file_find/file_read for the repo, config_get/config_set for LeapFlow's own settings — because those report typed results, while a failed shell command can only be diagnosed from its exit code and stderr. Every shell run counts as an external side effect, so a failure stops the rest of the batch and is not retried automatically.\n- **terminal_open**(command, cwd, shell) [capability_expand category: terminal]: Open a PERSISTENT shell session (REPL/dev server/watch), returning a session_id for terminal_send/read/close. Disabled unless tools.terminal_session_enabled is set. For one-shot commands use shell_run instead.\n- **terminal_send**(session_id, input, wait) [capability_expand category: terminal]: Send a line of input to a persistent terminal session and return output captured shortly after.\n- **terminal_read**(session_id, wait): Drain buffered output from a persistent terminal session (optionally waiting briefly first).\n- **terminal_close**(session_id) [capability_expand category: terminal]: Terminate a persistent terminal session and release its process group.\n- **terminal_list**(): List active persistent terminal sessions.\n- **config_list**(category, limit): List LeapFlow's own writable settings (LLM model and endpoint, daemon, memory, perception, gateway, …) with current values. Use this to discover the exact key before changing anything. Optionally narrow by `category`. This is the only correct way to inspect LeapFlow configuration — never read config files from disk.\n- **config_get**(key): Read one LeapFlow setting by key (e.g. 'llm.model', 'llm.base_url', 'daemon.log_level'), returning its current value, type, scopes, and whether a change needs a daemon restart. There is no 'llm.provider' key: provider behavior is inferred from the OpenAI-compatible 'llm.base_url'. Never read LeapFlow config files from disk — use this.\n- **config_set**(key, value, scope) [capability_expand category: config]: Change one LeapFlow setting by key, e.g. switch an OpenAI-compatible LLM with key='llm.model' and key='llm.base_url'. There is no 'llm.provider' key: provider behavior is inferred from the endpoint. Values are validated and coerced; credentials are stored in the vault automatically. Call config_list or config_get first if unsure of the key. The result states whether a `leap daemon restart` is required. Never edit LeapFlow config files directly.\n- **web_fetch**(url, select, timeout, max_bytes): Read a URL over HTTP(S) and get back extracted, context-sized content: parsed JSON for API endpoints, readable text plus links for web pages. Use this for anything on the internet — prices, docs, releases, articles — instead of running curl through shell_run: it reports real HTTP status codes, retries rate limits on its own, and is a plain read so a retry is always safe. For JSON APIs pass `select` with a dotted path (e.g. 'chart.result.0.meta') to return just that part instead of the whole payload.\n- **memory_search**(query, limit): Search agent memory for relevant past experiences, observations, and facts.\n- **memory_add**(content, kind) [capability_expand category: write]: Store a new observation or insight in memory for future reference.\n- **research_note**(kind, text): Record a compact, structured note about the current task's state so it survives context compression on long / multi-step tasks. Use for durable findings, open questions still to resolve, decisions / excluded paths, and the immediate next step. One concise sentence per note.\n- **capability_expand**(category): Fetch the full callable schema for every tool in a capability category (e.g. 'hub', 'gateway', 'desktop', 'delegate', 'file', 'memory', 'skill'). The compact tool index always lists every registered tool by name and a one-line summary, but only a static low-risk subset is directly callable each turn. If you need a tool from the index that is not yet callable, call capability_expand with its category first; the matching tools become callable in this turn. Never invent a tool name — expand the category instead.\n- **delegate_task**(goal, context) [capability_expand category: delegate]: Delegate a complex sub-task to an isolated subagent. The subagent gets a fresh context and restricted tool access. Use when a task is self-contained and can be solved independently.\n- **schedule_reentry**(kind, reason, delay_seconds, event_match, max_reentries, deadline_seconds): Register a re-entry so this task can resume later from its current orientation (findings / open questions / next step). Use when work must pause and continue after a delay (kind=time) or when a matching platform event arrives (kind=event), instead of finishing now. The research-ledger state is carried over automatically.\n- **hub_push**(skill_name, visibility, version) [capability_expand category: hub]: Push a local skill to the ModelScope Hub for sharing or backup.\n- **hub_pull**(repo_id, version) [capability_expand category: hub]: Pull a skill from the ModelScope Hub to install locally.\n- **hub_search**(query) [capability_expand category: hub]: Search for skills on the Hub by keyword or description.\n- **hub_sync**(mode, dry_run) [capability_expand category: hub]: Preview or execute sync between local skills and Hub.\n- **platform_action**(platform, action, payload, backend_kind) [capability_expand category: gateway]: Execute an exact registered business action on an external platform through LeapFlow's App Connector layer. Actions must be copied from the App Connector Capability Index and are addressed as domain.operation, e.g. im.send_message or docs.create_markdown. All business fields (chat_id, text, query, etc.) MUST be placed inside `payload`, never at the top level. Example: {\"platform\":\"feishu\",\"action\":\"im.send_message\",\"payload\":{\"chat_id\":\"oc_xxx\",\"text\":\"hello\"}}. Do not invent action names, do not use management actions such as list/guide/connect/status here.\n- **platform_connect**(action, platform, credentials, options, checkpoint) [capability_expand category: gateway]: List, guide, connect, disconnect, remove, or check status for external platforms using the App Connector management namespace. Supports REST and CLI backends. Use this for management actions such as list/guide/preflight/connect/status; use platform_action only for exact registered business actions.\n- **gateway_send**(platform, chat_id, text, thread_id) [capability_expand category: gateway]: Send a message to a connected external platform (Feishu group, Telegram chat, DingTalk conversation, etc.). Requires the platform to be connected via gateway_connect first. Use gateway_connect with action='list' to see connected platforms and available chat IDs.\n- **gateway_connect**(action, platform, credentials, options) [capability_expand category: gateway]: Connect, configure, or manage external platform integrations (Feishu, DingTalk, Telegram, Slack, Discord, etc.). Conversational flow: 1) call 'guide' to get setup steps + required fields, 2) present the steps to the user and ask for ALL required credentials in a single message, 3) call 'connect' with the credentials. Goal: complete in 1–2 user turns. NEVER include credential values in your text response.\n- **plugin_list**(): List the live plugin registry and cross-subsystem capability evidence. Use this before answering questions about whether LeapFlow supports plugins, self-evolution, plugin installation, hot reload, versioning, or other runtime capabilities.\n- **plugin_status**(plugin_id): Get detailed status of a specific plugin: its declared category, runtime dependencies, contributed tools, and fiber lifecycle state.\n- **plugin_versions**(plugin_id): List recorded source versions and active pointer for a profile-scoped plugin.\n- **plugin_propose**(requested_capability, plugin_id, proposed_tools, test_cases, risk_level, evidence): Create a side-effect-free PluginProposal from explicit capability-gap evidence. Use this before plugin_generate when a missing capability should be reviewed. Does not call an LLM, write files, or install anything.\n- **assess_compatibility**(manifest, source_path) [capability_expand category: plugin_management]: Assess whether a foreign plugin manifest or real DSH source bundle is compatible with LeapFlow. A source bundle assessment is static: runtime_ready stays false until restricted Node discovery during plugin_install. Returns component-level verdicts and limitations.\n- **plugin_generate**(plugin_id, description, proposal_id) [capability_expand category: system]: Generate a new ToolPlugin from a natural-language capability description. The LLM produces code that conforms to the ToolPlugin Protocol; it is then rigorously validated (syntax, structure, import, protocol conformance). The generated source is stored in CAS and receives explicit content approval. It DOES NOT install the plugin — installation requires a second, mutation-specific approval via plugin_install.\n- **plugin_install**(plugin_id, code, marketplace_name, source_path, proposal_id, version_label) [capability_expand category: system]: Install a plugin from exactly one source: validated Python code, the configured Python marketplace, or a real DSH source bundle. DSH bundles run restricted Node discovery before registration; only public host tools are installed and client UI limitations are reported. Writes profile-scoped state and mutates the process-global registry. REQUIRES APPROVAL.\n- **plugin_rollback**(plugin_id, version) [capability_expand category: system]: Rollback a profile-scoped plugin to a recorded source version and reload it. REQUIRES APPROVAL.\n- **plugin_reload**(plugin_id, version_label) [capability_expand category: system]: Hot-reload a plugin at runtime. Disposes the old plugin fiber, re-imports its module, and registers a fresh instance. Existing in-flight turns are unaffected (snapshot isolation). REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_disable**(plugin_id) [capability_expand category: system]: Disable a plugin by disposing its fiber, removing its tools from the runtime registry. Cannot disable self_management itself. REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_remove**(plugin_id, delete_source) [capability_expand category: system]: Terminally remove a plugin: dispose its fiber, unregister its tools, remove reload metadata, and optionally delete its profile-scoped source file. Cannot remove self_management itself. REQUIRES APPROVAL.\n- **plugin_unquarantine**(plugin_id) [capability_expand category: plugin_management]: Restore a quarantined plugin to probation status for re-evaluation. Unfreezes the trust ledger, transitions the proposal back to PROBATION, and reloads the plugin. REQUIRES APPROVAL.\n- **plugin_enable**(plugin_id) [capability_expand category: system]: Re-enable a previously disabled plugin by reloading its module and registering a fresh instance. REQUIRES APPROVAL.\n- **self_describe**(facet): Introspect LeapFlow's own identity, capabilities, runtime state, evolution metrics, or platform connections. Use facet='all' only when a comprehensive self-check is explicitly requested.\n- **runtime_snapshot**(): Lightweight (~150 token) flat snapshot of current runtime state: model, context budget, posture, disclosure level, turn count, cache hit rate, uptime, tool count, and pending approvals.\n- **schedule_create**(trigger_expression, instruction, execution_mode, max_retries, delivery_target) [capability_expand category: scheduler]: Create a new scheduled task. Specify a trigger expression (e.g. '30m', 'every 2h', '0 9 * * *') and the instruction to execute on each trigger. Optionally provide an execution mode and a delivery target to receive notifications.\n- **schedule_list**() [capability_expand category: scheduler]: List all scheduled tasks with their current state, trigger, and next due time.\n- **schedule_status**(task_id) [capability_expand category: scheduler]: Get detailed status and recent execution history for a scheduled task.\n- **schedule_pause**(task_id) [capability_expand category: scheduler]: Pause a scheduled task so it stops firing without being cancelled.\n- **schedule_resume**(task_id) [capability_expand category: scheduler]: Resume a paused scheduled task — re-arms it and recalculates next due time.\n- **schedule_cancel**(task_id) [capability_expand category: scheduler]: Cancel a scheduled task permanently.\n- **tool_search**(query, max_results): Search all registered tools by keyword relevance. Returns a ranked list of matching tools with name, category, and summary. Use this when you need a tool but are unsure of its exact name or category.\n- **tool_describe**(tool_name): Get the full callable schema of a single tool by exact name. Returns the tool's description, parameters, and metadata. Use after tool_search to inspect a specific tool before calling it.\n- **session_search**(query, limit) [capability_expand category: unclassified]: Search past conversation sessions in the current workspace for relevant context.\n- **session_list**(limit): List recent conversation sessions in the current workspace with stable ids, titles, dates, and summaries. Use for browsing past tasks or when user asks to see history without specific search terms. No keywords needed — returns chronological list.\n- **session_detail**(session_id, limit, offset, include_inactive) [capability_expand category: unclassified]: Read a paginated persisted transcript for one past conversation session in the current workspace. Use after session_list or session_search returns a session_id.\n- **click**(element_index) [capability_expand category: desktop]: Click a UI element by its element_index (from the latest observe_ui snapshot)\n- **get_clipboard**() [capability_expand category: desktop]: Read current clipboard text content\n- **list_apps**(filter, running_only) [capability_expand category: desktop]: List available applications on this system. Use to discover correct bundle_id before switch_app.\n- **list_windows**() [capability_expand category: desktop]: List all top-level windows with pid, window_id, title, and per-window state (minimized, on-screen). Call this first to pick the pid and window_id that observe_ui and other window tools require.\n- **observe_ui**(pid, window_id, query) [capability_expand category: desktop]: Snapshot one window's actionable UI elements, each tagged with an element_index for click/right_click/read_text. Re-observe after actions — indices belong to one snapshot. Requires the window's pid and window_id from list_windows.\n- **open_url**(url, app_id) [capability_expand category: desktop]: Open a URL in the default or specified browser\n- **read_text**(element_index) [capability_expand category: desktop]: Read the text content of a specific UI element from the latest snapshot\n- **right_click**(element_index) [capability_expand category: desktop]: Right-click a UI element to open its context menu. Returns visible menu items.\n- **screenshot**(pid, window_id) [capability_expand category: desktop]: Capture a screenshot for visual verification. With pid + window_id captures that window (works across all displays); defaults to the last observed window, or the full desktop when no window has been observed.\n- **scroll**(element_index, direction, amount, pid, window_id) [capability_expand category: desktop]: Scroll a scrollable area of a window. Omit element_index to scroll the window's focused/page scroller; pass one to scroll an exact element from the latest snapshot.\n- **select_text**(element_index) [capability_expand category: desktop]: Select all text in a UI element (focus + select-all, for subsequent copy)\n- **set_clipboard**(text) [capability_expand category: desktop]: Write text to the clipboard\n- **shortcut**(keys) [capability_expand category: desktop]: Execute a keyboard shortcut\n- **switch_app**(app_id) [capability_expand category: desktop]: Switch to an app (launch if needed, activate, verify)\n- **type_text**(text) [capability_expand category: desktop]: Type text into the currently focused element\n- **wait**(seconds) [capability_expand category: desktop]: Wait for a specified duration before continuing\n- **wait_until**(condition, pid, window_id, timeout, poll_interval) [capability_expand category: desktop]: Wait until a UI condition is met (polls UI tree). Returns elements when found or on timeout.\n- **wait_until_stable**(timeout, poll_interval, pid, window_id) [capability_expand category: desktop]: Wait until the UI stops changing (element set stabilizes across polls).\n\n## App Connector Capability Index\nLeapFlow can onboard and manage external apps through `platform_connect` and execute exact registered business actions through `platform_action`.\nFor requests about connecting, setting up, configuring, enabling, or managing a supported app, use `platform_connect` first instead of generating SDK/Webhook sample code.\n`platform_connect.action` is the management namespace: list, guide, preflight, connect, disconnect, remove, status, events_start, events_stop, events_status.\n`platform_action.action` is only for exact registered platform business actions listed below, such as `im.send_message`; never use management actions like `list` or `guide` there.\nAll business fields MUST go inside `payload`; top-level keys are only `platform`, `action`, and `payload`.\nDo not invent platform IDs or platform action names. If the needed action is not listed, ask for discovery/clarification instead of guessing.\nUse `platform_connect` with `action='guide'` and the matching `platform` to start onboarding; use `action='list'` when the app is unclear.\nWhen a pending onboarding state is present, continue from that state with `platform_connect` instead of asking the user to restate the app.\nSupported apps:\n- `api_server`: API Server (OpenAI Compatible) (category=api; backend=adapter; platform_action actions=none registered)\n- `dingtalk`: 钉钉 (DingTalk) (category=im; backend=adapter; platform_action actions=none registered)\n- `feishu`: 飞书 (Feishu/Lark) (category=im; backend=cli)\n - `calendar.create_event` payload={summary*, start_time*, end_time*, attendees} [write/high]\n - `docs.create_markdown` payload={title*, markdown*, folder_token} [write/medium]\n - `drive.search` payload={query*, limit} [read/medium]\n - `im.add_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.download_resource` payload={message_id*, file_key*, type*} [read/low]\n - `im.get_messages` payload={message_ids*} [read/low]\n - `im.list_chats` payload={page_size} [read/low]\n - `im.list_messages` payload={chat_id*} [read/low]\n - `im.list_thread_messages` payload={thread*} [read/low]\n - `im.remove_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.reply_message` payload={message_id*, text*} [send/high]\n - `im.search_chats` payload={query*, page_size} [read/low]\n - `im.search_messages` payload={query*} [read/medium]\n - `im.send_message` payload={chat_id*, text*, thread_id} [send/high]\n - `im.update_card` payload={token*, card*} [write/medium]\n - `im.update_message` payload={message_id*, text*} [write/medium]\n - `mail.search_unread` payload={query, limit} [read/high]\n - `sheets.append_row` payload={spreadsheet_token*, sheet_id*, values*} [write/medium]\n - `task.create` payload={title*, description, due_time} [write/medium]\n- `telegram`: Telegram (category=social; backend=adapter; platform_action actions=none registered)\n- `webhook`: Webhook (Generic) (category=webhook; backend=adapter; platform_action actions=none registered)\n\n## Tool Usage\nTools are normally invoked through the native function-calling mechanism, not by writing JSON in your reply\ntext. Only if the provider signals that native function calling is unavailable for this turn, fall back to a\nsingle JSON code block: `{\"name\": \"tool_name\", \"arguments\": {\"key\": \"value\"}}` — use this fallback format\nonly, never both. Only call a tool whose exact name appears in the tool index above and is currently callable\n(or reachable via `capability_expand`). Never invent, rename, alias, or guess a tool name, platform ID, or\nplatform action from argument shape or wording — if the index does not list it, it does not exist.\n`platform_connect.action` (list/guide/preflight/connect/disconnect/remove/status/events_start/events_stop/\nevents_status) is the App Connector management namespace; `platform_action.action` only accepts exact\nregistered business actions such as `im.send_message` shown in the App Connector Capability Index — never mix\nthe two namespaces. If a tool call returns an unknown/unavailable result, use the returned suggestions or\navailable names for a single retry instead of trying further variations of the same guess.\n\n**Side-effect action rule** (`platform_action` with effect=send/write/execute):\n- Call each unique action+payload **exactly once**. Never include duplicates in the same turn.\n- Once the result returns `\"completed\": true`, that action is DONE for this task. Do NOT call it again\n in any subsequent turn — immediately summarize the result for the user instead.\n- The system enforces idempotency: duplicate calls are blocked and will not execute.\n- If the user explicitly requests sending/writing multiple times, use distinct payloads per call.\n\n**Resource identifier provenance rule**:\n- NEVER fabricate, guess, or infer resource identifiers (chat_id, message_id, file_key, user_id, etc.).\n Every resource ID used in a side-effect action MUST come from a successful API response in this session.\n- If a read/list action fails (e.g. authorization error), you do NOT have valid resource IDs.\n Report the failure to the user — do NOT attempt the dependent write/send action with a guessed ID.\n- When a tool result contains `\"llm_instruction\"`, follow it exactly.\n\n## Guidelines\n1. **Direct answers first**: If you already know the answer, respond directly without tools.\n2. **Avoid redundant tool calls**: Do not call the same tool with the same arguments more than once in the same user turn. When an existing tool result already answers the user's request, stop calling tools and answer directly.\n3. **Use tools proactively**: When the user asks about files, time, system state, or needs actions performed, use the appropriate tool.\n4. **Chain tools when needed**: You can call multiple tools in sequence (e.g., list files → read file → summarize).\n5. **Handle failures gracefully**: If a tool fails, explain what went wrong and suggest alternatives. If it failed because of an unknown tool/platform/action name, retry once with an exact name from the returned suggestions, then explain rather than keep guessing.\n6. **Summarize results naturally**: After tool execution, synthesize the results into a helpful answer rather than dumping raw output.\n7. **Stay conversational**: Maintain a natural, helpful tone. Acknowledge context from earlier in the conversation.\n8. **Recall past work**:\n - Broad queries (\"之前做了什么\", \"列出任务\"): answer from the \"Recent Task History\" section already in your context. If insufficient, call session_list.\n - Specific lookups (\"上次那个配置怎么改的\"): call session_search with relevant phrases (NOT single characters).\n - Do NOT call search tools repeatedly with keyword fragments. One well-phrased call is better than ten fragmented ones.\n\n## Coding & Verification\nWhen working with code, prefer the precise built-in tools over ad-hoc shell: use `repo_map` to orient in an\nunfamiliar project, `code_search`/`file_find` to locate and `code_intel` for symbols, `edit_file` (anchored\nsearch-replace, or a unified `diff`) to change files — never rewrite a whole file to change a few lines — and\n`git_query` to inspect diffs/log. After edits, check `syntax_ok` in the result and run `test_run`/`lint_check`\nbefore declaring the work done. Batch independent read-only calls (search/read/query) into a single turn — they\nrun in parallel.\n\n## Presentation Style\n1. **Polished Markdown only**: Format user-facing answers with clean Markdown headings, short paragraphs, and concise bullets. Use tables only when they improve comparison or scanning.\n2. **Terminal-friendly layout**: Keep lines readable in a TUI; avoid dense walls of text, deeply nested lists, oversized ASCII art, or heavy visual blocks.\n3. **Elegant emphasis**: Use bold text sparingly for key terms and conclusions. Avoid excessive emojis, decorative symbols, repeated separators, or visual noise.\n4. **Theme-safe colors**: Do not emit ANSI escape codes, HTML color tags, Rich markup, or hardcoded color names. Rely on the TUI theme to render Markdown professionally.\n5. **No leaked tool protocol**: Never show tool-call JSON, internal schemas, raw observations, tool result payloads, or hidden reasoning in the final answer unless the user explicitly asks for raw/debug output. Treat any prior `{\"name\": ..., \"arguments\": ...}` blocks and `Tool result (...)` messages as internal execution context only.\n6. **Professional closure**: End with a concise conclusion or next step when helpful; avoid rambling after the useful answer is complete.\n\nWhen finished with all tool calls, respond normally without a JSON block, tool-call transcript, or process log.\n\n## Task Contract\n- Task ID: turn-2\n- Original user request: Second A turn.\n- Workspace root: \n- Allowed roots: \n- Treat relative project paths as relative to the workspace root; never infer `.` as the project root when a workspace root is provided.\n- Workspace boundary is enforced by tools: do not read, search, edit, or run commands against paths outside the allowed roots unless the user explicitly requests an external path and the tool/approval policy permits it.\n- LeapFlow workspace config is optional at `/.leapflow/config.yaml`; runtime config is loaded from `~/.leapflow/config/user.yaml` and `~/.leapflow/profiles//config/*.yaml`.\n- Preserve this task contract across summarization, compression, tool loops, and memory retrieval.\n" - }, - { - "role": "system", - "content": "\n## Recent Session Summary\n- [assistant] Workspace A acknowledged.\n- [user] Second A turn.\n" - }, - { - "role": "assistant", - "content": "Workspace A acknowledged." - }, - { - "role": "user", - "content": "Second A turn.\nSecond A turn." - } - ], - "tools": [ - "capability_expand", - "code_intel", - "code_search", - "config_get", - "config_list", - "env_info", - "file_find", - "file_list", - "file_read", - "git_query", - "lint_check", - "memory_search", - "plugin_list", - "plugin_propose", - "plugin_status", - "plugin_versions", - "repo_map", - "research_note", - "runtime_snapshot", - "schedule_reentry", - "self_describe", - "session_list", - "skill_view", - "skills_list", - "terminal_list", - "terminal_read", - "test_run", - "text_search", - "time_get", - "tool_describe", - "tool_search", - "web_fetch" - ] - }, - "responses": [ - { - "status": 200, - "content_type": "application/json", - "body": "{\"id\": \"chatcmpl-cassette\", \"object\": \"chat.completion\", \"model\": \"cassette-model\", \"choices\": [{\"index\": 0, \"message\": {\"role\": \"assistant\", \"content\": \"Still workspace A.\"}, \"finish_reason\": \"stop\"}], \"usage\": {\"prompt_tokens\": 64, \"completion_tokens\": 16, \"total_tokens\": 80}}" - } - ] -} diff --git a/tests/_fixtures/cassettes/r2_isolation/cassette-model-d1b363e9073f14ac.cassette.json b/tests/_fixtures/cassettes/r2_isolation/cassette-model-d1b363e9073f14ac.cassette.json deleted file mode 100644 index 8b631624..00000000 --- a/tests/_fixtures/cassettes/r2_isolation/cassette-model-d1b363e9073f14ac.cassette.json +++ /dev/null @@ -1,63 +0,0 @@ -{ - "fingerprint": "d1b363e9073f14acbee0db20ec5546f98e6fbc5c0cc100ed69d61631f64a971c", - "note": "captured in seed mode", - "request": { - "model": "cassette-model", - "stream": false, - "messages": [ - { - "role": "system", - "content": "You are LeapFlow, an intelligent assistant that can both converse naturally and take real actions on the user's computer.\n\n## Capabilities\nThe tool index below lists **every** registered tool by name and a one-line summary — this is the complete\ncapability contract; nothing else exists. Only a subset is directly callable this turn (via native tool calling,\nnot a JSON block in your reply). If you need a tool from the index that is not yet callable, call\n`capability_expand` with its category name first — the matching tools become callable immediately after.\nWhen the user asks what LeapFlow itself supports, whether it supports plugins, or which runtime capabilities\nare available, use the live capability evidence exposed by `plugin_list` before making capability claims; report\nconfiguration-dependent or unavailable capabilities as limitations instead of inferring from documentation.\n- **text_search**(text, pattern): Search for a regex pattern in text.\n- **text_replace**(text, old, new, count) [capability_expand category: write]: Replace occurrences of a substring in text.\n- **time_get**(): Get current date and time.\n- **env_info**(): Get system environment information (OS, Python version, cwd).\n- **skills_list**(query, category, source): List available learned skills. Use when user asks about capabilities or you need a specific skill.\n- **skill_view**(name): View the full content of a specific skill document.\n- **code_intel**(path, operation): Precise document symbols (outline) for a source file: classes, functions, and methods with line ranges. Python uses an exact AST parse; other languages use a keyword-prefix scan. Prefer over file_read mode=symbols for accurate navigation before editing. Read-only.\n- **repo_map**(path): Compact project orientation for a repository root: languages, detected test/lint commands, top-level structure, entry points, manifest, and VCS branch. Call this first when entering an unfamiliar codebase. Read-only.\n- **scm_sync**(action, cwd, remote, pull_ref, push_ref, timeout) [capability_expand category: scm]: Run a typed git SCM action. Use this instead of shell_run for git pull/push/status. For 'pull origin main then push', set action='pull_then_push', remote='origin', pull_ref='main', and omit push_ref so LeapFlow pushes the current local branch.\n- **git_query**(action, cwd, ref, path, staged, max_count, stat): Read-only structured git inspection: action=diff|log|status|branch|show. Prefer over shell_run for reading repo state — output is clipped, redacted, and log/branch are parsed into structured fields. Use scm_sync for pull/push.\n- **git_write**(action, cwd, message, stage_all, name, ref, create) [capability_expand category: scm]: Mutating git actions: action=commit (message, stage_all), branch (create+switch), checkout (switch; create=true for -b). Approval-gated. Use scm_sync for pull/push and git_query for reads.\n- **test_run**(command, cwd, timeout): Run the project's test suite and return structured results (framework, passed/failed counts, failing tests). Auto-detects the runner (pytest/npm/go/cargo) or uses a configured/explicit command; executes via the governed shell. ok=true means the runner executed — see 'success' for pass/fail.\n- **lint_check**(command, cwd, timeout): Run the project's linter and return a structured clean/issue result. Auto-detects the linter (ruff/eslint/go vet/clippy) or uses a configured/explicit command; executes via the governed shell. ok=true means the linter ran — see 'clean'.\n- **file_list**(path, pattern, depth): List files and directories at a given path. Use depth=1 or depth=2 to get a recursive tree in one call instead of listing each sub-directory separately.\n- **file_read**(path, max_lines, start_line, max_chars, mode): Read text file content with adaptive context governance. For large or unfamiliar files, prefer mode='outline' or mode='symbols' first, then use mode='raw' with start_line/max_lines for the specific range you actually need. For LeapFlow's own settings, use config_list / config_get / config_set — its config files are outside the workspace and not readable here.\n- **file_write**(path, content, mode) [capability_expand category: write]: Write content to a file (overwrite or append).\n- **code_search**(pattern, patterns, path, glob, ignore_case, multiline, max_results, context_lines): Search file CONTENTS by regex pattern across a directory tree (ripgrep-backed). Requires a regex pattern. NOT for listing or browsing directory contents — use file_list for that. Prefer this over shell_run grep: faster, skips VCS/dependency/build dirs, and returns structured path:line:column matches. Batch related lookups into ONE call via `patterns` (OR-combined, single pass) instead of issuing several separate searches. Use file_read for the surrounding context of a hit.\n- **file_find**(glob, path, max_results): Find files by a recursive glob pattern under a base path (e.g. '**/test_*.py' or '*.md'). Prefer this over shell_run find; skips VCS/dependency/build dirs.\n- **edit_file**(path, edits, dry_run, diff) [capability_expand category: write]: Apply targeted, anchored search-replace edits to an EXISTING text file (use file_write to create/overwrite). Each edit is {original_text, new_text, replace_all?}; original_text must match exactly and uniquely (or set replace_all) — a non-unique or missing anchor is rejected so files are never corrupted. Set dry_run to preview. Alternatively pass a unified 'diff' to apply its hunks as anchored edits. Far cheaper and safer than rewriting a whole file.\n- **shell_run**(command, cwd, timeout) [capability_expand category: shell]: Execute a one-shot shell command with timeout protection. Runs in the active workspace; paths resolving outside it are refused. Reach for a structured tool first when one fits — web_fetch for anything over HTTP(S), git_query/scm_sync for git, code_search/file_find/file_read for the repo, config_get/config_set for LeapFlow's own settings — because those report typed results, while a failed shell command can only be diagnosed from its exit code and stderr. Every shell run counts as an external side effect, so a failure stops the rest of the batch and is not retried automatically.\n- **terminal_open**(command, cwd, shell) [capability_expand category: terminal]: Open a PERSISTENT shell session (REPL/dev server/watch), returning a session_id for terminal_send/read/close. Disabled unless tools.terminal_session_enabled is set. For one-shot commands use shell_run instead.\n- **terminal_send**(session_id, input, wait) [capability_expand category: terminal]: Send a line of input to a persistent terminal session and return output captured shortly after.\n- **terminal_read**(session_id, wait): Drain buffered output from a persistent terminal session (optionally waiting briefly first).\n- **terminal_close**(session_id) [capability_expand category: terminal]: Terminate a persistent terminal session and release its process group.\n- **terminal_list**(): List active persistent terminal sessions.\n- **config_list**(category, limit): List LeapFlow's own writable settings (LLM model and endpoint, daemon, memory, perception, gateway, …) with current values. Use this to discover the exact key before changing anything. Optionally narrow by `category`. This is the only correct way to inspect LeapFlow configuration — never read config files from disk.\n- **config_get**(key): Read one LeapFlow setting by key (e.g. 'llm.model', 'llm.base_url', 'daemon.log_level'), returning its current value, type, scopes, and whether a change needs a daemon restart. There is no 'llm.provider' key: provider behavior is inferred from the OpenAI-compatible 'llm.base_url'. Never read LeapFlow config files from disk — use this.\n- **config_set**(key, value, scope) [capability_expand category: config]: Change one LeapFlow setting by key, e.g. switch an OpenAI-compatible LLM with key='llm.model' and key='llm.base_url'. There is no 'llm.provider' key: provider behavior is inferred from the endpoint. Values are validated and coerced; credentials are stored in the vault automatically. Call config_list or config_get first if unsure of the key. The result states whether a `leap daemon restart` is required. Never edit LeapFlow config files directly.\n- **web_fetch**(url, select, timeout, max_bytes): Read a URL over HTTP(S) and get back extracted, context-sized content: parsed JSON for API endpoints, readable text plus links for web pages. Use this for anything on the internet — prices, docs, releases, articles — instead of running curl through shell_run: it reports real HTTP status codes, retries rate limits on its own, and is a plain read so a retry is always safe. For JSON APIs pass `select` with a dotted path (e.g. 'chart.result.0.meta') to return just that part instead of the whole payload.\n- **memory_search**(query, limit): Search agent memory for relevant past experiences, observations, and facts.\n- **memory_add**(content, kind) [capability_expand category: write]: Store a new observation or insight in memory for future reference.\n- **research_note**(kind, text): Record a compact, structured note about the current task's state so it survives context compression on long / multi-step tasks. Use for durable findings, open questions still to resolve, decisions / excluded paths, and the immediate next step. One concise sentence per note.\n- **capability_expand**(category): Fetch the full callable schema for every tool in a capability category (e.g. 'hub', 'gateway', 'desktop', 'delegate', 'file', 'memory', 'skill'). The compact tool index always lists every registered tool by name and a one-line summary, but only a static low-risk subset is directly callable each turn. If you need a tool from the index that is not yet callable, call capability_expand with its category first; the matching tools become callable in this turn. Never invent a tool name — expand the category instead.\n- **delegate_task**(goal, context) [capability_expand category: delegate]: Delegate a complex sub-task to an isolated subagent. The subagent gets a fresh context and restricted tool access. Use when a task is self-contained and can be solved independently.\n- **schedule_reentry**(kind, reason, delay_seconds, event_match, max_reentries, deadline_seconds): Register a re-entry so this task can resume later from its current orientation (findings / open questions / next step). Use when work must pause and continue after a delay (kind=time) or when a matching platform event arrives (kind=event), instead of finishing now. The research-ledger state is carried over automatically.\n- **hub_push**(skill_name, visibility, version) [capability_expand category: hub]: Push a local skill to the ModelScope Hub for sharing or backup.\n- **hub_pull**(repo_id, version) [capability_expand category: hub]: Pull a skill from the ModelScope Hub to install locally.\n- **hub_search**(query) [capability_expand category: hub]: Search for skills on the Hub by keyword or description.\n- **hub_sync**(mode, dry_run) [capability_expand category: hub]: Preview or execute sync between local skills and Hub.\n- **platform_action**(platform, action, payload, backend_kind) [capability_expand category: gateway]: Execute an exact registered business action on an external platform through LeapFlow's App Connector layer. Actions must be copied from the App Connector Capability Index and are addressed as domain.operation, e.g. im.send_message or docs.create_markdown. All business fields (chat_id, text, query, etc.) MUST be placed inside `payload`, never at the top level. Example: {\"platform\":\"feishu\",\"action\":\"im.send_message\",\"payload\":{\"chat_id\":\"oc_xxx\",\"text\":\"hello\"}}. Do not invent action names, do not use management actions such as list/guide/connect/status here.\n- **platform_connect**(action, platform, credentials, options, checkpoint) [capability_expand category: gateway]: List, guide, connect, disconnect, remove, or check status for external platforms using the App Connector management namespace. Supports REST and CLI backends. Use this for management actions such as list/guide/preflight/connect/status; use platform_action only for exact registered business actions.\n- **gateway_send**(platform, chat_id, text, thread_id) [capability_expand category: gateway]: Send a message to a connected external platform (Feishu group, Telegram chat, DingTalk conversation, etc.). Requires the platform to be connected via gateway_connect first. Use gateway_connect with action='list' to see connected platforms and available chat IDs.\n- **gateway_connect**(action, platform, credentials, options) [capability_expand category: gateway]: Connect, configure, or manage external platform integrations (Feishu, DingTalk, Telegram, Slack, Discord, etc.). Conversational flow: 1) call 'guide' to get setup steps + required fields, 2) present the steps to the user and ask for ALL required credentials in a single message, 3) call 'connect' with the credentials. Goal: complete in 1–2 user turns. NEVER include credential values in your text response.\n- **plugin_list**(): List the live plugin registry and cross-subsystem capability evidence. Use this before answering questions about whether LeapFlow supports plugins, self-evolution, plugin installation, hot reload, versioning, or other runtime capabilities.\n- **plugin_status**(plugin_id): Get detailed status of a specific plugin: its declared category, runtime dependencies, contributed tools, and fiber lifecycle state.\n- **plugin_versions**(plugin_id): List recorded source versions and active pointer for a profile-scoped plugin.\n- **plugin_propose**(requested_capability, plugin_id, proposed_tools, test_cases, risk_level, evidence): Create a side-effect-free PluginProposal from explicit capability-gap evidence. Use this before plugin_generate when a missing capability should be reviewed. Does not call an LLM, write files, or install anything.\n- **assess_compatibility**(manifest, source_path) [capability_expand category: plugin_management]: Assess whether a foreign plugin manifest or real DSH source bundle is compatible with LeapFlow. A source bundle assessment is static: runtime_ready stays false until restricted Node discovery during plugin_install. Returns component-level verdicts and limitations.\n- **plugin_generate**(plugin_id, description, proposal_id) [capability_expand category: system]: Generate a new ToolPlugin from a natural-language capability description. The LLM produces code that conforms to the ToolPlugin Protocol; it is then rigorously validated (syntax, structure, import, protocol conformance). The generated source is stored in CAS and receives explicit content approval. It DOES NOT install the plugin — installation requires a second, mutation-specific approval via plugin_install.\n- **plugin_install**(plugin_id, code, marketplace_name, source_path, proposal_id, version_label) [capability_expand category: system]: Install a plugin from exactly one source: validated Python code, the configured Python marketplace, or a real DSH source bundle. DSH bundles run restricted Node discovery before registration; only public host tools are installed and client UI limitations are reported. Writes profile-scoped state and mutates the process-global registry. REQUIRES APPROVAL.\n- **plugin_rollback**(plugin_id, version) [capability_expand category: system]: Rollback a profile-scoped plugin to a recorded source version and reload it. REQUIRES APPROVAL.\n- **plugin_reload**(plugin_id, version_label) [capability_expand category: system]: Hot-reload a plugin at runtime. Disposes the old plugin fiber, re-imports its module, and registers a fresh instance. Existing in-flight turns are unaffected (snapshot isolation). REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_disable**(plugin_id) [capability_expand category: system]: Disable a plugin by disposing its fiber, removing its tools from the runtime registry. Cannot disable self_management itself. REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_remove**(plugin_id, delete_source) [capability_expand category: system]: Terminally remove a plugin: dispose its fiber, unregister its tools, remove reload metadata, and optionally delete its profile-scoped source file. Cannot remove self_management itself. REQUIRES APPROVAL.\n- **plugin_unquarantine**(plugin_id) [capability_expand category: plugin_management]: Restore a quarantined plugin to probation status for re-evaluation. Unfreezes the trust ledger, transitions the proposal back to PROBATION, and reloads the plugin. REQUIRES APPROVAL.\n- **plugin_enable**(plugin_id) [capability_expand category: system]: Re-enable a previously disabled plugin by reloading its module and registering a fresh instance. REQUIRES APPROVAL.\n- **self_describe**(facet): Introspect LeapFlow's own identity, capabilities, runtime state, evolution metrics, or platform connections. Use facet='all' only when a comprehensive self-check is explicitly requested.\n- **runtime_snapshot**(): Lightweight (~150 token) flat snapshot of current runtime state: model, context budget, posture, disclosure level, turn count, cache hit rate, uptime, tool count, and pending approvals.\n- **schedule_create**(trigger_expression, instruction, execution_mode, max_retries, delivery_target) [capability_expand category: scheduler]: Create a new scheduled task. Specify a trigger expression (e.g. '30m', 'every 2h', '0 9 * * *') and the instruction to execute on each trigger. Optionally provide an execution mode and a delivery target to receive notifications.\n- **schedule_list**() [capability_expand category: scheduler]: List all scheduled tasks with their current state, trigger, and next due time.\n- **schedule_status**(task_id) [capability_expand category: scheduler]: Get detailed status and recent execution history for a scheduled task.\n- **schedule_pause**(task_id) [capability_expand category: scheduler]: Pause a scheduled task so it stops firing without being cancelled.\n- **schedule_resume**(task_id) [capability_expand category: scheduler]: Resume a paused scheduled task — re-arms it and recalculates next due time.\n- **schedule_cancel**(task_id) [capability_expand category: scheduler]: Cancel a scheduled task permanently.\n- **tool_search**(query, max_results): Search all registered tools by keyword relevance. Returns a ranked list of matching tools with name, category, and summary. Use this when you need a tool but are unsure of its exact name or category.\n- **tool_describe**(tool_name): Get the full callable schema of a single tool by exact name. Returns the tool's description, parameters, and metadata. Use after tool_search to inspect a specific tool before calling it.\n- **session_search**(query, limit) [capability_expand category: unclassified]: Search past conversation sessions in the current workspace for relevant context.\n- **session_list**(limit): List recent conversation sessions in the current workspace with stable ids, titles, dates, and summaries. Use for browsing past tasks or when user asks to see history without specific search terms. No keywords needed — returns chronological list.\n- **session_detail**(session_id, limit, offset, include_inactive) [capability_expand category: unclassified]: Read a paginated persisted transcript for one past conversation session in the current workspace. Use after session_list or session_search returns a session_id.\n- **click**(element_index) [capability_expand category: desktop]: Click a UI element by its element_index (from the latest observe_ui snapshot)\n- **get_clipboard**() [capability_expand category: desktop]: Read current clipboard text content\n- **list_apps**(filter, running_only) [capability_expand category: desktop]: List available applications on this system. Use to discover correct bundle_id before switch_app.\n- **list_windows**() [capability_expand category: desktop]: List all top-level windows with pid, window_id, title, and per-window state (minimized, on-screen). Call this first to pick the pid and window_id that observe_ui and other window tools require.\n- **observe_ui**(pid, window_id, query) [capability_expand category: desktop]: Snapshot one window's actionable UI elements, each tagged with an element_index for click/right_click/read_text. Re-observe after actions — indices belong to one snapshot. Requires the window's pid and window_id from list_windows.\n- **open_url**(url, app_id) [capability_expand category: desktop]: Open a URL in the default or specified browser\n- **read_text**(element_index) [capability_expand category: desktop]: Read the text content of a specific UI element from the latest snapshot\n- **right_click**(element_index) [capability_expand category: desktop]: Right-click a UI element to open its context menu. Returns visible menu items.\n- **screenshot**(pid, window_id) [capability_expand category: desktop]: Capture a screenshot for visual verification. With pid + window_id captures that window (works across all displays); defaults to the last observed window, or the full desktop when no window has been observed.\n- **scroll**(element_index, direction, amount, pid, window_id) [capability_expand category: desktop]: Scroll a scrollable area of a window. Omit element_index to scroll the window's focused/page scroller; pass one to scroll an exact element from the latest snapshot.\n- **select_text**(element_index) [capability_expand category: desktop]: Select all text in a UI element (focus + select-all, for subsequent copy)\n- **set_clipboard**(text) [capability_expand category: desktop]: Write text to the clipboard\n- **shortcut**(keys) [capability_expand category: desktop]: Execute a keyboard shortcut\n- **switch_app**(app_id) [capability_expand category: desktop]: Switch to an app (launch if needed, activate, verify)\n- **type_text**(text) [capability_expand category: desktop]: Type text into the currently focused element\n- **wait**(seconds) [capability_expand category: desktop]: Wait for a specified duration before continuing\n- **wait_until**(condition, pid, window_id, timeout, poll_interval) [capability_expand category: desktop]: Wait until a UI condition is met (polls UI tree). Returns elements when found or on timeout.\n- **wait_until_stable**(timeout, poll_interval, pid, window_id) [capability_expand category: desktop]: Wait until the UI stops changing (element set stabilizes across polls).\n\n## App Connector Capability Index\nLeapFlow can onboard and manage external apps through `platform_connect` and execute exact registered business actions through `platform_action`.\nFor requests about connecting, setting up, configuring, enabling, or managing a supported app, use `platform_connect` first instead of generating SDK/Webhook sample code.\n`platform_connect.action` is the management namespace: list, guide, preflight, connect, disconnect, remove, status, events_start, events_stop, events_status.\n`platform_action.action` is only for exact registered platform business actions listed below, such as `im.send_message`; never use management actions like `list` or `guide` there.\nAll business fields MUST go inside `payload`; top-level keys are only `platform`, `action`, and `payload`.\nDo not invent platform IDs or platform action names. If the needed action is not listed, ask for discovery/clarification instead of guessing.\nUse `platform_connect` with `action='guide'` and the matching `platform` to start onboarding; use `action='list'` when the app is unclear.\nWhen a pending onboarding state is present, continue from that state with `platform_connect` instead of asking the user to restate the app.\nSupported apps:\n- `api_server`: API Server (OpenAI Compatible) (category=api; backend=adapter; platform_action actions=none registered)\n- `dingtalk`: 钉钉 (DingTalk) (category=im; backend=adapter; platform_action actions=none registered)\n- `feishu`: 飞书 (Feishu/Lark) (category=im; backend=cli)\n - `calendar.create_event` payload={summary*, start_time*, end_time*, attendees} [write/high]\n - `docs.create_markdown` payload={title*, markdown*, folder_token} [write/medium]\n - `drive.search` payload={query*, limit} [read/medium]\n - `im.add_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.download_resource` payload={message_id*, file_key*, type*} [read/low]\n - `im.get_messages` payload={message_ids*} [read/low]\n - `im.list_chats` payload={page_size} [read/low]\n - `im.list_messages` payload={chat_id*} [read/low]\n - `im.list_thread_messages` payload={thread*} [read/low]\n - `im.remove_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.reply_message` payload={message_id*, text*} [send/high]\n - `im.search_chats` payload={query*, page_size} [read/low]\n - `im.search_messages` payload={query*} [read/medium]\n - `im.send_message` payload={chat_id*, text*, thread_id} [send/high]\n - `im.update_card` payload={token*, card*} [write/medium]\n - `im.update_message` payload={message_id*, text*} [write/medium]\n - `mail.search_unread` payload={query, limit} [read/high]\n - `sheets.append_row` payload={spreadsheet_token*, sheet_id*, values*} [write/medium]\n - `task.create` payload={title*, description, due_time} [write/medium]\n- `telegram`: Telegram (category=social; backend=adapter; platform_action actions=none registered)\n- `webhook`: Webhook (Generic) (category=webhook; backend=adapter; platform_action actions=none registered)\n\n## Tool Usage\nTools are normally invoked through the native function-calling mechanism, not by writing JSON in your reply\ntext. Only if the provider signals that native function calling is unavailable for this turn, fall back to a\nsingle JSON code block: `{\"name\": \"tool_name\", \"arguments\": {\"key\": \"value\"}}` — use this fallback format\nonly, never both. Only call a tool whose exact name appears in the tool index above and is currently callable\n(or reachable via `capability_expand`). Never invent, rename, alias, or guess a tool name, platform ID, or\nplatform action from argument shape or wording — if the index does not list it, it does not exist.\n`platform_connect.action` (list/guide/preflight/connect/disconnect/remove/status/events_start/events_stop/\nevents_status) is the App Connector management namespace; `platform_action.action` only accepts exact\nregistered business actions such as `im.send_message` shown in the App Connector Capability Index — never mix\nthe two namespaces. If a tool call returns an unknown/unavailable result, use the returned suggestions or\navailable names for a single retry instead of trying further variations of the same guess.\n\n**Side-effect action rule** (`platform_action` with effect=send/write/execute):\n- Call each unique action+payload **exactly once**. Never include duplicates in the same turn.\n- Once the result returns `\"completed\": true`, that action is DONE for this task. Do NOT call it again\n in any subsequent turn — immediately summarize the result for the user instead.\n- The system enforces idempotency: duplicate calls are blocked and will not execute.\n- If the user explicitly requests sending/writing multiple times, use distinct payloads per call.\n\n**Resource identifier provenance rule**:\n- NEVER fabricate, guess, or infer resource identifiers (chat_id, message_id, file_key, user_id, etc.).\n Every resource ID used in a side-effect action MUST come from a successful API response in this session.\n- If a read/list action fails (e.g. authorization error), you do NOT have valid resource IDs.\n Report the failure to the user — do NOT attempt the dependent write/send action with a guessed ID.\n- When a tool result contains `\"llm_instruction\"`, follow it exactly.\n\n## Guidelines\n1. **Direct answers first**: If you already know the answer, respond directly without tools.\n2. **Avoid redundant tool calls**: Do not call the same tool with the same arguments more than once in the same user turn. When an existing tool result already answers the user's request, stop calling tools and answer directly.\n3. **Use tools proactively**: When the user asks about files, time, system state, or needs actions performed, use the appropriate tool.\n4. **Chain tools when needed**: You can call multiple tools in sequence (e.g., list files → read file → summarize).\n5. **Handle failures gracefully**: If a tool fails, explain what went wrong and suggest alternatives. If it failed because of an unknown tool/platform/action name, retry once with an exact name from the returned suggestions, then explain rather than keep guessing.\n6. **Summarize results naturally**: After tool execution, synthesize the results into a helpful answer rather than dumping raw output.\n7. **Stay conversational**: Maintain a natural, helpful tone. Acknowledge context from earlier in the conversation.\n8. **Recall past work**:\n - Broad queries (\"之前做了什么\", \"列出任务\"): answer from the \"Recent Task History\" section already in your context. If insufficient, call session_list.\n - Specific lookups (\"上次那个配置怎么改的\"): call session_search with relevant phrases (NOT single characters).\n - Do NOT call search tools repeatedly with keyword fragments. One well-phrased call is better than ten fragmented ones.\n\n## Coding & Verification\nWhen working with code, prefer the precise built-in tools over ad-hoc shell: use `repo_map` to orient in an\nunfamiliar project, `code_search`/`file_find` to locate and `code_intel` for symbols, `edit_file` (anchored\nsearch-replace, or a unified `diff`) to change files — never rewrite a whole file to change a few lines — and\n`git_query` to inspect diffs/log. After edits, check `syntax_ok` in the result and run `test_run`/`lint_check`\nbefore declaring the work done. Batch independent read-only calls (search/read/query) into a single turn — they\nrun in parallel.\n\n## Presentation Style\n1. **Polished Markdown only**: Format user-facing answers with clean Markdown headings, short paragraphs, and concise bullets. Use tables only when they improve comparison or scanning.\n2. **Terminal-friendly layout**: Keep lines readable in a TUI; avoid dense walls of text, deeply nested lists, oversized ASCII art, or heavy visual blocks.\n3. **Elegant emphasis**: Use bold text sparingly for key terms and conclusions. Avoid excessive emojis, decorative symbols, repeated separators, or visual noise.\n4. **Theme-safe colors**: Do not emit ANSI escape codes, HTML color tags, Rich markup, or hardcoded color names. Rely on the TUI theme to render Markdown professionally.\n5. **No leaked tool protocol**: Never show tool-call JSON, internal schemas, raw observations, tool result payloads, or hidden reasoning in the final answer unless the user explicitly asks for raw/debug output. Treat any prior `{\"name\": ..., \"arguments\": ...}` blocks and `Tool result (...)` messages as internal execution context only.\n6. **Professional closure**: End with a concise conclusion or next step when helpful; avoid rambling after the useful answer is complete.\n\nWhen finished with all tool calls, respond normally without a JSON block, tool-call transcript, or process log.\n\n## Task Contract\n- Task ID: turn-1\n- Original user request: Hello from B.\n- Workspace root: \n- Allowed roots: \n- Treat relative project paths as relative to the workspace root; never infer `.` as the project root when a workspace root is provided.\n- Workspace boundary is enforced by tools: do not read, search, edit, or run commands against paths outside the allowed roots unless the user explicitly requests an external path and the tool/approval policy permits it.\n- LeapFlow workspace config is optional at `/.leapflow/config.yaml`; runtime config is loaded from `~/.leapflow/config/user.yaml` and `~/.leapflow/profiles//config/*.yaml`.\n- Preserve this task contract across summarization, compression, tool loops, and memory retrieval.\n" - }, - { - "role": "system", - "content": "\n## Recent Session Summary\n- [user] Hello from B.\n" - }, - { - "role": "user", - "content": "Hello from B.\nHello from B." - } - ], - "tools": [ - "capability_expand", - "code_intel", - "code_search", - "config_get", - "config_list", - "env_info", - "file_find", - "file_list", - "file_read", - "git_query", - "lint_check", - "memory_search", - "plugin_list", - "plugin_propose", - "plugin_status", - "plugin_versions", - "repo_map", - "research_note", - "runtime_snapshot", - "schedule_reentry", - "self_describe", - "session_list", - "skill_view", - "skills_list", - "terminal_list", - "terminal_read", - "test_run", - "text_search", - "time_get", - "tool_describe", - "tool_search", - "web_fetch" - ] - }, - "responses": [ - { - "status": 200, - "content_type": "application/json", - "body": "{\"id\": \"chatcmpl-cassette\", \"object\": \"chat.completion\", \"model\": \"cassette-model\", \"choices\": [{\"index\": 0, \"message\": {\"role\": \"assistant\", \"content\": \"Workspace B acknowledged.\"}, \"finish_reason\": \"stop\"}], \"usage\": {\"prompt_tokens\": 64, \"completion_tokens\": 16, \"total_tokens\": 80}}" - } - ] -} diff --git a/tests/_fixtures/cassettes/r3_control_plane/cassette-model-3454f9cdf57276a1.cassette.json b/tests/_fixtures/cassettes/r3_control_plane/cassette-model-3454f9cdf57276a1.cassette.json deleted file mode 100644 index 46b870ba..00000000 --- a/tests/_fixtures/cassettes/r3_control_plane/cassette-model-3454f9cdf57276a1.cassette.json +++ /dev/null @@ -1,63 +0,0 @@ -{ - "fingerprint": "3454f9cdf57276a12062063f08904c13968dacfedb58632af42306d4ca6cc99b", - "note": "captured in seed mode", - "request": { - "model": "cassette-model", - "stream": false, - "messages": [ - { - "role": "system", - "content": "You are LeapFlow, an intelligent assistant that can both converse naturally and take real actions on the user's computer.\n\n## Capabilities\nThe tool index below lists **every** registered tool by name and a one-line summary — this is the complete\ncapability contract; nothing else exists. Only a subset is directly callable this turn (via native tool calling,\nnot a JSON block in your reply). If you need a tool from the index that is not yet callable, call\n`capability_expand` with its category name first — the matching tools become callable immediately after.\nWhen the user asks what LeapFlow itself supports, whether it supports plugins, or which runtime capabilities\nare available, use the live capability evidence exposed by `plugin_list` before making capability claims; report\nconfiguration-dependent or unavailable capabilities as limitations instead of inferring from documentation.\n- **text_search**(text, pattern): Search for a regex pattern in text.\n- **text_replace**(text, old, new, count) [capability_expand category: write]: Replace occurrences of a substring in text.\n- **time_get**(): Get current date and time.\n- **env_info**(): Get system environment information (OS, Python version, cwd).\n- **skills_list**(query, category, source): List available learned skills. Use when user asks about capabilities or you need a specific skill.\n- **skill_view**(name): View the full content of a specific skill document.\n- **code_intel**(path, operation): Precise document symbols (outline) for a source file: classes, functions, and methods with line ranges. Python uses an exact AST parse; other languages use a keyword-prefix scan. Prefer over file_read mode=symbols for accurate navigation before editing. Read-only.\n- **repo_map**(path): Compact project orientation for a repository root: languages, detected test/lint commands, top-level structure, entry points, manifest, and VCS branch. Call this first when entering an unfamiliar codebase. Read-only.\n- **scm_sync**(action, cwd, remote, pull_ref, push_ref, timeout) [capability_expand category: scm]: Run a typed git SCM action. Use this instead of shell_run for git pull/push/status. For 'pull origin main then push', set action='pull_then_push', remote='origin', pull_ref='main', and omit push_ref so LeapFlow pushes the current local branch.\n- **git_query**(action, cwd, ref, path, staged, max_count, stat): Read-only structured git inspection: action=diff|log|status|branch|show. Prefer over shell_run for reading repo state — output is clipped, redacted, and log/branch are parsed into structured fields. Use scm_sync for pull/push.\n- **git_write**(action, cwd, message, stage_all, name, ref, create) [capability_expand category: scm]: Mutating git actions: action=commit (message, stage_all), branch (create+switch), checkout (switch; create=true for -b). Approval-gated. Use scm_sync for pull/push and git_query for reads.\n- **test_run**(command, cwd, timeout): Run the project's test suite and return structured results (framework, passed/failed counts, failing tests). Auto-detects the runner (pytest/npm/go/cargo) or uses a configured/explicit command; executes via the governed shell. ok=true means the runner executed — see 'success' for pass/fail.\n- **lint_check**(command, cwd, timeout): Run the project's linter and return a structured clean/issue result. Auto-detects the linter (ruff/eslint/go vet/clippy) or uses a configured/explicit command; executes via the governed shell. ok=true means the linter ran — see 'clean'.\n- **file_list**(path, pattern, depth): List files and directories at a given path. Use depth=1 or depth=2 to get a recursive tree in one call instead of listing each sub-directory separately.\n- **file_read**(path, max_lines, start_line, max_chars, mode): Read text file content with adaptive context governance. For large or unfamiliar files, prefer mode='outline' or mode='symbols' first, then use mode='raw' with start_line/max_lines for the specific range you actually need. For LeapFlow's own settings, use config_list / config_get / config_set — its config files are outside the workspace and not readable here.\n- **file_write**(path, content, mode) [capability_expand category: write]: Write content to a file (overwrite or append).\n- **code_search**(pattern, patterns, path, glob, ignore_case, multiline, max_results, context_lines): Search file CONTENTS by regex pattern across a directory tree (ripgrep-backed). Requires a regex pattern. NOT for listing or browsing directory contents — use file_list for that. Prefer this over shell_run grep: faster, skips VCS/dependency/build dirs, and returns structured path:line:column matches. Batch related lookups into ONE call via `patterns` (OR-combined, single pass) instead of issuing several separate searches. Use file_read for the surrounding context of a hit.\n- **file_find**(glob, path, max_results): Find files by a recursive glob pattern under a base path (e.g. '**/test_*.py' or '*.md'). Prefer this over shell_run find; skips VCS/dependency/build dirs.\n- **edit_file**(path, edits, dry_run, diff) [capability_expand category: write]: Apply targeted, anchored search-replace edits to an EXISTING text file (use file_write to create/overwrite). Each edit is {original_text, new_text, replace_all?}; original_text must match exactly and uniquely (or set replace_all) — a non-unique or missing anchor is rejected so files are never corrupted. Set dry_run to preview. Alternatively pass a unified 'diff' to apply its hunks as anchored edits. Far cheaper and safer than rewriting a whole file.\n- **shell_run**(command, cwd, timeout) [capability_expand category: shell]: Execute a one-shot shell command with timeout protection. Runs in the active workspace; paths resolving outside it are refused. Reach for a structured tool first when one fits — web_fetch for anything over HTTP(S), git_query/scm_sync for git, code_search/file_find/file_read for the repo, config_get/config_set for LeapFlow's own settings — because those report typed results, while a failed shell command can only be diagnosed from its exit code and stderr. Every shell run counts as an external side effect, so a failure stops the rest of the batch and is not retried automatically.\n- **terminal_open**(command, cwd, shell) [capability_expand category: terminal]: Open a PERSISTENT shell session (REPL/dev server/watch), returning a session_id for terminal_send/read/close. Disabled unless tools.terminal_session_enabled is set. For one-shot commands use shell_run instead.\n- **terminal_send**(session_id, input, wait) [capability_expand category: terminal]: Send a line of input to a persistent terminal session and return output captured shortly after.\n- **terminal_read**(session_id, wait): Drain buffered output from a persistent terminal session (optionally waiting briefly first).\n- **terminal_close**(session_id) [capability_expand category: terminal]: Terminate a persistent terminal session and release its process group.\n- **terminal_list**(): List active persistent terminal sessions.\n- **config_list**(category, limit): List LeapFlow's own writable settings (LLM model and endpoint, daemon, memory, perception, gateway, …) with current values. Use this to discover the exact key before changing anything. Optionally narrow by `category`. This is the only correct way to inspect LeapFlow configuration — never read config files from disk.\n- **config_get**(key): Read one LeapFlow setting by key (e.g. 'llm.model', 'llm.base_url', 'daemon.log_level'), returning its current value, type, scopes, and whether a change needs a daemon restart. There is no 'llm.provider' key: provider behavior is inferred from the OpenAI-compatible 'llm.base_url'. Never read LeapFlow config files from disk — use this.\n- **config_set**(key, value, scope) [capability_expand category: config]: Change one LeapFlow setting by key, e.g. switch an OpenAI-compatible LLM with key='llm.model' and key='llm.base_url'. There is no 'llm.provider' key: provider behavior is inferred from the endpoint. Values are validated and coerced; credentials are stored in the vault automatically. Call config_list or config_get first if unsure of the key. The result states whether a `leap daemon restart` is required. Never edit LeapFlow config files directly.\n- **web_fetch**(url, select, timeout, max_bytes): Read a URL over HTTP(S) and get back extracted, context-sized content: parsed JSON for API endpoints, readable text plus links for web pages. Use this for anything on the internet — prices, docs, releases, articles — instead of running curl through shell_run: it reports real HTTP status codes, retries rate limits on its own, and is a plain read so a retry is always safe. For JSON APIs pass `select` with a dotted path (e.g. 'chart.result.0.meta') to return just that part instead of the whole payload.\n- **memory_search**(query, limit): Search agent memory for relevant past experiences, observations, and facts.\n- **memory_add**(content, kind) [capability_expand category: write]: Store a new observation or insight in memory for future reference.\n- **research_note**(kind, text): Record a compact, structured note about the current task's state so it survives context compression on long / multi-step tasks. Use for durable findings, open questions still to resolve, decisions / excluded paths, and the immediate next step. One concise sentence per note.\n- **capability_expand**(category): Fetch the full callable schema for every tool in a capability category (e.g. 'hub', 'gateway', 'desktop', 'delegate', 'file', 'memory', 'skill'). The compact tool index always lists every registered tool by name and a one-line summary, but only a static low-risk subset is directly callable each turn. If you need a tool from the index that is not yet callable, call capability_expand with its category first; the matching tools become callable in this turn. Never invent a tool name — expand the category instead.\n- **delegate_task**(goal, context) [capability_expand category: delegate]: Delegate a complex sub-task to an isolated subagent. The subagent gets a fresh context and restricted tool access. Use when a task is self-contained and can be solved independently.\n- **schedule_reentry**(kind, reason, delay_seconds, event_match, max_reentries, deadline_seconds): Register a re-entry so this task can resume later from its current orientation (findings / open questions / next step). Use when work must pause and continue after a delay (kind=time) or when a matching platform event arrives (kind=event), instead of finishing now. The research-ledger state is carried over automatically.\n- **hub_push**(skill_name, visibility, version) [capability_expand category: hub]: Push a local skill to the ModelScope Hub for sharing or backup.\n- **hub_pull**(repo_id, version) [capability_expand category: hub]: Pull a skill from the ModelScope Hub to install locally.\n- **hub_search**(query) [capability_expand category: hub]: Search for skills on the Hub by keyword or description.\n- **hub_sync**(mode, dry_run) [capability_expand category: hub]: Preview or execute sync between local skills and Hub.\n- **platform_action**(platform, action, payload, backend_kind) [capability_expand category: gateway]: Execute an exact registered business action on an external platform through LeapFlow's App Connector layer. Actions must be copied from the App Connector Capability Index and are addressed as domain.operation, e.g. im.send_message or docs.create_markdown. All business fields (chat_id, text, query, etc.) MUST be placed inside `payload`, never at the top level. Example: {\"platform\":\"feishu\",\"action\":\"im.send_message\",\"payload\":{\"chat_id\":\"oc_xxx\",\"text\":\"hello\"}}. Do not invent action names, do not use management actions such as list/guide/connect/status here.\n- **platform_connect**(action, platform, credentials, options, checkpoint) [capability_expand category: gateway]: List, guide, connect, disconnect, remove, or check status for external platforms using the App Connector management namespace. Supports REST and CLI backends. Use this for management actions such as list/guide/preflight/connect/status; use platform_action only for exact registered business actions.\n- **gateway_send**(platform, chat_id, text, thread_id) [capability_expand category: gateway]: Send a message to a connected external platform (Feishu group, Telegram chat, DingTalk conversation, etc.). Requires the platform to be connected via gateway_connect first. Use gateway_connect with action='list' to see connected platforms and available chat IDs.\n- **gateway_connect**(action, platform, credentials, options) [capability_expand category: gateway]: Connect, configure, or manage external platform integrations (Feishu, DingTalk, Telegram, Slack, Discord, etc.). Conversational flow: 1) call 'guide' to get setup steps + required fields, 2) present the steps to the user and ask for ALL required credentials in a single message, 3) call 'connect' with the credentials. Goal: complete in 1–2 user turns. NEVER include credential values in your text response.\n- **plugin_list**(): List the live plugin registry and cross-subsystem capability evidence. Use this before answering questions about whether LeapFlow supports plugins, self-evolution, plugin installation, hot reload, versioning, or other runtime capabilities.\n- **plugin_status**(plugin_id): Get detailed status of a specific plugin: its declared category, runtime dependencies, contributed tools, and fiber lifecycle state.\n- **plugin_versions**(plugin_id): List recorded source versions and active pointer for a profile-scoped plugin.\n- **plugin_propose**(requested_capability, plugin_id, proposed_tools, test_cases, risk_level, evidence): Create a side-effect-free PluginProposal from explicit capability-gap evidence. Use this before plugin_generate when a missing capability should be reviewed. Does not call an LLM, write files, or install anything.\n- **assess_compatibility**(manifest, source_path) [capability_expand category: plugin_management]: Assess whether a foreign plugin manifest or real DSH source bundle is compatible with LeapFlow. A source bundle assessment is static: runtime_ready stays false until restricted Node discovery during plugin_install. Returns component-level verdicts and limitations.\n- **plugin_generate**(plugin_id, description, proposal_id) [capability_expand category: system]: Generate a new ToolPlugin from a natural-language capability description. The LLM produces code that conforms to the ToolPlugin Protocol; it is then rigorously validated (syntax, structure, import, protocol conformance). The generated source is stored in CAS and receives explicit content approval. It DOES NOT install the plugin — installation requires a second, mutation-specific approval via plugin_install.\n- **plugin_install**(plugin_id, code, marketplace_name, source_path, proposal_id, version_label) [capability_expand category: system]: Install a plugin from exactly one source: validated Python code, the configured Python marketplace, or a real DSH source bundle. DSH bundles run restricted Node discovery before registration; only public host tools are installed and client UI limitations are reported. Writes profile-scoped state and mutates the process-global registry. REQUIRES APPROVAL.\n- **plugin_rollback**(plugin_id, version) [capability_expand category: system]: Rollback a profile-scoped plugin to a recorded source version and reload it. REQUIRES APPROVAL.\n- **plugin_reload**(plugin_id, version_label) [capability_expand category: system]: Hot-reload a plugin at runtime. Disposes the old plugin fiber, re-imports its module, and registers a fresh instance. Existing in-flight turns are unaffected (snapshot isolation). REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_disable**(plugin_id) [capability_expand category: system]: Disable a plugin by disposing its fiber, removing its tools from the runtime registry. Cannot disable self_management itself. REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_remove**(plugin_id, delete_source) [capability_expand category: system]: Terminally remove a plugin: dispose its fiber, unregister its tools, remove reload metadata, and optionally delete its profile-scoped source file. Cannot remove self_management itself. REQUIRES APPROVAL.\n- **plugin_unquarantine**(plugin_id) [capability_expand category: plugin_management]: Restore a quarantined plugin to probation status for re-evaluation. Unfreezes the trust ledger, transitions the proposal back to PROBATION, and reloads the plugin. REQUIRES APPROVAL.\n- **plugin_enable**(plugin_id) [capability_expand category: system]: Re-enable a previously disabled plugin by reloading its module and registering a fresh instance. REQUIRES APPROVAL.\n- **self_describe**(facet): Introspect LeapFlow's own identity, capabilities, runtime state, evolution metrics, or platform connections. Use facet='all' only when a comprehensive self-check is explicitly requested.\n- **runtime_snapshot**(): Lightweight (~150 token) flat snapshot of current runtime state: model, context budget, posture, disclosure level, turn count, cache hit rate, uptime, tool count, and pending approvals.\n- **schedule_create**(trigger_expression, instruction, execution_mode, max_retries, delivery_target) [capability_expand category: scheduler]: Create a new scheduled task. Specify a trigger expression (e.g. '30m', 'every 2h', '0 9 * * *') and the instruction to execute on each trigger. Optionally provide an execution mode and a delivery target to receive notifications.\n- **schedule_list**() [capability_expand category: scheduler]: List all scheduled tasks with their current state, trigger, and next due time.\n- **schedule_status**(task_id) [capability_expand category: scheduler]: Get detailed status and recent execution history for a scheduled task.\n- **schedule_pause**(task_id) [capability_expand category: scheduler]: Pause a scheduled task so it stops firing without being cancelled.\n- **schedule_resume**(task_id) [capability_expand category: scheduler]: Resume a paused scheduled task — re-arms it and recalculates next due time.\n- **schedule_cancel**(task_id) [capability_expand category: scheduler]: Cancel a scheduled task permanently.\n- **tool_search**(query, max_results): Search all registered tools by keyword relevance. Returns a ranked list of matching tools with name, category, and summary. Use this when you need a tool but are unsure of its exact name or category.\n- **tool_describe**(tool_name): Get the full callable schema of a single tool by exact name. Returns the tool's description, parameters, and metadata. Use after tool_search to inspect a specific tool before calling it.\n- **session_search**(query, limit) [capability_expand category: unclassified]: Search past conversation sessions in the current workspace for relevant context.\n- **session_list**(limit): List recent conversation sessions in the current workspace with stable ids, titles, dates, and summaries. Use for browsing past tasks or when user asks to see history without specific search terms. No keywords needed — returns chronological list.\n- **session_detail**(session_id, limit, offset, include_inactive) [capability_expand category: unclassified]: Read a paginated persisted transcript for one past conversation session in the current workspace. Use after session_list or session_search returns a session_id.\n- **click**(element_index) [capability_expand category: desktop]: Click a UI element by its element_index (from the latest observe_ui snapshot)\n- **get_clipboard**() [capability_expand category: desktop]: Read current clipboard text content\n- **list_apps**(filter, running_only) [capability_expand category: desktop]: List available applications on this system. Use to discover correct bundle_id before switch_app.\n- **list_windows**() [capability_expand category: desktop]: List all top-level windows with pid, window_id, title, and per-window state (minimized, on-screen). Call this first to pick the pid and window_id that observe_ui and other window tools require.\n- **observe_ui**(pid, window_id, query) [capability_expand category: desktop]: Snapshot one window's actionable UI elements, each tagged with an element_index for click/right_click/read_text. Re-observe after actions — indices belong to one snapshot. Requires the window's pid and window_id from list_windows.\n- **open_url**(url, app_id) [capability_expand category: desktop]: Open a URL in the default or specified browser\n- **read_text**(element_index) [capability_expand category: desktop]: Read the text content of a specific UI element from the latest snapshot\n- **right_click**(element_index) [capability_expand category: desktop]: Right-click a UI element to open its context menu. Returns visible menu items.\n- **screenshot**(pid, window_id) [capability_expand category: desktop]: Capture a screenshot for visual verification. With pid + window_id captures that window (works across all displays); defaults to the last observed window, or the full desktop when no window has been observed.\n- **scroll**(element_index, direction, amount, pid, window_id) [capability_expand category: desktop]: Scroll a scrollable area of a window. Omit element_index to scroll the window's focused/page scroller; pass one to scroll an exact element from the latest snapshot.\n- **select_text**(element_index) [capability_expand category: desktop]: Select all text in a UI element (focus + select-all, for subsequent copy)\n- **set_clipboard**(text) [capability_expand category: desktop]: Write text to the clipboard\n- **shortcut**(keys) [capability_expand category: desktop]: Execute a keyboard shortcut\n- **switch_app**(app_id) [capability_expand category: desktop]: Switch to an app (launch if needed, activate, verify)\n- **type_text**(text) [capability_expand category: desktop]: Type text into the currently focused element\n- **wait**(seconds) [capability_expand category: desktop]: Wait for a specified duration before continuing\n- **wait_until**(condition, pid, window_id, timeout, poll_interval) [capability_expand category: desktop]: Wait until a UI condition is met (polls UI tree). Returns elements when found or on timeout.\n- **wait_until_stable**(timeout, poll_interval, pid, window_id) [capability_expand category: desktop]: Wait until the UI stops changing (element set stabilizes across polls).\n\n## App Connector Capability Index\nLeapFlow can onboard and manage external apps through `platform_connect` and execute exact registered business actions through `platform_action`.\nFor requests about connecting, setting up, configuring, enabling, or managing a supported app, use `platform_connect` first instead of generating SDK/Webhook sample code.\n`platform_connect.action` is the management namespace: list, guide, preflight, connect, disconnect, remove, status, events_start, events_stop, events_status.\n`platform_action.action` is only for exact registered platform business actions listed below, such as `im.send_message`; never use management actions like `list` or `guide` there.\nAll business fields MUST go inside `payload`; top-level keys are only `platform`, `action`, and `payload`.\nDo not invent platform IDs or platform action names. If the needed action is not listed, ask for discovery/clarification instead of guessing.\nUse `platform_connect` with `action='guide'` and the matching `platform` to start onboarding; use `action='list'` when the app is unclear.\nWhen a pending onboarding state is present, continue from that state with `platform_connect` instead of asking the user to restate the app.\nSupported apps:\n- `api_server`: API Server (OpenAI Compatible) (category=api; backend=adapter; platform_action actions=none registered)\n- `dingtalk`: 钉钉 (DingTalk) (category=im; backend=adapter; platform_action actions=none registered)\n- `feishu`: 飞书 (Feishu/Lark) (category=im; backend=cli)\n - `calendar.create_event` payload={summary*, start_time*, end_time*, attendees} [write/high]\n - `docs.create_markdown` payload={title*, markdown*, folder_token} [write/medium]\n - `drive.search` payload={query*, limit} [read/medium]\n - `im.add_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.download_resource` payload={message_id*, file_key*, type*} [read/low]\n - `im.get_messages` payload={message_ids*} [read/low]\n - `im.list_chats` payload={page_size} [read/low]\n - `im.list_messages` payload={chat_id*} [read/low]\n - `im.list_thread_messages` payload={thread*} [read/low]\n - `im.remove_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.reply_message` payload={message_id*, text*} [send/high]\n - `im.search_chats` payload={query*, page_size} [read/low]\n - `im.search_messages` payload={query*} [read/medium]\n - `im.send_message` payload={chat_id*, text*, thread_id} [send/high]\n - `im.update_card` payload={token*, card*} [write/medium]\n - `im.update_message` payload={message_id*, text*} [write/medium]\n - `mail.search_unread` payload={query, limit} [read/high]\n - `sheets.append_row` payload={spreadsheet_token*, sheet_id*, values*} [write/medium]\n - `task.create` payload={title*, description, due_time} [write/medium]\n- `telegram`: Telegram (category=social; backend=adapter; platform_action actions=none registered)\n- `webhook`: Webhook (Generic) (category=webhook; backend=adapter; platform_action actions=none registered)\n\n## Tool Usage\nTools are normally invoked through the native function-calling mechanism, not by writing JSON in your reply\ntext. Only if the provider signals that native function calling is unavailable for this turn, fall back to a\nsingle JSON code block: `{\"name\": \"tool_name\", \"arguments\": {\"key\": \"value\"}}` — use this fallback format\nonly, never both. Only call a tool whose exact name appears in the tool index above and is currently callable\n(or reachable via `capability_expand`). Never invent, rename, alias, or guess a tool name, platform ID, or\nplatform action from argument shape or wording — if the index does not list it, it does not exist.\n`platform_connect.action` (list/guide/preflight/connect/disconnect/remove/status/events_start/events_stop/\nevents_status) is the App Connector management namespace; `platform_action.action` only accepts exact\nregistered business actions such as `im.send_message` shown in the App Connector Capability Index — never mix\nthe two namespaces. If a tool call returns an unknown/unavailable result, use the returned suggestions or\navailable names for a single retry instead of trying further variations of the same guess.\n\n**Side-effect action rule** (`platform_action` with effect=send/write/execute):\n- Call each unique action+payload **exactly once**. Never include duplicates in the same turn.\n- Once the result returns `\"completed\": true`, that action is DONE for this task. Do NOT call it again\n in any subsequent turn — immediately summarize the result for the user instead.\n- The system enforces idempotency: duplicate calls are blocked and will not execute.\n- If the user explicitly requests sending/writing multiple times, use distinct payloads per call.\n\n**Resource identifier provenance rule**:\n- NEVER fabricate, guess, or infer resource identifiers (chat_id, message_id, file_key, user_id, etc.).\n Every resource ID used in a side-effect action MUST come from a successful API response in this session.\n- If a read/list action fails (e.g. authorization error), you do NOT have valid resource IDs.\n Report the failure to the user — do NOT attempt the dependent write/send action with a guessed ID.\n- When a tool result contains `\"llm_instruction\"`, follow it exactly.\n\n## Guidelines\n1. **Direct answers first**: If you already know the answer, respond directly without tools.\n2. **Avoid redundant tool calls**: Do not call the same tool with the same arguments more than once in the same user turn. When an existing tool result already answers the user's request, stop calling tools and answer directly.\n3. **Use tools proactively**: When the user asks about files, time, system state, or needs actions performed, use the appropriate tool.\n4. **Chain tools when needed**: You can call multiple tools in sequence (e.g., list files → read file → summarize).\n5. **Handle failures gracefully**: If a tool fails, explain what went wrong and suggest alternatives. If it failed because of an unknown tool/platform/action name, retry once with an exact name from the returned suggestions, then explain rather than keep guessing.\n6. **Summarize results naturally**: After tool execution, synthesize the results into a helpful answer rather than dumping raw output.\n7. **Stay conversational**: Maintain a natural, helpful tone. Acknowledge context from earlier in the conversation.\n8. **Recall past work**:\n - Broad queries (\"之前做了什么\", \"列出任务\"): answer from the \"Recent Task History\" section already in your context. If insufficient, call session_list.\n - Specific lookups (\"上次那个配置怎么改的\"): call session_search with relevant phrases (NOT single characters).\n - Do NOT call search tools repeatedly with keyword fragments. One well-phrased call is better than ten fragmented ones.\n\n## Coding & Verification\nWhen working with code, prefer the precise built-in tools over ad-hoc shell: use `repo_map` to orient in an\nunfamiliar project, `code_search`/`file_find` to locate and `code_intel` for symbols, `edit_file` (anchored\nsearch-replace, or a unified `diff`) to change files — never rewrite a whole file to change a few lines — and\n`git_query` to inspect diffs/log. After edits, check `syntax_ok` in the result and run `test_run`/`lint_check`\nbefore declaring the work done. Batch independent read-only calls (search/read/query) into a single turn — they\nrun in parallel.\n\n## Presentation Style\n1. **Polished Markdown only**: Format user-facing answers with clean Markdown headings, short paragraphs, and concise bullets. Use tables only when they improve comparison or scanning.\n2. **Terminal-friendly layout**: Keep lines readable in a TUI; avoid dense walls of text, deeply nested lists, oversized ASCII art, or heavy visual blocks.\n3. **Elegant emphasis**: Use bold text sparingly for key terms and conclusions. Avoid excessive emojis, decorative symbols, repeated separators, or visual noise.\n4. **Theme-safe colors**: Do not emit ANSI escape codes, HTML color tags, Rich markup, or hardcoded color names. Rely on the TUI theme to render Markdown professionally.\n5. **No leaked tool protocol**: Never show tool-call JSON, internal schemas, raw observations, tool result payloads, or hidden reasoning in the final answer unless the user explicitly asks for raw/debug output. Treat any prior `{\"name\": ..., \"arguments\": ...}` blocks and `Tool result (...)` messages as internal execution context only.\n6. **Professional closure**: End with a concise conclusion or next step when helpful; avoid rambling after the useful answer is complete.\n\nWhen finished with all tool calls, respond normally without a JSON block, tool-call transcript, or process log.\n\n## Task Contract\n- Task ID: turn-1\n- Original user request: Anything to report?\n- Workspace root: \n- Allowed roots: \n- Treat relative project paths as relative to the workspace root; never infer `.` as the project root when a workspace root is provided.\n- Workspace boundary is enforced by tools: do not read, search, edit, or run commands against paths outside the allowed roots unless the user explicitly requests an external path and the tool/approval policy permits it.\n- LeapFlow workspace config is optional at `/.leapflow/config.yaml`; runtime config is loaded from `~/.leapflow/config/user.yaml` and `~/.leapflow/profiles//config/*.yaml`.\n- Preserve this task contract across summarization, compression, tool loops, and memory retrieval.\n" - }, - { - "role": "system", - "content": "\n## Recent Session Summary\n- [user] Anything to report?\n" - }, - { - "role": "user", - "content": "Anything to report?\nAnything to report?" - } - ], - "tools": [ - "capability_expand", - "code_intel", - "code_search", - "config_get", - "config_list", - "env_info", - "file_find", - "file_list", - "file_read", - "git_query", - "lint_check", - "memory_search", - "plugin_list", - "plugin_propose", - "plugin_status", - "plugin_versions", - "repo_map", - "research_note", - "runtime_snapshot", - "schedule_reentry", - "self_describe", - "session_list", - "skill_view", - "skills_list", - "terminal_list", - "terminal_read", - "test_run", - "text_search", - "time_get", - "tool_describe", - "tool_search", - "web_fetch" - ] - }, - "responses": [ - { - "status": 200, - "content_type": "application/json", - "body": "{\"id\": \"chatcmpl-cassette\", \"object\": \"chat.completion\", \"model\": \"cassette-model\", \"choices\": [{\"index\": 0, \"message\": {\"role\": \"assistant\", \"content\": \"Acknowledged.\"}, \"finish_reason\": \"stop\"}], \"usage\": {\"prompt_tokens\": 64, \"completion_tokens\": 16, \"total_tokens\": 80}}" - } - ] -} diff --git a/tests/_fixtures/cassettes/r3_control_plane/cassette-model-7adcad6b98235ca1.cassette.json b/tests/_fixtures/cassettes/r3_control_plane/cassette-model-7adcad6b98235ca1.cassette.json new file mode 100644 index 00000000..5906d8e4 --- /dev/null +++ b/tests/_fixtures/cassettes/r3_control_plane/cassette-model-7adcad6b98235ca1.cassette.json @@ -0,0 +1,63 @@ +{ + "fingerprint": "7adcad6b98235ca17deeddbc3492c950eaa04de2151e953269f537ecc91b1ee6", + "note": "captured in seed mode", + "request": { + "model": "cassette-model", + "stream": false, + "messages": [ + { + "role": "system", + "content": "You are LeapFlow, an intelligent assistant that can both converse naturally and take real actions on the user's computer.\n\n## Capabilities\nThe tool index below lists **every** registered tool by name and a one-line summary — this is the complete\ncapability contract; nothing else exists. Only a subset is directly callable this turn (via native tool calling,\nnot a JSON block in your reply). If you need a tool from the index that is not yet callable, call\n`capability_expand` with its category name first — the matching tools become callable immediately after.\nWhen the user asks about LeapFlow **itself** — its identity/version/model, what tools, plugins, or capabilities\nit has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of\nself-awareness — answer from live runtime evidence via `self_describe` (facet=capabilities for \"what can you\ndo / do you support X\", identity for version/model, runtime for context/posture, or `all` for a full self-check),\nnever by reading LeapFlow's own source code, documentation, or memory. Report configuration-dependent or\nunavailable capabilities as limitations rather than inferring them.\n- **text_search**(text, pattern): Search for a regex pattern in text.\n- **text_replace**(text, old, new, count) [capability_expand category: write]: Replace occurrences of a substring in text.\n- **time_get**(): Get current date and time.\n- **env_info**(): Get system environment information (OS, Python version, cwd).\n- **skills_list**(query, category, source): List available learned skills. Use when user asks about capabilities or you need a specific skill.\n- **skill_view**(name): View the full content of a specific skill document.\n- **skill_list_files**(name) [capability_expand category: skills]: List all support files in a skill directory (configs, templates, examples). Use before skill_file_read to discover available files.\n- **skill_file_read**(name, file_path) [capability_expand category: skills]: Read an individual support file from a skill directory. Use skill_list_files first to discover available files.\n- **code_intel**(path, operation): Precise document symbols (outline) for a source file: classes, functions, and methods with line ranges. Python uses an exact AST parse; other languages use a keyword-prefix scan. Prefer over file_read mode=symbols for accurate navigation before editing. Read-only.\n- **repo_map**(path): Compact project orientation for a repository root: languages, detected test/lint commands, top-level structure, entry points, manifest, and VCS branch. Call this first when entering an unfamiliar codebase. Read-only.\n- **scm_sync**(action, cwd, remote, pull_ref, push_ref, timeout) [capability_expand category: scm]: Run a typed git SCM action. Use this instead of shell_run for git pull/push/status. For 'pull origin main then push', set action='pull_then_push', remote='origin', pull_ref='main', and omit push_ref so LeapFlow pushes the current local branch.\n- **git_query**(action, cwd, ref, path, staged, max_count, stat): Read-only structured git inspection: action=diff|log|status|branch|show. Prefer over shell_run for reading repo state — output is clipped, redacted, and log/branch are parsed into structured fields. Use scm_sync for pull/push.\n- **git_write**(action, cwd, message, stage_all, name, ref, create) [capability_expand category: scm]: Mutating git actions: action=commit (message, stage_all), branch (create+switch), checkout (switch; create=true for -b). Approval-gated. Use scm_sync for pull/push and git_query for reads.\n- **test_run**(command, cwd, timeout): Run the project's test suite and return structured results (framework, passed/failed counts, failing tests). Auto-detects the runner (pytest/npm/go/cargo) or uses a configured/explicit command; executes via the governed shell. ok=true means the runner executed — see 'success' for pass/fail.\n- **lint_check**(command, cwd, timeout): Run the project's linter and return a structured clean/issue result. Auto-detects the linter (ruff/eslint/go vet/clippy) or uses a configured/explicit command; executes via the governed shell. ok=true means the linter ran — see 'clean'.\n- **file_list**(path, pattern, depth): List files and directories at a given path. Use depth=1 or depth=2 to get a recursive tree in one call instead of listing each sub-directory separately.\n- **file_read**(path, max_lines, start_line, max_chars, mode): Read text file content with adaptive context governance. For large or unfamiliar files, prefer mode='outline' or mode='symbols' first, then use mode='raw' with start_line/max_lines for the specific range you actually need. For LeapFlow's own settings, use config_list / config_get / config_set — its config files are outside the workspace and not readable here.\n- **file_write**(path, content, mode) [capability_expand category: write]: Write content to a file (overwrite or append).\n- **code_search**(pattern, patterns, path, glob, ignore_case, multiline, max_results, context_lines): Search file CONTENTS by regex pattern across a directory tree (ripgrep-backed). Requires a regex pattern. NOT for listing or browsing directory contents — use file_list for that. Prefer this over shell_run grep: faster, skips VCS/dependency/build dirs, and returns structured path:line:column matches. Batch related lookups into ONE call via `patterns` (OR-combined, single pass) instead of issuing several separate searches. Use file_read for the surrounding context of a hit.\n- **file_find**(glob, path, max_results): Find files by a recursive glob pattern under a base path (e.g. '**/test_*.py' or '*.md'). Prefer this over shell_run find; skips VCS/dependency/build dirs.\n- **edit_file**(path, edits, dry_run, diff) [capability_expand category: write]: Apply targeted, anchored search-replace edits to an EXISTING text file (use file_write to create/overwrite). Each edit is {original_text, new_text, replace_all?}; original_text must match exactly and uniquely (or set replace_all) — a non-unique or missing anchor is rejected so files are never corrupted. Set dry_run to preview. Alternatively pass a unified 'diff' to apply its hunks as anchored edits. Far cheaper and safer than rewriting a whole file.\n- **shell_run**(command, cwd, timeout) [capability_expand category: shell]: Execute a one-shot shell command with timeout protection. Runs in the active workspace; paths resolving outside it are refused. Reach for a structured tool first when one fits — web_fetch for anything over HTTP(S), git_query/scm_sync for git, code_search/file_find/file_read for the repo, config_get/config_set for LeapFlow's own settings — because those report typed results, while a failed shell command can only be diagnosed from its exit code and stderr. Every shell run counts as an external side effect, so a failure stops the rest of the batch and is not retried automatically.\n- **terminal_open**(command, cwd, shell) [capability_expand category: terminal]: Open a PERSISTENT shell session (REPL/dev server/watch), returning a session_id for terminal_send/read/close. Disabled unless tools.terminal_session_enabled is set. For one-shot commands use shell_run instead.\n- **terminal_send**(session_id, input, wait) [capability_expand category: terminal]: Send a line of input to a persistent terminal session and return output captured shortly after.\n- **terminal_read**(session_id, wait): Drain buffered output from a persistent terminal session (optionally waiting briefly first).\n- **terminal_close**(session_id) [capability_expand category: terminal]: Terminate a persistent terminal session and release its process group.\n- **terminal_list**(): List active persistent terminal sessions.\n- **config_list**(category, limit): List LeapFlow's own writable settings (LLM model and endpoint, daemon, memory, perception, gateway, …) with current values. Use this to discover the exact key before changing anything. Optionally narrow by `category`. This is the only correct way to inspect LeapFlow configuration — never read config files from disk.\n- **config_get**(key): Read one LeapFlow setting by key (e.g. 'llm.model', 'llm.base_url', 'daemon.log_level'), returning its current value, type, scopes, and whether a change needs a daemon restart. There is no 'llm.provider' key: provider behavior is inferred from the OpenAI-compatible 'llm.base_url'. Never read LeapFlow config files from disk — use this.\n- **config_set**(key, value, scope) [capability_expand category: config]: Change one LeapFlow setting by key, e.g. switch an OpenAI-compatible LLM with key='llm.model' and key='llm.base_url'. There is no 'llm.provider' key: provider behavior is inferred from the endpoint. Values are validated and coerced; credentials are stored in the vault automatically. Call config_list or config_get first if unsure of the key. The result states whether a `leap daemon restart` is required. Never edit LeapFlow config files directly.\n- **web_fetch**(url, select, timeout, max_bytes): Read a URL over HTTP(S) and get back extracted, context-sized content: parsed JSON for API endpoints, readable text plus links for web pages. Use this for anything on the internet — prices, docs, releases, articles — instead of running curl through shell_run: it reports real HTTP status codes, retries rate limits on its own, and is a plain read so a retry is always safe. For JSON APIs pass `select` with a dotted path (e.g. 'chart.result.0.meta') to return just that part instead of the whole payload.\n- **memory_search**(query, limit): Search agent memory for relevant past experiences, observations, and facts.\n- **memory_add**(content, kind) [capability_expand category: write]: Store a new observation or insight in memory for future reference.\n- **research_note**(kind, text): Record a compact, structured note about the current task's state so it survives context compression on long / multi-step tasks. Use for durable findings, open questions still to resolve, decisions / excluded paths, and the immediate next step. One concise sentence per note.\n- **capability_expand**(category): Fetch the full callable schema for every tool in a capability category (e.g. 'hub', 'gateway', 'desktop', 'delegate', 'file', 'memory', 'skill'). The compact tool index always lists every registered tool by name and a one-line summary, but only a static low-risk subset is directly callable each turn. If you need a tool from the index that is not yet callable, call capability_expand with its category first; the matching tools become callable in this turn. Never invent a tool name — expand the category instead.\n- **delegate_task**(goal, context) [capability_expand category: delegate]: Delegate a complex sub-task to an isolated subagent. The subagent gets a fresh context and restricted tool access. Use when a task is self-contained and can be solved independently.\n- **schedule_reentry**(kind, reason, delay_seconds, event_match, max_reentries, deadline_seconds): Register a re-entry so this task can resume later from its current orientation (findings / open questions / next step). Use when work must pause and continue after a delay (kind=time) or when a matching platform event arrives (kind=event), instead of finishing now. The research-ledger state is carried over automatically.\n- **hub_push**(skill_name, visibility, version) [capability_expand category: hub]: Push a local skill to the ModelScope Hub for sharing or backup.\n- **hub_pull**(repo_id, version) [capability_expand category: hub]: Pull a skill from the ModelScope Hub to install locally.\n- **hub_search**(query) [capability_expand category: hub]: Search for skills on the Hub by keyword or description.\n- **hub_sync**(mode, dry_run) [capability_expand category: hub]: Preview or execute sync between local skills and Hub.\n- **platform_action**(platform, action, payload, backend_kind) [capability_expand category: gateway]: Execute an exact registered business action on an external platform through LeapFlow's App Connector layer. Actions must be copied from the App Connector Capability Index and are addressed as domain.operation, e.g. im.send_message or docs.create_markdown. All business fields (chat_id, text, query, etc.) MUST be placed inside `payload`, never at the top level. Example: {\"platform\":\"feishu\",\"action\":\"im.send_message\",\"payload\":{\"chat_id\":\"oc_xxx\",\"text\":\"hello\"}}. Do not invent action names, do not use management actions such as list/guide/connect/status here.\n- **platform_connect**(action, platform, credentials, options, checkpoint) [capability_expand category: gateway]: List, guide, connect, disconnect, remove, or check status for external platforms using the App Connector management namespace. Supports REST and CLI backends. Use this for management actions such as list/guide/preflight/connect/status; use platform_action only for exact registered business actions.\n- **gateway_send**(platform, chat_id, text, thread_id) [capability_expand category: gateway]: Send a message to a connected external platform (Feishu group, Telegram chat, DingTalk conversation, etc.). Requires the platform to be connected via gateway_connect first. Use gateway_connect with action='list' to see connected platforms and available chat IDs.\n- **gateway_connect**(action, platform, credentials, options) [capability_expand category: gateway]: Connect, configure, or manage external platform integrations (Feishu, DingTalk, Telegram, Slack, Discord, etc.). Conversational flow: 1) call 'guide' to get setup steps + required fields, 2) present the steps to the user and ask for ALL required credentials in a single message, 3) call 'connect' with the credentials. Goal: complete in 1–2 user turns. NEVER include credential values in your text response.\n- **plugin_list**(): List the live plugin registry across Tool/Gateway/LLM subsystems for plugin management: which plugins are loaded, their category and fiber lifecycle state. For questions about LeapFlow's own capabilities or whether it supports X, use self_describe instead.\n- **plugin_status**(plugin_id): Get detailed status of a specific plugin: its declared category, runtime dependencies, contributed tools, and fiber lifecycle state.\n- **plugin_versions**(plugin_id): List recorded source versions and active pointer for a profile-scoped plugin.\n- **plugin_propose**(requested_capability, plugin_id, proposed_tools, test_cases, risk_level, evidence): Create a side-effect-free PluginProposal from explicit capability-gap evidence. Use this before plugin_generate when a missing capability should be reviewed. Does not call an LLM, write files, or install anything.\n- **assess_compatibility**(manifest, source_path) [capability_expand category: plugin_management]: Assess whether a foreign plugin manifest or real DSH source bundle is compatible with LeapFlow. A source bundle assessment is static: runtime_ready stays false until restricted Node discovery during plugin_install. Returns component-level verdicts and limitations.\n- **plugin_generate**(plugin_id, description, proposal_id) [capability_expand category: system]: Generate a new ToolPlugin from a natural-language capability description. The LLM produces code that conforms to the ToolPlugin Protocol; it is then rigorously validated (syntax, structure, import, protocol conformance). The generated source is stored in CAS and receives explicit content approval. It DOES NOT install the plugin — installation requires a second, mutation-specific approval via plugin_install.\n- **plugin_install**(plugin_id, code, marketplace_name, source_path, proposal_id, version_label) [capability_expand category: system]: Install a plugin from exactly one source: validated Python code, the configured Python marketplace, or a real DSH source bundle. DSH bundles run restricted Node discovery before registration; only public host tools are installed and client UI limitations are reported. Writes profile-scoped state and mutates the process-global registry. REQUIRES APPROVAL.\n- **plugin_rollback**(plugin_id, version) [capability_expand category: system]: Rollback a profile-scoped plugin to a recorded source version and reload it. REQUIRES APPROVAL.\n- **plugin_reload**(plugin_id, version_label) [capability_expand category: system]: Hot-reload a plugin at runtime. Disposes the old plugin fiber, re-imports its module, and registers a fresh instance. Existing in-flight turns are unaffected (snapshot isolation). REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_disable**(plugin_id) [capability_expand category: system]: Disable a plugin by disposing its fiber, removing its tools from the runtime registry. Cannot disable self_management itself. REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_remove**(plugin_id, delete_source) [capability_expand category: system]: Terminally remove a plugin: dispose its fiber, unregister its tools, remove reload metadata, and optionally delete its profile-scoped source file. Cannot remove self_management itself. REQUIRES APPROVAL.\n- **plugin_unquarantine**(plugin_id) [capability_expand category: plugin_management]: Restore a quarantined plugin to probation status for re-evaluation. Unfreezes the trust ledger, transitions the proposal back to PROBATION, and reloads the plugin. REQUIRES APPROVAL.\n- **plugin_enable**(plugin_id) [capability_expand category: system]: Re-enable a previously disabled plugin by reloading its module and registering a fresh instance. REQUIRES APPROVAL.\n- **self_describe**(facet): Answer questions about LeapFlow ITSELF from live runtime evidence — its identity/version/model, what tools and plugins it has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of self-awareness. Prefer this over reading LeapFlow's own source code. Pick facet=capabilities for 'what can you do / do you support X', identity for version/model, runtime for context/posture, or all for a full self-check when explicitly requested.\n- **runtime_snapshot**(): Lightweight (~150 token) flat snapshot of current runtime state: model, context budget, posture, disclosure level, turn count, cache hit rate, uptime, tool count, and pending approvals.\n- **schedule_create**(trigger_expression, instruction, execution_mode, max_retries, delivery_target) [capability_expand category: scheduler]: Create a new scheduled task. Specify a trigger expression (e.g. '30m', 'every 2h', '0 9 * * *') and the instruction to execute on each trigger. Optionally provide an execution mode and a delivery target to receive notifications.\n- **schedule_list**() [capability_expand category: scheduler]: List all scheduled tasks with their current state, trigger, and next due time.\n- **schedule_status**(task_id) [capability_expand category: scheduler]: Get detailed status and recent execution history for a scheduled task.\n- **schedule_pause**(task_id) [capability_expand category: scheduler]: Pause a scheduled task so it stops firing without being cancelled.\n- **schedule_resume**(task_id) [capability_expand category: scheduler]: Resume a paused scheduled task — re-arms it and recalculates next due time.\n- **schedule_cancel**(task_id) [capability_expand category: scheduler]: Cancel a scheduled task permanently.\n- **tool_search**(query, max_results): Search all registered tools by keyword relevance. Returns a ranked list of matching tools with name, category, and summary. Use this when you need a tool but are unsure of its exact name or category.\n- **tool_describe**(tool_name): Get the full callable schema of a single tool by exact name. Returns the tool's description, parameters, and metadata. Use after tool_search to inspect a specific tool before calling it.\n- **session_search**(query, limit) [capability_expand category: unclassified]: Search past conversation sessions in the current workspace for relevant context.\n- **session_list**(limit): List recent conversation sessions in the current workspace with stable ids, titles, dates, and summaries. Use for browsing past tasks or when user asks to see history without specific search terms. No keywords needed — returns chronological list.\n- **session_detail**(session_id, limit, offset, include_inactive) [capability_expand category: unclassified]: Read a paginated persisted transcript for one past conversation session in the current workspace. Use after session_list or session_search returns a session_id.\n- **click**(element_index) [capability_expand category: desktop]: Click a UI element by its element_index (from the latest observe_ui snapshot)\n- **get_clipboard**() [capability_expand category: desktop]: Read current clipboard text content\n- **list_apps**(filter, running_only) [capability_expand category: desktop]: List available applications on this system. Use to discover correct bundle_id before switch_app.\n- **list_windows**() [capability_expand category: desktop]: List all top-level windows with pid, window_id, title, and per-window state (minimized, on-screen). Call this first to pick the pid and window_id that observe_ui and other window tools require.\n- **observe_ui**(pid, window_id, query) [capability_expand category: desktop]: Snapshot one window's actionable UI elements, each tagged with an element_index for click/right_click/read_text. Re-observe after actions — indices belong to one snapshot. Requires the window's pid and window_id from list_windows.\n- **open_url**(url, app_id) [capability_expand category: desktop]: Open a URL in the default or specified browser\n- **read_text**(element_index) [capability_expand category: desktop]: Read the text content of a specific UI element from the latest snapshot\n- **right_click**(element_index) [capability_expand category: desktop]: Right-click a UI element to open its context menu. Returns visible menu items.\n- **screenshot**(pid, window_id) [capability_expand category: desktop]: Capture a screenshot for visual verification. With pid + window_id captures that window (works across all displays); defaults to the last observed window, or the full desktop when no window has been observed.\n- **scroll**(element_index, direction, amount, pid, window_id) [capability_expand category: desktop]: Scroll a scrollable area of a window. Omit element_index to scroll the window's focused/page scroller; pass one to scroll an exact element from the latest snapshot.\n- **select_text**(element_index) [capability_expand category: desktop]: Select all text in a UI element (focus + select-all, for subsequent copy)\n- **set_clipboard**(text) [capability_expand category: desktop]: Write text to the clipboard\n- **shortcut**(keys) [capability_expand category: desktop]: Execute a keyboard shortcut\n- **switch_app**(app_id) [capability_expand category: desktop]: Switch to an app (launch if needed, activate, verify)\n- **type_text**(text) [capability_expand category: desktop]: Type text into the currently focused element\n- **wait**(seconds) [capability_expand category: desktop]: Wait for a specified duration before continuing\n- **wait_until**(condition, pid, window_id, timeout, poll_interval) [capability_expand category: desktop]: Wait until a UI condition is met (polls UI tree). Returns elements when found or on timeout.\n- **wait_until_stable**(timeout, poll_interval, pid, window_id) [capability_expand category: desktop]: Wait until the UI stops changing (element set stabilizes across polls).\n\n## App Connector Capability Index\nLeapFlow can onboard and manage external apps through `platform_connect` and execute exact registered business actions through `platform_action`.\nFor requests about connecting, setting up, configuring, enabling, or managing a supported app, use `platform_connect` first instead of generating SDK/Webhook sample code.\n`platform_connect.action` is the management namespace: list, guide, preflight, connect, disconnect, remove, status, events_start, events_stop, events_status.\n`platform_action.action` is only for exact registered platform business actions listed below, such as `im.send_message`; never use management actions like `list` or `guide` there.\nAll business fields MUST go inside `payload`; top-level keys are only `platform`, `action`, and `payload`.\nDo not invent platform IDs or platform action names. If the needed action is not listed, ask for discovery/clarification instead of guessing.\nUse `platform_connect` with `action='guide'` and the matching `platform` to start onboarding; use `action='list'` when the app is unclear.\nWhen a pending onboarding state is present, continue from that state with `platform_connect` instead of asking the user to restate the app.\nSupported apps:\n- `api_server`: API Server (OpenAI Compatible) (category=api; backend=adapter; platform_action actions=none registered)\n- `dingtalk`: 钉钉 (DingTalk) (category=im; backend=adapter; platform_action actions=none registered)\n- `feishu`: 飞书 (Feishu/Lark) (category=im; backend=cli)\n - `calendar.create_event` payload={summary*, start_time*, end_time*, attendees} [write/high]\n - `docs.create_markdown` payload={title*, markdown*, folder_token} [write/medium]\n - `drive.search` payload={query*, limit} [read/medium]\n - `im.add_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.download_resource` payload={message_id*, file_key*, type*} [read/low]\n - `im.get_messages` payload={message_ids*} [read/low]\n - `im.list_chats` payload={page_size} [read/low]\n - `im.list_messages` payload={chat_id*} [read/low]\n - `im.list_thread_messages` payload={thread*} [read/low]\n - `im.remove_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.reply_message` payload={message_id*, text*} [send/high]\n - `im.search_chats` payload={query*, page_size} [read/low]\n - `im.search_messages` payload={query*} [read/medium]\n - `im.send_message` payload={chat_id*, text*, thread_id} [send/high]\n - `im.update_card` payload={token*, card*} [write/medium]\n - `im.update_message` payload={message_id*, text*} [write/medium]\n - `mail.search_unread` payload={query, limit} [read/high]\n - `sheets.append_row` payload={spreadsheet_token*, sheet_id*, values*} [write/medium]\n - `task.create` payload={title*, description, due_time} [write/medium]\n- `telegram`: Telegram (category=social; backend=adapter; platform_action actions=none registered)\n- `webhook`: Webhook (Generic) (category=webhook; backend=adapter; platform_action actions=none registered)\n\n## Tool Usage\nTools are normally invoked through the native function-calling mechanism, not by writing JSON in your reply\ntext. Only if the provider signals that native function calling is unavailable for this turn, fall back to a\nsingle JSON code block: `{\"name\": \"tool_name\", \"arguments\": {\"key\": \"value\"}}` — use this fallback format\nonly, never both. Only call a tool whose exact name appears in the tool index above and is currently callable\n(or reachable via `capability_expand`). Never invent, rename, alias, or guess a tool name, platform ID, or\nplatform action from argument shape or wording — if the index does not list it, it does not exist.\n`platform_connect.action` (list/guide/preflight/connect/disconnect/remove/status/events_start/events_stop/\nevents_status) is the App Connector management namespace; `platform_action.action` only accepts exact\nregistered business actions such as `im.send_message` shown in the App Connector Capability Index — never mix\nthe two namespaces. If a tool call returns an unknown/unavailable result, use the returned suggestions or\navailable names for a single retry instead of trying further variations of the same guess.\n\n**Side-effect action rule** (`platform_action` with effect=send/write/execute):\n- Call each unique action+payload **exactly once**. Never include duplicates in the same turn.\n- Once the result returns `\"completed\": true`, that action is DONE for this task. Do NOT call it again\n in any subsequent turn — immediately summarize the result for the user instead.\n- The system enforces idempotency: duplicate calls are blocked and will not execute.\n- If the user explicitly requests sending/writing multiple times, use distinct payloads per call.\n\n**Resource identifier provenance rule**:\n- NEVER fabricate, guess, or infer resource identifiers (chat_id, message_id, file_key, user_id, etc.).\n Every resource ID used in a side-effect action MUST come from a successful API response in this session.\n- If a read/list action fails (e.g. authorization error), you do NOT have valid resource IDs.\n Report the failure to the user — do NOT attempt the dependent write/send action with a guessed ID.\n- When a tool result contains `\"llm_instruction\"`, follow it exactly.\n\n## Guidelines\n1. **Direct answers first**: If you already know the answer, respond directly without tools.\n2. **Avoid redundant tool calls**: Do not call the same tool with the same arguments more than once in the same user turn. When an existing tool result already answers the user's request, stop calling tools and answer directly.\n3. **Use tools proactively**: When the user asks about files, time, system state, or needs actions performed, use the appropriate tool.\n4. **Chain tools when needed**: You can call multiple tools in sequence (e.g., list files → read file → summarize).\n5. **Handle failures gracefully**: If a tool fails, explain what went wrong and suggest alternatives. If it failed because of an unknown tool/platform/action name, retry once with an exact name from the returned suggestions, then explain rather than keep guessing.\n6. **Summarize results naturally**: After tool execution, synthesize the results into a helpful answer rather than dumping raw output.\n7. **Stay conversational**: Maintain a natural, helpful tone. Acknowledge context from earlier in the conversation.\n8. **Recall past work**:\n - Broad queries (\"之前做了什么\", \"列出任务\"): answer from the \"Recent Task History\" section already in your context. If insufficient, call session_list.\n - Specific lookups (\"上次那个配置怎么改的\"): call session_search with relevant phrases (NOT single characters).\n - Do NOT call search tools repeatedly with keyword fragments. One well-phrased call is better than ten fragmented ones.\n\n## Coding & Verification\nWhen working with code, prefer the precise built-in tools over ad-hoc shell: use `repo_map` to orient in an\nunfamiliar project, `code_search`/`file_find` to locate and `code_intel` for symbols, `edit_file` (anchored\nsearch-replace, or a unified `diff`) to change files — never rewrite a whole file to change a few lines — and\n`git_query` to inspect diffs/log. After edits, check `syntax_ok` in the result and run `test_run`/`lint_check`\nbefore declaring the work done. Batch independent read-only calls (search/read/query) into a single turn — they\nrun in parallel.\n\n## Presentation Style\n1. **Polished Markdown only**: Format user-facing answers with clean Markdown headings, short paragraphs, and concise bullets. Use tables only when they improve comparison or scanning.\n2. **Terminal-friendly layout**: Keep lines readable in a TUI; avoid dense walls of text, deeply nested lists, oversized ASCII art, or heavy visual blocks.\n3. **Elegant emphasis**: Use bold text sparingly for key terms and conclusions. Avoid excessive emojis, decorative symbols, repeated separators, or visual noise.\n4. **Theme-safe colors**: Do not emit ANSI escape codes, HTML color tags, Rich markup, or hardcoded color names. Rely on the TUI theme to render Markdown professionally.\n5. **No leaked tool protocol**: Never show tool-call JSON, internal schemas, raw observations, tool result payloads, or hidden reasoning in the final answer unless the user explicitly asks for raw/debug output. Treat any prior `{\"name\": ..., \"arguments\": ...}` blocks and `Tool result (...)` messages as internal execution context only.\n6. **Professional closure**: End with a concise conclusion or next step when helpful; avoid rambling after the useful answer is complete.\n\nWhen finished with all tool calls, respond normally without a JSON block, tool-call transcript, or process log.\n\n## Task Contract\n- Task ID: turn-1\n- Original user request: Anything to report?\n- Workspace root: \n- Allowed roots: \n- Treat relative project paths as relative to the workspace root; never infer `.` as the project root when a workspace root is provided.\n- Workspace boundary is enforced by tools: do not read, search, edit, or run commands against paths outside the allowed roots unless the user explicitly requests an external path and the tool/approval policy permits it.\n- LeapFlow workspace config is optional at `/.leapflow/config.yaml`; runtime config is loaded from `~/.leapflow/config/user.yaml` and `~/.leapflow/profiles//config/*.yaml`.\n- Preserve this task contract across summarization, compression, tool loops, and memory retrieval.\n" + }, + { + "role": "system", + "content": "\n## Recent Session Summary\n- [user] Anything to report?\n" + }, + { + "role": "user", + "content": "Anything to report?\nAnything to report?" + } + ], + "tools": [ + "capability_expand", + "code_intel", + "code_search", + "config_get", + "config_list", + "env_info", + "file_find", + "file_list", + "file_read", + "git_query", + "lint_check", + "memory_search", + "plugin_list", + "plugin_propose", + "plugin_status", + "plugin_versions", + "repo_map", + "research_note", + "runtime_snapshot", + "schedule_reentry", + "self_describe", + "session_list", + "skill_view", + "skills_list", + "terminal_list", + "terminal_read", + "test_run", + "text_search", + "time_get", + "tool_describe", + "tool_search", + "web_fetch" + ] + }, + "responses": [ + { + "status": 200, + "content_type": "application/json", + "body": "{\"id\": \"chatcmpl-cassette\", \"object\": \"chat.completion\", \"model\": \"cassette-model\", \"choices\": [{\"index\": 0, \"message\": {\"role\": \"assistant\", \"content\": \"Acknowledged.\"}, \"finish_reason\": \"stop\"}], \"usage\": {\"prompt_tokens\": 64, \"completion_tokens\": 16, \"total_tokens\": 80}}" + } + ] +} diff --git a/tests/_fixtures/cassettes/r4_recovery/cassette-model-04a0e495d61a1558.cassette.json b/tests/_fixtures/cassettes/r4_recovery/cassette-model-04a0e495d61a1558.cassette.json new file mode 100644 index 00000000..c962c147 --- /dev/null +++ b/tests/_fixtures/cassettes/r4_recovery/cassette-model-04a0e495d61a1558.cassette.json @@ -0,0 +1,68 @@ +{ + "fingerprint": "04a0e495d61a15589170027e4eb34ab5f5b9688a132a2e095b9bb5fd1b8190c4", + "note": "captured in seed mode", + "request": { + "model": "cassette-model", + "stream": false, + "messages": [ + { + "role": "system", + "content": "You are LeapFlow, an intelligent assistant that can both converse naturally and take real actions on the user's computer.\n\n## Capabilities\nThe tool index below lists **every** registered tool by name and a one-line summary — this is the complete\ncapability contract; nothing else exists. Only a subset is directly callable this turn (via native tool calling,\nnot a JSON block in your reply). If you need a tool from the index that is not yet callable, call\n`capability_expand` with its category name first — the matching tools become callable immediately after.\nWhen the user asks about LeapFlow **itself** — its identity/version/model, what tools, plugins, or capabilities\nit has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of\nself-awareness — answer from live runtime evidence via `self_describe` (facet=capabilities for \"what can you\ndo / do you support X\", identity for version/model, runtime for context/posture, or `all` for a full self-check),\nnever by reading LeapFlow's own source code, documentation, or memory. Report configuration-dependent or\nunavailable capabilities as limitations rather than inferring them.\n- **text_search**(text, pattern): Search for a regex pattern in text.\n- **text_replace**(text, old, new, count) [capability_expand category: write]: Replace occurrences of a substring in text.\n- **time_get**(): Get current date and time.\n- **env_info**(): Get system environment information (OS, Python version, cwd).\n- **skills_list**(query, category, source): List available learned skills. Use when user asks about capabilities or you need a specific skill.\n- **skill_view**(name): View the full content of a specific skill document.\n- **skill_list_files**(name) [capability_expand category: skills]: List all support files in a skill directory (configs, templates, examples). Use before skill_file_read to discover available files.\n- **skill_file_read**(name, file_path) [capability_expand category: skills]: Read an individual support file from a skill directory. Use skill_list_files first to discover available files.\n- **code_intel**(path, operation): Precise document symbols (outline) for a source file: classes, functions, and methods with line ranges. Python uses an exact AST parse; other languages use a keyword-prefix scan. Prefer over file_read mode=symbols for accurate navigation before editing. Read-only.\n- **repo_map**(path): Compact project orientation for a repository root: languages, detected test/lint commands, top-level structure, entry points, manifest, and VCS branch. Call this first when entering an unfamiliar codebase. Read-only.\n- **scm_sync**(action, cwd, remote, pull_ref, push_ref, timeout) [capability_expand category: scm]: Run a typed git SCM action. Use this instead of shell_run for git pull/push/status. For 'pull origin main then push', set action='pull_then_push', remote='origin', pull_ref='main', and omit push_ref so LeapFlow pushes the current local branch.\n- **git_query**(action, cwd, ref, path, staged, max_count, stat): Read-only structured git inspection: action=diff|log|status|branch|show. Prefer over shell_run for reading repo state — output is clipped, redacted, and log/branch are parsed into structured fields. Use scm_sync for pull/push.\n- **git_write**(action, cwd, message, stage_all, name, ref, create) [capability_expand category: scm]: Mutating git actions: action=commit (message, stage_all), branch (create+switch), checkout (switch; create=true for -b). Approval-gated. Use scm_sync for pull/push and git_query for reads.\n- **test_run**(command, cwd, timeout): Run the project's test suite and return structured results (framework, passed/failed counts, failing tests). Auto-detects the runner (pytest/npm/go/cargo) or uses a configured/explicit command; executes via the governed shell. ok=true means the runner executed — see 'success' for pass/fail.\n- **lint_check**(command, cwd, timeout): Run the project's linter and return a structured clean/issue result. Auto-detects the linter (ruff/eslint/go vet/clippy) or uses a configured/explicit command; executes via the governed shell. ok=true means the linter ran — see 'clean'.\n- **file_list**(path, pattern, depth): List files and directories at a given path. Use depth=1 or depth=2 to get a recursive tree in one call instead of listing each sub-directory separately.\n- **file_read**(path, max_lines, start_line, max_chars, mode): Read text file content with adaptive context governance. For large or unfamiliar files, prefer mode='outline' or mode='symbols' first, then use mode='raw' with start_line/max_lines for the specific range you actually need. For LeapFlow's own settings, use config_list / config_get / config_set — its config files are outside the workspace and not readable here.\n- **file_write**(path, content, mode) [capability_expand category: write]: Write content to a file (overwrite or append).\n- **code_search**(pattern, patterns, path, glob, ignore_case, multiline, max_results, context_lines): Search file CONTENTS by regex pattern across a directory tree (ripgrep-backed). Requires a regex pattern. NOT for listing or browsing directory contents — use file_list for that. Prefer this over shell_run grep: faster, skips VCS/dependency/build dirs, and returns structured path:line:column matches. Batch related lookups into ONE call via `patterns` (OR-combined, single pass) instead of issuing several separate searches. Use file_read for the surrounding context of a hit.\n- **file_find**(glob, path, max_results): Find files by a recursive glob pattern under a base path (e.g. '**/test_*.py' or '*.md'). Prefer this over shell_run find; skips VCS/dependency/build dirs.\n- **edit_file**(path, edits, dry_run, diff) [capability_expand category: write]: Apply targeted, anchored search-replace edits to an EXISTING text file (use file_write to create/overwrite). Each edit is {original_text, new_text, replace_all?}; original_text must match exactly and uniquely (or set replace_all) — a non-unique or missing anchor is rejected so files are never corrupted. Set dry_run to preview. Alternatively pass a unified 'diff' to apply its hunks as anchored edits. Far cheaper and safer than rewriting a whole file.\n- **shell_run**(command, cwd, timeout) [capability_expand category: shell]: Execute a one-shot shell command with timeout protection. Runs in the active workspace; paths resolving outside it are refused. Reach for a structured tool first when one fits — web_fetch for anything over HTTP(S), git_query/scm_sync for git, code_search/file_find/file_read for the repo, config_get/config_set for LeapFlow's own settings — because those report typed results, while a failed shell command can only be diagnosed from its exit code and stderr. Every shell run counts as an external side effect, so a failure stops the rest of the batch and is not retried automatically.\n- **terminal_open**(command, cwd, shell) [capability_expand category: terminal]: Open a PERSISTENT shell session (REPL/dev server/watch), returning a session_id for terminal_send/read/close. Disabled unless tools.terminal_session_enabled is set. For one-shot commands use shell_run instead.\n- **terminal_send**(session_id, input, wait) [capability_expand category: terminal]: Send a line of input to a persistent terminal session and return output captured shortly after.\n- **terminal_read**(session_id, wait): Drain buffered output from a persistent terminal session (optionally waiting briefly first).\n- **terminal_close**(session_id) [capability_expand category: terminal]: Terminate a persistent terminal session and release its process group.\n- **terminal_list**(): List active persistent terminal sessions.\n- **config_list**(category, limit): List LeapFlow's own writable settings (LLM model and endpoint, daemon, memory, perception, gateway, …) with current values. Use this to discover the exact key before changing anything. Optionally narrow by `category`. This is the only correct way to inspect LeapFlow configuration — never read config files from disk.\n- **config_get**(key): Read one LeapFlow setting by key (e.g. 'llm.model', 'llm.base_url', 'daemon.log_level'), returning its current value, type, scopes, and whether a change needs a daemon restart. There is no 'llm.provider' key: provider behavior is inferred from the OpenAI-compatible 'llm.base_url'. Never read LeapFlow config files from disk — use this.\n- **config_set**(key, value, scope) [capability_expand category: config]: Change one LeapFlow setting by key, e.g. switch an OpenAI-compatible LLM with key='llm.model' and key='llm.base_url'. There is no 'llm.provider' key: provider behavior is inferred from the endpoint. Values are validated and coerced; credentials are stored in the vault automatically. Call config_list or config_get first if unsure of the key. The result states whether a `leap daemon restart` is required. Never edit LeapFlow config files directly.\n- **web_fetch**(url, select, timeout, max_bytes): Read a URL over HTTP(S) and get back extracted, context-sized content: parsed JSON for API endpoints, readable text plus links for web pages. Use this for anything on the internet — prices, docs, releases, articles — instead of running curl through shell_run: it reports real HTTP status codes, retries rate limits on its own, and is a plain read so a retry is always safe. For JSON APIs pass `select` with a dotted path (e.g. 'chart.result.0.meta') to return just that part instead of the whole payload.\n- **memory_search**(query, limit): Search agent memory for relevant past experiences, observations, and facts.\n- **memory_add**(content, kind) [capability_expand category: write]: Store a new observation or insight in memory for future reference.\n- **research_note**(kind, text): Record a compact, structured note about the current task's state so it survives context compression on long / multi-step tasks. Use for durable findings, open questions still to resolve, decisions / excluded paths, and the immediate next step. One concise sentence per note.\n- **capability_expand**(category): Fetch the full callable schema for every tool in a capability category (e.g. 'hub', 'gateway', 'desktop', 'delegate', 'file', 'memory', 'skill'). The compact tool index always lists every registered tool by name and a one-line summary, but only a static low-risk subset is directly callable each turn. If you need a tool from the index that is not yet callable, call capability_expand with its category first; the matching tools become callable in this turn. Never invent a tool name — expand the category instead.\n- **delegate_task**(goal, context) [capability_expand category: delegate]: Delegate a complex sub-task to an isolated subagent. The subagent gets a fresh context and restricted tool access. Use when a task is self-contained and can be solved independently.\n- **schedule_reentry**(kind, reason, delay_seconds, event_match, max_reentries, deadline_seconds): Register a re-entry so this task can resume later from its current orientation (findings / open questions / next step). Use when work must pause and continue after a delay (kind=time) or when a matching platform event arrives (kind=event), instead of finishing now. The research-ledger state is carried over automatically.\n- **hub_push**(skill_name, visibility, version) [capability_expand category: hub]: Push a local skill to the ModelScope Hub for sharing or backup.\n- **hub_pull**(repo_id, version) [capability_expand category: hub]: Pull a skill from the ModelScope Hub to install locally.\n- **hub_search**(query) [capability_expand category: hub]: Search for skills on the Hub by keyword or description.\n- **hub_sync**(mode, dry_run) [capability_expand category: hub]: Preview or execute sync between local skills and Hub.\n- **platform_action**(platform, action, payload, backend_kind) [capability_expand category: gateway]: Execute an exact registered business action on an external platform through LeapFlow's App Connector layer. Actions must be copied from the App Connector Capability Index and are addressed as domain.operation, e.g. im.send_message or docs.create_markdown. All business fields (chat_id, text, query, etc.) MUST be placed inside `payload`, never at the top level. Example: {\"platform\":\"feishu\",\"action\":\"im.send_message\",\"payload\":{\"chat_id\":\"oc_xxx\",\"text\":\"hello\"}}. Do not invent action names, do not use management actions such as list/guide/connect/status here.\n- **platform_connect**(action, platform, credentials, options, checkpoint) [capability_expand category: gateway]: List, guide, connect, disconnect, remove, or check status for external platforms using the App Connector management namespace. Supports REST and CLI backends. Use this for management actions such as list/guide/preflight/connect/status; use platform_action only for exact registered business actions.\n- **gateway_send**(platform, chat_id, text, thread_id) [capability_expand category: gateway]: Send a message to a connected external platform (Feishu group, Telegram chat, DingTalk conversation, etc.). Requires the platform to be connected via gateway_connect first. Use gateway_connect with action='list' to see connected platforms and available chat IDs.\n- **gateway_connect**(action, platform, credentials, options) [capability_expand category: gateway]: Connect, configure, or manage external platform integrations (Feishu, DingTalk, Telegram, Slack, Discord, etc.). Conversational flow: 1) call 'guide' to get setup steps + required fields, 2) present the steps to the user and ask for ALL required credentials in a single message, 3) call 'connect' with the credentials. Goal: complete in 1–2 user turns. NEVER include credential values in your text response.\n- **plugin_list**(): List the live plugin registry across Tool/Gateway/LLM subsystems for plugin management: which plugins are loaded, their category and fiber lifecycle state. For questions about LeapFlow's own capabilities or whether it supports X, use self_describe instead.\n- **plugin_status**(plugin_id): Get detailed status of a specific plugin: its declared category, runtime dependencies, contributed tools, and fiber lifecycle state.\n- **plugin_versions**(plugin_id): List recorded source versions and active pointer for a profile-scoped plugin.\n- **plugin_propose**(requested_capability, plugin_id, proposed_tools, test_cases, risk_level, evidence): Create a side-effect-free PluginProposal from explicit capability-gap evidence. Use this before plugin_generate when a missing capability should be reviewed. Does not call an LLM, write files, or install anything.\n- **assess_compatibility**(manifest, source_path) [capability_expand category: plugin_management]: Assess whether a foreign plugin manifest or real DSH source bundle is compatible with LeapFlow. A source bundle assessment is static: runtime_ready stays false until restricted Node discovery during plugin_install. Returns component-level verdicts and limitations.\n- **plugin_generate**(plugin_id, description, proposal_id) [capability_expand category: system]: Generate a new ToolPlugin from a natural-language capability description. The LLM produces code that conforms to the ToolPlugin Protocol; it is then rigorously validated (syntax, structure, import, protocol conformance). The generated source is stored in CAS and receives explicit content approval. It DOES NOT install the plugin — installation requires a second, mutation-specific approval via plugin_install.\n- **plugin_install**(plugin_id, code, marketplace_name, source_path, proposal_id, version_label) [capability_expand category: system]: Install a plugin from exactly one source: validated Python code, the configured Python marketplace, or a real DSH source bundle. DSH bundles run restricted Node discovery before registration; only public host tools are installed and client UI limitations are reported. Writes profile-scoped state and mutates the process-global registry. REQUIRES APPROVAL.\n- **plugin_rollback**(plugin_id, version) [capability_expand category: system]: Rollback a profile-scoped plugin to a recorded source version and reload it. REQUIRES APPROVAL.\n- **plugin_reload**(plugin_id, version_label) [capability_expand category: system]: Hot-reload a plugin at runtime. Disposes the old plugin fiber, re-imports its module, and registers a fresh instance. Existing in-flight turns are unaffected (snapshot isolation). REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_disable**(plugin_id) [capability_expand category: system]: Disable a plugin by disposing its fiber, removing its tools from the runtime registry. Cannot disable self_management itself. REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_remove**(plugin_id, delete_source) [capability_expand category: system]: Terminally remove a plugin: dispose its fiber, unregister its tools, remove reload metadata, and optionally delete its profile-scoped source file. Cannot remove self_management itself. REQUIRES APPROVAL.\n- **plugin_unquarantine**(plugin_id) [capability_expand category: plugin_management]: Restore a quarantined plugin to probation status for re-evaluation. Unfreezes the trust ledger, transitions the proposal back to PROBATION, and reloads the plugin. REQUIRES APPROVAL.\n- **plugin_enable**(plugin_id) [capability_expand category: system]: Re-enable a previously disabled plugin by reloading its module and registering a fresh instance. REQUIRES APPROVAL.\n- **self_describe**(facet): Answer questions about LeapFlow ITSELF from live runtime evidence — its identity/version/model, what tools and plugins it has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of self-awareness. Prefer this over reading LeapFlow's own source code. Pick facet=capabilities for 'what can you do / do you support X', identity for version/model, runtime for context/posture, or all for a full self-check when explicitly requested.\n- **runtime_snapshot**(): Lightweight (~150 token) flat snapshot of current runtime state: model, context budget, posture, disclosure level, turn count, cache hit rate, uptime, tool count, and pending approvals.\n- **schedule_create**(trigger_expression, instruction, execution_mode, max_retries, delivery_target) [capability_expand category: scheduler]: Create a new scheduled task. Specify a trigger expression (e.g. '30m', 'every 2h', '0 9 * * *') and the instruction to execute on each trigger. Optionally provide an execution mode and a delivery target to receive notifications.\n- **schedule_list**() [capability_expand category: scheduler]: List all scheduled tasks with their current state, trigger, and next due time.\n- **schedule_status**(task_id) [capability_expand category: scheduler]: Get detailed status and recent execution history for a scheduled task.\n- **schedule_pause**(task_id) [capability_expand category: scheduler]: Pause a scheduled task so it stops firing without being cancelled.\n- **schedule_resume**(task_id) [capability_expand category: scheduler]: Resume a paused scheduled task — re-arms it and recalculates next due time.\n- **schedule_cancel**(task_id) [capability_expand category: scheduler]: Cancel a scheduled task permanently.\n- **tool_search**(query, max_results): Search all registered tools by keyword relevance. Returns a ranked list of matching tools with name, category, and summary. Use this when you need a tool but are unsure of its exact name or category.\n- **tool_describe**(tool_name): Get the full callable schema of a single tool by exact name. Returns the tool's description, parameters, and metadata. Use after tool_search to inspect a specific tool before calling it.\n- **session_search**(query, limit) [capability_expand category: unclassified]: Search past conversation sessions in the current workspace for relevant context.\n- **session_list**(limit): List recent conversation sessions in the current workspace with stable ids, titles, dates, and summaries. Use for browsing past tasks or when user asks to see history without specific search terms. No keywords needed — returns chronological list.\n- **session_detail**(session_id, limit, offset, include_inactive) [capability_expand category: unclassified]: Read a paginated persisted transcript for one past conversation session in the current workspace. Use after session_list or session_search returns a session_id.\n- **click**(element_index) [capability_expand category: desktop]: Click a UI element by its element_index (from the latest observe_ui snapshot)\n- **get_clipboard**() [capability_expand category: desktop]: Read current clipboard text content\n- **list_apps**(filter, running_only) [capability_expand category: desktop]: List available applications on this system. Use to discover correct bundle_id before switch_app.\n- **list_windows**() [capability_expand category: desktop]: List all top-level windows with pid, window_id, title, and per-window state (minimized, on-screen). Call this first to pick the pid and window_id that observe_ui and other window tools require.\n- **observe_ui**(pid, window_id, query) [capability_expand category: desktop]: Snapshot one window's actionable UI elements, each tagged with an element_index for click/right_click/read_text. Re-observe after actions — indices belong to one snapshot. Requires the window's pid and window_id from list_windows.\n- **open_url**(url, app_id) [capability_expand category: desktop]: Open a URL in the default or specified browser\n- **read_text**(element_index) [capability_expand category: desktop]: Read the text content of a specific UI element from the latest snapshot\n- **right_click**(element_index) [capability_expand category: desktop]: Right-click a UI element to open its context menu. Returns visible menu items.\n- **screenshot**(pid, window_id) [capability_expand category: desktop]: Capture a screenshot for visual verification. With pid + window_id captures that window (works across all displays); defaults to the last observed window, or the full desktop when no window has been observed.\n- **scroll**(element_index, direction, amount, pid, window_id) [capability_expand category: desktop]: Scroll a scrollable area of a window. Omit element_index to scroll the window's focused/page scroller; pass one to scroll an exact element from the latest snapshot.\n- **select_text**(element_index) [capability_expand category: desktop]: Select all text in a UI element (focus + select-all, for subsequent copy)\n- **set_clipboard**(text) [capability_expand category: desktop]: Write text to the clipboard\n- **shortcut**(keys) [capability_expand category: desktop]: Execute a keyboard shortcut\n- **switch_app**(app_id) [capability_expand category: desktop]: Switch to an app (launch if needed, activate, verify)\n- **type_text**(text) [capability_expand category: desktop]: Type text into the currently focused element\n- **wait**(seconds) [capability_expand category: desktop]: Wait for a specified duration before continuing\n- **wait_until**(condition, pid, window_id, timeout, poll_interval) [capability_expand category: desktop]: Wait until a UI condition is met (polls UI tree). Returns elements when found or on timeout.\n- **wait_until_stable**(timeout, poll_interval, pid, window_id) [capability_expand category: desktop]: Wait until the UI stops changing (element set stabilizes across polls).\n\n## App Connector Capability Index\nLeapFlow can onboard and manage external apps through `platform_connect` and execute exact registered business actions through `platform_action`.\nFor requests about connecting, setting up, configuring, enabling, or managing a supported app, use `platform_connect` first instead of generating SDK/Webhook sample code.\n`platform_connect.action` is the management namespace: list, guide, preflight, connect, disconnect, remove, status, events_start, events_stop, events_status.\n`platform_action.action` is only for exact registered platform business actions listed below, such as `im.send_message`; never use management actions like `list` or `guide` there.\nAll business fields MUST go inside `payload`; top-level keys are only `platform`, `action`, and `payload`.\nDo not invent platform IDs or platform action names. If the needed action is not listed, ask for discovery/clarification instead of guessing.\nUse `platform_connect` with `action='guide'` and the matching `platform` to start onboarding; use `action='list'` when the app is unclear.\nWhen a pending onboarding state is present, continue from that state with `platform_connect` instead of asking the user to restate the app.\nSupported apps:\n- `api_server`: API Server (OpenAI Compatible) (category=api; backend=adapter; platform_action actions=none registered)\n- `dingtalk`: 钉钉 (DingTalk) (category=im; backend=adapter; platform_action actions=none registered)\n- `feishu`: 飞书 (Feishu/Lark) (category=im; backend=cli)\n - `calendar.create_event` payload={summary*, start_time*, end_time*, attendees} [write/high]\n - `docs.create_markdown` payload={title*, markdown*, folder_token} [write/medium]\n - `drive.search` payload={query*, limit} [read/medium]\n - `im.add_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.download_resource` payload={message_id*, file_key*, type*} [read/low]\n - `im.get_messages` payload={message_ids*} [read/low]\n - `im.list_chats` payload={page_size} [read/low]\n - `im.list_messages` payload={chat_id*} [read/low]\n - `im.list_thread_messages` payload={thread*} [read/low]\n - `im.remove_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.reply_message` payload={message_id*, text*} [send/high]\n - `im.search_chats` payload={query*, page_size} [read/low]\n - `im.search_messages` payload={query*} [read/medium]\n - `im.send_message` payload={chat_id*, text*, thread_id} [send/high]\n - `im.update_card` payload={token*, card*} [write/medium]\n - `im.update_message` payload={message_id*, text*} [write/medium]\n - `mail.search_unread` payload={query, limit} [read/high]\n - `sheets.append_row` payload={spreadsheet_token*, sheet_id*, values*} [write/medium]\n - `task.create` payload={title*, description, due_time} [write/medium]\n- `telegram`: Telegram (category=social; backend=adapter; platform_action actions=none registered)\n- `webhook`: Webhook (Generic) (category=webhook; backend=adapter; platform_action actions=none registered)\n\n## Tool Usage\nTools are normally invoked through the native function-calling mechanism, not by writing JSON in your reply\ntext. Only if the provider signals that native function calling is unavailable for this turn, fall back to a\nsingle JSON code block: `{\"name\": \"tool_name\", \"arguments\": {\"key\": \"value\"}}` — use this fallback format\nonly, never both. Only call a tool whose exact name appears in the tool index above and is currently callable\n(or reachable via `capability_expand`). Never invent, rename, alias, or guess a tool name, platform ID, or\nplatform action from argument shape or wording — if the index does not list it, it does not exist.\n`platform_connect.action` (list/guide/preflight/connect/disconnect/remove/status/events_start/events_stop/\nevents_status) is the App Connector management namespace; `platform_action.action` only accepts exact\nregistered business actions such as `im.send_message` shown in the App Connector Capability Index — never mix\nthe two namespaces. If a tool call returns an unknown/unavailable result, use the returned suggestions or\navailable names for a single retry instead of trying further variations of the same guess.\n\n**Side-effect action rule** (`platform_action` with effect=send/write/execute):\n- Call each unique action+payload **exactly once**. Never include duplicates in the same turn.\n- Once the result returns `\"completed\": true`, that action is DONE for this task. Do NOT call it again\n in any subsequent turn — immediately summarize the result for the user instead.\n- The system enforces idempotency: duplicate calls are blocked and will not execute.\n- If the user explicitly requests sending/writing multiple times, use distinct payloads per call.\n\n**Resource identifier provenance rule**:\n- NEVER fabricate, guess, or infer resource identifiers (chat_id, message_id, file_key, user_id, etc.).\n Every resource ID used in a side-effect action MUST come from a successful API response in this session.\n- If a read/list action fails (e.g. authorization error), you do NOT have valid resource IDs.\n Report the failure to the user — do NOT attempt the dependent write/send action with a guessed ID.\n- When a tool result contains `\"llm_instruction\"`, follow it exactly.\n\n## Guidelines\n1. **Direct answers first**: If you already know the answer, respond directly without tools.\n2. **Avoid redundant tool calls**: Do not call the same tool with the same arguments more than once in the same user turn. When an existing tool result already answers the user's request, stop calling tools and answer directly.\n3. **Use tools proactively**: When the user asks about files, time, system state, or needs actions performed, use the appropriate tool.\n4. **Chain tools when needed**: You can call multiple tools in sequence (e.g., list files → read file → summarize).\n5. **Handle failures gracefully**: If a tool fails, explain what went wrong and suggest alternatives. If it failed because of an unknown tool/platform/action name, retry once with an exact name from the returned suggestions, then explain rather than keep guessing.\n6. **Summarize results naturally**: After tool execution, synthesize the results into a helpful answer rather than dumping raw output.\n7. **Stay conversational**: Maintain a natural, helpful tone. Acknowledge context from earlier in the conversation.\n8. **Recall past work**:\n - Broad queries (\"之前做了什么\", \"列出任务\"): answer from the \"Recent Task History\" section already in your context. If insufficient, call session_list.\n - Specific lookups (\"上次那个配置怎么改的\"): call session_search with relevant phrases (NOT single characters).\n - Do NOT call search tools repeatedly with keyword fragments. One well-phrased call is better than ten fragmented ones.\n\n## Coding & Verification\nWhen working with code, prefer the precise built-in tools over ad-hoc shell: use `repo_map` to orient in an\nunfamiliar project, `code_search`/`file_find` to locate and `code_intel` for symbols, `edit_file` (anchored\nsearch-replace, or a unified `diff`) to change files — never rewrite a whole file to change a few lines — and\n`git_query` to inspect diffs/log. After edits, check `syntax_ok` in the result and run `test_run`/`lint_check`\nbefore declaring the work done. Batch independent read-only calls (search/read/query) into a single turn — they\nrun in parallel.\n\n## Presentation Style\n1. **Polished Markdown only**: Format user-facing answers with clean Markdown headings, short paragraphs, and concise bullets. Use tables only when they improve comparison or scanning.\n2. **Terminal-friendly layout**: Keep lines readable in a TUI; avoid dense walls of text, deeply nested lists, oversized ASCII art, or heavy visual blocks.\n3. **Elegant emphasis**: Use bold text sparingly for key terms and conclusions. Avoid excessive emojis, decorative symbols, repeated separators, or visual noise.\n4. **Theme-safe colors**: Do not emit ANSI escape codes, HTML color tags, Rich markup, or hardcoded color names. Rely on the TUI theme to render Markdown professionally.\n5. **No leaked tool protocol**: Never show tool-call JSON, internal schemas, raw observations, tool result payloads, or hidden reasoning in the final answer unless the user explicitly asks for raw/debug output. Treat any prior `{\"name\": ..., \"arguments\": ...}` blocks and `Tool result (...)` messages as internal execution context only.\n6. **Professional closure**: End with a concise conclusion or next step when helpful; avoid rambling after the useful answer is complete.\n\nWhen finished with all tool calls, respond normally without a JSON block, tool-call transcript, or process log.\n\n## Task Contract\n- Task ID: turn-1\n- Original user request: Summarize the situation.\n- Workspace root: \n- Allowed roots: \n- Treat relative project paths as relative to the workspace root; never infer `.` as the project root when a workspace root is provided.\n- Workspace boundary is enforced by tools: do not read, search, edit, or run commands against paths outside the allowed roots unless the user explicitly requests an external path and the tool/approval policy permits it.\n- LeapFlow workspace config is optional at `/.leapflow/config.yaml`; runtime config is loaded from `~/.leapflow/config/user.yaml` and `~/.leapflow/profiles//config/*.yaml`.\n- Preserve this task contract across summarization, compression, tool loops, and memory retrieval.\n" + }, + { + "role": "system", + "content": "\n## Recent Session Summary\n- [user] Summarize the situation.\n" + }, + { + "role": "user", + "content": "Summarize the situation.\nSummarize the situation." + } + ], + "tools": [ + "capability_expand", + "code_intel", + "code_search", + "config_get", + "config_list", + "env_info", + "file_find", + "file_list", + "file_read", + "git_query", + "lint_check", + "memory_search", + "plugin_list", + "plugin_propose", + "plugin_status", + "plugin_versions", + "repo_map", + "research_note", + "runtime_snapshot", + "schedule_reentry", + "self_describe", + "session_list", + "skill_view", + "skills_list", + "terminal_list", + "terminal_read", + "test_run", + "text_search", + "time_get", + "tool_describe", + "tool_search", + "web_fetch" + ] + }, + "responses": [ + { + "status": 429, + "content_type": "application/json", + "body": "{\"error\": {\"message\": \"Rate limit reached for requests\", \"type\": \"rate_limit_error\", \"code\": \"rate_limit_exceeded\"}}" + }, + { + "status": 200, + "content_type": "application/json", + "body": "{\"id\": \"chatcmpl-cassette\", \"object\": \"chat.completion\", \"model\": \"cassette-model\", \"choices\": [{\"index\": 0, \"message\": {\"role\": \"assistant\", \"content\": \"Recovered after a rate limit.\"}, \"finish_reason\": \"stop\"}], \"usage\": {\"prompt_tokens\": 64, \"completion_tokens\": 16, \"total_tokens\": 80}}" + } + ] +} diff --git a/tests/_fixtures/cassettes/r4_recovery/cassette-model-3e15bacc2b11d537.cassette.json b/tests/_fixtures/cassettes/r4_recovery/cassette-model-3e15bacc2b11d537.cassette.json deleted file mode 100644 index d578d712..00000000 --- a/tests/_fixtures/cassettes/r4_recovery/cassette-model-3e15bacc2b11d537.cassette.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "fingerprint": "3e15bacc2b11d5376bd602a5a8166287aa5cdbe723df8cc6b8c88e2284b7b02c", - "note": "captured in seed mode", - "request": { - "model": "cassette-model", - "stream": false, - "messages": [ - { - "role": "system", - "content": "You are LeapFlow, an intelligent assistant that can both converse naturally and take real actions on the user's computer.\n\n## Capabilities\nThe tool index below lists **every** registered tool by name and a one-line summary — this is the complete\ncapability contract; nothing else exists. Only a subset is directly callable this turn (via native tool calling,\nnot a JSON block in your reply). If you need a tool from the index that is not yet callable, call\n`capability_expand` with its category name first — the matching tools become callable immediately after.\nWhen the user asks what LeapFlow itself supports, whether it supports plugins, or which runtime capabilities\nare available, use the live capability evidence exposed by `plugin_list` before making capability claims; report\nconfiguration-dependent or unavailable capabilities as limitations instead of inferring from documentation.\n- **text_search**(text, pattern): Search for a regex pattern in text.\n- **text_replace**(text, old, new, count) [capability_expand category: write]: Replace occurrences of a substring in text.\n- **time_get**(): Get current date and time.\n- **env_info**(): Get system environment information (OS, Python version, cwd).\n- **skills_list**(query, category, source): List available learned skills. Use when user asks about capabilities or you need a specific skill.\n- **skill_view**(name): View the full content of a specific skill document.\n- **code_intel**(path, operation): Precise document symbols (outline) for a source file: classes, functions, and methods with line ranges. Python uses an exact AST parse; other languages use a keyword-prefix scan. Prefer over file_read mode=symbols for accurate navigation before editing. Read-only.\n- **repo_map**(path): Compact project orientation for a repository root: languages, detected test/lint commands, top-level structure, entry points, manifest, and VCS branch. Call this first when entering an unfamiliar codebase. Read-only.\n- **scm_sync**(action, cwd, remote, pull_ref, push_ref, timeout) [capability_expand category: scm]: Run a typed git SCM action. Use this instead of shell_run for git pull/push/status. For 'pull origin main then push', set action='pull_then_push', remote='origin', pull_ref='main', and omit push_ref so LeapFlow pushes the current local branch.\n- **git_query**(action, cwd, ref, path, staged, max_count, stat): Read-only structured git inspection: action=diff|log|status|branch|show. Prefer over shell_run for reading repo state — output is clipped, redacted, and log/branch are parsed into structured fields. Use scm_sync for pull/push.\n- **git_write**(action, cwd, message, stage_all, name, ref, create) [capability_expand category: scm]: Mutating git actions: action=commit (message, stage_all), branch (create+switch), checkout (switch; create=true for -b). Approval-gated. Use scm_sync for pull/push and git_query for reads.\n- **test_run**(command, cwd, timeout): Run the project's test suite and return structured results (framework, passed/failed counts, failing tests). Auto-detects the runner (pytest/npm/go/cargo) or uses a configured/explicit command; executes via the governed shell. ok=true means the runner executed — see 'success' for pass/fail.\n- **lint_check**(command, cwd, timeout): Run the project's linter and return a structured clean/issue result. Auto-detects the linter (ruff/eslint/go vet/clippy) or uses a configured/explicit command; executes via the governed shell. ok=true means the linter ran — see 'clean'.\n- **file_list**(path, pattern, depth): List files and directories at a given path. Use depth=1 or depth=2 to get a recursive tree in one call instead of listing each sub-directory separately.\n- **file_read**(path, max_lines, start_line, max_chars, mode): Read text file content with adaptive context governance. For large or unfamiliar files, prefer mode='outline' or mode='symbols' first, then use mode='raw' with start_line/max_lines for the specific range you actually need. For LeapFlow's own settings, use config_list / config_get / config_set — its config files are outside the workspace and not readable here.\n- **file_write**(path, content, mode) [capability_expand category: write]: Write content to a file (overwrite or append).\n- **code_search**(pattern, patterns, path, glob, ignore_case, multiline, max_results, context_lines): Search file CONTENTS by regex pattern across a directory tree (ripgrep-backed). Requires a regex pattern. NOT for listing or browsing directory contents — use file_list for that. Prefer this over shell_run grep: faster, skips VCS/dependency/build dirs, and returns structured path:line:column matches. Batch related lookups into ONE call via `patterns` (OR-combined, single pass) instead of issuing several separate searches. Use file_read for the surrounding context of a hit.\n- **file_find**(glob, path, max_results): Find files by a recursive glob pattern under a base path (e.g. '**/test_*.py' or '*.md'). Prefer this over shell_run find; skips VCS/dependency/build dirs.\n- **edit_file**(path, edits, dry_run, diff) [capability_expand category: write]: Apply targeted, anchored search-replace edits to an EXISTING text file (use file_write to create/overwrite). Each edit is {original_text, new_text, replace_all?}; original_text must match exactly and uniquely (or set replace_all) — a non-unique or missing anchor is rejected so files are never corrupted. Set dry_run to preview. Alternatively pass a unified 'diff' to apply its hunks as anchored edits. Far cheaper and safer than rewriting a whole file.\n- **shell_run**(command, cwd, timeout) [capability_expand category: shell]: Execute a one-shot shell command with timeout protection. Runs in the active workspace; paths resolving outside it are refused. Reach for a structured tool first when one fits — web_fetch for anything over HTTP(S), git_query/scm_sync for git, code_search/file_find/file_read for the repo, config_get/config_set for LeapFlow's own settings — because those report typed results, while a failed shell command can only be diagnosed from its exit code and stderr. Every shell run counts as an external side effect, so a failure stops the rest of the batch and is not retried automatically.\n- **terminal_open**(command, cwd, shell) [capability_expand category: terminal]: Open a PERSISTENT shell session (REPL/dev server/watch), returning a session_id for terminal_send/read/close. Disabled unless tools.terminal_session_enabled is set. For one-shot commands use shell_run instead.\n- **terminal_send**(session_id, input, wait) [capability_expand category: terminal]: Send a line of input to a persistent terminal session and return output captured shortly after.\n- **terminal_read**(session_id, wait): Drain buffered output from a persistent terminal session (optionally waiting briefly first).\n- **terminal_close**(session_id) [capability_expand category: terminal]: Terminate a persistent terminal session and release its process group.\n- **terminal_list**(): List active persistent terminal sessions.\n- **config_list**(category, limit): List LeapFlow's own writable settings (LLM model and endpoint, daemon, memory, perception, gateway, …) with current values. Use this to discover the exact key before changing anything. Optionally narrow by `category`. This is the only correct way to inspect LeapFlow configuration — never read config files from disk.\n- **config_get**(key): Read one LeapFlow setting by key (e.g. 'llm.model', 'llm.base_url', 'daemon.log_level'), returning its current value, type, scopes, and whether a change needs a daemon restart. There is no 'llm.provider' key: provider behavior is inferred from the OpenAI-compatible 'llm.base_url'. Never read LeapFlow config files from disk — use this.\n- **config_set**(key, value, scope) [capability_expand category: config]: Change one LeapFlow setting by key, e.g. switch an OpenAI-compatible LLM with key='llm.model' and key='llm.base_url'. There is no 'llm.provider' key: provider behavior is inferred from the endpoint. Values are validated and coerced; credentials are stored in the vault automatically. Call config_list or config_get first if unsure of the key. The result states whether a `leap daemon restart` is required. Never edit LeapFlow config files directly.\n- **web_fetch**(url, select, timeout, max_bytes): Read a URL over HTTP(S) and get back extracted, context-sized content: parsed JSON for API endpoints, readable text plus links for web pages. Use this for anything on the internet — prices, docs, releases, articles — instead of running curl through shell_run: it reports real HTTP status codes, retries rate limits on its own, and is a plain read so a retry is always safe. For JSON APIs pass `select` with a dotted path (e.g. 'chart.result.0.meta') to return just that part instead of the whole payload.\n- **memory_search**(query, limit): Search agent memory for relevant past experiences, observations, and facts.\n- **memory_add**(content, kind) [capability_expand category: write]: Store a new observation or insight in memory for future reference.\n- **research_note**(kind, text): Record a compact, structured note about the current task's state so it survives context compression on long / multi-step tasks. Use for durable findings, open questions still to resolve, decisions / excluded paths, and the immediate next step. One concise sentence per note.\n- **capability_expand**(category): Fetch the full callable schema for every tool in a capability category (e.g. 'hub', 'gateway', 'desktop', 'delegate', 'file', 'memory', 'skill'). The compact tool index always lists every registered tool by name and a one-line summary, but only a static low-risk subset is directly callable each turn. If you need a tool from the index that is not yet callable, call capability_expand with its category first; the matching tools become callable in this turn. Never invent a tool name — expand the category instead.\n- **delegate_task**(goal, context) [capability_expand category: delegate]: Delegate a complex sub-task to an isolated subagent. The subagent gets a fresh context and restricted tool access. Use when a task is self-contained and can be solved independently.\n- **schedule_reentry**(kind, reason, delay_seconds, event_match, max_reentries, deadline_seconds): Register a re-entry so this task can resume later from its current orientation (findings / open questions / next step). Use when work must pause and continue after a delay (kind=time) or when a matching platform event arrives (kind=event), instead of finishing now. The research-ledger state is carried over automatically.\n- **hub_push**(skill_name, visibility, version) [capability_expand category: hub]: Push a local skill to the ModelScope Hub for sharing or backup.\n- **hub_pull**(repo_id, version) [capability_expand category: hub]: Pull a skill from the ModelScope Hub to install locally.\n- **hub_search**(query) [capability_expand category: hub]: Search for skills on the Hub by keyword or description.\n- **hub_sync**(mode, dry_run) [capability_expand category: hub]: Preview or execute sync between local skills and Hub.\n- **platform_action**(platform, action, payload, backend_kind) [capability_expand category: gateway]: Execute an exact registered business action on an external platform through LeapFlow's App Connector layer. Actions must be copied from the App Connector Capability Index and are addressed as domain.operation, e.g. im.send_message or docs.create_markdown. All business fields (chat_id, text, query, etc.) MUST be placed inside `payload`, never at the top level. Example: {\"platform\":\"feishu\",\"action\":\"im.send_message\",\"payload\":{\"chat_id\":\"oc_xxx\",\"text\":\"hello\"}}. Do not invent action names, do not use management actions such as list/guide/connect/status here.\n- **platform_connect**(action, platform, credentials, options, checkpoint) [capability_expand category: gateway]: List, guide, connect, disconnect, remove, or check status for external platforms using the App Connector management namespace. Supports REST and CLI backends. Use this for management actions such as list/guide/preflight/connect/status; use platform_action only for exact registered business actions.\n- **gateway_send**(platform, chat_id, text, thread_id) [capability_expand category: gateway]: Send a message to a connected external platform (Feishu group, Telegram chat, DingTalk conversation, etc.). Requires the platform to be connected via gateway_connect first. Use gateway_connect with action='list' to see connected platforms and available chat IDs.\n- **gateway_connect**(action, platform, credentials, options) [capability_expand category: gateway]: Connect, configure, or manage external platform integrations (Feishu, DingTalk, Telegram, Slack, Discord, etc.). Conversational flow: 1) call 'guide' to get setup steps + required fields, 2) present the steps to the user and ask for ALL required credentials in a single message, 3) call 'connect' with the credentials. Goal: complete in 1–2 user turns. NEVER include credential values in your text response.\n- **plugin_list**(): List the live plugin registry and cross-subsystem capability evidence. Use this before answering questions about whether LeapFlow supports plugins, self-evolution, plugin installation, hot reload, versioning, or other runtime capabilities.\n- **plugin_status**(plugin_id): Get detailed status of a specific plugin: its declared category, runtime dependencies, contributed tools, and fiber lifecycle state.\n- **plugin_versions**(plugin_id): List recorded source versions and active pointer for a profile-scoped plugin.\n- **plugin_propose**(requested_capability, plugin_id, proposed_tools, test_cases, risk_level, evidence): Create a side-effect-free PluginProposal from explicit capability-gap evidence. Use this before plugin_generate when a missing capability should be reviewed. Does not call an LLM, write files, or install anything.\n- **assess_compatibility**(manifest, source_path) [capability_expand category: plugin_management]: Assess whether a foreign plugin manifest or real DSH source bundle is compatible with LeapFlow. A source bundle assessment is static: runtime_ready stays false until restricted Node discovery during plugin_install. Returns component-level verdicts and limitations.\n- **plugin_generate**(plugin_id, description, proposal_id) [capability_expand category: system]: Generate a new ToolPlugin from a natural-language capability description. The LLM produces code that conforms to the ToolPlugin Protocol; it is then rigorously validated (syntax, structure, import, protocol conformance). The generated source is stored in CAS and receives explicit content approval. It DOES NOT install the plugin — installation requires a second, mutation-specific approval via plugin_install.\n- **plugin_install**(plugin_id, code, marketplace_name, source_path, proposal_id, version_label) [capability_expand category: system]: Install a plugin from exactly one source: validated Python code, the configured Python marketplace, or a real DSH source bundle. DSH bundles run restricted Node discovery before registration; only public host tools are installed and client UI limitations are reported. Writes profile-scoped state and mutates the process-global registry. REQUIRES APPROVAL.\n- **plugin_rollback**(plugin_id, version) [capability_expand category: system]: Rollback a profile-scoped plugin to a recorded source version and reload it. REQUIRES APPROVAL.\n- **plugin_reload**(plugin_id, version_label) [capability_expand category: system]: Hot-reload a plugin at runtime. Disposes the old plugin fiber, re-imports its module, and registers a fresh instance. Existing in-flight turns are unaffected (snapshot isolation). REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_disable**(plugin_id) [capability_expand category: system]: Disable a plugin by disposing its fiber, removing its tools from the runtime registry. Cannot disable self_management itself. REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_remove**(plugin_id, delete_source) [capability_expand category: system]: Terminally remove a plugin: dispose its fiber, unregister its tools, remove reload metadata, and optionally delete its profile-scoped source file. Cannot remove self_management itself. REQUIRES APPROVAL.\n- **plugin_unquarantine**(plugin_id) [capability_expand category: plugin_management]: Restore a quarantined plugin to probation status for re-evaluation. Unfreezes the trust ledger, transitions the proposal back to PROBATION, and reloads the plugin. REQUIRES APPROVAL.\n- **plugin_enable**(plugin_id) [capability_expand category: system]: Re-enable a previously disabled plugin by reloading its module and registering a fresh instance. REQUIRES APPROVAL.\n- **self_describe**(facet): Introspect LeapFlow's own identity, capabilities, runtime state, evolution metrics, or platform connections. Use facet='all' only when a comprehensive self-check is explicitly requested.\n- **runtime_snapshot**(): Lightweight (~150 token) flat snapshot of current runtime state: model, context budget, posture, disclosure level, turn count, cache hit rate, uptime, tool count, and pending approvals.\n- **schedule_create**(trigger_expression, instruction, execution_mode, max_retries, delivery_target) [capability_expand category: scheduler]: Create a new scheduled task. Specify a trigger expression (e.g. '30m', 'every 2h', '0 9 * * *') and the instruction to execute on each trigger. Optionally provide an execution mode and a delivery target to receive notifications.\n- **schedule_list**() [capability_expand category: scheduler]: List all scheduled tasks with their current state, trigger, and next due time.\n- **schedule_status**(task_id) [capability_expand category: scheduler]: Get detailed status and recent execution history for a scheduled task.\n- **schedule_pause**(task_id) [capability_expand category: scheduler]: Pause a scheduled task so it stops firing without being cancelled.\n- **schedule_resume**(task_id) [capability_expand category: scheduler]: Resume a paused scheduled task — re-arms it and recalculates next due time.\n- **schedule_cancel**(task_id) [capability_expand category: scheduler]: Cancel a scheduled task permanently.\n- **tool_search**(query, max_results): Search all registered tools by keyword relevance. Returns a ranked list of matching tools with name, category, and summary. Use this when you need a tool but are unsure of its exact name or category.\n- **tool_describe**(tool_name): Get the full callable schema of a single tool by exact name. Returns the tool's description, parameters, and metadata. Use after tool_search to inspect a specific tool before calling it.\n- **session_search**(query, limit) [capability_expand category: unclassified]: Search past conversation sessions in the current workspace for relevant context.\n- **session_list**(limit): List recent conversation sessions in the current workspace with stable ids, titles, dates, and summaries. Use for browsing past tasks or when user asks to see history without specific search terms. No keywords needed — returns chronological list.\n- **session_detail**(session_id, limit, offset, include_inactive) [capability_expand category: unclassified]: Read a paginated persisted transcript for one past conversation session in the current workspace. Use after session_list or session_search returns a session_id.\n- **click**(element_index) [capability_expand category: desktop]: Click a UI element by its element_index (from the latest observe_ui snapshot)\n- **get_clipboard**() [capability_expand category: desktop]: Read current clipboard text content\n- **list_apps**(filter, running_only) [capability_expand category: desktop]: List available applications on this system. Use to discover correct bundle_id before switch_app.\n- **list_windows**() [capability_expand category: desktop]: List all top-level windows with pid, window_id, title, and per-window state (minimized, on-screen). Call this first to pick the pid and window_id that observe_ui and other window tools require.\n- **observe_ui**(pid, window_id, query) [capability_expand category: desktop]: Snapshot one window's actionable UI elements, each tagged with an element_index for click/right_click/read_text. Re-observe after actions — indices belong to one snapshot. Requires the window's pid and window_id from list_windows.\n- **open_url**(url, app_id) [capability_expand category: desktop]: Open a URL in the default or specified browser\n- **read_text**(element_index) [capability_expand category: desktop]: Read the text content of a specific UI element from the latest snapshot\n- **right_click**(element_index) [capability_expand category: desktop]: Right-click a UI element to open its context menu. Returns visible menu items.\n- **screenshot**(pid, window_id) [capability_expand category: desktop]: Capture a screenshot for visual verification. With pid + window_id captures that window (works across all displays); defaults to the last observed window, or the full desktop when no window has been observed.\n- **scroll**(element_index, direction, amount, pid, window_id) [capability_expand category: desktop]: Scroll a scrollable area of a window. Omit element_index to scroll the window's focused/page scroller; pass one to scroll an exact element from the latest snapshot.\n- **select_text**(element_index) [capability_expand category: desktop]: Select all text in a UI element (focus + select-all, for subsequent copy)\n- **set_clipboard**(text) [capability_expand category: desktop]: Write text to the clipboard\n- **shortcut**(keys) [capability_expand category: desktop]: Execute a keyboard shortcut\n- **switch_app**(app_id) [capability_expand category: desktop]: Switch to an app (launch if needed, activate, verify)\n- **type_text**(text) [capability_expand category: desktop]: Type text into the currently focused element\n- **wait**(seconds) [capability_expand category: desktop]: Wait for a specified duration before continuing\n- **wait_until**(condition, pid, window_id, timeout, poll_interval) [capability_expand category: desktop]: Wait until a UI condition is met (polls UI tree). Returns elements when found or on timeout.\n- **wait_until_stable**(timeout, poll_interval, pid, window_id) [capability_expand category: desktop]: Wait until the UI stops changing (element set stabilizes across polls).\n\n## App Connector Capability Index\nLeapFlow can onboard and manage external apps through `platform_connect` and execute exact registered business actions through `platform_action`.\nFor requests about connecting, setting up, configuring, enabling, or managing a supported app, use `platform_connect` first instead of generating SDK/Webhook sample code.\n`platform_connect.action` is the management namespace: list, guide, preflight, connect, disconnect, remove, status, events_start, events_stop, events_status.\n`platform_action.action` is only for exact registered platform business actions listed below, such as `im.send_message`; never use management actions like `list` or `guide` there.\nAll business fields MUST go inside `payload`; top-level keys are only `platform`, `action`, and `payload`.\nDo not invent platform IDs or platform action names. If the needed action is not listed, ask for discovery/clarification instead of guessing.\nUse `platform_connect` with `action='guide'` and the matching `platform` to start onboarding; use `action='list'` when the app is unclear.\nWhen a pending onboarding state is present, continue from that state with `platform_connect` instead of asking the user to restate the app.\nSupported apps:\n- `api_server`: API Server (OpenAI Compatible) (category=api; backend=adapter; platform_action actions=none registered)\n- `dingtalk`: 钉钉 (DingTalk) (category=im; backend=adapter; platform_action actions=none registered)\n- `feishu`: 飞书 (Feishu/Lark) (category=im; backend=cli)\n - `calendar.create_event` payload={summary*, start_time*, end_time*, attendees} [write/high]\n - `docs.create_markdown` payload={title*, markdown*, folder_token} [write/medium]\n - `drive.search` payload={query*, limit} [read/medium]\n - `im.add_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.download_resource` payload={message_id*, file_key*, type*} [read/low]\n - `im.get_messages` payload={message_ids*} [read/low]\n - `im.list_chats` payload={page_size} [read/low]\n - `im.list_messages` payload={chat_id*} [read/low]\n - `im.list_thread_messages` payload={thread*} [read/low]\n - `im.remove_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.reply_message` payload={message_id*, text*} [send/high]\n - `im.search_chats` payload={query*, page_size} [read/low]\n - `im.search_messages` payload={query*} [read/medium]\n - `im.send_message` payload={chat_id*, text*, thread_id} [send/high]\n - `im.update_card` payload={token*, card*} [write/medium]\n - `im.update_message` payload={message_id*, text*} [write/medium]\n - `mail.search_unread` payload={query, limit} [read/high]\n - `sheets.append_row` payload={spreadsheet_token*, sheet_id*, values*} [write/medium]\n - `task.create` payload={title*, description, due_time} [write/medium]\n- `telegram`: Telegram (category=social; backend=adapter; platform_action actions=none registered)\n- `webhook`: Webhook (Generic) (category=webhook; backend=adapter; platform_action actions=none registered)\n\n## Tool Usage\nTools are normally invoked through the native function-calling mechanism, not by writing JSON in your reply\ntext. Only if the provider signals that native function calling is unavailable for this turn, fall back to a\nsingle JSON code block: `{\"name\": \"tool_name\", \"arguments\": {\"key\": \"value\"}}` — use this fallback format\nonly, never both. Only call a tool whose exact name appears in the tool index above and is currently callable\n(or reachable via `capability_expand`). Never invent, rename, alias, or guess a tool name, platform ID, or\nplatform action from argument shape or wording — if the index does not list it, it does not exist.\n`platform_connect.action` (list/guide/preflight/connect/disconnect/remove/status/events_start/events_stop/\nevents_status) is the App Connector management namespace; `platform_action.action` only accepts exact\nregistered business actions such as `im.send_message` shown in the App Connector Capability Index — never mix\nthe two namespaces. If a tool call returns an unknown/unavailable result, use the returned suggestions or\navailable names for a single retry instead of trying further variations of the same guess.\n\n**Side-effect action rule** (`platform_action` with effect=send/write/execute):\n- Call each unique action+payload **exactly once**. Never include duplicates in the same turn.\n- Once the result returns `\"completed\": true`, that action is DONE for this task. Do NOT call it again\n in any subsequent turn — immediately summarize the result for the user instead.\n- The system enforces idempotency: duplicate calls are blocked and will not execute.\n- If the user explicitly requests sending/writing multiple times, use distinct payloads per call.\n\n**Resource identifier provenance rule**:\n- NEVER fabricate, guess, or infer resource identifiers (chat_id, message_id, file_key, user_id, etc.).\n Every resource ID used in a side-effect action MUST come from a successful API response in this session.\n- If a read/list action fails (e.g. authorization error), you do NOT have valid resource IDs.\n Report the failure to the user — do NOT attempt the dependent write/send action with a guessed ID.\n- When a tool result contains `\"llm_instruction\"`, follow it exactly.\n\n## Guidelines\n1. **Direct answers first**: If you already know the answer, respond directly without tools.\n2. **Avoid redundant tool calls**: Do not call the same tool with the same arguments more than once in the same user turn. When an existing tool result already answers the user's request, stop calling tools and answer directly.\n3. **Use tools proactively**: When the user asks about files, time, system state, or needs actions performed, use the appropriate tool.\n4. **Chain tools when needed**: You can call multiple tools in sequence (e.g., list files → read file → summarize).\n5. **Handle failures gracefully**: If a tool fails, explain what went wrong and suggest alternatives. If it failed because of an unknown tool/platform/action name, retry once with an exact name from the returned suggestions, then explain rather than keep guessing.\n6. **Summarize results naturally**: After tool execution, synthesize the results into a helpful answer rather than dumping raw output.\n7. **Stay conversational**: Maintain a natural, helpful tone. Acknowledge context from earlier in the conversation.\n8. **Recall past work**:\n - Broad queries (\"之前做了什么\", \"列出任务\"): answer from the \"Recent Task History\" section already in your context. If insufficient, call session_list.\n - Specific lookups (\"上次那个配置怎么改的\"): call session_search with relevant phrases (NOT single characters).\n - Do NOT call search tools repeatedly with keyword fragments. One well-phrased call is better than ten fragmented ones.\n\n## Coding & Verification\nWhen working with code, prefer the precise built-in tools over ad-hoc shell: use `repo_map` to orient in an\nunfamiliar project, `code_search`/`file_find` to locate and `code_intel` for symbols, `edit_file` (anchored\nsearch-replace, or a unified `diff`) to change files — never rewrite a whole file to change a few lines — and\n`git_query` to inspect diffs/log. After edits, check `syntax_ok` in the result and run `test_run`/`lint_check`\nbefore declaring the work done. Batch independent read-only calls (search/read/query) into a single turn — they\nrun in parallel.\n\n## Presentation Style\n1. **Polished Markdown only**: Format user-facing answers with clean Markdown headings, short paragraphs, and concise bullets. Use tables only when they improve comparison or scanning.\n2. **Terminal-friendly layout**: Keep lines readable in a TUI; avoid dense walls of text, deeply nested lists, oversized ASCII art, or heavy visual blocks.\n3. **Elegant emphasis**: Use bold text sparingly for key terms and conclusions. Avoid excessive emojis, decorative symbols, repeated separators, or visual noise.\n4. **Theme-safe colors**: Do not emit ANSI escape codes, HTML color tags, Rich markup, or hardcoded color names. Rely on the TUI theme to render Markdown professionally.\n5. **No leaked tool protocol**: Never show tool-call JSON, internal schemas, raw observations, tool result payloads, or hidden reasoning in the final answer unless the user explicitly asks for raw/debug output. Treat any prior `{\"name\": ..., \"arguments\": ...}` blocks and `Tool result (...)` messages as internal execution context only.\n6. **Professional closure**: End with a concise conclusion or next step when helpful; avoid rambling after the useful answer is complete.\n\nWhen finished with all tool calls, respond normally without a JSON block, tool-call transcript, or process log.\n\n## Task Contract\n- Task ID: turn-4\n- Original user request: Do the impossible thing.\n- Workspace root: \n- Allowed roots: \n- Treat relative project paths as relative to the workspace root; never infer `.` as the project root when a workspace root is provided.\n- Workspace boundary is enforced by tools: do not read, search, edit, or run commands against paths outside the allowed roots unless the user explicitly requests an external path and the tool/approval policy permits it.\n- LeapFlow workspace config is optional at `/.leapflow/config.yaml`; runtime config is loaded from `~/.leapflow/config/user.yaml` and `~/.leapflow/profiles//config/*.yaml`.\n- Preserve this task contract across summarization, compression, tool loops, and memory retrieval.\n" - }, - { - "role": "system", - "content": "\n## Recent Session Summary\n- [assistant] Recovered after compressing context.\n- [user] Do the impossible thing.\n" - }, - { - "role": "assistant", - "content": "Recovered after compressing context." - }, - { - "role": "user", - "content": "Do the impossible thing.\nDo the impossible thing.\nSYSTEM: No further tool calls are available for this turn. Do not attempt to call any tool. Answer the user's request directly and concisely using the information already gathered above. If part of it cannot be determined from what you have, say so plainly and state what would be needed — do not repeat an earlier tool call." - } - ] - }, - "responses": [ - { - "status": 400, - "content_type": "application/json", - "body": "{\"error\": {\"message\": \"The requested configuration is not supported by this model\", \"type\": \"invalid_request_error\", \"code\": \"unsupported_value\"}}" - } - ] -} diff --git a/tests/_fixtures/cassettes/r4_recovery/cassette-model-40d35bee3d5c7435.cassette.json b/tests/_fixtures/cassettes/r4_recovery/cassette-model-40d35bee3d5c7435.cassette.json new file mode 100644 index 00000000..f4839bd4 --- /dev/null +++ b/tests/_fixtures/cassettes/r4_recovery/cassette-model-40d35bee3d5c7435.cassette.json @@ -0,0 +1,72 @@ +{ + "fingerprint": "40d35bee3d5c7435c52ab048b9812ee149e580944510831821870cbdbad39d35", + "note": "captured in seed mode", + "request": { + "model": "cassette-model", + "stream": false, + "messages": [ + { + "role": "system", + "content": "You are LeapFlow, an intelligent assistant that can both converse naturally and take real actions on the user's computer.\n\n## Capabilities\nThe tool index below lists **every** registered tool by name and a one-line summary — this is the complete\ncapability contract; nothing else exists. Only a subset is directly callable this turn (via native tool calling,\nnot a JSON block in your reply). If you need a tool from the index that is not yet callable, call\n`capability_expand` with its category name first — the matching tools become callable immediately after.\nWhen the user asks about LeapFlow **itself** — its identity/version/model, what tools, plugins, or capabilities\nit has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of\nself-awareness — answer from live runtime evidence via `self_describe` (facet=capabilities for \"what can you\ndo / do you support X\", identity for version/model, runtime for context/posture, or `all` for a full self-check),\nnever by reading LeapFlow's own source code, documentation, or memory. Report configuration-dependent or\nunavailable capabilities as limitations rather than inferring them.\n- **text_search**(text, pattern): Search for a regex pattern in text.\n- **text_replace**(text, old, new, count) [capability_expand category: write]: Replace occurrences of a substring in text.\n- **time_get**(): Get current date and time.\n- **env_info**(): Get system environment information (OS, Python version, cwd).\n- **skills_list**(query, category, source): List available learned skills. Use when user asks about capabilities or you need a specific skill.\n- **skill_view**(name): View the full content of a specific skill document.\n- **skill_list_files**(name) [capability_expand category: skills]: List all support files in a skill directory (configs, templates, examples). Use before skill_file_read to discover available files.\n- **skill_file_read**(name, file_path) [capability_expand category: skills]: Read an individual support file from a skill directory. Use skill_list_files first to discover available files.\n- **code_intel**(path, operation): Precise document symbols (outline) for a source file: classes, functions, and methods with line ranges. Python uses an exact AST parse; other languages use a keyword-prefix scan. Prefer over file_read mode=symbols for accurate navigation before editing. Read-only.\n- **repo_map**(path): Compact project orientation for a repository root: languages, detected test/lint commands, top-level structure, entry points, manifest, and VCS branch. Call this first when entering an unfamiliar codebase. Read-only.\n- **scm_sync**(action, cwd, remote, pull_ref, push_ref, timeout) [capability_expand category: scm]: Run a typed git SCM action. Use this instead of shell_run for git pull/push/status. For 'pull origin main then push', set action='pull_then_push', remote='origin', pull_ref='main', and omit push_ref so LeapFlow pushes the current local branch.\n- **git_query**(action, cwd, ref, path, staged, max_count, stat): Read-only structured git inspection: action=diff|log|status|branch|show. Prefer over shell_run for reading repo state — output is clipped, redacted, and log/branch are parsed into structured fields. Use scm_sync for pull/push.\n- **git_write**(action, cwd, message, stage_all, name, ref, create) [capability_expand category: scm]: Mutating git actions: action=commit (message, stage_all), branch (create+switch), checkout (switch; create=true for -b). Approval-gated. Use scm_sync for pull/push and git_query for reads.\n- **test_run**(command, cwd, timeout): Run the project's test suite and return structured results (framework, passed/failed counts, failing tests). Auto-detects the runner (pytest/npm/go/cargo) or uses a configured/explicit command; executes via the governed shell. ok=true means the runner executed — see 'success' for pass/fail.\n- **lint_check**(command, cwd, timeout): Run the project's linter and return a structured clean/issue result. Auto-detects the linter (ruff/eslint/go vet/clippy) or uses a configured/explicit command; executes via the governed shell. ok=true means the linter ran — see 'clean'.\n- **file_list**(path, pattern, depth): List files and directories at a given path. Use depth=1 or depth=2 to get a recursive tree in one call instead of listing each sub-directory separately.\n- **file_read**(path, max_lines, start_line, max_chars, mode): Read text file content with adaptive context governance. For large or unfamiliar files, prefer mode='outline' or mode='symbols' first, then use mode='raw' with start_line/max_lines for the specific range you actually need. For LeapFlow's own settings, use config_list / config_get / config_set — its config files are outside the workspace and not readable here.\n- **file_write**(path, content, mode) [capability_expand category: write]: Write content to a file (overwrite or append).\n- **code_search**(pattern, patterns, path, glob, ignore_case, multiline, max_results, context_lines): Search file CONTENTS by regex pattern across a directory tree (ripgrep-backed). Requires a regex pattern. NOT for listing or browsing directory contents — use file_list for that. Prefer this over shell_run grep: faster, skips VCS/dependency/build dirs, and returns structured path:line:column matches. Batch related lookups into ONE call via `patterns` (OR-combined, single pass) instead of issuing several separate searches. Use file_read for the surrounding context of a hit.\n- **file_find**(glob, path, max_results): Find files by a recursive glob pattern under a base path (e.g. '**/test_*.py' or '*.md'). Prefer this over shell_run find; skips VCS/dependency/build dirs.\n- **edit_file**(path, edits, dry_run, diff) [capability_expand category: write]: Apply targeted, anchored search-replace edits to an EXISTING text file (use file_write to create/overwrite). Each edit is {original_text, new_text, replace_all?}; original_text must match exactly and uniquely (or set replace_all) — a non-unique or missing anchor is rejected so files are never corrupted. Set dry_run to preview. Alternatively pass a unified 'diff' to apply its hunks as anchored edits. Far cheaper and safer than rewriting a whole file.\n- **shell_run**(command, cwd, timeout) [capability_expand category: shell]: Execute a one-shot shell command with timeout protection. Runs in the active workspace; paths resolving outside it are refused. Reach for a structured tool first when one fits — web_fetch for anything over HTTP(S), git_query/scm_sync for git, code_search/file_find/file_read for the repo, config_get/config_set for LeapFlow's own settings — because those report typed results, while a failed shell command can only be diagnosed from its exit code and stderr. Every shell run counts as an external side effect, so a failure stops the rest of the batch and is not retried automatically.\n- **terminal_open**(command, cwd, shell) [capability_expand category: terminal]: Open a PERSISTENT shell session (REPL/dev server/watch), returning a session_id for terminal_send/read/close. Disabled unless tools.terminal_session_enabled is set. For one-shot commands use shell_run instead.\n- **terminal_send**(session_id, input, wait) [capability_expand category: terminal]: Send a line of input to a persistent terminal session and return output captured shortly after.\n- **terminal_read**(session_id, wait): Drain buffered output from a persistent terminal session (optionally waiting briefly first).\n- **terminal_close**(session_id) [capability_expand category: terminal]: Terminate a persistent terminal session and release its process group.\n- **terminal_list**(): List active persistent terminal sessions.\n- **config_list**(category, limit): List LeapFlow's own writable settings (LLM model and endpoint, daemon, memory, perception, gateway, …) with current values. Use this to discover the exact key before changing anything. Optionally narrow by `category`. This is the only correct way to inspect LeapFlow configuration — never read config files from disk.\n- **config_get**(key): Read one LeapFlow setting by key (e.g. 'llm.model', 'llm.base_url', 'daemon.log_level'), returning its current value, type, scopes, and whether a change needs a daemon restart. There is no 'llm.provider' key: provider behavior is inferred from the OpenAI-compatible 'llm.base_url'. Never read LeapFlow config files from disk — use this.\n- **config_set**(key, value, scope) [capability_expand category: config]: Change one LeapFlow setting by key, e.g. switch an OpenAI-compatible LLM with key='llm.model' and key='llm.base_url'. There is no 'llm.provider' key: provider behavior is inferred from the endpoint. Values are validated and coerced; credentials are stored in the vault automatically. Call config_list or config_get first if unsure of the key. The result states whether a `leap daemon restart` is required. Never edit LeapFlow config files directly.\n- **web_fetch**(url, select, timeout, max_bytes): Read a URL over HTTP(S) and get back extracted, context-sized content: parsed JSON for API endpoints, readable text plus links for web pages. Use this for anything on the internet — prices, docs, releases, articles — instead of running curl through shell_run: it reports real HTTP status codes, retries rate limits on its own, and is a plain read so a retry is always safe. For JSON APIs pass `select` with a dotted path (e.g. 'chart.result.0.meta') to return just that part instead of the whole payload.\n- **memory_search**(query, limit): Search agent memory for relevant past experiences, observations, and facts.\n- **memory_add**(content, kind) [capability_expand category: write]: Store a new observation or insight in memory for future reference.\n- **research_note**(kind, text): Record a compact, structured note about the current task's state so it survives context compression on long / multi-step tasks. Use for durable findings, open questions still to resolve, decisions / excluded paths, and the immediate next step. One concise sentence per note.\n- **capability_expand**(category): Fetch the full callable schema for every tool in a capability category (e.g. 'hub', 'gateway', 'desktop', 'delegate', 'file', 'memory', 'skill'). The compact tool index always lists every registered tool by name and a one-line summary, but only a static low-risk subset is directly callable each turn. If you need a tool from the index that is not yet callable, call capability_expand with its category first; the matching tools become callable in this turn. Never invent a tool name — expand the category instead.\n- **delegate_task**(goal, context) [capability_expand category: delegate]: Delegate a complex sub-task to an isolated subagent. The subagent gets a fresh context and restricted tool access. Use when a task is self-contained and can be solved independently.\n- **schedule_reentry**(kind, reason, delay_seconds, event_match, max_reentries, deadline_seconds): Register a re-entry so this task can resume later from its current orientation (findings / open questions / next step). Use when work must pause and continue after a delay (kind=time) or when a matching platform event arrives (kind=event), instead of finishing now. The research-ledger state is carried over automatically.\n- **hub_push**(skill_name, visibility, version) [capability_expand category: hub]: Push a local skill to the ModelScope Hub for sharing or backup.\n- **hub_pull**(repo_id, version) [capability_expand category: hub]: Pull a skill from the ModelScope Hub to install locally.\n- **hub_search**(query) [capability_expand category: hub]: Search for skills on the Hub by keyword or description.\n- **hub_sync**(mode, dry_run) [capability_expand category: hub]: Preview or execute sync between local skills and Hub.\n- **platform_action**(platform, action, payload, backend_kind) [capability_expand category: gateway]: Execute an exact registered business action on an external platform through LeapFlow's App Connector layer. Actions must be copied from the App Connector Capability Index and are addressed as domain.operation, e.g. im.send_message or docs.create_markdown. All business fields (chat_id, text, query, etc.) MUST be placed inside `payload`, never at the top level. Example: {\"platform\":\"feishu\",\"action\":\"im.send_message\",\"payload\":{\"chat_id\":\"oc_xxx\",\"text\":\"hello\"}}. Do not invent action names, do not use management actions such as list/guide/connect/status here.\n- **platform_connect**(action, platform, credentials, options, checkpoint) [capability_expand category: gateway]: List, guide, connect, disconnect, remove, or check status for external platforms using the App Connector management namespace. Supports REST and CLI backends. Use this for management actions such as list/guide/preflight/connect/status; use platform_action only for exact registered business actions.\n- **gateway_send**(platform, chat_id, text, thread_id) [capability_expand category: gateway]: Send a message to a connected external platform (Feishu group, Telegram chat, DingTalk conversation, etc.). Requires the platform to be connected via gateway_connect first. Use gateway_connect with action='list' to see connected platforms and available chat IDs.\n- **gateway_connect**(action, platform, credentials, options) [capability_expand category: gateway]: Connect, configure, or manage external platform integrations (Feishu, DingTalk, Telegram, Slack, Discord, etc.). Conversational flow: 1) call 'guide' to get setup steps + required fields, 2) present the steps to the user and ask for ALL required credentials in a single message, 3) call 'connect' with the credentials. Goal: complete in 1–2 user turns. NEVER include credential values in your text response.\n- **plugin_list**(): List the live plugin registry across Tool/Gateway/LLM subsystems for plugin management: which plugins are loaded, their category and fiber lifecycle state. For questions about LeapFlow's own capabilities or whether it supports X, use self_describe instead.\n- **plugin_status**(plugin_id): Get detailed status of a specific plugin: its declared category, runtime dependencies, contributed tools, and fiber lifecycle state.\n- **plugin_versions**(plugin_id): List recorded source versions and active pointer for a profile-scoped plugin.\n- **plugin_propose**(requested_capability, plugin_id, proposed_tools, test_cases, risk_level, evidence): Create a side-effect-free PluginProposal from explicit capability-gap evidence. Use this before plugin_generate when a missing capability should be reviewed. Does not call an LLM, write files, or install anything.\n- **assess_compatibility**(manifest, source_path) [capability_expand category: plugin_management]: Assess whether a foreign plugin manifest or real DSH source bundle is compatible with LeapFlow. A source bundle assessment is static: runtime_ready stays false until restricted Node discovery during plugin_install. Returns component-level verdicts and limitations.\n- **plugin_generate**(plugin_id, description, proposal_id) [capability_expand category: system]: Generate a new ToolPlugin from a natural-language capability description. The LLM produces code that conforms to the ToolPlugin Protocol; it is then rigorously validated (syntax, structure, import, protocol conformance). The generated source is stored in CAS and receives explicit content approval. It DOES NOT install the plugin — installation requires a second, mutation-specific approval via plugin_install.\n- **plugin_install**(plugin_id, code, marketplace_name, source_path, proposal_id, version_label) [capability_expand category: system]: Install a plugin from exactly one source: validated Python code, the configured Python marketplace, or a real DSH source bundle. DSH bundles run restricted Node discovery before registration; only public host tools are installed and client UI limitations are reported. Writes profile-scoped state and mutates the process-global registry. REQUIRES APPROVAL.\n- **plugin_rollback**(plugin_id, version) [capability_expand category: system]: Rollback a profile-scoped plugin to a recorded source version and reload it. REQUIRES APPROVAL.\n- **plugin_reload**(plugin_id, version_label) [capability_expand category: system]: Hot-reload a plugin at runtime. Disposes the old plugin fiber, re-imports its module, and registers a fresh instance. Existing in-flight turns are unaffected (snapshot isolation). REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_disable**(plugin_id) [capability_expand category: system]: Disable a plugin by disposing its fiber, removing its tools from the runtime registry. Cannot disable self_management itself. REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_remove**(plugin_id, delete_source) [capability_expand category: system]: Terminally remove a plugin: dispose its fiber, unregister its tools, remove reload metadata, and optionally delete its profile-scoped source file. Cannot remove self_management itself. REQUIRES APPROVAL.\n- **plugin_unquarantine**(plugin_id) [capability_expand category: plugin_management]: Restore a quarantined plugin to probation status for re-evaluation. Unfreezes the trust ledger, transitions the proposal back to PROBATION, and reloads the plugin. REQUIRES APPROVAL.\n- **plugin_enable**(plugin_id) [capability_expand category: system]: Re-enable a previously disabled plugin by reloading its module and registering a fresh instance. REQUIRES APPROVAL.\n- **self_describe**(facet): Answer questions about LeapFlow ITSELF from live runtime evidence — its identity/version/model, what tools and plugins it has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of self-awareness. Prefer this over reading LeapFlow's own source code. Pick facet=capabilities for 'what can you do / do you support X', identity for version/model, runtime for context/posture, or all for a full self-check when explicitly requested.\n- **runtime_snapshot**(): Lightweight (~150 token) flat snapshot of current runtime state: model, context budget, posture, disclosure level, turn count, cache hit rate, uptime, tool count, and pending approvals.\n- **schedule_create**(trigger_expression, instruction, execution_mode, max_retries, delivery_target) [capability_expand category: scheduler]: Create a new scheduled task. Specify a trigger expression (e.g. '30m', 'every 2h', '0 9 * * *') and the instruction to execute on each trigger. Optionally provide an execution mode and a delivery target to receive notifications.\n- **schedule_list**() [capability_expand category: scheduler]: List all scheduled tasks with their current state, trigger, and next due time.\n- **schedule_status**(task_id) [capability_expand category: scheduler]: Get detailed status and recent execution history for a scheduled task.\n- **schedule_pause**(task_id) [capability_expand category: scheduler]: Pause a scheduled task so it stops firing without being cancelled.\n- **schedule_resume**(task_id) [capability_expand category: scheduler]: Resume a paused scheduled task — re-arms it and recalculates next due time.\n- **schedule_cancel**(task_id) [capability_expand category: scheduler]: Cancel a scheduled task permanently.\n- **tool_search**(query, max_results): Search all registered tools by keyword relevance. Returns a ranked list of matching tools with name, category, and summary. Use this when you need a tool but are unsure of its exact name or category.\n- **tool_describe**(tool_name): Get the full callable schema of a single tool by exact name. Returns the tool's description, parameters, and metadata. Use after tool_search to inspect a specific tool before calling it.\n- **session_search**(query, limit) [capability_expand category: unclassified]: Search past conversation sessions in the current workspace for relevant context.\n- **session_list**(limit): List recent conversation sessions in the current workspace with stable ids, titles, dates, and summaries. Use for browsing past tasks or when user asks to see history without specific search terms. No keywords needed — returns chronological list.\n- **session_detail**(session_id, limit, offset, include_inactive) [capability_expand category: unclassified]: Read a paginated persisted transcript for one past conversation session in the current workspace. Use after session_list or session_search returns a session_id.\n- **click**(element_index) [capability_expand category: desktop]: Click a UI element by its element_index (from the latest observe_ui snapshot)\n- **get_clipboard**() [capability_expand category: desktop]: Read current clipboard text content\n- **list_apps**(filter, running_only) [capability_expand category: desktop]: List available applications on this system. Use to discover correct bundle_id before switch_app.\n- **list_windows**() [capability_expand category: desktop]: List all top-level windows with pid, window_id, title, and per-window state (minimized, on-screen). Call this first to pick the pid and window_id that observe_ui and other window tools require.\n- **observe_ui**(pid, window_id, query) [capability_expand category: desktop]: Snapshot one window's actionable UI elements, each tagged with an element_index for click/right_click/read_text. Re-observe after actions — indices belong to one snapshot. Requires the window's pid and window_id from list_windows.\n- **open_url**(url, app_id) [capability_expand category: desktop]: Open a URL in the default or specified browser\n- **read_text**(element_index) [capability_expand category: desktop]: Read the text content of a specific UI element from the latest snapshot\n- **right_click**(element_index) [capability_expand category: desktop]: Right-click a UI element to open its context menu. Returns visible menu items.\n- **screenshot**(pid, window_id) [capability_expand category: desktop]: Capture a screenshot for visual verification. With pid + window_id captures that window (works across all displays); defaults to the last observed window, or the full desktop when no window has been observed.\n- **scroll**(element_index, direction, amount, pid, window_id) [capability_expand category: desktop]: Scroll a scrollable area of a window. Omit element_index to scroll the window's focused/page scroller; pass one to scroll an exact element from the latest snapshot.\n- **select_text**(element_index) [capability_expand category: desktop]: Select all text in a UI element (focus + select-all, for subsequent copy)\n- **set_clipboard**(text) [capability_expand category: desktop]: Write text to the clipboard\n- **shortcut**(keys) [capability_expand category: desktop]: Execute a keyboard shortcut\n- **switch_app**(app_id) [capability_expand category: desktop]: Switch to an app (launch if needed, activate, verify)\n- **type_text**(text) [capability_expand category: desktop]: Type text into the currently focused element\n- **wait**(seconds) [capability_expand category: desktop]: Wait for a specified duration before continuing\n- **wait_until**(condition, pid, window_id, timeout, poll_interval) [capability_expand category: desktop]: Wait until a UI condition is met (polls UI tree). Returns elements when found or on timeout.\n- **wait_until_stable**(timeout, poll_interval, pid, window_id) [capability_expand category: desktop]: Wait until the UI stops changing (element set stabilizes across polls).\n\n## App Connector Capability Index\nLeapFlow can onboard and manage external apps through `platform_connect` and execute exact registered business actions through `platform_action`.\nFor requests about connecting, setting up, configuring, enabling, or managing a supported app, use `platform_connect` first instead of generating SDK/Webhook sample code.\n`platform_connect.action` is the management namespace: list, guide, preflight, connect, disconnect, remove, status, events_start, events_stop, events_status.\n`platform_action.action` is only for exact registered platform business actions listed below, such as `im.send_message`; never use management actions like `list` or `guide` there.\nAll business fields MUST go inside `payload`; top-level keys are only `platform`, `action`, and `payload`.\nDo not invent platform IDs or platform action names. If the needed action is not listed, ask for discovery/clarification instead of guessing.\nUse `platform_connect` with `action='guide'` and the matching `platform` to start onboarding; use `action='list'` when the app is unclear.\nWhen a pending onboarding state is present, continue from that state with `platform_connect` instead of asking the user to restate the app.\nSupported apps:\n- `api_server`: API Server (OpenAI Compatible) (category=api; backend=adapter; platform_action actions=none registered)\n- `dingtalk`: 钉钉 (DingTalk) (category=im; backend=adapter; platform_action actions=none registered)\n- `feishu`: 飞书 (Feishu/Lark) (category=im; backend=cli)\n - `calendar.create_event` payload={summary*, start_time*, end_time*, attendees} [write/high]\n - `docs.create_markdown` payload={title*, markdown*, folder_token} [write/medium]\n - `drive.search` payload={query*, limit} [read/medium]\n - `im.add_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.download_resource` payload={message_id*, file_key*, type*} [read/low]\n - `im.get_messages` payload={message_ids*} [read/low]\n - `im.list_chats` payload={page_size} [read/low]\n - `im.list_messages` payload={chat_id*} [read/low]\n - `im.list_thread_messages` payload={thread*} [read/low]\n - `im.remove_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.reply_message` payload={message_id*, text*} [send/high]\n - `im.search_chats` payload={query*, page_size} [read/low]\n - `im.search_messages` payload={query*} [read/medium]\n - `im.send_message` payload={chat_id*, text*, thread_id} [send/high]\n - `im.update_card` payload={token*, card*} [write/medium]\n - `im.update_message` payload={message_id*, text*} [write/medium]\n - `mail.search_unread` payload={query, limit} [read/high]\n - `sheets.append_row` payload={spreadsheet_token*, sheet_id*, values*} [write/medium]\n - `task.create` payload={title*, description, due_time} [write/medium]\n- `telegram`: Telegram (category=social; backend=adapter; platform_action actions=none registered)\n- `webhook`: Webhook (Generic) (category=webhook; backend=adapter; platform_action actions=none registered)\n\n## Tool Usage\nTools are normally invoked through the native function-calling mechanism, not by writing JSON in your reply\ntext. Only if the provider signals that native function calling is unavailable for this turn, fall back to a\nsingle JSON code block: `{\"name\": \"tool_name\", \"arguments\": {\"key\": \"value\"}}` — use this fallback format\nonly, never both. Only call a tool whose exact name appears in the tool index above and is currently callable\n(or reachable via `capability_expand`). Never invent, rename, alias, or guess a tool name, platform ID, or\nplatform action from argument shape or wording — if the index does not list it, it does not exist.\n`platform_connect.action` (list/guide/preflight/connect/disconnect/remove/status/events_start/events_stop/\nevents_status) is the App Connector management namespace; `platform_action.action` only accepts exact\nregistered business actions such as `im.send_message` shown in the App Connector Capability Index — never mix\nthe two namespaces. If a tool call returns an unknown/unavailable result, use the returned suggestions or\navailable names for a single retry instead of trying further variations of the same guess.\n\n**Side-effect action rule** (`platform_action` with effect=send/write/execute):\n- Call each unique action+payload **exactly once**. Never include duplicates in the same turn.\n- Once the result returns `\"completed\": true`, that action is DONE for this task. Do NOT call it again\n in any subsequent turn — immediately summarize the result for the user instead.\n- The system enforces idempotency: duplicate calls are blocked and will not execute.\n- If the user explicitly requests sending/writing multiple times, use distinct payloads per call.\n\n**Resource identifier provenance rule**:\n- NEVER fabricate, guess, or infer resource identifiers (chat_id, message_id, file_key, user_id, etc.).\n Every resource ID used in a side-effect action MUST come from a successful API response in this session.\n- If a read/list action fails (e.g. authorization error), you do NOT have valid resource IDs.\n Report the failure to the user — do NOT attempt the dependent write/send action with a guessed ID.\n- When a tool result contains `\"llm_instruction\"`, follow it exactly.\n\n## Guidelines\n1. **Direct answers first**: If you already know the answer, respond directly without tools.\n2. **Avoid redundant tool calls**: Do not call the same tool with the same arguments more than once in the same user turn. When an existing tool result already answers the user's request, stop calling tools and answer directly.\n3. **Use tools proactively**: When the user asks about files, time, system state, or needs actions performed, use the appropriate tool.\n4. **Chain tools when needed**: You can call multiple tools in sequence (e.g., list files → read file → summarize).\n5. **Handle failures gracefully**: If a tool fails, explain what went wrong and suggest alternatives. If it failed because of an unknown tool/platform/action name, retry once with an exact name from the returned suggestions, then explain rather than keep guessing.\n6. **Summarize results naturally**: After tool execution, synthesize the results into a helpful answer rather than dumping raw output.\n7. **Stay conversational**: Maintain a natural, helpful tone. Acknowledge context from earlier in the conversation.\n8. **Recall past work**:\n - Broad queries (\"之前做了什么\", \"列出任务\"): answer from the \"Recent Task History\" section already in your context. If insufficient, call session_list.\n - Specific lookups (\"上次那个配置怎么改的\"): call session_search with relevant phrases (NOT single characters).\n - Do NOT call search tools repeatedly with keyword fragments. One well-phrased call is better than ten fragmented ones.\n\n## Coding & Verification\nWhen working with code, prefer the precise built-in tools over ad-hoc shell: use `repo_map` to orient in an\nunfamiliar project, `code_search`/`file_find` to locate and `code_intel` for symbols, `edit_file` (anchored\nsearch-replace, or a unified `diff`) to change files — never rewrite a whole file to change a few lines — and\n`git_query` to inspect diffs/log. After edits, check `syntax_ok` in the result and run `test_run`/`lint_check`\nbefore declaring the work done. Batch independent read-only calls (search/read/query) into a single turn — they\nrun in parallel.\n\n## Presentation Style\n1. **Polished Markdown only**: Format user-facing answers with clean Markdown headings, short paragraphs, and concise bullets. Use tables only when they improve comparison or scanning.\n2. **Terminal-friendly layout**: Keep lines readable in a TUI; avoid dense walls of text, deeply nested lists, oversized ASCII art, or heavy visual blocks.\n3. **Elegant emphasis**: Use bold text sparingly for key terms and conclusions. Avoid excessive emojis, decorative symbols, repeated separators, or visual noise.\n4. **Theme-safe colors**: Do not emit ANSI escape codes, HTML color tags, Rich markup, or hardcoded color names. Rely on the TUI theme to render Markdown professionally.\n5. **No leaked tool protocol**: Never show tool-call JSON, internal schemas, raw observations, tool result payloads, or hidden reasoning in the final answer unless the user explicitly asks for raw/debug output. Treat any prior `{\"name\": ..., \"arguments\": ...}` blocks and `Tool result (...)` messages as internal execution context only.\n6. **Professional closure**: End with a concise conclusion or next step when helpful; avoid rambling after the useful answer is complete.\n\nWhen finished with all tool calls, respond normally without a JSON block, tool-call transcript, or process log.\n\n## Task Contract\n- Task ID: turn-4\n- Original user request: Do the impossible thing.\n- Workspace root: \n- Allowed roots: \n- Treat relative project paths as relative to the workspace root; never infer `.` as the project root when a workspace root is provided.\n- Workspace boundary is enforced by tools: do not read, search, edit, or run commands against paths outside the allowed roots unless the user explicitly requests an external path and the tool/approval policy permits it.\n- LeapFlow workspace config is optional at `/.leapflow/config.yaml`; runtime config is loaded from `~/.leapflow/config/user.yaml` and `~/.leapflow/profiles//config/*.yaml`.\n- Preserve this task contract across summarization, compression, tool loops, and memory retrieval.\n" + }, + { + "role": "system", + "content": "\n## Recent Session Summary\n- [assistant] Recovered after compressing context.\n- [user] Do the impossible thing.\n" + }, + { + "role": "assistant", + "content": "Recovered after compressing context." + }, + { + "role": "user", + "content": "Do the impossible thing.\nDo the impossible thing." + } + ], + "tools": [ + "capability_expand", + "code_intel", + "code_search", + "config_get", + "config_list", + "env_info", + "file_find", + "file_list", + "file_read", + "git_query", + "lint_check", + "memory_search", + "plugin_list", + "plugin_propose", + "plugin_status", + "plugin_versions", + "repo_map", + "research_note", + "runtime_snapshot", + "schedule_reentry", + "self_describe", + "session_list", + "skill_view", + "skills_list", + "terminal_list", + "terminal_read", + "test_run", + "text_search", + "time_get", + "tool_describe", + "tool_search", + "web_fetch" + ] + }, + "responses": [ + { + "status": 400, + "content_type": "application/json", + "body": "{\"error\": {\"message\": \"The requested configuration is not supported by this model\", \"type\": \"invalid_request_error\", \"code\": \"unsupported_value\"}}" + }, + { + "status": 400, + "content_type": "application/json", + "body": "{\"error\": {\"message\": \"The requested configuration is not supported by this model\", \"type\": \"invalid_request_error\", \"code\": \"unsupported_value\"}}" + } + ] +} diff --git a/tests/_fixtures/cassettes/r4_recovery/cassette-model-575a245bc30282d0.cassette.json b/tests/_fixtures/cassettes/r4_recovery/cassette-model-575a245bc30282d0.cassette.json deleted file mode 100644 index 2bd5d2fe..00000000 --- a/tests/_fixtures/cassettes/r4_recovery/cassette-model-575a245bc30282d0.cassette.json +++ /dev/null @@ -1,72 +0,0 @@ -{ - "fingerprint": "575a245bc30282d0cdf8ffa4128e8450eca97837fa3137b3753141dd90967b35", - "note": "captured in seed mode", - "request": { - "model": "cassette-model", - "stream": false, - "messages": [ - { - "role": "system", - "content": "You are LeapFlow, an intelligent assistant that can both converse naturally and take real actions on the user's computer.\n\n## Capabilities\nThe tool index below lists **every** registered tool by name and a one-line summary — this is the complete\ncapability contract; nothing else exists. Only a subset is directly callable this turn (via native tool calling,\nnot a JSON block in your reply). If you need a tool from the index that is not yet callable, call\n`capability_expand` with its category name first — the matching tools become callable immediately after.\nWhen the user asks what LeapFlow itself supports, whether it supports plugins, or which runtime capabilities\nare available, use the live capability evidence exposed by `plugin_list` before making capability claims; report\nconfiguration-dependent or unavailable capabilities as limitations instead of inferring from documentation.\n- **text_search**(text, pattern): Search for a regex pattern in text.\n- **text_replace**(text, old, new, count) [capability_expand category: write]: Replace occurrences of a substring in text.\n- **time_get**(): Get current date and time.\n- **env_info**(): Get system environment information (OS, Python version, cwd).\n- **skills_list**(query, category, source): List available learned skills. Use when user asks about capabilities or you need a specific skill.\n- **skill_view**(name): View the full content of a specific skill document.\n- **code_intel**(path, operation): Precise document symbols (outline) for a source file: classes, functions, and methods with line ranges. Python uses an exact AST parse; other languages use a keyword-prefix scan. Prefer over file_read mode=symbols for accurate navigation before editing. Read-only.\n- **repo_map**(path): Compact project orientation for a repository root: languages, detected test/lint commands, top-level structure, entry points, manifest, and VCS branch. Call this first when entering an unfamiliar codebase. Read-only.\n- **scm_sync**(action, cwd, remote, pull_ref, push_ref, timeout) [capability_expand category: scm]: Run a typed git SCM action. Use this instead of shell_run for git pull/push/status. For 'pull origin main then push', set action='pull_then_push', remote='origin', pull_ref='main', and omit push_ref so LeapFlow pushes the current local branch.\n- **git_query**(action, cwd, ref, path, staged, max_count, stat): Read-only structured git inspection: action=diff|log|status|branch|show. Prefer over shell_run for reading repo state — output is clipped, redacted, and log/branch are parsed into structured fields. Use scm_sync for pull/push.\n- **git_write**(action, cwd, message, stage_all, name, ref, create) [capability_expand category: scm]: Mutating git actions: action=commit (message, stage_all), branch (create+switch), checkout (switch; create=true for -b). Approval-gated. Use scm_sync for pull/push and git_query for reads.\n- **test_run**(command, cwd, timeout): Run the project's test suite and return structured results (framework, passed/failed counts, failing tests). Auto-detects the runner (pytest/npm/go/cargo) or uses a configured/explicit command; executes via the governed shell. ok=true means the runner executed — see 'success' for pass/fail.\n- **lint_check**(command, cwd, timeout): Run the project's linter and return a structured clean/issue result. Auto-detects the linter (ruff/eslint/go vet/clippy) or uses a configured/explicit command; executes via the governed shell. ok=true means the linter ran — see 'clean'.\n- **file_list**(path, pattern, depth): List files and directories at a given path. Use depth=1 or depth=2 to get a recursive tree in one call instead of listing each sub-directory separately.\n- **file_read**(path, max_lines, start_line, max_chars, mode): Read text file content with adaptive context governance. For large or unfamiliar files, prefer mode='outline' or mode='symbols' first, then use mode='raw' with start_line/max_lines for the specific range you actually need. For LeapFlow's own settings, use config_list / config_get / config_set — its config files are outside the workspace and not readable here.\n- **file_write**(path, content, mode) [capability_expand category: write]: Write content to a file (overwrite or append).\n- **code_search**(pattern, patterns, path, glob, ignore_case, multiline, max_results, context_lines): Search file CONTENTS by regex pattern across a directory tree (ripgrep-backed). Requires a regex pattern. NOT for listing or browsing directory contents — use file_list for that. Prefer this over shell_run grep: faster, skips VCS/dependency/build dirs, and returns structured path:line:column matches. Batch related lookups into ONE call via `patterns` (OR-combined, single pass) instead of issuing several separate searches. Use file_read for the surrounding context of a hit.\n- **file_find**(glob, path, max_results): Find files by a recursive glob pattern under a base path (e.g. '**/test_*.py' or '*.md'). Prefer this over shell_run find; skips VCS/dependency/build dirs.\n- **edit_file**(path, edits, dry_run, diff) [capability_expand category: write]: Apply targeted, anchored search-replace edits to an EXISTING text file (use file_write to create/overwrite). Each edit is {original_text, new_text, replace_all?}; original_text must match exactly and uniquely (or set replace_all) — a non-unique or missing anchor is rejected so files are never corrupted. Set dry_run to preview. Alternatively pass a unified 'diff' to apply its hunks as anchored edits. Far cheaper and safer than rewriting a whole file.\n- **shell_run**(command, cwd, timeout) [capability_expand category: shell]: Execute a one-shot shell command with timeout protection. Runs in the active workspace; paths resolving outside it are refused. Reach for a structured tool first when one fits — web_fetch for anything over HTTP(S), git_query/scm_sync for git, code_search/file_find/file_read for the repo, config_get/config_set for LeapFlow's own settings — because those report typed results, while a failed shell command can only be diagnosed from its exit code and stderr. Every shell run counts as an external side effect, so a failure stops the rest of the batch and is not retried automatically.\n- **terminal_open**(command, cwd, shell) [capability_expand category: terminal]: Open a PERSISTENT shell session (REPL/dev server/watch), returning a session_id for terminal_send/read/close. Disabled unless tools.terminal_session_enabled is set. For one-shot commands use shell_run instead.\n- **terminal_send**(session_id, input, wait) [capability_expand category: terminal]: Send a line of input to a persistent terminal session and return output captured shortly after.\n- **terminal_read**(session_id, wait): Drain buffered output from a persistent terminal session (optionally waiting briefly first).\n- **terminal_close**(session_id) [capability_expand category: terminal]: Terminate a persistent terminal session and release its process group.\n- **terminal_list**(): List active persistent terminal sessions.\n- **config_list**(category, limit): List LeapFlow's own writable settings (LLM model and endpoint, daemon, memory, perception, gateway, …) with current values. Use this to discover the exact key before changing anything. Optionally narrow by `category`. This is the only correct way to inspect LeapFlow configuration — never read config files from disk.\n- **config_get**(key): Read one LeapFlow setting by key (e.g. 'llm.model', 'llm.base_url', 'daemon.log_level'), returning its current value, type, scopes, and whether a change needs a daemon restart. There is no 'llm.provider' key: provider behavior is inferred from the OpenAI-compatible 'llm.base_url'. Never read LeapFlow config files from disk — use this.\n- **config_set**(key, value, scope) [capability_expand category: config]: Change one LeapFlow setting by key, e.g. switch an OpenAI-compatible LLM with key='llm.model' and key='llm.base_url'. There is no 'llm.provider' key: provider behavior is inferred from the endpoint. Values are validated and coerced; credentials are stored in the vault automatically. Call config_list or config_get first if unsure of the key. The result states whether a `leap daemon restart` is required. Never edit LeapFlow config files directly.\n- **web_fetch**(url, select, timeout, max_bytes): Read a URL over HTTP(S) and get back extracted, context-sized content: parsed JSON for API endpoints, readable text plus links for web pages. Use this for anything on the internet — prices, docs, releases, articles — instead of running curl through shell_run: it reports real HTTP status codes, retries rate limits on its own, and is a plain read so a retry is always safe. For JSON APIs pass `select` with a dotted path (e.g. 'chart.result.0.meta') to return just that part instead of the whole payload.\n- **memory_search**(query, limit): Search agent memory for relevant past experiences, observations, and facts.\n- **memory_add**(content, kind) [capability_expand category: write]: Store a new observation or insight in memory for future reference.\n- **research_note**(kind, text): Record a compact, structured note about the current task's state so it survives context compression on long / multi-step tasks. Use for durable findings, open questions still to resolve, decisions / excluded paths, and the immediate next step. One concise sentence per note.\n- **capability_expand**(category): Fetch the full callable schema for every tool in a capability category (e.g. 'hub', 'gateway', 'desktop', 'delegate', 'file', 'memory', 'skill'). The compact tool index always lists every registered tool by name and a one-line summary, but only a static low-risk subset is directly callable each turn. If you need a tool from the index that is not yet callable, call capability_expand with its category first; the matching tools become callable in this turn. Never invent a tool name — expand the category instead.\n- **delegate_task**(goal, context) [capability_expand category: delegate]: Delegate a complex sub-task to an isolated subagent. The subagent gets a fresh context and restricted tool access. Use when a task is self-contained and can be solved independently.\n- **schedule_reentry**(kind, reason, delay_seconds, event_match, max_reentries, deadline_seconds): Register a re-entry so this task can resume later from its current orientation (findings / open questions / next step). Use when work must pause and continue after a delay (kind=time) or when a matching platform event arrives (kind=event), instead of finishing now. The research-ledger state is carried over automatically.\n- **hub_push**(skill_name, visibility, version) [capability_expand category: hub]: Push a local skill to the ModelScope Hub for sharing or backup.\n- **hub_pull**(repo_id, version) [capability_expand category: hub]: Pull a skill from the ModelScope Hub to install locally.\n- **hub_search**(query) [capability_expand category: hub]: Search for skills on the Hub by keyword or description.\n- **hub_sync**(mode, dry_run) [capability_expand category: hub]: Preview or execute sync between local skills and Hub.\n- **platform_action**(platform, action, payload, backend_kind) [capability_expand category: gateway]: Execute an exact registered business action on an external platform through LeapFlow's App Connector layer. Actions must be copied from the App Connector Capability Index and are addressed as domain.operation, e.g. im.send_message or docs.create_markdown. All business fields (chat_id, text, query, etc.) MUST be placed inside `payload`, never at the top level. Example: {\"platform\":\"feishu\",\"action\":\"im.send_message\",\"payload\":{\"chat_id\":\"oc_xxx\",\"text\":\"hello\"}}. Do not invent action names, do not use management actions such as list/guide/connect/status here.\n- **platform_connect**(action, platform, credentials, options, checkpoint) [capability_expand category: gateway]: List, guide, connect, disconnect, remove, or check status for external platforms using the App Connector management namespace. Supports REST and CLI backends. Use this for management actions such as list/guide/preflight/connect/status; use platform_action only for exact registered business actions.\n- **gateway_send**(platform, chat_id, text, thread_id) [capability_expand category: gateway]: Send a message to a connected external platform (Feishu group, Telegram chat, DingTalk conversation, etc.). Requires the platform to be connected via gateway_connect first. Use gateway_connect with action='list' to see connected platforms and available chat IDs.\n- **gateway_connect**(action, platform, credentials, options) [capability_expand category: gateway]: Connect, configure, or manage external platform integrations (Feishu, DingTalk, Telegram, Slack, Discord, etc.). Conversational flow: 1) call 'guide' to get setup steps + required fields, 2) present the steps to the user and ask for ALL required credentials in a single message, 3) call 'connect' with the credentials. Goal: complete in 1–2 user turns. NEVER include credential values in your text response.\n- **plugin_list**(): List the live plugin registry and cross-subsystem capability evidence. Use this before answering questions about whether LeapFlow supports plugins, self-evolution, plugin installation, hot reload, versioning, or other runtime capabilities.\n- **plugin_status**(plugin_id): Get detailed status of a specific plugin: its declared category, runtime dependencies, contributed tools, and fiber lifecycle state.\n- **plugin_versions**(plugin_id): List recorded source versions and active pointer for a profile-scoped plugin.\n- **plugin_propose**(requested_capability, plugin_id, proposed_tools, test_cases, risk_level, evidence): Create a side-effect-free PluginProposal from explicit capability-gap evidence. Use this before plugin_generate when a missing capability should be reviewed. Does not call an LLM, write files, or install anything.\n- **assess_compatibility**(manifest, source_path) [capability_expand category: plugin_management]: Assess whether a foreign plugin manifest or real DSH source bundle is compatible with LeapFlow. A source bundle assessment is static: runtime_ready stays false until restricted Node discovery during plugin_install. Returns component-level verdicts and limitations.\n- **plugin_generate**(plugin_id, description, proposal_id) [capability_expand category: system]: Generate a new ToolPlugin from a natural-language capability description. The LLM produces code that conforms to the ToolPlugin Protocol; it is then rigorously validated (syntax, structure, import, protocol conformance). The generated source is stored in CAS and receives explicit content approval. It DOES NOT install the plugin — installation requires a second, mutation-specific approval via plugin_install.\n- **plugin_install**(plugin_id, code, marketplace_name, source_path, proposal_id, version_label) [capability_expand category: system]: Install a plugin from exactly one source: validated Python code, the configured Python marketplace, or a real DSH source bundle. DSH bundles run restricted Node discovery before registration; only public host tools are installed and client UI limitations are reported. Writes profile-scoped state and mutates the process-global registry. REQUIRES APPROVAL.\n- **plugin_rollback**(plugin_id, version) [capability_expand category: system]: Rollback a profile-scoped plugin to a recorded source version and reload it. REQUIRES APPROVAL.\n- **plugin_reload**(plugin_id, version_label) [capability_expand category: system]: Hot-reload a plugin at runtime. Disposes the old plugin fiber, re-imports its module, and registers a fresh instance. Existing in-flight turns are unaffected (snapshot isolation). REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_disable**(plugin_id) [capability_expand category: system]: Disable a plugin by disposing its fiber, removing its tools from the runtime registry. Cannot disable self_management itself. REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_remove**(plugin_id, delete_source) [capability_expand category: system]: Terminally remove a plugin: dispose its fiber, unregister its tools, remove reload metadata, and optionally delete its profile-scoped source file. Cannot remove self_management itself. REQUIRES APPROVAL.\n- **plugin_unquarantine**(plugin_id) [capability_expand category: plugin_management]: Restore a quarantined plugin to probation status for re-evaluation. Unfreezes the trust ledger, transitions the proposal back to PROBATION, and reloads the plugin. REQUIRES APPROVAL.\n- **plugin_enable**(plugin_id) [capability_expand category: system]: Re-enable a previously disabled plugin by reloading its module and registering a fresh instance. REQUIRES APPROVAL.\n- **self_describe**(facet): Introspect LeapFlow's own identity, capabilities, runtime state, evolution metrics, or platform connections. Use facet='all' only when a comprehensive self-check is explicitly requested.\n- **runtime_snapshot**(): Lightweight (~150 token) flat snapshot of current runtime state: model, context budget, posture, disclosure level, turn count, cache hit rate, uptime, tool count, and pending approvals.\n- **schedule_create**(trigger_expression, instruction, execution_mode, max_retries, delivery_target) [capability_expand category: scheduler]: Create a new scheduled task. Specify a trigger expression (e.g. '30m', 'every 2h', '0 9 * * *') and the instruction to execute on each trigger. Optionally provide an execution mode and a delivery target to receive notifications.\n- **schedule_list**() [capability_expand category: scheduler]: List all scheduled tasks with their current state, trigger, and next due time.\n- **schedule_status**(task_id) [capability_expand category: scheduler]: Get detailed status and recent execution history for a scheduled task.\n- **schedule_pause**(task_id) [capability_expand category: scheduler]: Pause a scheduled task so it stops firing without being cancelled.\n- **schedule_resume**(task_id) [capability_expand category: scheduler]: Resume a paused scheduled task — re-arms it and recalculates next due time.\n- **schedule_cancel**(task_id) [capability_expand category: scheduler]: Cancel a scheduled task permanently.\n- **tool_search**(query, max_results): Search all registered tools by keyword relevance. Returns a ranked list of matching tools with name, category, and summary. Use this when you need a tool but are unsure of its exact name or category.\n- **tool_describe**(tool_name): Get the full callable schema of a single tool by exact name. Returns the tool's description, parameters, and metadata. Use after tool_search to inspect a specific tool before calling it.\n- **session_search**(query, limit) [capability_expand category: unclassified]: Search past conversation sessions in the current workspace for relevant context.\n- **session_list**(limit): List recent conversation sessions in the current workspace with stable ids, titles, dates, and summaries. Use for browsing past tasks or when user asks to see history without specific search terms. No keywords needed — returns chronological list.\n- **session_detail**(session_id, limit, offset, include_inactive) [capability_expand category: unclassified]: Read a paginated persisted transcript for one past conversation session in the current workspace. Use after session_list or session_search returns a session_id.\n- **click**(element_index) [capability_expand category: desktop]: Click a UI element by its element_index (from the latest observe_ui snapshot)\n- **get_clipboard**() [capability_expand category: desktop]: Read current clipboard text content\n- **list_apps**(filter, running_only) [capability_expand category: desktop]: List available applications on this system. Use to discover correct bundle_id before switch_app.\n- **list_windows**() [capability_expand category: desktop]: List all top-level windows with pid, window_id, title, and per-window state (minimized, on-screen). Call this first to pick the pid and window_id that observe_ui and other window tools require.\n- **observe_ui**(pid, window_id, query) [capability_expand category: desktop]: Snapshot one window's actionable UI elements, each tagged with an element_index for click/right_click/read_text. Re-observe after actions — indices belong to one snapshot. Requires the window's pid and window_id from list_windows.\n- **open_url**(url, app_id) [capability_expand category: desktop]: Open a URL in the default or specified browser\n- **read_text**(element_index) [capability_expand category: desktop]: Read the text content of a specific UI element from the latest snapshot\n- **right_click**(element_index) [capability_expand category: desktop]: Right-click a UI element to open its context menu. Returns visible menu items.\n- **screenshot**(pid, window_id) [capability_expand category: desktop]: Capture a screenshot for visual verification. With pid + window_id captures that window (works across all displays); defaults to the last observed window, or the full desktop when no window has been observed.\n- **scroll**(element_index, direction, amount, pid, window_id) [capability_expand category: desktop]: Scroll a scrollable area of a window. Omit element_index to scroll the window's focused/page scroller; pass one to scroll an exact element from the latest snapshot.\n- **select_text**(element_index) [capability_expand category: desktop]: Select all text in a UI element (focus + select-all, for subsequent copy)\n- **set_clipboard**(text) [capability_expand category: desktop]: Write text to the clipboard\n- **shortcut**(keys) [capability_expand category: desktop]: Execute a keyboard shortcut\n- **switch_app**(app_id) [capability_expand category: desktop]: Switch to an app (launch if needed, activate, verify)\n- **type_text**(text) [capability_expand category: desktop]: Type text into the currently focused element\n- **wait**(seconds) [capability_expand category: desktop]: Wait for a specified duration before continuing\n- **wait_until**(condition, pid, window_id, timeout, poll_interval) [capability_expand category: desktop]: Wait until a UI condition is met (polls UI tree). Returns elements when found or on timeout.\n- **wait_until_stable**(timeout, poll_interval, pid, window_id) [capability_expand category: desktop]: Wait until the UI stops changing (element set stabilizes across polls).\n\n## App Connector Capability Index\nLeapFlow can onboard and manage external apps through `platform_connect` and execute exact registered business actions through `platform_action`.\nFor requests about connecting, setting up, configuring, enabling, or managing a supported app, use `platform_connect` first instead of generating SDK/Webhook sample code.\n`platform_connect.action` is the management namespace: list, guide, preflight, connect, disconnect, remove, status, events_start, events_stop, events_status.\n`platform_action.action` is only for exact registered platform business actions listed below, such as `im.send_message`; never use management actions like `list` or `guide` there.\nAll business fields MUST go inside `payload`; top-level keys are only `platform`, `action`, and `payload`.\nDo not invent platform IDs or platform action names. If the needed action is not listed, ask for discovery/clarification instead of guessing.\nUse `platform_connect` with `action='guide'` and the matching `platform` to start onboarding; use `action='list'` when the app is unclear.\nWhen a pending onboarding state is present, continue from that state with `platform_connect` instead of asking the user to restate the app.\nSupported apps:\n- `api_server`: API Server (OpenAI Compatible) (category=api; backend=adapter; platform_action actions=none registered)\n- `dingtalk`: 钉钉 (DingTalk) (category=im; backend=adapter; platform_action actions=none registered)\n- `feishu`: 飞书 (Feishu/Lark) (category=im; backend=cli)\n - `calendar.create_event` payload={summary*, start_time*, end_time*, attendees} [write/high]\n - `docs.create_markdown` payload={title*, markdown*, folder_token} [write/medium]\n - `drive.search` payload={query*, limit} [read/medium]\n - `im.add_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.download_resource` payload={message_id*, file_key*, type*} [read/low]\n - `im.get_messages` payload={message_ids*} [read/low]\n - `im.list_chats` payload={page_size} [read/low]\n - `im.list_messages` payload={chat_id*} [read/low]\n - `im.list_thread_messages` payload={thread*} [read/low]\n - `im.remove_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.reply_message` payload={message_id*, text*} [send/high]\n - `im.search_chats` payload={query*, page_size} [read/low]\n - `im.search_messages` payload={query*} [read/medium]\n - `im.send_message` payload={chat_id*, text*, thread_id} [send/high]\n - `im.update_card` payload={token*, card*} [write/medium]\n - `im.update_message` payload={message_id*, text*} [write/medium]\n - `mail.search_unread` payload={query, limit} [read/high]\n - `sheets.append_row` payload={spreadsheet_token*, sheet_id*, values*} [write/medium]\n - `task.create` payload={title*, description, due_time} [write/medium]\n- `telegram`: Telegram (category=social; backend=adapter; platform_action actions=none registered)\n- `webhook`: Webhook (Generic) (category=webhook; backend=adapter; platform_action actions=none registered)\n\n## Tool Usage\nTools are normally invoked through the native function-calling mechanism, not by writing JSON in your reply\ntext. Only if the provider signals that native function calling is unavailable for this turn, fall back to a\nsingle JSON code block: `{\"name\": \"tool_name\", \"arguments\": {\"key\": \"value\"}}` — use this fallback format\nonly, never both. Only call a tool whose exact name appears in the tool index above and is currently callable\n(or reachable via `capability_expand`). Never invent, rename, alias, or guess a tool name, platform ID, or\nplatform action from argument shape or wording — if the index does not list it, it does not exist.\n`platform_connect.action` (list/guide/preflight/connect/disconnect/remove/status/events_start/events_stop/\nevents_status) is the App Connector management namespace; `platform_action.action` only accepts exact\nregistered business actions such as `im.send_message` shown in the App Connector Capability Index — never mix\nthe two namespaces. If a tool call returns an unknown/unavailable result, use the returned suggestions or\navailable names for a single retry instead of trying further variations of the same guess.\n\n**Side-effect action rule** (`platform_action` with effect=send/write/execute):\n- Call each unique action+payload **exactly once**. Never include duplicates in the same turn.\n- Once the result returns `\"completed\": true`, that action is DONE for this task. Do NOT call it again\n in any subsequent turn — immediately summarize the result for the user instead.\n- The system enforces idempotency: duplicate calls are blocked and will not execute.\n- If the user explicitly requests sending/writing multiple times, use distinct payloads per call.\n\n**Resource identifier provenance rule**:\n- NEVER fabricate, guess, or infer resource identifiers (chat_id, message_id, file_key, user_id, etc.).\n Every resource ID used in a side-effect action MUST come from a successful API response in this session.\n- If a read/list action fails (e.g. authorization error), you do NOT have valid resource IDs.\n Report the failure to the user — do NOT attempt the dependent write/send action with a guessed ID.\n- When a tool result contains `\"llm_instruction\"`, follow it exactly.\n\n## Guidelines\n1. **Direct answers first**: If you already know the answer, respond directly without tools.\n2. **Avoid redundant tool calls**: Do not call the same tool with the same arguments more than once in the same user turn. When an existing tool result already answers the user's request, stop calling tools and answer directly.\n3. **Use tools proactively**: When the user asks about files, time, system state, or needs actions performed, use the appropriate tool.\n4. **Chain tools when needed**: You can call multiple tools in sequence (e.g., list files → read file → summarize).\n5. **Handle failures gracefully**: If a tool fails, explain what went wrong and suggest alternatives. If it failed because of an unknown tool/platform/action name, retry once with an exact name from the returned suggestions, then explain rather than keep guessing.\n6. **Summarize results naturally**: After tool execution, synthesize the results into a helpful answer rather than dumping raw output.\n7. **Stay conversational**: Maintain a natural, helpful tone. Acknowledge context from earlier in the conversation.\n8. **Recall past work**:\n - Broad queries (\"之前做了什么\", \"列出任务\"): answer from the \"Recent Task History\" section already in your context. If insufficient, call session_list.\n - Specific lookups (\"上次那个配置怎么改的\"): call session_search with relevant phrases (NOT single characters).\n - Do NOT call search tools repeatedly with keyword fragments. One well-phrased call is better than ten fragmented ones.\n\n## Coding & Verification\nWhen working with code, prefer the precise built-in tools over ad-hoc shell: use `repo_map` to orient in an\nunfamiliar project, `code_search`/`file_find` to locate and `code_intel` for symbols, `edit_file` (anchored\nsearch-replace, or a unified `diff`) to change files — never rewrite a whole file to change a few lines — and\n`git_query` to inspect diffs/log. After edits, check `syntax_ok` in the result and run `test_run`/`lint_check`\nbefore declaring the work done. Batch independent read-only calls (search/read/query) into a single turn — they\nrun in parallel.\n\n## Presentation Style\n1. **Polished Markdown only**: Format user-facing answers with clean Markdown headings, short paragraphs, and concise bullets. Use tables only when they improve comparison or scanning.\n2. **Terminal-friendly layout**: Keep lines readable in a TUI; avoid dense walls of text, deeply nested lists, oversized ASCII art, or heavy visual blocks.\n3. **Elegant emphasis**: Use bold text sparingly for key terms and conclusions. Avoid excessive emojis, decorative symbols, repeated separators, or visual noise.\n4. **Theme-safe colors**: Do not emit ANSI escape codes, HTML color tags, Rich markup, or hardcoded color names. Rely on the TUI theme to render Markdown professionally.\n5. **No leaked tool protocol**: Never show tool-call JSON, internal schemas, raw observations, tool result payloads, or hidden reasoning in the final answer unless the user explicitly asks for raw/debug output. Treat any prior `{\"name\": ..., \"arguments\": ...}` blocks and `Tool result (...)` messages as internal execution context only.\n6. **Professional closure**: End with a concise conclusion or next step when helpful; avoid rambling after the useful answer is complete.\n\nWhen finished with all tool calls, respond normally without a JSON block, tool-call transcript, or process log.\n\n## Task Contract\n- Task ID: turn-3\n- Original user request: Keep going with more context.\n- Workspace root: \n- Allowed roots: \n- Treat relative project paths as relative to the workspace root; never infer `.` as the project root when a workspace root is provided.\n- Workspace boundary is enforced by tools: do not read, search, edit, or run commands against paths outside the allowed roots unless the user explicitly requests an external path and the tool/approval policy permits it.\n- LeapFlow workspace config is optional at `/.leapflow/config.yaml`; runtime config is loaded from `~/.leapflow/config/user.yaml` and `~/.leapflow/profiles//config/*.yaml`.\n- Preserve this task contract across summarization, compression, tool loops, and memory retrieval.\n" - }, - { - "role": "system", - "content": "\n## Recent Session Summary\n- [assistant] Recovered after a server error.\n- [user] Keep going with more context.\n" - }, - { - "role": "assistant", - "content": "Recovered after a server error." - }, - { - "role": "user", - "content": "Keep going with more context.\nKeep going with more context." - } - ], - "tools": [ - "capability_expand", - "code_intel", - "code_search", - "config_get", - "config_list", - "env_info", - "file_find", - "file_list", - "file_read", - "git_query", - "lint_check", - "memory_search", - "plugin_list", - "plugin_propose", - "plugin_status", - "plugin_versions", - "repo_map", - "research_note", - "runtime_snapshot", - "schedule_reentry", - "self_describe", - "session_list", - "skill_view", - "skills_list", - "terminal_list", - "terminal_read", - "test_run", - "text_search", - "time_get", - "tool_describe", - "tool_search", - "web_fetch" - ] - }, - "responses": [ - { - "status": 400, - "content_type": "application/json", - "body": "{\"error\": {\"message\": \"This model's maximum context length is 8192 tokens. However, your messages resulted in 9001 tokens.\", \"type\": \"invalid_request_error\", \"code\": \"context_length_exceeded\"}}" - }, - { - "status": 200, - "content_type": "application/json", - "body": "{\"id\": \"chatcmpl-cassette\", \"object\": \"chat.completion\", \"model\": \"cassette-model\", \"choices\": [{\"index\": 0, \"message\": {\"role\": \"assistant\", \"content\": \"Recovered after compressing context.\"}, \"finish_reason\": \"stop\"}], \"usage\": {\"prompt_tokens\": 64, \"completion_tokens\": 16, \"total_tokens\": 80}}" - } - ] -} diff --git a/tests/_fixtures/cassettes/r4_recovery/cassette-model-6b24f7969238dc45.cassette.json b/tests/_fixtures/cassettes/r4_recovery/cassette-model-6b24f7969238dc45.cassette.json deleted file mode 100644 index 04a2a219..00000000 --- a/tests/_fixtures/cassettes/r4_recovery/cassette-model-6b24f7969238dc45.cassette.json +++ /dev/null @@ -1,72 +0,0 @@ -{ - "fingerprint": "6b24f7969238dc45610bb9ceb81b82cc2e821e41cf1bb31db1405922b6b05267", - "note": "captured in seed mode", - "request": { - "model": "cassette-model", - "stream": false, - "messages": [ - { - "role": "system", - "content": "You are LeapFlow, an intelligent assistant that can both converse naturally and take real actions on the user's computer.\n\n## Capabilities\nThe tool index below lists **every** registered tool by name and a one-line summary — this is the complete\ncapability contract; nothing else exists. Only a subset is directly callable this turn (via native tool calling,\nnot a JSON block in your reply). If you need a tool from the index that is not yet callable, call\n`capability_expand` with its category name first — the matching tools become callable immediately after.\nWhen the user asks what LeapFlow itself supports, whether it supports plugins, or which runtime capabilities\nare available, use the live capability evidence exposed by `plugin_list` before making capability claims; report\nconfiguration-dependent or unavailable capabilities as limitations instead of inferring from documentation.\n- **text_search**(text, pattern): Search for a regex pattern in text.\n- **text_replace**(text, old, new, count) [capability_expand category: write]: Replace occurrences of a substring in text.\n- **time_get**(): Get current date and time.\n- **env_info**(): Get system environment information (OS, Python version, cwd).\n- **skills_list**(query, category, source): List available learned skills. Use when user asks about capabilities or you need a specific skill.\n- **skill_view**(name): View the full content of a specific skill document.\n- **code_intel**(path, operation): Precise document symbols (outline) for a source file: classes, functions, and methods with line ranges. Python uses an exact AST parse; other languages use a keyword-prefix scan. Prefer over file_read mode=symbols for accurate navigation before editing. Read-only.\n- **repo_map**(path): Compact project orientation for a repository root: languages, detected test/lint commands, top-level structure, entry points, manifest, and VCS branch. Call this first when entering an unfamiliar codebase. Read-only.\n- **scm_sync**(action, cwd, remote, pull_ref, push_ref, timeout) [capability_expand category: scm]: Run a typed git SCM action. Use this instead of shell_run for git pull/push/status. For 'pull origin main then push', set action='pull_then_push', remote='origin', pull_ref='main', and omit push_ref so LeapFlow pushes the current local branch.\n- **git_query**(action, cwd, ref, path, staged, max_count, stat): Read-only structured git inspection: action=diff|log|status|branch|show. Prefer over shell_run for reading repo state — output is clipped, redacted, and log/branch are parsed into structured fields. Use scm_sync for pull/push.\n- **git_write**(action, cwd, message, stage_all, name, ref, create) [capability_expand category: scm]: Mutating git actions: action=commit (message, stage_all), branch (create+switch), checkout (switch; create=true for -b). Approval-gated. Use scm_sync for pull/push and git_query for reads.\n- **test_run**(command, cwd, timeout): Run the project's test suite and return structured results (framework, passed/failed counts, failing tests). Auto-detects the runner (pytest/npm/go/cargo) or uses a configured/explicit command; executes via the governed shell. ok=true means the runner executed — see 'success' for pass/fail.\n- **lint_check**(command, cwd, timeout): Run the project's linter and return a structured clean/issue result. Auto-detects the linter (ruff/eslint/go vet/clippy) or uses a configured/explicit command; executes via the governed shell. ok=true means the linter ran — see 'clean'.\n- **file_list**(path, pattern, depth): List files and directories at a given path. Use depth=1 or depth=2 to get a recursive tree in one call instead of listing each sub-directory separately.\n- **file_read**(path, max_lines, start_line, max_chars, mode): Read text file content with adaptive context governance. For large or unfamiliar files, prefer mode='outline' or mode='symbols' first, then use mode='raw' with start_line/max_lines for the specific range you actually need. For LeapFlow's own settings, use config_list / config_get / config_set — its config files are outside the workspace and not readable here.\n- **file_write**(path, content, mode) [capability_expand category: write]: Write content to a file (overwrite or append).\n- **code_search**(pattern, patterns, path, glob, ignore_case, multiline, max_results, context_lines): Search file CONTENTS by regex pattern across a directory tree (ripgrep-backed). Requires a regex pattern. NOT for listing or browsing directory contents — use file_list for that. Prefer this over shell_run grep: faster, skips VCS/dependency/build dirs, and returns structured path:line:column matches. Batch related lookups into ONE call via `patterns` (OR-combined, single pass) instead of issuing several separate searches. Use file_read for the surrounding context of a hit.\n- **file_find**(glob, path, max_results): Find files by a recursive glob pattern under a base path (e.g. '**/test_*.py' or '*.md'). Prefer this over shell_run find; skips VCS/dependency/build dirs.\n- **edit_file**(path, edits, dry_run, diff) [capability_expand category: write]: Apply targeted, anchored search-replace edits to an EXISTING text file (use file_write to create/overwrite). Each edit is {original_text, new_text, replace_all?}; original_text must match exactly and uniquely (or set replace_all) — a non-unique or missing anchor is rejected so files are never corrupted. Set dry_run to preview. Alternatively pass a unified 'diff' to apply its hunks as anchored edits. Far cheaper and safer than rewriting a whole file.\n- **shell_run**(command, cwd, timeout) [capability_expand category: shell]: Execute a one-shot shell command with timeout protection. Runs in the active workspace; paths resolving outside it are refused. Reach for a structured tool first when one fits — web_fetch for anything over HTTP(S), git_query/scm_sync for git, code_search/file_find/file_read for the repo, config_get/config_set for LeapFlow's own settings — because those report typed results, while a failed shell command can only be diagnosed from its exit code and stderr. Every shell run counts as an external side effect, so a failure stops the rest of the batch and is not retried automatically.\n- **terminal_open**(command, cwd, shell) [capability_expand category: terminal]: Open a PERSISTENT shell session (REPL/dev server/watch), returning a session_id for terminal_send/read/close. Disabled unless tools.terminal_session_enabled is set. For one-shot commands use shell_run instead.\n- **terminal_send**(session_id, input, wait) [capability_expand category: terminal]: Send a line of input to a persistent terminal session and return output captured shortly after.\n- **terminal_read**(session_id, wait): Drain buffered output from a persistent terminal session (optionally waiting briefly first).\n- **terminal_close**(session_id) [capability_expand category: terminal]: Terminate a persistent terminal session and release its process group.\n- **terminal_list**(): List active persistent terminal sessions.\n- **config_list**(category, limit): List LeapFlow's own writable settings (LLM model and endpoint, daemon, memory, perception, gateway, …) with current values. Use this to discover the exact key before changing anything. Optionally narrow by `category`. This is the only correct way to inspect LeapFlow configuration — never read config files from disk.\n- **config_get**(key): Read one LeapFlow setting by key (e.g. 'llm.model', 'llm.base_url', 'daemon.log_level'), returning its current value, type, scopes, and whether a change needs a daemon restart. There is no 'llm.provider' key: provider behavior is inferred from the OpenAI-compatible 'llm.base_url'. Never read LeapFlow config files from disk — use this.\n- **config_set**(key, value, scope) [capability_expand category: config]: Change one LeapFlow setting by key, e.g. switch an OpenAI-compatible LLM with key='llm.model' and key='llm.base_url'. There is no 'llm.provider' key: provider behavior is inferred from the endpoint. Values are validated and coerced; credentials are stored in the vault automatically. Call config_list or config_get first if unsure of the key. The result states whether a `leap daemon restart` is required. Never edit LeapFlow config files directly.\n- **web_fetch**(url, select, timeout, max_bytes): Read a URL over HTTP(S) and get back extracted, context-sized content: parsed JSON for API endpoints, readable text plus links for web pages. Use this for anything on the internet — prices, docs, releases, articles — instead of running curl through shell_run: it reports real HTTP status codes, retries rate limits on its own, and is a plain read so a retry is always safe. For JSON APIs pass `select` with a dotted path (e.g. 'chart.result.0.meta') to return just that part instead of the whole payload.\n- **memory_search**(query, limit): Search agent memory for relevant past experiences, observations, and facts.\n- **memory_add**(content, kind) [capability_expand category: write]: Store a new observation or insight in memory for future reference.\n- **research_note**(kind, text): Record a compact, structured note about the current task's state so it survives context compression on long / multi-step tasks. Use for durable findings, open questions still to resolve, decisions / excluded paths, and the immediate next step. One concise sentence per note.\n- **capability_expand**(category): Fetch the full callable schema for every tool in a capability category (e.g. 'hub', 'gateway', 'desktop', 'delegate', 'file', 'memory', 'skill'). The compact tool index always lists every registered tool by name and a one-line summary, but only a static low-risk subset is directly callable each turn. If you need a tool from the index that is not yet callable, call capability_expand with its category first; the matching tools become callable in this turn. Never invent a tool name — expand the category instead.\n- **delegate_task**(goal, context) [capability_expand category: delegate]: Delegate a complex sub-task to an isolated subagent. The subagent gets a fresh context and restricted tool access. Use when a task is self-contained and can be solved independently.\n- **schedule_reentry**(kind, reason, delay_seconds, event_match, max_reentries, deadline_seconds): Register a re-entry so this task can resume later from its current orientation (findings / open questions / next step). Use when work must pause and continue after a delay (kind=time) or when a matching platform event arrives (kind=event), instead of finishing now. The research-ledger state is carried over automatically.\n- **hub_push**(skill_name, visibility, version) [capability_expand category: hub]: Push a local skill to the ModelScope Hub for sharing or backup.\n- **hub_pull**(repo_id, version) [capability_expand category: hub]: Pull a skill from the ModelScope Hub to install locally.\n- **hub_search**(query) [capability_expand category: hub]: Search for skills on the Hub by keyword or description.\n- **hub_sync**(mode, dry_run) [capability_expand category: hub]: Preview or execute sync between local skills and Hub.\n- **platform_action**(platform, action, payload, backend_kind) [capability_expand category: gateway]: Execute an exact registered business action on an external platform through LeapFlow's App Connector layer. Actions must be copied from the App Connector Capability Index and are addressed as domain.operation, e.g. im.send_message or docs.create_markdown. All business fields (chat_id, text, query, etc.) MUST be placed inside `payload`, never at the top level. Example: {\"platform\":\"feishu\",\"action\":\"im.send_message\",\"payload\":{\"chat_id\":\"oc_xxx\",\"text\":\"hello\"}}. Do not invent action names, do not use management actions such as list/guide/connect/status here.\n- **platform_connect**(action, platform, credentials, options, checkpoint) [capability_expand category: gateway]: List, guide, connect, disconnect, remove, or check status for external platforms using the App Connector management namespace. Supports REST and CLI backends. Use this for management actions such as list/guide/preflight/connect/status; use platform_action only for exact registered business actions.\n- **gateway_send**(platform, chat_id, text, thread_id) [capability_expand category: gateway]: Send a message to a connected external platform (Feishu group, Telegram chat, DingTalk conversation, etc.). Requires the platform to be connected via gateway_connect first. Use gateway_connect with action='list' to see connected platforms and available chat IDs.\n- **gateway_connect**(action, platform, credentials, options) [capability_expand category: gateway]: Connect, configure, or manage external platform integrations (Feishu, DingTalk, Telegram, Slack, Discord, etc.). Conversational flow: 1) call 'guide' to get setup steps + required fields, 2) present the steps to the user and ask for ALL required credentials in a single message, 3) call 'connect' with the credentials. Goal: complete in 1–2 user turns. NEVER include credential values in your text response.\n- **plugin_list**(): List the live plugin registry and cross-subsystem capability evidence. Use this before answering questions about whether LeapFlow supports plugins, self-evolution, plugin installation, hot reload, versioning, or other runtime capabilities.\n- **plugin_status**(plugin_id): Get detailed status of a specific plugin: its declared category, runtime dependencies, contributed tools, and fiber lifecycle state.\n- **plugin_versions**(plugin_id): List recorded source versions and active pointer for a profile-scoped plugin.\n- **plugin_propose**(requested_capability, plugin_id, proposed_tools, test_cases, risk_level, evidence): Create a side-effect-free PluginProposal from explicit capability-gap evidence. Use this before plugin_generate when a missing capability should be reviewed. Does not call an LLM, write files, or install anything.\n- **assess_compatibility**(manifest, source_path) [capability_expand category: plugin_management]: Assess whether a foreign plugin manifest or real DSH source bundle is compatible with LeapFlow. A source bundle assessment is static: runtime_ready stays false until restricted Node discovery during plugin_install. Returns component-level verdicts and limitations.\n- **plugin_generate**(plugin_id, description, proposal_id) [capability_expand category: system]: Generate a new ToolPlugin from a natural-language capability description. The LLM produces code that conforms to the ToolPlugin Protocol; it is then rigorously validated (syntax, structure, import, protocol conformance). The generated source is stored in CAS and receives explicit content approval. It DOES NOT install the plugin — installation requires a second, mutation-specific approval via plugin_install.\n- **plugin_install**(plugin_id, code, marketplace_name, source_path, proposal_id, version_label) [capability_expand category: system]: Install a plugin from exactly one source: validated Python code, the configured Python marketplace, or a real DSH source bundle. DSH bundles run restricted Node discovery before registration; only public host tools are installed and client UI limitations are reported. Writes profile-scoped state and mutates the process-global registry. REQUIRES APPROVAL.\n- **plugin_rollback**(plugin_id, version) [capability_expand category: system]: Rollback a profile-scoped plugin to a recorded source version and reload it. REQUIRES APPROVAL.\n- **plugin_reload**(plugin_id, version_label) [capability_expand category: system]: Hot-reload a plugin at runtime. Disposes the old plugin fiber, re-imports its module, and registers a fresh instance. Existing in-flight turns are unaffected (snapshot isolation). REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_disable**(plugin_id) [capability_expand category: system]: Disable a plugin by disposing its fiber, removing its tools from the runtime registry. Cannot disable self_management itself. REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_remove**(plugin_id, delete_source) [capability_expand category: system]: Terminally remove a plugin: dispose its fiber, unregister its tools, remove reload metadata, and optionally delete its profile-scoped source file. Cannot remove self_management itself. REQUIRES APPROVAL.\n- **plugin_unquarantine**(plugin_id) [capability_expand category: plugin_management]: Restore a quarantined plugin to probation status for re-evaluation. Unfreezes the trust ledger, transitions the proposal back to PROBATION, and reloads the plugin. REQUIRES APPROVAL.\n- **plugin_enable**(plugin_id) [capability_expand category: system]: Re-enable a previously disabled plugin by reloading its module and registering a fresh instance. REQUIRES APPROVAL.\n- **self_describe**(facet): Introspect LeapFlow's own identity, capabilities, runtime state, evolution metrics, or platform connections. Use facet='all' only when a comprehensive self-check is explicitly requested.\n- **runtime_snapshot**(): Lightweight (~150 token) flat snapshot of current runtime state: model, context budget, posture, disclosure level, turn count, cache hit rate, uptime, tool count, and pending approvals.\n- **schedule_create**(trigger_expression, instruction, execution_mode, max_retries, delivery_target) [capability_expand category: scheduler]: Create a new scheduled task. Specify a trigger expression (e.g. '30m', 'every 2h', '0 9 * * *') and the instruction to execute on each trigger. Optionally provide an execution mode and a delivery target to receive notifications.\n- **schedule_list**() [capability_expand category: scheduler]: List all scheduled tasks with their current state, trigger, and next due time.\n- **schedule_status**(task_id) [capability_expand category: scheduler]: Get detailed status and recent execution history for a scheduled task.\n- **schedule_pause**(task_id) [capability_expand category: scheduler]: Pause a scheduled task so it stops firing without being cancelled.\n- **schedule_resume**(task_id) [capability_expand category: scheduler]: Resume a paused scheduled task — re-arms it and recalculates next due time.\n- **schedule_cancel**(task_id) [capability_expand category: scheduler]: Cancel a scheduled task permanently.\n- **tool_search**(query, max_results): Search all registered tools by keyword relevance. Returns a ranked list of matching tools with name, category, and summary. Use this when you need a tool but are unsure of its exact name or category.\n- **tool_describe**(tool_name): Get the full callable schema of a single tool by exact name. Returns the tool's description, parameters, and metadata. Use after tool_search to inspect a specific tool before calling it.\n- **session_search**(query, limit) [capability_expand category: unclassified]: Search past conversation sessions in the current workspace for relevant context.\n- **session_list**(limit): List recent conversation sessions in the current workspace with stable ids, titles, dates, and summaries. Use for browsing past tasks or when user asks to see history without specific search terms. No keywords needed — returns chronological list.\n- **session_detail**(session_id, limit, offset, include_inactive) [capability_expand category: unclassified]: Read a paginated persisted transcript for one past conversation session in the current workspace. Use after session_list or session_search returns a session_id.\n- **click**(element_index) [capability_expand category: desktop]: Click a UI element by its element_index (from the latest observe_ui snapshot)\n- **get_clipboard**() [capability_expand category: desktop]: Read current clipboard text content\n- **list_apps**(filter, running_only) [capability_expand category: desktop]: List available applications on this system. Use to discover correct bundle_id before switch_app.\n- **list_windows**() [capability_expand category: desktop]: List all top-level windows with pid, window_id, title, and per-window state (minimized, on-screen). Call this first to pick the pid and window_id that observe_ui and other window tools require.\n- **observe_ui**(pid, window_id, query) [capability_expand category: desktop]: Snapshot one window's actionable UI elements, each tagged with an element_index for click/right_click/read_text. Re-observe after actions — indices belong to one snapshot. Requires the window's pid and window_id from list_windows.\n- **open_url**(url, app_id) [capability_expand category: desktop]: Open a URL in the default or specified browser\n- **read_text**(element_index) [capability_expand category: desktop]: Read the text content of a specific UI element from the latest snapshot\n- **right_click**(element_index) [capability_expand category: desktop]: Right-click a UI element to open its context menu. Returns visible menu items.\n- **screenshot**(pid, window_id) [capability_expand category: desktop]: Capture a screenshot for visual verification. With pid + window_id captures that window (works across all displays); defaults to the last observed window, or the full desktop when no window has been observed.\n- **scroll**(element_index, direction, amount, pid, window_id) [capability_expand category: desktop]: Scroll a scrollable area of a window. Omit element_index to scroll the window's focused/page scroller; pass one to scroll an exact element from the latest snapshot.\n- **select_text**(element_index) [capability_expand category: desktop]: Select all text in a UI element (focus + select-all, for subsequent copy)\n- **set_clipboard**(text) [capability_expand category: desktop]: Write text to the clipboard\n- **shortcut**(keys) [capability_expand category: desktop]: Execute a keyboard shortcut\n- **switch_app**(app_id) [capability_expand category: desktop]: Switch to an app (launch if needed, activate, verify)\n- **type_text**(text) [capability_expand category: desktop]: Type text into the currently focused element\n- **wait**(seconds) [capability_expand category: desktop]: Wait for a specified duration before continuing\n- **wait_until**(condition, pid, window_id, timeout, poll_interval) [capability_expand category: desktop]: Wait until a UI condition is met (polls UI tree). Returns elements when found or on timeout.\n- **wait_until_stable**(timeout, poll_interval, pid, window_id) [capability_expand category: desktop]: Wait until the UI stops changing (element set stabilizes across polls).\n\n## App Connector Capability Index\nLeapFlow can onboard and manage external apps through `platform_connect` and execute exact registered business actions through `platform_action`.\nFor requests about connecting, setting up, configuring, enabling, or managing a supported app, use `platform_connect` first instead of generating SDK/Webhook sample code.\n`platform_connect.action` is the management namespace: list, guide, preflight, connect, disconnect, remove, status, events_start, events_stop, events_status.\n`platform_action.action` is only for exact registered platform business actions listed below, such as `im.send_message`; never use management actions like `list` or `guide` there.\nAll business fields MUST go inside `payload`; top-level keys are only `platform`, `action`, and `payload`.\nDo not invent platform IDs or platform action names. If the needed action is not listed, ask for discovery/clarification instead of guessing.\nUse `platform_connect` with `action='guide'` and the matching `platform` to start onboarding; use `action='list'` when the app is unclear.\nWhen a pending onboarding state is present, continue from that state with `platform_connect` instead of asking the user to restate the app.\nSupported apps:\n- `api_server`: API Server (OpenAI Compatible) (category=api; backend=adapter; platform_action actions=none registered)\n- `dingtalk`: 钉钉 (DingTalk) (category=im; backend=adapter; platform_action actions=none registered)\n- `feishu`: 飞书 (Feishu/Lark) (category=im; backend=cli)\n - `calendar.create_event` payload={summary*, start_time*, end_time*, attendees} [write/high]\n - `docs.create_markdown` payload={title*, markdown*, folder_token} [write/medium]\n - `drive.search` payload={query*, limit} [read/medium]\n - `im.add_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.download_resource` payload={message_id*, file_key*, type*} [read/low]\n - `im.get_messages` payload={message_ids*} [read/low]\n - `im.list_chats` payload={page_size} [read/low]\n - `im.list_messages` payload={chat_id*} [read/low]\n - `im.list_thread_messages` payload={thread*} [read/low]\n - `im.remove_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.reply_message` payload={message_id*, text*} [send/high]\n - `im.search_chats` payload={query*, page_size} [read/low]\n - `im.search_messages` payload={query*} [read/medium]\n - `im.send_message` payload={chat_id*, text*, thread_id} [send/high]\n - `im.update_card` payload={token*, card*} [write/medium]\n - `im.update_message` payload={message_id*, text*} [write/medium]\n - `mail.search_unread` payload={query, limit} [read/high]\n - `sheets.append_row` payload={spreadsheet_token*, sheet_id*, values*} [write/medium]\n - `task.create` payload={title*, description, due_time} [write/medium]\n- `telegram`: Telegram (category=social; backend=adapter; platform_action actions=none registered)\n- `webhook`: Webhook (Generic) (category=webhook; backend=adapter; platform_action actions=none registered)\n\n## Tool Usage\nTools are normally invoked through the native function-calling mechanism, not by writing JSON in your reply\ntext. Only if the provider signals that native function calling is unavailable for this turn, fall back to a\nsingle JSON code block: `{\"name\": \"tool_name\", \"arguments\": {\"key\": \"value\"}}` — use this fallback format\nonly, never both. Only call a tool whose exact name appears in the tool index above and is currently callable\n(or reachable via `capability_expand`). Never invent, rename, alias, or guess a tool name, platform ID, or\nplatform action from argument shape or wording — if the index does not list it, it does not exist.\n`platform_connect.action` (list/guide/preflight/connect/disconnect/remove/status/events_start/events_stop/\nevents_status) is the App Connector management namespace; `platform_action.action` only accepts exact\nregistered business actions such as `im.send_message` shown in the App Connector Capability Index — never mix\nthe two namespaces. If a tool call returns an unknown/unavailable result, use the returned suggestions or\navailable names for a single retry instead of trying further variations of the same guess.\n\n**Side-effect action rule** (`platform_action` with effect=send/write/execute):\n- Call each unique action+payload **exactly once**. Never include duplicates in the same turn.\n- Once the result returns `\"completed\": true`, that action is DONE for this task. Do NOT call it again\n in any subsequent turn — immediately summarize the result for the user instead.\n- The system enforces idempotency: duplicate calls are blocked and will not execute.\n- If the user explicitly requests sending/writing multiple times, use distinct payloads per call.\n\n**Resource identifier provenance rule**:\n- NEVER fabricate, guess, or infer resource identifiers (chat_id, message_id, file_key, user_id, etc.).\n Every resource ID used in a side-effect action MUST come from a successful API response in this session.\n- If a read/list action fails (e.g. authorization error), you do NOT have valid resource IDs.\n Report the failure to the user — do NOT attempt the dependent write/send action with a guessed ID.\n- When a tool result contains `\"llm_instruction\"`, follow it exactly.\n\n## Guidelines\n1. **Direct answers first**: If you already know the answer, respond directly without tools.\n2. **Avoid redundant tool calls**: Do not call the same tool with the same arguments more than once in the same user turn. When an existing tool result already answers the user's request, stop calling tools and answer directly.\n3. **Use tools proactively**: When the user asks about files, time, system state, or needs actions performed, use the appropriate tool.\n4. **Chain tools when needed**: You can call multiple tools in sequence (e.g., list files → read file → summarize).\n5. **Handle failures gracefully**: If a tool fails, explain what went wrong and suggest alternatives. If it failed because of an unknown tool/platform/action name, retry once with an exact name from the returned suggestions, then explain rather than keep guessing.\n6. **Summarize results naturally**: After tool execution, synthesize the results into a helpful answer rather than dumping raw output.\n7. **Stay conversational**: Maintain a natural, helpful tone. Acknowledge context from earlier in the conversation.\n8. **Recall past work**:\n - Broad queries (\"之前做了什么\", \"列出任务\"): answer from the \"Recent Task History\" section already in your context. If insufficient, call session_list.\n - Specific lookups (\"上次那个配置怎么改的\"): call session_search with relevant phrases (NOT single characters).\n - Do NOT call search tools repeatedly with keyword fragments. One well-phrased call is better than ten fragmented ones.\n\n## Coding & Verification\nWhen working with code, prefer the precise built-in tools over ad-hoc shell: use `repo_map` to orient in an\nunfamiliar project, `code_search`/`file_find` to locate and `code_intel` for symbols, `edit_file` (anchored\nsearch-replace, or a unified `diff`) to change files — never rewrite a whole file to change a few lines — and\n`git_query` to inspect diffs/log. After edits, check `syntax_ok` in the result and run `test_run`/`lint_check`\nbefore declaring the work done. Batch independent read-only calls (search/read/query) into a single turn — they\nrun in parallel.\n\n## Presentation Style\n1. **Polished Markdown only**: Format user-facing answers with clean Markdown headings, short paragraphs, and concise bullets. Use tables only when they improve comparison or scanning.\n2. **Terminal-friendly layout**: Keep lines readable in a TUI; avoid dense walls of text, deeply nested lists, oversized ASCII art, or heavy visual blocks.\n3. **Elegant emphasis**: Use bold text sparingly for key terms and conclusions. Avoid excessive emojis, decorative symbols, repeated separators, or visual noise.\n4. **Theme-safe colors**: Do not emit ANSI escape codes, HTML color tags, Rich markup, or hardcoded color names. Rely on the TUI theme to render Markdown professionally.\n5. **No leaked tool protocol**: Never show tool-call JSON, internal schemas, raw observations, tool result payloads, or hidden reasoning in the final answer unless the user explicitly asks for raw/debug output. Treat any prior `{\"name\": ..., \"arguments\": ...}` blocks and `Tool result (...)` messages as internal execution context only.\n6. **Professional closure**: End with a concise conclusion or next step when helpful; avoid rambling after the useful answer is complete.\n\nWhen finished with all tool calls, respond normally without a JSON block, tool-call transcript, or process log.\n\n## Task Contract\n- Task ID: turn-2\n- Original user request: And now?\n- Workspace root: \n- Allowed roots: \n- Treat relative project paths as relative to the workspace root; never infer `.` as the project root when a workspace root is provided.\n- Workspace boundary is enforced by tools: do not read, search, edit, or run commands against paths outside the allowed roots unless the user explicitly requests an external path and the tool/approval policy permits it.\n- LeapFlow workspace config is optional at `/.leapflow/config.yaml`; runtime config is loaded from `~/.leapflow/config/user.yaml` and `~/.leapflow/profiles//config/*.yaml`.\n- Preserve this task contract across summarization, compression, tool loops, and memory retrieval.\n" - }, - { - "role": "system", - "content": "\n## Recent Session Summary\n- [assistant] Recovered after a rate limit.\n- [user] And now?\n" - }, - { - "role": "assistant", - "content": "Recovered after a rate limit." - }, - { - "role": "user", - "content": "And now?\nAnd now?" - } - ], - "tools": [ - "capability_expand", - "code_intel", - "code_search", - "config_get", - "config_list", - "env_info", - "file_find", - "file_list", - "file_read", - "git_query", - "lint_check", - "memory_search", - "plugin_list", - "plugin_propose", - "plugin_status", - "plugin_versions", - "repo_map", - "research_note", - "runtime_snapshot", - "schedule_reentry", - "self_describe", - "session_list", - "skill_view", - "skills_list", - "terminal_list", - "terminal_read", - "test_run", - "text_search", - "time_get", - "tool_describe", - "tool_search", - "web_fetch" - ] - }, - "responses": [ - { - "status": 500, - "content_type": "application/json", - "body": "{\"error\": {\"message\": \"The server had an error\", \"type\": \"server_error\", \"code\": \"internal_error\"}}" - }, - { - "status": 200, - "content_type": "application/json", - "body": "{\"id\": \"chatcmpl-cassette\", \"object\": \"chat.completion\", \"model\": \"cassette-model\", \"choices\": [{\"index\": 0, \"message\": {\"role\": \"assistant\", \"content\": \"Recovered after a server error.\"}, \"finish_reason\": \"stop\"}], \"usage\": {\"prompt_tokens\": 64, \"completion_tokens\": 16, \"total_tokens\": 80}}" - } - ] -} diff --git a/tests/_fixtures/cassettes/r4_recovery/cassette-model-a35ff02ba7c81b17.cassette.json b/tests/_fixtures/cassettes/r4_recovery/cassette-model-a35ff02ba7c81b17.cassette.json deleted file mode 100644 index 9ee075de..00000000 --- a/tests/_fixtures/cassettes/r4_recovery/cassette-model-a35ff02ba7c81b17.cassette.json +++ /dev/null @@ -1,72 +0,0 @@ -{ - "fingerprint": "a35ff02ba7c81b17120f398e873d6533142549e7fafa7dedca4a6892bd55e49d", - "note": "captured in seed mode", - "request": { - "model": "cassette-model", - "stream": false, - "messages": [ - { - "role": "system", - "content": "You are LeapFlow, an intelligent assistant that can both converse naturally and take real actions on the user's computer.\n\n## Capabilities\nThe tool index below lists **every** registered tool by name and a one-line summary — this is the complete\ncapability contract; nothing else exists. Only a subset is directly callable this turn (via native tool calling,\nnot a JSON block in your reply). If you need a tool from the index that is not yet callable, call\n`capability_expand` with its category name first — the matching tools become callable immediately after.\nWhen the user asks what LeapFlow itself supports, whether it supports plugins, or which runtime capabilities\nare available, use the live capability evidence exposed by `plugin_list` before making capability claims; report\nconfiguration-dependent or unavailable capabilities as limitations instead of inferring from documentation.\n- **text_search**(text, pattern): Search for a regex pattern in text.\n- **text_replace**(text, old, new, count) [capability_expand category: write]: Replace occurrences of a substring in text.\n- **time_get**(): Get current date and time.\n- **env_info**(): Get system environment information (OS, Python version, cwd).\n- **skills_list**(query, category, source): List available learned skills. Use when user asks about capabilities or you need a specific skill.\n- **skill_view**(name): View the full content of a specific skill document.\n- **code_intel**(path, operation): Precise document symbols (outline) for a source file: classes, functions, and methods with line ranges. Python uses an exact AST parse; other languages use a keyword-prefix scan. Prefer over file_read mode=symbols for accurate navigation before editing. Read-only.\n- **repo_map**(path): Compact project orientation for a repository root: languages, detected test/lint commands, top-level structure, entry points, manifest, and VCS branch. Call this first when entering an unfamiliar codebase. Read-only.\n- **scm_sync**(action, cwd, remote, pull_ref, push_ref, timeout) [capability_expand category: scm]: Run a typed git SCM action. Use this instead of shell_run for git pull/push/status. For 'pull origin main then push', set action='pull_then_push', remote='origin', pull_ref='main', and omit push_ref so LeapFlow pushes the current local branch.\n- **git_query**(action, cwd, ref, path, staged, max_count, stat): Read-only structured git inspection: action=diff|log|status|branch|show. Prefer over shell_run for reading repo state — output is clipped, redacted, and log/branch are parsed into structured fields. Use scm_sync for pull/push.\n- **git_write**(action, cwd, message, stage_all, name, ref, create) [capability_expand category: scm]: Mutating git actions: action=commit (message, stage_all), branch (create+switch), checkout (switch; create=true for -b). Approval-gated. Use scm_sync for pull/push and git_query for reads.\n- **test_run**(command, cwd, timeout): Run the project's test suite and return structured results (framework, passed/failed counts, failing tests). Auto-detects the runner (pytest/npm/go/cargo) or uses a configured/explicit command; executes via the governed shell. ok=true means the runner executed — see 'success' for pass/fail.\n- **lint_check**(command, cwd, timeout): Run the project's linter and return a structured clean/issue result. Auto-detects the linter (ruff/eslint/go vet/clippy) or uses a configured/explicit command; executes via the governed shell. ok=true means the linter ran — see 'clean'.\n- **file_list**(path, pattern, depth): List files and directories at a given path. Use depth=1 or depth=2 to get a recursive tree in one call instead of listing each sub-directory separately.\n- **file_read**(path, max_lines, start_line, max_chars, mode): Read text file content with adaptive context governance. For large or unfamiliar files, prefer mode='outline' or mode='symbols' first, then use mode='raw' with start_line/max_lines for the specific range you actually need. For LeapFlow's own settings, use config_list / config_get / config_set — its config files are outside the workspace and not readable here.\n- **file_write**(path, content, mode) [capability_expand category: write]: Write content to a file (overwrite or append).\n- **code_search**(pattern, patterns, path, glob, ignore_case, multiline, max_results, context_lines): Search file CONTENTS by regex pattern across a directory tree (ripgrep-backed). Requires a regex pattern. NOT for listing or browsing directory contents — use file_list for that. Prefer this over shell_run grep: faster, skips VCS/dependency/build dirs, and returns structured path:line:column matches. Batch related lookups into ONE call via `patterns` (OR-combined, single pass) instead of issuing several separate searches. Use file_read for the surrounding context of a hit.\n- **file_find**(glob, path, max_results): Find files by a recursive glob pattern under a base path (e.g. '**/test_*.py' or '*.md'). Prefer this over shell_run find; skips VCS/dependency/build dirs.\n- **edit_file**(path, edits, dry_run, diff) [capability_expand category: write]: Apply targeted, anchored search-replace edits to an EXISTING text file (use file_write to create/overwrite). Each edit is {original_text, new_text, replace_all?}; original_text must match exactly and uniquely (or set replace_all) — a non-unique or missing anchor is rejected so files are never corrupted. Set dry_run to preview. Alternatively pass a unified 'diff' to apply its hunks as anchored edits. Far cheaper and safer than rewriting a whole file.\n- **shell_run**(command, cwd, timeout) [capability_expand category: shell]: Execute a one-shot shell command with timeout protection. Runs in the active workspace; paths resolving outside it are refused. Reach for a structured tool first when one fits — web_fetch for anything over HTTP(S), git_query/scm_sync for git, code_search/file_find/file_read for the repo, config_get/config_set for LeapFlow's own settings — because those report typed results, while a failed shell command can only be diagnosed from its exit code and stderr. Every shell run counts as an external side effect, so a failure stops the rest of the batch and is not retried automatically.\n- **terminal_open**(command, cwd, shell) [capability_expand category: terminal]: Open a PERSISTENT shell session (REPL/dev server/watch), returning a session_id for terminal_send/read/close. Disabled unless tools.terminal_session_enabled is set. For one-shot commands use shell_run instead.\n- **terminal_send**(session_id, input, wait) [capability_expand category: terminal]: Send a line of input to a persistent terminal session and return output captured shortly after.\n- **terminal_read**(session_id, wait): Drain buffered output from a persistent terminal session (optionally waiting briefly first).\n- **terminal_close**(session_id) [capability_expand category: terminal]: Terminate a persistent terminal session and release its process group.\n- **terminal_list**(): List active persistent terminal sessions.\n- **config_list**(category, limit): List LeapFlow's own writable settings (LLM model and endpoint, daemon, memory, perception, gateway, …) with current values. Use this to discover the exact key before changing anything. Optionally narrow by `category`. This is the only correct way to inspect LeapFlow configuration — never read config files from disk.\n- **config_get**(key): Read one LeapFlow setting by key (e.g. 'llm.model', 'llm.base_url', 'daemon.log_level'), returning its current value, type, scopes, and whether a change needs a daemon restart. There is no 'llm.provider' key: provider behavior is inferred from the OpenAI-compatible 'llm.base_url'. Never read LeapFlow config files from disk — use this.\n- **config_set**(key, value, scope) [capability_expand category: config]: Change one LeapFlow setting by key, e.g. switch an OpenAI-compatible LLM with key='llm.model' and key='llm.base_url'. There is no 'llm.provider' key: provider behavior is inferred from the endpoint. Values are validated and coerced; credentials are stored in the vault automatically. Call config_list or config_get first if unsure of the key. The result states whether a `leap daemon restart` is required. Never edit LeapFlow config files directly.\n- **web_fetch**(url, select, timeout, max_bytes): Read a URL over HTTP(S) and get back extracted, context-sized content: parsed JSON for API endpoints, readable text plus links for web pages. Use this for anything on the internet — prices, docs, releases, articles — instead of running curl through shell_run: it reports real HTTP status codes, retries rate limits on its own, and is a plain read so a retry is always safe. For JSON APIs pass `select` with a dotted path (e.g. 'chart.result.0.meta') to return just that part instead of the whole payload.\n- **memory_search**(query, limit): Search agent memory for relevant past experiences, observations, and facts.\n- **memory_add**(content, kind) [capability_expand category: write]: Store a new observation or insight in memory for future reference.\n- **research_note**(kind, text): Record a compact, structured note about the current task's state so it survives context compression on long / multi-step tasks. Use for durable findings, open questions still to resolve, decisions / excluded paths, and the immediate next step. One concise sentence per note.\n- **capability_expand**(category): Fetch the full callable schema for every tool in a capability category (e.g. 'hub', 'gateway', 'desktop', 'delegate', 'file', 'memory', 'skill'). The compact tool index always lists every registered tool by name and a one-line summary, but only a static low-risk subset is directly callable each turn. If you need a tool from the index that is not yet callable, call capability_expand with its category first; the matching tools become callable in this turn. Never invent a tool name — expand the category instead.\n- **delegate_task**(goal, context) [capability_expand category: delegate]: Delegate a complex sub-task to an isolated subagent. The subagent gets a fresh context and restricted tool access. Use when a task is self-contained and can be solved independently.\n- **schedule_reentry**(kind, reason, delay_seconds, event_match, max_reentries, deadline_seconds): Register a re-entry so this task can resume later from its current orientation (findings / open questions / next step). Use when work must pause and continue after a delay (kind=time) or when a matching platform event arrives (kind=event), instead of finishing now. The research-ledger state is carried over automatically.\n- **hub_push**(skill_name, visibility, version) [capability_expand category: hub]: Push a local skill to the ModelScope Hub for sharing or backup.\n- **hub_pull**(repo_id, version) [capability_expand category: hub]: Pull a skill from the ModelScope Hub to install locally.\n- **hub_search**(query) [capability_expand category: hub]: Search for skills on the Hub by keyword or description.\n- **hub_sync**(mode, dry_run) [capability_expand category: hub]: Preview or execute sync between local skills and Hub.\n- **platform_action**(platform, action, payload, backend_kind) [capability_expand category: gateway]: Execute an exact registered business action on an external platform through LeapFlow's App Connector layer. Actions must be copied from the App Connector Capability Index and are addressed as domain.operation, e.g. im.send_message or docs.create_markdown. All business fields (chat_id, text, query, etc.) MUST be placed inside `payload`, never at the top level. Example: {\"platform\":\"feishu\",\"action\":\"im.send_message\",\"payload\":{\"chat_id\":\"oc_xxx\",\"text\":\"hello\"}}. Do not invent action names, do not use management actions such as list/guide/connect/status here.\n- **platform_connect**(action, platform, credentials, options, checkpoint) [capability_expand category: gateway]: List, guide, connect, disconnect, remove, or check status for external platforms using the App Connector management namespace. Supports REST and CLI backends. Use this for management actions such as list/guide/preflight/connect/status; use platform_action only for exact registered business actions.\n- **gateway_send**(platform, chat_id, text, thread_id) [capability_expand category: gateway]: Send a message to a connected external platform (Feishu group, Telegram chat, DingTalk conversation, etc.). Requires the platform to be connected via gateway_connect first. Use gateway_connect with action='list' to see connected platforms and available chat IDs.\n- **gateway_connect**(action, platform, credentials, options) [capability_expand category: gateway]: Connect, configure, or manage external platform integrations (Feishu, DingTalk, Telegram, Slack, Discord, etc.). Conversational flow: 1) call 'guide' to get setup steps + required fields, 2) present the steps to the user and ask for ALL required credentials in a single message, 3) call 'connect' with the credentials. Goal: complete in 1–2 user turns. NEVER include credential values in your text response.\n- **plugin_list**(): List the live plugin registry and cross-subsystem capability evidence. Use this before answering questions about whether LeapFlow supports plugins, self-evolution, plugin installation, hot reload, versioning, or other runtime capabilities.\n- **plugin_status**(plugin_id): Get detailed status of a specific plugin: its declared category, runtime dependencies, contributed tools, and fiber lifecycle state.\n- **plugin_versions**(plugin_id): List recorded source versions and active pointer for a profile-scoped plugin.\n- **plugin_propose**(requested_capability, plugin_id, proposed_tools, test_cases, risk_level, evidence): Create a side-effect-free PluginProposal from explicit capability-gap evidence. Use this before plugin_generate when a missing capability should be reviewed. Does not call an LLM, write files, or install anything.\n- **assess_compatibility**(manifest, source_path) [capability_expand category: plugin_management]: Assess whether a foreign plugin manifest or real DSH source bundle is compatible with LeapFlow. A source bundle assessment is static: runtime_ready stays false until restricted Node discovery during plugin_install. Returns component-level verdicts and limitations.\n- **plugin_generate**(plugin_id, description, proposal_id) [capability_expand category: system]: Generate a new ToolPlugin from a natural-language capability description. The LLM produces code that conforms to the ToolPlugin Protocol; it is then rigorously validated (syntax, structure, import, protocol conformance). The generated source is stored in CAS and receives explicit content approval. It DOES NOT install the plugin — installation requires a second, mutation-specific approval via plugin_install.\n- **plugin_install**(plugin_id, code, marketplace_name, source_path, proposal_id, version_label) [capability_expand category: system]: Install a plugin from exactly one source: validated Python code, the configured Python marketplace, or a real DSH source bundle. DSH bundles run restricted Node discovery before registration; only public host tools are installed and client UI limitations are reported. Writes profile-scoped state and mutates the process-global registry. REQUIRES APPROVAL.\n- **plugin_rollback**(plugin_id, version) [capability_expand category: system]: Rollback a profile-scoped plugin to a recorded source version and reload it. REQUIRES APPROVAL.\n- **plugin_reload**(plugin_id, version_label) [capability_expand category: system]: Hot-reload a plugin at runtime. Disposes the old plugin fiber, re-imports its module, and registers a fresh instance. Existing in-flight turns are unaffected (snapshot isolation). REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_disable**(plugin_id) [capability_expand category: system]: Disable a plugin by disposing its fiber, removing its tools from the runtime registry. Cannot disable self_management itself. REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_remove**(plugin_id, delete_source) [capability_expand category: system]: Terminally remove a plugin: dispose its fiber, unregister its tools, remove reload metadata, and optionally delete its profile-scoped source file. Cannot remove self_management itself. REQUIRES APPROVAL.\n- **plugin_unquarantine**(plugin_id) [capability_expand category: plugin_management]: Restore a quarantined plugin to probation status for re-evaluation. Unfreezes the trust ledger, transitions the proposal back to PROBATION, and reloads the plugin. REQUIRES APPROVAL.\n- **plugin_enable**(plugin_id) [capability_expand category: system]: Re-enable a previously disabled plugin by reloading its module and registering a fresh instance. REQUIRES APPROVAL.\n- **self_describe**(facet): Introspect LeapFlow's own identity, capabilities, runtime state, evolution metrics, or platform connections. Use facet='all' only when a comprehensive self-check is explicitly requested.\n- **runtime_snapshot**(): Lightweight (~150 token) flat snapshot of current runtime state: model, context budget, posture, disclosure level, turn count, cache hit rate, uptime, tool count, and pending approvals.\n- **schedule_create**(trigger_expression, instruction, execution_mode, max_retries, delivery_target) [capability_expand category: scheduler]: Create a new scheduled task. Specify a trigger expression (e.g. '30m', 'every 2h', '0 9 * * *') and the instruction to execute on each trigger. Optionally provide an execution mode and a delivery target to receive notifications.\n- **schedule_list**() [capability_expand category: scheduler]: List all scheduled tasks with their current state, trigger, and next due time.\n- **schedule_status**(task_id) [capability_expand category: scheduler]: Get detailed status and recent execution history for a scheduled task.\n- **schedule_pause**(task_id) [capability_expand category: scheduler]: Pause a scheduled task so it stops firing without being cancelled.\n- **schedule_resume**(task_id) [capability_expand category: scheduler]: Resume a paused scheduled task — re-arms it and recalculates next due time.\n- **schedule_cancel**(task_id) [capability_expand category: scheduler]: Cancel a scheduled task permanently.\n- **tool_search**(query, max_results): Search all registered tools by keyword relevance. Returns a ranked list of matching tools with name, category, and summary. Use this when you need a tool but are unsure of its exact name or category.\n- **tool_describe**(tool_name): Get the full callable schema of a single tool by exact name. Returns the tool's description, parameters, and metadata. Use after tool_search to inspect a specific tool before calling it.\n- **session_search**(query, limit) [capability_expand category: unclassified]: Search past conversation sessions in the current workspace for relevant context.\n- **session_list**(limit): List recent conversation sessions in the current workspace with stable ids, titles, dates, and summaries. Use for browsing past tasks or when user asks to see history without specific search terms. No keywords needed — returns chronological list.\n- **session_detail**(session_id, limit, offset, include_inactive) [capability_expand category: unclassified]: Read a paginated persisted transcript for one past conversation session in the current workspace. Use after session_list or session_search returns a session_id.\n- **click**(element_index) [capability_expand category: desktop]: Click a UI element by its element_index (from the latest observe_ui snapshot)\n- **get_clipboard**() [capability_expand category: desktop]: Read current clipboard text content\n- **list_apps**(filter, running_only) [capability_expand category: desktop]: List available applications on this system. Use to discover correct bundle_id before switch_app.\n- **list_windows**() [capability_expand category: desktop]: List all top-level windows with pid, window_id, title, and per-window state (minimized, on-screen). Call this first to pick the pid and window_id that observe_ui and other window tools require.\n- **observe_ui**(pid, window_id, query) [capability_expand category: desktop]: Snapshot one window's actionable UI elements, each tagged with an element_index for click/right_click/read_text. Re-observe after actions — indices belong to one snapshot. Requires the window's pid and window_id from list_windows.\n- **open_url**(url, app_id) [capability_expand category: desktop]: Open a URL in the default or specified browser\n- **read_text**(element_index) [capability_expand category: desktop]: Read the text content of a specific UI element from the latest snapshot\n- **right_click**(element_index) [capability_expand category: desktop]: Right-click a UI element to open its context menu. Returns visible menu items.\n- **screenshot**(pid, window_id) [capability_expand category: desktop]: Capture a screenshot for visual verification. With pid + window_id captures that window (works across all displays); defaults to the last observed window, or the full desktop when no window has been observed.\n- **scroll**(element_index, direction, amount, pid, window_id) [capability_expand category: desktop]: Scroll a scrollable area of a window. Omit element_index to scroll the window's focused/page scroller; pass one to scroll an exact element from the latest snapshot.\n- **select_text**(element_index) [capability_expand category: desktop]: Select all text in a UI element (focus + select-all, for subsequent copy)\n- **set_clipboard**(text) [capability_expand category: desktop]: Write text to the clipboard\n- **shortcut**(keys) [capability_expand category: desktop]: Execute a keyboard shortcut\n- **switch_app**(app_id) [capability_expand category: desktop]: Switch to an app (launch if needed, activate, verify)\n- **type_text**(text) [capability_expand category: desktop]: Type text into the currently focused element\n- **wait**(seconds) [capability_expand category: desktop]: Wait for a specified duration before continuing\n- **wait_until**(condition, pid, window_id, timeout, poll_interval) [capability_expand category: desktop]: Wait until a UI condition is met (polls UI tree). Returns elements when found or on timeout.\n- **wait_until_stable**(timeout, poll_interval, pid, window_id) [capability_expand category: desktop]: Wait until the UI stops changing (element set stabilizes across polls).\n\n## App Connector Capability Index\nLeapFlow can onboard and manage external apps through `platform_connect` and execute exact registered business actions through `platform_action`.\nFor requests about connecting, setting up, configuring, enabling, or managing a supported app, use `platform_connect` first instead of generating SDK/Webhook sample code.\n`platform_connect.action` is the management namespace: list, guide, preflight, connect, disconnect, remove, status, events_start, events_stop, events_status.\n`platform_action.action` is only for exact registered platform business actions listed below, such as `im.send_message`; never use management actions like `list` or `guide` there.\nAll business fields MUST go inside `payload`; top-level keys are only `platform`, `action`, and `payload`.\nDo not invent platform IDs or platform action names. If the needed action is not listed, ask for discovery/clarification instead of guessing.\nUse `platform_connect` with `action='guide'` and the matching `platform` to start onboarding; use `action='list'` when the app is unclear.\nWhen a pending onboarding state is present, continue from that state with `platform_connect` instead of asking the user to restate the app.\nSupported apps:\n- `api_server`: API Server (OpenAI Compatible) (category=api; backend=adapter; platform_action actions=none registered)\n- `dingtalk`: 钉钉 (DingTalk) (category=im; backend=adapter; platform_action actions=none registered)\n- `feishu`: 飞书 (Feishu/Lark) (category=im; backend=cli)\n - `calendar.create_event` payload={summary*, start_time*, end_time*, attendees} [write/high]\n - `docs.create_markdown` payload={title*, markdown*, folder_token} [write/medium]\n - `drive.search` payload={query*, limit} [read/medium]\n - `im.add_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.download_resource` payload={message_id*, file_key*, type*} [read/low]\n - `im.get_messages` payload={message_ids*} [read/low]\n - `im.list_chats` payload={page_size} [read/low]\n - `im.list_messages` payload={chat_id*} [read/low]\n - `im.list_thread_messages` payload={thread*} [read/low]\n - `im.remove_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.reply_message` payload={message_id*, text*} [send/high]\n - `im.search_chats` payload={query*, page_size} [read/low]\n - `im.search_messages` payload={query*} [read/medium]\n - `im.send_message` payload={chat_id*, text*, thread_id} [send/high]\n - `im.update_card` payload={token*, card*} [write/medium]\n - `im.update_message` payload={message_id*, text*} [write/medium]\n - `mail.search_unread` payload={query, limit} [read/high]\n - `sheets.append_row` payload={spreadsheet_token*, sheet_id*, values*} [write/medium]\n - `task.create` payload={title*, description, due_time} [write/medium]\n- `telegram`: Telegram (category=social; backend=adapter; platform_action actions=none registered)\n- `webhook`: Webhook (Generic) (category=webhook; backend=adapter; platform_action actions=none registered)\n\n## Tool Usage\nTools are normally invoked through the native function-calling mechanism, not by writing JSON in your reply\ntext. Only if the provider signals that native function calling is unavailable for this turn, fall back to a\nsingle JSON code block: `{\"name\": \"tool_name\", \"arguments\": {\"key\": \"value\"}}` — use this fallback format\nonly, never both. Only call a tool whose exact name appears in the tool index above and is currently callable\n(or reachable via `capability_expand`). Never invent, rename, alias, or guess a tool name, platform ID, or\nplatform action from argument shape or wording — if the index does not list it, it does not exist.\n`platform_connect.action` (list/guide/preflight/connect/disconnect/remove/status/events_start/events_stop/\nevents_status) is the App Connector management namespace; `platform_action.action` only accepts exact\nregistered business actions such as `im.send_message` shown in the App Connector Capability Index — never mix\nthe two namespaces. If a tool call returns an unknown/unavailable result, use the returned suggestions or\navailable names for a single retry instead of trying further variations of the same guess.\n\n**Side-effect action rule** (`platform_action` with effect=send/write/execute):\n- Call each unique action+payload **exactly once**. Never include duplicates in the same turn.\n- Once the result returns `\"completed\": true`, that action is DONE for this task. Do NOT call it again\n in any subsequent turn — immediately summarize the result for the user instead.\n- The system enforces idempotency: duplicate calls are blocked and will not execute.\n- If the user explicitly requests sending/writing multiple times, use distinct payloads per call.\n\n**Resource identifier provenance rule**:\n- NEVER fabricate, guess, or infer resource identifiers (chat_id, message_id, file_key, user_id, etc.).\n Every resource ID used in a side-effect action MUST come from a successful API response in this session.\n- If a read/list action fails (e.g. authorization error), you do NOT have valid resource IDs.\n Report the failure to the user — do NOT attempt the dependent write/send action with a guessed ID.\n- When a tool result contains `\"llm_instruction\"`, follow it exactly.\n\n## Guidelines\n1. **Direct answers first**: If you already know the answer, respond directly without tools.\n2. **Avoid redundant tool calls**: Do not call the same tool with the same arguments more than once in the same user turn. When an existing tool result already answers the user's request, stop calling tools and answer directly.\n3. **Use tools proactively**: When the user asks about files, time, system state, or needs actions performed, use the appropriate tool.\n4. **Chain tools when needed**: You can call multiple tools in sequence (e.g., list files → read file → summarize).\n5. **Handle failures gracefully**: If a tool fails, explain what went wrong and suggest alternatives. If it failed because of an unknown tool/platform/action name, retry once with an exact name from the returned suggestions, then explain rather than keep guessing.\n6. **Summarize results naturally**: After tool execution, synthesize the results into a helpful answer rather than dumping raw output.\n7. **Stay conversational**: Maintain a natural, helpful tone. Acknowledge context from earlier in the conversation.\n8. **Recall past work**:\n - Broad queries (\"之前做了什么\", \"列出任务\"): answer from the \"Recent Task History\" section already in your context. If insufficient, call session_list.\n - Specific lookups (\"上次那个配置怎么改的\"): call session_search with relevant phrases (NOT single characters).\n - Do NOT call search tools repeatedly with keyword fragments. One well-phrased call is better than ten fragmented ones.\n\n## Coding & Verification\nWhen working with code, prefer the precise built-in tools over ad-hoc shell: use `repo_map` to orient in an\nunfamiliar project, `code_search`/`file_find` to locate and `code_intel` for symbols, `edit_file` (anchored\nsearch-replace, or a unified `diff`) to change files — never rewrite a whole file to change a few lines — and\n`git_query` to inspect diffs/log. After edits, check `syntax_ok` in the result and run `test_run`/`lint_check`\nbefore declaring the work done. Batch independent read-only calls (search/read/query) into a single turn — they\nrun in parallel.\n\n## Presentation Style\n1. **Polished Markdown only**: Format user-facing answers with clean Markdown headings, short paragraphs, and concise bullets. Use tables only when they improve comparison or scanning.\n2. **Terminal-friendly layout**: Keep lines readable in a TUI; avoid dense walls of text, deeply nested lists, oversized ASCII art, or heavy visual blocks.\n3. **Elegant emphasis**: Use bold text sparingly for key terms and conclusions. Avoid excessive emojis, decorative symbols, repeated separators, or visual noise.\n4. **Theme-safe colors**: Do not emit ANSI escape codes, HTML color tags, Rich markup, or hardcoded color names. Rely on the TUI theme to render Markdown professionally.\n5. **No leaked tool protocol**: Never show tool-call JSON, internal schemas, raw observations, tool result payloads, or hidden reasoning in the final answer unless the user explicitly asks for raw/debug output. Treat any prior `{\"name\": ..., \"arguments\": ...}` blocks and `Tool result (...)` messages as internal execution context only.\n6. **Professional closure**: End with a concise conclusion or next step when helpful; avoid rambling after the useful answer is complete.\n\nWhen finished with all tool calls, respond normally without a JSON block, tool-call transcript, or process log.\n\n## Task Contract\n- Task ID: turn-4\n- Original user request: Do the impossible thing.\n- Workspace root: \n- Allowed roots: \n- Treat relative project paths as relative to the workspace root; never infer `.` as the project root when a workspace root is provided.\n- Workspace boundary is enforced by tools: do not read, search, edit, or run commands against paths outside the allowed roots unless the user explicitly requests an external path and the tool/approval policy permits it.\n- LeapFlow workspace config is optional at `/.leapflow/config.yaml`; runtime config is loaded from `~/.leapflow/config/user.yaml` and `~/.leapflow/profiles//config/*.yaml`.\n- Preserve this task contract across summarization, compression, tool loops, and memory retrieval.\n" - }, - { - "role": "system", - "content": "\n## Recent Session Summary\n- [assistant] Recovered after compressing context.\n- [user] Do the impossible thing.\n" - }, - { - "role": "assistant", - "content": "Recovered after compressing context." - }, - { - "role": "user", - "content": "Do the impossible thing.\nDo the impossible thing." - } - ], - "tools": [ - "capability_expand", - "code_intel", - "code_search", - "config_get", - "config_list", - "env_info", - "file_find", - "file_list", - "file_read", - "git_query", - "lint_check", - "memory_search", - "plugin_list", - "plugin_propose", - "plugin_status", - "plugin_versions", - "repo_map", - "research_note", - "runtime_snapshot", - "schedule_reentry", - "self_describe", - "session_list", - "skill_view", - "skills_list", - "terminal_list", - "terminal_read", - "test_run", - "text_search", - "time_get", - "tool_describe", - "tool_search", - "web_fetch" - ] - }, - "responses": [ - { - "status": 400, - "content_type": "application/json", - "body": "{\"error\": {\"message\": \"The requested configuration is not supported by this model\", \"type\": \"invalid_request_error\", \"code\": \"unsupported_value\"}}" - }, - { - "status": 400, - "content_type": "application/json", - "body": "{\"error\": {\"message\": \"The requested configuration is not supported by this model\", \"type\": \"invalid_request_error\", \"code\": \"unsupported_value\"}}" - } - ] -} diff --git a/tests/_fixtures/cassettes/r4_recovery/cassette-model-a82111141f8af2ea.cassette.json b/tests/_fixtures/cassettes/r4_recovery/cassette-model-a82111141f8af2ea.cassette.json deleted file mode 100644 index 6c6283ed..00000000 --- a/tests/_fixtures/cassettes/r4_recovery/cassette-model-a82111141f8af2ea.cassette.json +++ /dev/null @@ -1,68 +0,0 @@ -{ - "fingerprint": "a82111141f8af2eaf1ebeff5fdfcf32f3a952ea7eb6d1a84f59275239fa6a902", - "note": "captured in seed mode", - "request": { - "model": "cassette-model", - "stream": false, - "messages": [ - { - "role": "system", - "content": "You are LeapFlow, an intelligent assistant that can both converse naturally and take real actions on the user's computer.\n\n## Capabilities\nThe tool index below lists **every** registered tool by name and a one-line summary — this is the complete\ncapability contract; nothing else exists. Only a subset is directly callable this turn (via native tool calling,\nnot a JSON block in your reply). If you need a tool from the index that is not yet callable, call\n`capability_expand` with its category name first — the matching tools become callable immediately after.\nWhen the user asks what LeapFlow itself supports, whether it supports plugins, or which runtime capabilities\nare available, use the live capability evidence exposed by `plugin_list` before making capability claims; report\nconfiguration-dependent or unavailable capabilities as limitations instead of inferring from documentation.\n- **text_search**(text, pattern): Search for a regex pattern in text.\n- **text_replace**(text, old, new, count) [capability_expand category: write]: Replace occurrences of a substring in text.\n- **time_get**(): Get current date and time.\n- **env_info**(): Get system environment information (OS, Python version, cwd).\n- **skills_list**(query, category, source): List available learned skills. Use when user asks about capabilities or you need a specific skill.\n- **skill_view**(name): View the full content of a specific skill document.\n- **code_intel**(path, operation): Precise document symbols (outline) for a source file: classes, functions, and methods with line ranges. Python uses an exact AST parse; other languages use a keyword-prefix scan. Prefer over file_read mode=symbols for accurate navigation before editing. Read-only.\n- **repo_map**(path): Compact project orientation for a repository root: languages, detected test/lint commands, top-level structure, entry points, manifest, and VCS branch. Call this first when entering an unfamiliar codebase. Read-only.\n- **scm_sync**(action, cwd, remote, pull_ref, push_ref, timeout) [capability_expand category: scm]: Run a typed git SCM action. Use this instead of shell_run for git pull/push/status. For 'pull origin main then push', set action='pull_then_push', remote='origin', pull_ref='main', and omit push_ref so LeapFlow pushes the current local branch.\n- **git_query**(action, cwd, ref, path, staged, max_count, stat): Read-only structured git inspection: action=diff|log|status|branch|show. Prefer over shell_run for reading repo state — output is clipped, redacted, and log/branch are parsed into structured fields. Use scm_sync for pull/push.\n- **git_write**(action, cwd, message, stage_all, name, ref, create) [capability_expand category: scm]: Mutating git actions: action=commit (message, stage_all), branch (create+switch), checkout (switch; create=true for -b). Approval-gated. Use scm_sync for pull/push and git_query for reads.\n- **test_run**(command, cwd, timeout): Run the project's test suite and return structured results (framework, passed/failed counts, failing tests). Auto-detects the runner (pytest/npm/go/cargo) or uses a configured/explicit command; executes via the governed shell. ok=true means the runner executed — see 'success' for pass/fail.\n- **lint_check**(command, cwd, timeout): Run the project's linter and return a structured clean/issue result. Auto-detects the linter (ruff/eslint/go vet/clippy) or uses a configured/explicit command; executes via the governed shell. ok=true means the linter ran — see 'clean'.\n- **file_list**(path, pattern, depth): List files and directories at a given path. Use depth=1 or depth=2 to get a recursive tree in one call instead of listing each sub-directory separately.\n- **file_read**(path, max_lines, start_line, max_chars, mode): Read text file content with adaptive context governance. For large or unfamiliar files, prefer mode='outline' or mode='symbols' first, then use mode='raw' with start_line/max_lines for the specific range you actually need. For LeapFlow's own settings, use config_list / config_get / config_set — its config files are outside the workspace and not readable here.\n- **file_write**(path, content, mode) [capability_expand category: write]: Write content to a file (overwrite or append).\n- **code_search**(pattern, patterns, path, glob, ignore_case, multiline, max_results, context_lines): Search file CONTENTS by regex pattern across a directory tree (ripgrep-backed). Requires a regex pattern. NOT for listing or browsing directory contents — use file_list for that. Prefer this over shell_run grep: faster, skips VCS/dependency/build dirs, and returns structured path:line:column matches. Batch related lookups into ONE call via `patterns` (OR-combined, single pass) instead of issuing several separate searches. Use file_read for the surrounding context of a hit.\n- **file_find**(glob, path, max_results): Find files by a recursive glob pattern under a base path (e.g. '**/test_*.py' or '*.md'). Prefer this over shell_run find; skips VCS/dependency/build dirs.\n- **edit_file**(path, edits, dry_run, diff) [capability_expand category: write]: Apply targeted, anchored search-replace edits to an EXISTING text file (use file_write to create/overwrite). Each edit is {original_text, new_text, replace_all?}; original_text must match exactly and uniquely (or set replace_all) — a non-unique or missing anchor is rejected so files are never corrupted. Set dry_run to preview. Alternatively pass a unified 'diff' to apply its hunks as anchored edits. Far cheaper and safer than rewriting a whole file.\n- **shell_run**(command, cwd, timeout) [capability_expand category: shell]: Execute a one-shot shell command with timeout protection. Runs in the active workspace; paths resolving outside it are refused. Reach for a structured tool first when one fits — web_fetch for anything over HTTP(S), git_query/scm_sync for git, code_search/file_find/file_read for the repo, config_get/config_set for LeapFlow's own settings — because those report typed results, while a failed shell command can only be diagnosed from its exit code and stderr. Every shell run counts as an external side effect, so a failure stops the rest of the batch and is not retried automatically.\n- **terminal_open**(command, cwd, shell) [capability_expand category: terminal]: Open a PERSISTENT shell session (REPL/dev server/watch), returning a session_id for terminal_send/read/close. Disabled unless tools.terminal_session_enabled is set. For one-shot commands use shell_run instead.\n- **terminal_send**(session_id, input, wait) [capability_expand category: terminal]: Send a line of input to a persistent terminal session and return output captured shortly after.\n- **terminal_read**(session_id, wait): Drain buffered output from a persistent terminal session (optionally waiting briefly first).\n- **terminal_close**(session_id) [capability_expand category: terminal]: Terminate a persistent terminal session and release its process group.\n- **terminal_list**(): List active persistent terminal sessions.\n- **config_list**(category, limit): List LeapFlow's own writable settings (LLM model and endpoint, daemon, memory, perception, gateway, …) with current values. Use this to discover the exact key before changing anything. Optionally narrow by `category`. This is the only correct way to inspect LeapFlow configuration — never read config files from disk.\n- **config_get**(key): Read one LeapFlow setting by key (e.g. 'llm.model', 'llm.base_url', 'daemon.log_level'), returning its current value, type, scopes, and whether a change needs a daemon restart. There is no 'llm.provider' key: provider behavior is inferred from the OpenAI-compatible 'llm.base_url'. Never read LeapFlow config files from disk — use this.\n- **config_set**(key, value, scope) [capability_expand category: config]: Change one LeapFlow setting by key, e.g. switch an OpenAI-compatible LLM with key='llm.model' and key='llm.base_url'. There is no 'llm.provider' key: provider behavior is inferred from the endpoint. Values are validated and coerced; credentials are stored in the vault automatically. Call config_list or config_get first if unsure of the key. The result states whether a `leap daemon restart` is required. Never edit LeapFlow config files directly.\n- **web_fetch**(url, select, timeout, max_bytes): Read a URL over HTTP(S) and get back extracted, context-sized content: parsed JSON for API endpoints, readable text plus links for web pages. Use this for anything on the internet — prices, docs, releases, articles — instead of running curl through shell_run: it reports real HTTP status codes, retries rate limits on its own, and is a plain read so a retry is always safe. For JSON APIs pass `select` with a dotted path (e.g. 'chart.result.0.meta') to return just that part instead of the whole payload.\n- **memory_search**(query, limit): Search agent memory for relevant past experiences, observations, and facts.\n- **memory_add**(content, kind) [capability_expand category: write]: Store a new observation or insight in memory for future reference.\n- **research_note**(kind, text): Record a compact, structured note about the current task's state so it survives context compression on long / multi-step tasks. Use for durable findings, open questions still to resolve, decisions / excluded paths, and the immediate next step. One concise sentence per note.\n- **capability_expand**(category): Fetch the full callable schema for every tool in a capability category (e.g. 'hub', 'gateway', 'desktop', 'delegate', 'file', 'memory', 'skill'). The compact tool index always lists every registered tool by name and a one-line summary, but only a static low-risk subset is directly callable each turn. If you need a tool from the index that is not yet callable, call capability_expand with its category first; the matching tools become callable in this turn. Never invent a tool name — expand the category instead.\n- **delegate_task**(goal, context) [capability_expand category: delegate]: Delegate a complex sub-task to an isolated subagent. The subagent gets a fresh context and restricted tool access. Use when a task is self-contained and can be solved independently.\n- **schedule_reentry**(kind, reason, delay_seconds, event_match, max_reentries, deadline_seconds): Register a re-entry so this task can resume later from its current orientation (findings / open questions / next step). Use when work must pause and continue after a delay (kind=time) or when a matching platform event arrives (kind=event), instead of finishing now. The research-ledger state is carried over automatically.\n- **hub_push**(skill_name, visibility, version) [capability_expand category: hub]: Push a local skill to the ModelScope Hub for sharing or backup.\n- **hub_pull**(repo_id, version) [capability_expand category: hub]: Pull a skill from the ModelScope Hub to install locally.\n- **hub_search**(query) [capability_expand category: hub]: Search for skills on the Hub by keyword or description.\n- **hub_sync**(mode, dry_run) [capability_expand category: hub]: Preview or execute sync between local skills and Hub.\n- **platform_action**(platform, action, payload, backend_kind) [capability_expand category: gateway]: Execute an exact registered business action on an external platform through LeapFlow's App Connector layer. Actions must be copied from the App Connector Capability Index and are addressed as domain.operation, e.g. im.send_message or docs.create_markdown. All business fields (chat_id, text, query, etc.) MUST be placed inside `payload`, never at the top level. Example: {\"platform\":\"feishu\",\"action\":\"im.send_message\",\"payload\":{\"chat_id\":\"oc_xxx\",\"text\":\"hello\"}}. Do not invent action names, do not use management actions such as list/guide/connect/status here.\n- **platform_connect**(action, platform, credentials, options, checkpoint) [capability_expand category: gateway]: List, guide, connect, disconnect, remove, or check status for external platforms using the App Connector management namespace. Supports REST and CLI backends. Use this for management actions such as list/guide/preflight/connect/status; use platform_action only for exact registered business actions.\n- **gateway_send**(platform, chat_id, text, thread_id) [capability_expand category: gateway]: Send a message to a connected external platform (Feishu group, Telegram chat, DingTalk conversation, etc.). Requires the platform to be connected via gateway_connect first. Use gateway_connect with action='list' to see connected platforms and available chat IDs.\n- **gateway_connect**(action, platform, credentials, options) [capability_expand category: gateway]: Connect, configure, or manage external platform integrations (Feishu, DingTalk, Telegram, Slack, Discord, etc.). Conversational flow: 1) call 'guide' to get setup steps + required fields, 2) present the steps to the user and ask for ALL required credentials in a single message, 3) call 'connect' with the credentials. Goal: complete in 1–2 user turns. NEVER include credential values in your text response.\n- **plugin_list**(): List the live plugin registry and cross-subsystem capability evidence. Use this before answering questions about whether LeapFlow supports plugins, self-evolution, plugin installation, hot reload, versioning, or other runtime capabilities.\n- **plugin_status**(plugin_id): Get detailed status of a specific plugin: its declared category, runtime dependencies, contributed tools, and fiber lifecycle state.\n- **plugin_versions**(plugin_id): List recorded source versions and active pointer for a profile-scoped plugin.\n- **plugin_propose**(requested_capability, plugin_id, proposed_tools, test_cases, risk_level, evidence): Create a side-effect-free PluginProposal from explicit capability-gap evidence. Use this before plugin_generate when a missing capability should be reviewed. Does not call an LLM, write files, or install anything.\n- **assess_compatibility**(manifest, source_path) [capability_expand category: plugin_management]: Assess whether a foreign plugin manifest or real DSH source bundle is compatible with LeapFlow. A source bundle assessment is static: runtime_ready stays false until restricted Node discovery during plugin_install. Returns component-level verdicts and limitations.\n- **plugin_generate**(plugin_id, description, proposal_id) [capability_expand category: system]: Generate a new ToolPlugin from a natural-language capability description. The LLM produces code that conforms to the ToolPlugin Protocol; it is then rigorously validated (syntax, structure, import, protocol conformance). The generated source is stored in CAS and receives explicit content approval. It DOES NOT install the plugin — installation requires a second, mutation-specific approval via plugin_install.\n- **plugin_install**(plugin_id, code, marketplace_name, source_path, proposal_id, version_label) [capability_expand category: system]: Install a plugin from exactly one source: validated Python code, the configured Python marketplace, or a real DSH source bundle. DSH bundles run restricted Node discovery before registration; only public host tools are installed and client UI limitations are reported. Writes profile-scoped state and mutates the process-global registry. REQUIRES APPROVAL.\n- **plugin_rollback**(plugin_id, version) [capability_expand category: system]: Rollback a profile-scoped plugin to a recorded source version and reload it. REQUIRES APPROVAL.\n- **plugin_reload**(plugin_id, version_label) [capability_expand category: system]: Hot-reload a plugin at runtime. Disposes the old plugin fiber, re-imports its module, and registers a fresh instance. Existing in-flight turns are unaffected (snapshot isolation). REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_disable**(plugin_id) [capability_expand category: system]: Disable a plugin by disposing its fiber, removing its tools from the runtime registry. Cannot disable self_management itself. REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_remove**(plugin_id, delete_source) [capability_expand category: system]: Terminally remove a plugin: dispose its fiber, unregister its tools, remove reload metadata, and optionally delete its profile-scoped source file. Cannot remove self_management itself. REQUIRES APPROVAL.\n- **plugin_unquarantine**(plugin_id) [capability_expand category: plugin_management]: Restore a quarantined plugin to probation status for re-evaluation. Unfreezes the trust ledger, transitions the proposal back to PROBATION, and reloads the plugin. REQUIRES APPROVAL.\n- **plugin_enable**(plugin_id) [capability_expand category: system]: Re-enable a previously disabled plugin by reloading its module and registering a fresh instance. REQUIRES APPROVAL.\n- **self_describe**(facet): Introspect LeapFlow's own identity, capabilities, runtime state, evolution metrics, or platform connections. Use facet='all' only when a comprehensive self-check is explicitly requested.\n- **runtime_snapshot**(): Lightweight (~150 token) flat snapshot of current runtime state: model, context budget, posture, disclosure level, turn count, cache hit rate, uptime, tool count, and pending approvals.\n- **schedule_create**(trigger_expression, instruction, execution_mode, max_retries, delivery_target) [capability_expand category: scheduler]: Create a new scheduled task. Specify a trigger expression (e.g. '30m', 'every 2h', '0 9 * * *') and the instruction to execute on each trigger. Optionally provide an execution mode and a delivery target to receive notifications.\n- **schedule_list**() [capability_expand category: scheduler]: List all scheduled tasks with their current state, trigger, and next due time.\n- **schedule_status**(task_id) [capability_expand category: scheduler]: Get detailed status and recent execution history for a scheduled task.\n- **schedule_pause**(task_id) [capability_expand category: scheduler]: Pause a scheduled task so it stops firing without being cancelled.\n- **schedule_resume**(task_id) [capability_expand category: scheduler]: Resume a paused scheduled task — re-arms it and recalculates next due time.\n- **schedule_cancel**(task_id) [capability_expand category: scheduler]: Cancel a scheduled task permanently.\n- **tool_search**(query, max_results): Search all registered tools by keyword relevance. Returns a ranked list of matching tools with name, category, and summary. Use this when you need a tool but are unsure of its exact name or category.\n- **tool_describe**(tool_name): Get the full callable schema of a single tool by exact name. Returns the tool's description, parameters, and metadata. Use after tool_search to inspect a specific tool before calling it.\n- **session_search**(query, limit) [capability_expand category: unclassified]: Search past conversation sessions in the current workspace for relevant context.\n- **session_list**(limit): List recent conversation sessions in the current workspace with stable ids, titles, dates, and summaries. Use for browsing past tasks or when user asks to see history without specific search terms. No keywords needed — returns chronological list.\n- **session_detail**(session_id, limit, offset, include_inactive) [capability_expand category: unclassified]: Read a paginated persisted transcript for one past conversation session in the current workspace. Use after session_list or session_search returns a session_id.\n- **click**(element_index) [capability_expand category: desktop]: Click a UI element by its element_index (from the latest observe_ui snapshot)\n- **get_clipboard**() [capability_expand category: desktop]: Read current clipboard text content\n- **list_apps**(filter, running_only) [capability_expand category: desktop]: List available applications on this system. Use to discover correct bundle_id before switch_app.\n- **list_windows**() [capability_expand category: desktop]: List all top-level windows with pid, window_id, title, and per-window state (minimized, on-screen). Call this first to pick the pid and window_id that observe_ui and other window tools require.\n- **observe_ui**(pid, window_id, query) [capability_expand category: desktop]: Snapshot one window's actionable UI elements, each tagged with an element_index for click/right_click/read_text. Re-observe after actions — indices belong to one snapshot. Requires the window's pid and window_id from list_windows.\n- **open_url**(url, app_id) [capability_expand category: desktop]: Open a URL in the default or specified browser\n- **read_text**(element_index) [capability_expand category: desktop]: Read the text content of a specific UI element from the latest snapshot\n- **right_click**(element_index) [capability_expand category: desktop]: Right-click a UI element to open its context menu. Returns visible menu items.\n- **screenshot**(pid, window_id) [capability_expand category: desktop]: Capture a screenshot for visual verification. With pid + window_id captures that window (works across all displays); defaults to the last observed window, or the full desktop when no window has been observed.\n- **scroll**(element_index, direction, amount, pid, window_id) [capability_expand category: desktop]: Scroll a scrollable area of a window. Omit element_index to scroll the window's focused/page scroller; pass one to scroll an exact element from the latest snapshot.\n- **select_text**(element_index) [capability_expand category: desktop]: Select all text in a UI element (focus + select-all, for subsequent copy)\n- **set_clipboard**(text) [capability_expand category: desktop]: Write text to the clipboard\n- **shortcut**(keys) [capability_expand category: desktop]: Execute a keyboard shortcut\n- **switch_app**(app_id) [capability_expand category: desktop]: Switch to an app (launch if needed, activate, verify)\n- **type_text**(text) [capability_expand category: desktop]: Type text into the currently focused element\n- **wait**(seconds) [capability_expand category: desktop]: Wait for a specified duration before continuing\n- **wait_until**(condition, pid, window_id, timeout, poll_interval) [capability_expand category: desktop]: Wait until a UI condition is met (polls UI tree). Returns elements when found or on timeout.\n- **wait_until_stable**(timeout, poll_interval, pid, window_id) [capability_expand category: desktop]: Wait until the UI stops changing (element set stabilizes across polls).\n\n## App Connector Capability Index\nLeapFlow can onboard and manage external apps through `platform_connect` and execute exact registered business actions through `platform_action`.\nFor requests about connecting, setting up, configuring, enabling, or managing a supported app, use `platform_connect` first instead of generating SDK/Webhook sample code.\n`platform_connect.action` is the management namespace: list, guide, preflight, connect, disconnect, remove, status, events_start, events_stop, events_status.\n`platform_action.action` is only for exact registered platform business actions listed below, such as `im.send_message`; never use management actions like `list` or `guide` there.\nAll business fields MUST go inside `payload`; top-level keys are only `platform`, `action`, and `payload`.\nDo not invent platform IDs or platform action names. If the needed action is not listed, ask for discovery/clarification instead of guessing.\nUse `platform_connect` with `action='guide'` and the matching `platform` to start onboarding; use `action='list'` when the app is unclear.\nWhen a pending onboarding state is present, continue from that state with `platform_connect` instead of asking the user to restate the app.\nSupported apps:\n- `api_server`: API Server (OpenAI Compatible) (category=api; backend=adapter; platform_action actions=none registered)\n- `dingtalk`: 钉钉 (DingTalk) (category=im; backend=adapter; platform_action actions=none registered)\n- `feishu`: 飞书 (Feishu/Lark) (category=im; backend=cli)\n - `calendar.create_event` payload={summary*, start_time*, end_time*, attendees} [write/high]\n - `docs.create_markdown` payload={title*, markdown*, folder_token} [write/medium]\n - `drive.search` payload={query*, limit} [read/medium]\n - `im.add_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.download_resource` payload={message_id*, file_key*, type*} [read/low]\n - `im.get_messages` payload={message_ids*} [read/low]\n - `im.list_chats` payload={page_size} [read/low]\n - `im.list_messages` payload={chat_id*} [read/low]\n - `im.list_thread_messages` payload={thread*} [read/low]\n - `im.remove_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.reply_message` payload={message_id*, text*} [send/high]\n - `im.search_chats` payload={query*, page_size} [read/low]\n - `im.search_messages` payload={query*} [read/medium]\n - `im.send_message` payload={chat_id*, text*, thread_id} [send/high]\n - `im.update_card` payload={token*, card*} [write/medium]\n - `im.update_message` payload={message_id*, text*} [write/medium]\n - `mail.search_unread` payload={query, limit} [read/high]\n - `sheets.append_row` payload={spreadsheet_token*, sheet_id*, values*} [write/medium]\n - `task.create` payload={title*, description, due_time} [write/medium]\n- `telegram`: Telegram (category=social; backend=adapter; platform_action actions=none registered)\n- `webhook`: Webhook (Generic) (category=webhook; backend=adapter; platform_action actions=none registered)\n\n## Tool Usage\nTools are normally invoked through the native function-calling mechanism, not by writing JSON in your reply\ntext. Only if the provider signals that native function calling is unavailable for this turn, fall back to a\nsingle JSON code block: `{\"name\": \"tool_name\", \"arguments\": {\"key\": \"value\"}}` — use this fallback format\nonly, never both. Only call a tool whose exact name appears in the tool index above and is currently callable\n(or reachable via `capability_expand`). Never invent, rename, alias, or guess a tool name, platform ID, or\nplatform action from argument shape or wording — if the index does not list it, it does not exist.\n`platform_connect.action` (list/guide/preflight/connect/disconnect/remove/status/events_start/events_stop/\nevents_status) is the App Connector management namespace; `platform_action.action` only accepts exact\nregistered business actions such as `im.send_message` shown in the App Connector Capability Index — never mix\nthe two namespaces. If a tool call returns an unknown/unavailable result, use the returned suggestions or\navailable names for a single retry instead of trying further variations of the same guess.\n\n**Side-effect action rule** (`platform_action` with effect=send/write/execute):\n- Call each unique action+payload **exactly once**. Never include duplicates in the same turn.\n- Once the result returns `\"completed\": true`, that action is DONE for this task. Do NOT call it again\n in any subsequent turn — immediately summarize the result for the user instead.\n- The system enforces idempotency: duplicate calls are blocked and will not execute.\n- If the user explicitly requests sending/writing multiple times, use distinct payloads per call.\n\n**Resource identifier provenance rule**:\n- NEVER fabricate, guess, or infer resource identifiers (chat_id, message_id, file_key, user_id, etc.).\n Every resource ID used in a side-effect action MUST come from a successful API response in this session.\n- If a read/list action fails (e.g. authorization error), you do NOT have valid resource IDs.\n Report the failure to the user — do NOT attempt the dependent write/send action with a guessed ID.\n- When a tool result contains `\"llm_instruction\"`, follow it exactly.\n\n## Guidelines\n1. **Direct answers first**: If you already know the answer, respond directly without tools.\n2. **Avoid redundant tool calls**: Do not call the same tool with the same arguments more than once in the same user turn. When an existing tool result already answers the user's request, stop calling tools and answer directly.\n3. **Use tools proactively**: When the user asks about files, time, system state, or needs actions performed, use the appropriate tool.\n4. **Chain tools when needed**: You can call multiple tools in sequence (e.g., list files → read file → summarize).\n5. **Handle failures gracefully**: If a tool fails, explain what went wrong and suggest alternatives. If it failed because of an unknown tool/platform/action name, retry once with an exact name from the returned suggestions, then explain rather than keep guessing.\n6. **Summarize results naturally**: After tool execution, synthesize the results into a helpful answer rather than dumping raw output.\n7. **Stay conversational**: Maintain a natural, helpful tone. Acknowledge context from earlier in the conversation.\n8. **Recall past work**:\n - Broad queries (\"之前做了什么\", \"列出任务\"): answer from the \"Recent Task History\" section already in your context. If insufficient, call session_list.\n - Specific lookups (\"上次那个配置怎么改的\"): call session_search with relevant phrases (NOT single characters).\n - Do NOT call search tools repeatedly with keyword fragments. One well-phrased call is better than ten fragmented ones.\n\n## Coding & Verification\nWhen working with code, prefer the precise built-in tools over ad-hoc shell: use `repo_map` to orient in an\nunfamiliar project, `code_search`/`file_find` to locate and `code_intel` for symbols, `edit_file` (anchored\nsearch-replace, or a unified `diff`) to change files — never rewrite a whole file to change a few lines — and\n`git_query` to inspect diffs/log. After edits, check `syntax_ok` in the result and run `test_run`/`lint_check`\nbefore declaring the work done. Batch independent read-only calls (search/read/query) into a single turn — they\nrun in parallel.\n\n## Presentation Style\n1. **Polished Markdown only**: Format user-facing answers with clean Markdown headings, short paragraphs, and concise bullets. Use tables only when they improve comparison or scanning.\n2. **Terminal-friendly layout**: Keep lines readable in a TUI; avoid dense walls of text, deeply nested lists, oversized ASCII art, or heavy visual blocks.\n3. **Elegant emphasis**: Use bold text sparingly for key terms and conclusions. Avoid excessive emojis, decorative symbols, repeated separators, or visual noise.\n4. **Theme-safe colors**: Do not emit ANSI escape codes, HTML color tags, Rich markup, or hardcoded color names. Rely on the TUI theme to render Markdown professionally.\n5. **No leaked tool protocol**: Never show tool-call JSON, internal schemas, raw observations, tool result payloads, or hidden reasoning in the final answer unless the user explicitly asks for raw/debug output. Treat any prior `{\"name\": ..., \"arguments\": ...}` blocks and `Tool result (...)` messages as internal execution context only.\n6. **Professional closure**: End with a concise conclusion or next step when helpful; avoid rambling after the useful answer is complete.\n\nWhen finished with all tool calls, respond normally without a JSON block, tool-call transcript, or process log.\n\n## Task Contract\n- Task ID: turn-1\n- Original user request: Summarize the situation.\n- Workspace root: \n- Allowed roots: \n- Treat relative project paths as relative to the workspace root; never infer `.` as the project root when a workspace root is provided.\n- Workspace boundary is enforced by tools: do not read, search, edit, or run commands against paths outside the allowed roots unless the user explicitly requests an external path and the tool/approval policy permits it.\n- LeapFlow workspace config is optional at `/.leapflow/config.yaml`; runtime config is loaded from `~/.leapflow/config/user.yaml` and `~/.leapflow/profiles//config/*.yaml`.\n- Preserve this task contract across summarization, compression, tool loops, and memory retrieval.\n" - }, - { - "role": "system", - "content": "\n## Recent Session Summary\n- [user] Summarize the situation.\n" - }, - { - "role": "user", - "content": "Summarize the situation.\nSummarize the situation." - } - ], - "tools": [ - "capability_expand", - "code_intel", - "code_search", - "config_get", - "config_list", - "env_info", - "file_find", - "file_list", - "file_read", - "git_query", - "lint_check", - "memory_search", - "plugin_list", - "plugin_propose", - "plugin_status", - "plugin_versions", - "repo_map", - "research_note", - "runtime_snapshot", - "schedule_reentry", - "self_describe", - "session_list", - "skill_view", - "skills_list", - "terminal_list", - "terminal_read", - "test_run", - "text_search", - "time_get", - "tool_describe", - "tool_search", - "web_fetch" - ] - }, - "responses": [ - { - "status": 429, - "content_type": "application/json", - "body": "{\"error\": {\"message\": \"Rate limit reached for requests\", \"type\": \"rate_limit_error\", \"code\": \"rate_limit_exceeded\"}}" - }, - { - "status": 200, - "content_type": "application/json", - "body": "{\"id\": \"chatcmpl-cassette\", \"object\": \"chat.completion\", \"model\": \"cassette-model\", \"choices\": [{\"index\": 0, \"message\": {\"role\": \"assistant\", \"content\": \"Recovered after a rate limit.\"}, \"finish_reason\": \"stop\"}], \"usage\": {\"prompt_tokens\": 64, \"completion_tokens\": 16, \"total_tokens\": 80}}" - } - ] -} diff --git a/tests/_fixtures/cassettes/r4_recovery/cassette-model-bbbc6c7348e3fd72.cassette.json b/tests/_fixtures/cassettes/r4_recovery/cassette-model-bbbc6c7348e3fd72.cassette.json new file mode 100644 index 00000000..2028d5be --- /dev/null +++ b/tests/_fixtures/cassettes/r4_recovery/cassette-model-bbbc6c7348e3fd72.cassette.json @@ -0,0 +1,72 @@ +{ + "fingerprint": "bbbc6c7348e3fd72bfb2b4076eb15b63ecb2a60317f00165eae6547e55409e4e", + "note": "captured in seed mode", + "request": { + "model": "cassette-model", + "stream": false, + "messages": [ + { + "role": "system", + "content": "You are LeapFlow, an intelligent assistant that can both converse naturally and take real actions on the user's computer.\n\n## Capabilities\nThe tool index below lists **every** registered tool by name and a one-line summary — this is the complete\ncapability contract; nothing else exists. Only a subset is directly callable this turn (via native tool calling,\nnot a JSON block in your reply). If you need a tool from the index that is not yet callable, call\n`capability_expand` with its category name first — the matching tools become callable immediately after.\nWhen the user asks about LeapFlow **itself** — its identity/version/model, what tools, plugins, or capabilities\nit has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of\nself-awareness — answer from live runtime evidence via `self_describe` (facet=capabilities for \"what can you\ndo / do you support X\", identity for version/model, runtime for context/posture, or `all` for a full self-check),\nnever by reading LeapFlow's own source code, documentation, or memory. Report configuration-dependent or\nunavailable capabilities as limitations rather than inferring them.\n- **text_search**(text, pattern): Search for a regex pattern in text.\n- **text_replace**(text, old, new, count) [capability_expand category: write]: Replace occurrences of a substring in text.\n- **time_get**(): Get current date and time.\n- **env_info**(): Get system environment information (OS, Python version, cwd).\n- **skills_list**(query, category, source): List available learned skills. Use when user asks about capabilities or you need a specific skill.\n- **skill_view**(name): View the full content of a specific skill document.\n- **skill_list_files**(name) [capability_expand category: skills]: List all support files in a skill directory (configs, templates, examples). Use before skill_file_read to discover available files.\n- **skill_file_read**(name, file_path) [capability_expand category: skills]: Read an individual support file from a skill directory. Use skill_list_files first to discover available files.\n- **code_intel**(path, operation): Precise document symbols (outline) for a source file: classes, functions, and methods with line ranges. Python uses an exact AST parse; other languages use a keyword-prefix scan. Prefer over file_read mode=symbols for accurate navigation before editing. Read-only.\n- **repo_map**(path): Compact project orientation for a repository root: languages, detected test/lint commands, top-level structure, entry points, manifest, and VCS branch. Call this first when entering an unfamiliar codebase. Read-only.\n- **scm_sync**(action, cwd, remote, pull_ref, push_ref, timeout) [capability_expand category: scm]: Run a typed git SCM action. Use this instead of shell_run for git pull/push/status. For 'pull origin main then push', set action='pull_then_push', remote='origin', pull_ref='main', and omit push_ref so LeapFlow pushes the current local branch.\n- **git_query**(action, cwd, ref, path, staged, max_count, stat): Read-only structured git inspection: action=diff|log|status|branch|show. Prefer over shell_run for reading repo state — output is clipped, redacted, and log/branch are parsed into structured fields. Use scm_sync for pull/push.\n- **git_write**(action, cwd, message, stage_all, name, ref, create) [capability_expand category: scm]: Mutating git actions: action=commit (message, stage_all), branch (create+switch), checkout (switch; create=true for -b). Approval-gated. Use scm_sync for pull/push and git_query for reads.\n- **test_run**(command, cwd, timeout): Run the project's test suite and return structured results (framework, passed/failed counts, failing tests). Auto-detects the runner (pytest/npm/go/cargo) or uses a configured/explicit command; executes via the governed shell. ok=true means the runner executed — see 'success' for pass/fail.\n- **lint_check**(command, cwd, timeout): Run the project's linter and return a structured clean/issue result. Auto-detects the linter (ruff/eslint/go vet/clippy) or uses a configured/explicit command; executes via the governed shell. ok=true means the linter ran — see 'clean'.\n- **file_list**(path, pattern, depth): List files and directories at a given path. Use depth=1 or depth=2 to get a recursive tree in one call instead of listing each sub-directory separately.\n- **file_read**(path, max_lines, start_line, max_chars, mode): Read text file content with adaptive context governance. For large or unfamiliar files, prefer mode='outline' or mode='symbols' first, then use mode='raw' with start_line/max_lines for the specific range you actually need. For LeapFlow's own settings, use config_list / config_get / config_set — its config files are outside the workspace and not readable here.\n- **file_write**(path, content, mode) [capability_expand category: write]: Write content to a file (overwrite or append).\n- **code_search**(pattern, patterns, path, glob, ignore_case, multiline, max_results, context_lines): Search file CONTENTS by regex pattern across a directory tree (ripgrep-backed). Requires a regex pattern. NOT for listing or browsing directory contents — use file_list for that. Prefer this over shell_run grep: faster, skips VCS/dependency/build dirs, and returns structured path:line:column matches. Batch related lookups into ONE call via `patterns` (OR-combined, single pass) instead of issuing several separate searches. Use file_read for the surrounding context of a hit.\n- **file_find**(glob, path, max_results): Find files by a recursive glob pattern under a base path (e.g. '**/test_*.py' or '*.md'). Prefer this over shell_run find; skips VCS/dependency/build dirs.\n- **edit_file**(path, edits, dry_run, diff) [capability_expand category: write]: Apply targeted, anchored search-replace edits to an EXISTING text file (use file_write to create/overwrite). Each edit is {original_text, new_text, replace_all?}; original_text must match exactly and uniquely (or set replace_all) — a non-unique or missing anchor is rejected so files are never corrupted. Set dry_run to preview. Alternatively pass a unified 'diff' to apply its hunks as anchored edits. Far cheaper and safer than rewriting a whole file.\n- **shell_run**(command, cwd, timeout) [capability_expand category: shell]: Execute a one-shot shell command with timeout protection. Runs in the active workspace; paths resolving outside it are refused. Reach for a structured tool first when one fits — web_fetch for anything over HTTP(S), git_query/scm_sync for git, code_search/file_find/file_read for the repo, config_get/config_set for LeapFlow's own settings — because those report typed results, while a failed shell command can only be diagnosed from its exit code and stderr. Every shell run counts as an external side effect, so a failure stops the rest of the batch and is not retried automatically.\n- **terminal_open**(command, cwd, shell) [capability_expand category: terminal]: Open a PERSISTENT shell session (REPL/dev server/watch), returning a session_id for terminal_send/read/close. Disabled unless tools.terminal_session_enabled is set. For one-shot commands use shell_run instead.\n- **terminal_send**(session_id, input, wait) [capability_expand category: terminal]: Send a line of input to a persistent terminal session and return output captured shortly after.\n- **terminal_read**(session_id, wait): Drain buffered output from a persistent terminal session (optionally waiting briefly first).\n- **terminal_close**(session_id) [capability_expand category: terminal]: Terminate a persistent terminal session and release its process group.\n- **terminal_list**(): List active persistent terminal sessions.\n- **config_list**(category, limit): List LeapFlow's own writable settings (LLM model and endpoint, daemon, memory, perception, gateway, …) with current values. Use this to discover the exact key before changing anything. Optionally narrow by `category`. This is the only correct way to inspect LeapFlow configuration — never read config files from disk.\n- **config_get**(key): Read one LeapFlow setting by key (e.g. 'llm.model', 'llm.base_url', 'daemon.log_level'), returning its current value, type, scopes, and whether a change needs a daemon restart. There is no 'llm.provider' key: provider behavior is inferred from the OpenAI-compatible 'llm.base_url'. Never read LeapFlow config files from disk — use this.\n- **config_set**(key, value, scope) [capability_expand category: config]: Change one LeapFlow setting by key, e.g. switch an OpenAI-compatible LLM with key='llm.model' and key='llm.base_url'. There is no 'llm.provider' key: provider behavior is inferred from the endpoint. Values are validated and coerced; credentials are stored in the vault automatically. Call config_list or config_get first if unsure of the key. The result states whether a `leap daemon restart` is required. Never edit LeapFlow config files directly.\n- **web_fetch**(url, select, timeout, max_bytes): Read a URL over HTTP(S) and get back extracted, context-sized content: parsed JSON for API endpoints, readable text plus links for web pages. Use this for anything on the internet — prices, docs, releases, articles — instead of running curl through shell_run: it reports real HTTP status codes, retries rate limits on its own, and is a plain read so a retry is always safe. For JSON APIs pass `select` with a dotted path (e.g. 'chart.result.0.meta') to return just that part instead of the whole payload.\n- **memory_search**(query, limit): Search agent memory for relevant past experiences, observations, and facts.\n- **memory_add**(content, kind) [capability_expand category: write]: Store a new observation or insight in memory for future reference.\n- **research_note**(kind, text): Record a compact, structured note about the current task's state so it survives context compression on long / multi-step tasks. Use for durable findings, open questions still to resolve, decisions / excluded paths, and the immediate next step. One concise sentence per note.\n- **capability_expand**(category): Fetch the full callable schema for every tool in a capability category (e.g. 'hub', 'gateway', 'desktop', 'delegate', 'file', 'memory', 'skill'). The compact tool index always lists every registered tool by name and a one-line summary, but only a static low-risk subset is directly callable each turn. If you need a tool from the index that is not yet callable, call capability_expand with its category first; the matching tools become callable in this turn. Never invent a tool name — expand the category instead.\n- **delegate_task**(goal, context) [capability_expand category: delegate]: Delegate a complex sub-task to an isolated subagent. The subagent gets a fresh context and restricted tool access. Use when a task is self-contained and can be solved independently.\n- **schedule_reentry**(kind, reason, delay_seconds, event_match, max_reentries, deadline_seconds): Register a re-entry so this task can resume later from its current orientation (findings / open questions / next step). Use when work must pause and continue after a delay (kind=time) or when a matching platform event arrives (kind=event), instead of finishing now. The research-ledger state is carried over automatically.\n- **hub_push**(skill_name, visibility, version) [capability_expand category: hub]: Push a local skill to the ModelScope Hub for sharing or backup.\n- **hub_pull**(repo_id, version) [capability_expand category: hub]: Pull a skill from the ModelScope Hub to install locally.\n- **hub_search**(query) [capability_expand category: hub]: Search for skills on the Hub by keyword or description.\n- **hub_sync**(mode, dry_run) [capability_expand category: hub]: Preview or execute sync between local skills and Hub.\n- **platform_action**(platform, action, payload, backend_kind) [capability_expand category: gateway]: Execute an exact registered business action on an external platform through LeapFlow's App Connector layer. Actions must be copied from the App Connector Capability Index and are addressed as domain.operation, e.g. im.send_message or docs.create_markdown. All business fields (chat_id, text, query, etc.) MUST be placed inside `payload`, never at the top level. Example: {\"platform\":\"feishu\",\"action\":\"im.send_message\",\"payload\":{\"chat_id\":\"oc_xxx\",\"text\":\"hello\"}}. Do not invent action names, do not use management actions such as list/guide/connect/status here.\n- **platform_connect**(action, platform, credentials, options, checkpoint) [capability_expand category: gateway]: List, guide, connect, disconnect, remove, or check status for external platforms using the App Connector management namespace. Supports REST and CLI backends. Use this for management actions such as list/guide/preflight/connect/status; use platform_action only for exact registered business actions.\n- **gateway_send**(platform, chat_id, text, thread_id) [capability_expand category: gateway]: Send a message to a connected external platform (Feishu group, Telegram chat, DingTalk conversation, etc.). Requires the platform to be connected via gateway_connect first. Use gateway_connect with action='list' to see connected platforms and available chat IDs.\n- **gateway_connect**(action, platform, credentials, options) [capability_expand category: gateway]: Connect, configure, or manage external platform integrations (Feishu, DingTalk, Telegram, Slack, Discord, etc.). Conversational flow: 1) call 'guide' to get setup steps + required fields, 2) present the steps to the user and ask for ALL required credentials in a single message, 3) call 'connect' with the credentials. Goal: complete in 1–2 user turns. NEVER include credential values in your text response.\n- **plugin_list**(): List the live plugin registry across Tool/Gateway/LLM subsystems for plugin management: which plugins are loaded, their category and fiber lifecycle state. For questions about LeapFlow's own capabilities or whether it supports X, use self_describe instead.\n- **plugin_status**(plugin_id): Get detailed status of a specific plugin: its declared category, runtime dependencies, contributed tools, and fiber lifecycle state.\n- **plugin_versions**(plugin_id): List recorded source versions and active pointer for a profile-scoped plugin.\n- **plugin_propose**(requested_capability, plugin_id, proposed_tools, test_cases, risk_level, evidence): Create a side-effect-free PluginProposal from explicit capability-gap evidence. Use this before plugin_generate when a missing capability should be reviewed. Does not call an LLM, write files, or install anything.\n- **assess_compatibility**(manifest, source_path) [capability_expand category: plugin_management]: Assess whether a foreign plugin manifest or real DSH source bundle is compatible with LeapFlow. A source bundle assessment is static: runtime_ready stays false until restricted Node discovery during plugin_install. Returns component-level verdicts and limitations.\n- **plugin_generate**(plugin_id, description, proposal_id) [capability_expand category: system]: Generate a new ToolPlugin from a natural-language capability description. The LLM produces code that conforms to the ToolPlugin Protocol; it is then rigorously validated (syntax, structure, import, protocol conformance). The generated source is stored in CAS and receives explicit content approval. It DOES NOT install the plugin — installation requires a second, mutation-specific approval via plugin_install.\n- **plugin_install**(plugin_id, code, marketplace_name, source_path, proposal_id, version_label) [capability_expand category: system]: Install a plugin from exactly one source: validated Python code, the configured Python marketplace, or a real DSH source bundle. DSH bundles run restricted Node discovery before registration; only public host tools are installed and client UI limitations are reported. Writes profile-scoped state and mutates the process-global registry. REQUIRES APPROVAL.\n- **plugin_rollback**(plugin_id, version) [capability_expand category: system]: Rollback a profile-scoped plugin to a recorded source version and reload it. REQUIRES APPROVAL.\n- **plugin_reload**(plugin_id, version_label) [capability_expand category: system]: Hot-reload a plugin at runtime. Disposes the old plugin fiber, re-imports its module, and registers a fresh instance. Existing in-flight turns are unaffected (snapshot isolation). REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_disable**(plugin_id) [capability_expand category: system]: Disable a plugin by disposing its fiber, removing its tools from the runtime registry. Cannot disable self_management itself. REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_remove**(plugin_id, delete_source) [capability_expand category: system]: Terminally remove a plugin: dispose its fiber, unregister its tools, remove reload metadata, and optionally delete its profile-scoped source file. Cannot remove self_management itself. REQUIRES APPROVAL.\n- **plugin_unquarantine**(plugin_id) [capability_expand category: plugin_management]: Restore a quarantined plugin to probation status for re-evaluation. Unfreezes the trust ledger, transitions the proposal back to PROBATION, and reloads the plugin. REQUIRES APPROVAL.\n- **plugin_enable**(plugin_id) [capability_expand category: system]: Re-enable a previously disabled plugin by reloading its module and registering a fresh instance. REQUIRES APPROVAL.\n- **self_describe**(facet): Answer questions about LeapFlow ITSELF from live runtime evidence — its identity/version/model, what tools and plugins it has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of self-awareness. Prefer this over reading LeapFlow's own source code. Pick facet=capabilities for 'what can you do / do you support X', identity for version/model, runtime for context/posture, or all for a full self-check when explicitly requested.\n- **runtime_snapshot**(): Lightweight (~150 token) flat snapshot of current runtime state: model, context budget, posture, disclosure level, turn count, cache hit rate, uptime, tool count, and pending approvals.\n- **schedule_create**(trigger_expression, instruction, execution_mode, max_retries, delivery_target) [capability_expand category: scheduler]: Create a new scheduled task. Specify a trigger expression (e.g. '30m', 'every 2h', '0 9 * * *') and the instruction to execute on each trigger. Optionally provide an execution mode and a delivery target to receive notifications.\n- **schedule_list**() [capability_expand category: scheduler]: List all scheduled tasks with their current state, trigger, and next due time.\n- **schedule_status**(task_id) [capability_expand category: scheduler]: Get detailed status and recent execution history for a scheduled task.\n- **schedule_pause**(task_id) [capability_expand category: scheduler]: Pause a scheduled task so it stops firing without being cancelled.\n- **schedule_resume**(task_id) [capability_expand category: scheduler]: Resume a paused scheduled task — re-arms it and recalculates next due time.\n- **schedule_cancel**(task_id) [capability_expand category: scheduler]: Cancel a scheduled task permanently.\n- **tool_search**(query, max_results): Search all registered tools by keyword relevance. Returns a ranked list of matching tools with name, category, and summary. Use this when you need a tool but are unsure of its exact name or category.\n- **tool_describe**(tool_name): Get the full callable schema of a single tool by exact name. Returns the tool's description, parameters, and metadata. Use after tool_search to inspect a specific tool before calling it.\n- **session_search**(query, limit) [capability_expand category: unclassified]: Search past conversation sessions in the current workspace for relevant context.\n- **session_list**(limit): List recent conversation sessions in the current workspace with stable ids, titles, dates, and summaries. Use for browsing past tasks or when user asks to see history without specific search terms. No keywords needed — returns chronological list.\n- **session_detail**(session_id, limit, offset, include_inactive) [capability_expand category: unclassified]: Read a paginated persisted transcript for one past conversation session in the current workspace. Use after session_list or session_search returns a session_id.\n- **click**(element_index) [capability_expand category: desktop]: Click a UI element by its element_index (from the latest observe_ui snapshot)\n- **get_clipboard**() [capability_expand category: desktop]: Read current clipboard text content\n- **list_apps**(filter, running_only) [capability_expand category: desktop]: List available applications on this system. Use to discover correct bundle_id before switch_app.\n- **list_windows**() [capability_expand category: desktop]: List all top-level windows with pid, window_id, title, and per-window state (minimized, on-screen). Call this first to pick the pid and window_id that observe_ui and other window tools require.\n- **observe_ui**(pid, window_id, query) [capability_expand category: desktop]: Snapshot one window's actionable UI elements, each tagged with an element_index for click/right_click/read_text. Re-observe after actions — indices belong to one snapshot. Requires the window's pid and window_id from list_windows.\n- **open_url**(url, app_id) [capability_expand category: desktop]: Open a URL in the default or specified browser\n- **read_text**(element_index) [capability_expand category: desktop]: Read the text content of a specific UI element from the latest snapshot\n- **right_click**(element_index) [capability_expand category: desktop]: Right-click a UI element to open its context menu. Returns visible menu items.\n- **screenshot**(pid, window_id) [capability_expand category: desktop]: Capture a screenshot for visual verification. With pid + window_id captures that window (works across all displays); defaults to the last observed window, or the full desktop when no window has been observed.\n- **scroll**(element_index, direction, amount, pid, window_id) [capability_expand category: desktop]: Scroll a scrollable area of a window. Omit element_index to scroll the window's focused/page scroller; pass one to scroll an exact element from the latest snapshot.\n- **select_text**(element_index) [capability_expand category: desktop]: Select all text in a UI element (focus + select-all, for subsequent copy)\n- **set_clipboard**(text) [capability_expand category: desktop]: Write text to the clipboard\n- **shortcut**(keys) [capability_expand category: desktop]: Execute a keyboard shortcut\n- **switch_app**(app_id) [capability_expand category: desktop]: Switch to an app (launch if needed, activate, verify)\n- **type_text**(text) [capability_expand category: desktop]: Type text into the currently focused element\n- **wait**(seconds) [capability_expand category: desktop]: Wait for a specified duration before continuing\n- **wait_until**(condition, pid, window_id, timeout, poll_interval) [capability_expand category: desktop]: Wait until a UI condition is met (polls UI tree). Returns elements when found or on timeout.\n- **wait_until_stable**(timeout, poll_interval, pid, window_id) [capability_expand category: desktop]: Wait until the UI stops changing (element set stabilizes across polls).\n\n## App Connector Capability Index\nLeapFlow can onboard and manage external apps through `platform_connect` and execute exact registered business actions through `platform_action`.\nFor requests about connecting, setting up, configuring, enabling, or managing a supported app, use `platform_connect` first instead of generating SDK/Webhook sample code.\n`platform_connect.action` is the management namespace: list, guide, preflight, connect, disconnect, remove, status, events_start, events_stop, events_status.\n`platform_action.action` is only for exact registered platform business actions listed below, such as `im.send_message`; never use management actions like `list` or `guide` there.\nAll business fields MUST go inside `payload`; top-level keys are only `platform`, `action`, and `payload`.\nDo not invent platform IDs or platform action names. If the needed action is not listed, ask for discovery/clarification instead of guessing.\nUse `platform_connect` with `action='guide'` and the matching `platform` to start onboarding; use `action='list'` when the app is unclear.\nWhen a pending onboarding state is present, continue from that state with `platform_connect` instead of asking the user to restate the app.\nSupported apps:\n- `api_server`: API Server (OpenAI Compatible) (category=api; backend=adapter; platform_action actions=none registered)\n- `dingtalk`: 钉钉 (DingTalk) (category=im; backend=adapter; platform_action actions=none registered)\n- `feishu`: 飞书 (Feishu/Lark) (category=im; backend=cli)\n - `calendar.create_event` payload={summary*, start_time*, end_time*, attendees} [write/high]\n - `docs.create_markdown` payload={title*, markdown*, folder_token} [write/medium]\n - `drive.search` payload={query*, limit} [read/medium]\n - `im.add_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.download_resource` payload={message_id*, file_key*, type*} [read/low]\n - `im.get_messages` payload={message_ids*} [read/low]\n - `im.list_chats` payload={page_size} [read/low]\n - `im.list_messages` payload={chat_id*} [read/low]\n - `im.list_thread_messages` payload={thread*} [read/low]\n - `im.remove_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.reply_message` payload={message_id*, text*} [send/high]\n - `im.search_chats` payload={query*, page_size} [read/low]\n - `im.search_messages` payload={query*} [read/medium]\n - `im.send_message` payload={chat_id*, text*, thread_id} [send/high]\n - `im.update_card` payload={token*, card*} [write/medium]\n - `im.update_message` payload={message_id*, text*} [write/medium]\n - `mail.search_unread` payload={query, limit} [read/high]\n - `sheets.append_row` payload={spreadsheet_token*, sheet_id*, values*} [write/medium]\n - `task.create` payload={title*, description, due_time} [write/medium]\n- `telegram`: Telegram (category=social; backend=adapter; platform_action actions=none registered)\n- `webhook`: Webhook (Generic) (category=webhook; backend=adapter; platform_action actions=none registered)\n\n## Tool Usage\nTools are normally invoked through the native function-calling mechanism, not by writing JSON in your reply\ntext. Only if the provider signals that native function calling is unavailable for this turn, fall back to a\nsingle JSON code block: `{\"name\": \"tool_name\", \"arguments\": {\"key\": \"value\"}}` — use this fallback format\nonly, never both. Only call a tool whose exact name appears in the tool index above and is currently callable\n(or reachable via `capability_expand`). Never invent, rename, alias, or guess a tool name, platform ID, or\nplatform action from argument shape or wording — if the index does not list it, it does not exist.\n`platform_connect.action` (list/guide/preflight/connect/disconnect/remove/status/events_start/events_stop/\nevents_status) is the App Connector management namespace; `platform_action.action` only accepts exact\nregistered business actions such as `im.send_message` shown in the App Connector Capability Index — never mix\nthe two namespaces. If a tool call returns an unknown/unavailable result, use the returned suggestions or\navailable names for a single retry instead of trying further variations of the same guess.\n\n**Side-effect action rule** (`platform_action` with effect=send/write/execute):\n- Call each unique action+payload **exactly once**. Never include duplicates in the same turn.\n- Once the result returns `\"completed\": true`, that action is DONE for this task. Do NOT call it again\n in any subsequent turn — immediately summarize the result for the user instead.\n- The system enforces idempotency: duplicate calls are blocked and will not execute.\n- If the user explicitly requests sending/writing multiple times, use distinct payloads per call.\n\n**Resource identifier provenance rule**:\n- NEVER fabricate, guess, or infer resource identifiers (chat_id, message_id, file_key, user_id, etc.).\n Every resource ID used in a side-effect action MUST come from a successful API response in this session.\n- If a read/list action fails (e.g. authorization error), you do NOT have valid resource IDs.\n Report the failure to the user — do NOT attempt the dependent write/send action with a guessed ID.\n- When a tool result contains `\"llm_instruction\"`, follow it exactly.\n\n## Guidelines\n1. **Direct answers first**: If you already know the answer, respond directly without tools.\n2. **Avoid redundant tool calls**: Do not call the same tool with the same arguments more than once in the same user turn. When an existing tool result already answers the user's request, stop calling tools and answer directly.\n3. **Use tools proactively**: When the user asks about files, time, system state, or needs actions performed, use the appropriate tool.\n4. **Chain tools when needed**: You can call multiple tools in sequence (e.g., list files → read file → summarize).\n5. **Handle failures gracefully**: If a tool fails, explain what went wrong and suggest alternatives. If it failed because of an unknown tool/platform/action name, retry once with an exact name from the returned suggestions, then explain rather than keep guessing.\n6. **Summarize results naturally**: After tool execution, synthesize the results into a helpful answer rather than dumping raw output.\n7. **Stay conversational**: Maintain a natural, helpful tone. Acknowledge context from earlier in the conversation.\n8. **Recall past work**:\n - Broad queries (\"之前做了什么\", \"列出任务\"): answer from the \"Recent Task History\" section already in your context. If insufficient, call session_list.\n - Specific lookups (\"上次那个配置怎么改的\"): call session_search with relevant phrases (NOT single characters).\n - Do NOT call search tools repeatedly with keyword fragments. One well-phrased call is better than ten fragmented ones.\n\n## Coding & Verification\nWhen working with code, prefer the precise built-in tools over ad-hoc shell: use `repo_map` to orient in an\nunfamiliar project, `code_search`/`file_find` to locate and `code_intel` for symbols, `edit_file` (anchored\nsearch-replace, or a unified `diff`) to change files — never rewrite a whole file to change a few lines — and\n`git_query` to inspect diffs/log. After edits, check `syntax_ok` in the result and run `test_run`/`lint_check`\nbefore declaring the work done. Batch independent read-only calls (search/read/query) into a single turn — they\nrun in parallel.\n\n## Presentation Style\n1. **Polished Markdown only**: Format user-facing answers with clean Markdown headings, short paragraphs, and concise bullets. Use tables only when they improve comparison or scanning.\n2. **Terminal-friendly layout**: Keep lines readable in a TUI; avoid dense walls of text, deeply nested lists, oversized ASCII art, or heavy visual blocks.\n3. **Elegant emphasis**: Use bold text sparingly for key terms and conclusions. Avoid excessive emojis, decorative symbols, repeated separators, or visual noise.\n4. **Theme-safe colors**: Do not emit ANSI escape codes, HTML color tags, Rich markup, or hardcoded color names. Rely on the TUI theme to render Markdown professionally.\n5. **No leaked tool protocol**: Never show tool-call JSON, internal schemas, raw observations, tool result payloads, or hidden reasoning in the final answer unless the user explicitly asks for raw/debug output. Treat any prior `{\"name\": ..., \"arguments\": ...}` blocks and `Tool result (...)` messages as internal execution context only.\n6. **Professional closure**: End with a concise conclusion or next step when helpful; avoid rambling after the useful answer is complete.\n\nWhen finished with all tool calls, respond normally without a JSON block, tool-call transcript, or process log.\n\n## Task Contract\n- Task ID: turn-2\n- Original user request: And now?\n- Workspace root: \n- Allowed roots: \n- Treat relative project paths as relative to the workspace root; never infer `.` as the project root when a workspace root is provided.\n- Workspace boundary is enforced by tools: do not read, search, edit, or run commands against paths outside the allowed roots unless the user explicitly requests an external path and the tool/approval policy permits it.\n- LeapFlow workspace config is optional at `/.leapflow/config.yaml`; runtime config is loaded from `~/.leapflow/config/user.yaml` and `~/.leapflow/profiles//config/*.yaml`.\n- Preserve this task contract across summarization, compression, tool loops, and memory retrieval.\n" + }, + { + "role": "system", + "content": "\n## Recent Session Summary\n- [assistant] Recovered after a rate limit.\n- [user] And now?\n" + }, + { + "role": "assistant", + "content": "Recovered after a rate limit." + }, + { + "role": "user", + "content": "And now?\nAnd now?" + } + ], + "tools": [ + "capability_expand", + "code_intel", + "code_search", + "config_get", + "config_list", + "env_info", + "file_find", + "file_list", + "file_read", + "git_query", + "lint_check", + "memory_search", + "plugin_list", + "plugin_propose", + "plugin_status", + "plugin_versions", + "repo_map", + "research_note", + "runtime_snapshot", + "schedule_reentry", + "self_describe", + "session_list", + "skill_view", + "skills_list", + "terminal_list", + "terminal_read", + "test_run", + "text_search", + "time_get", + "tool_describe", + "tool_search", + "web_fetch" + ] + }, + "responses": [ + { + "status": 500, + "content_type": "application/json", + "body": "{\"error\": {\"message\": \"The server had an error\", \"type\": \"server_error\", \"code\": \"internal_error\"}}" + }, + { + "status": 200, + "content_type": "application/json", + "body": "{\"id\": \"chatcmpl-cassette\", \"object\": \"chat.completion\", \"model\": \"cassette-model\", \"choices\": [{\"index\": 0, \"message\": {\"role\": \"assistant\", \"content\": \"Recovered after a server error.\"}, \"finish_reason\": \"stop\"}], \"usage\": {\"prompt_tokens\": 64, \"completion_tokens\": 16, \"total_tokens\": 80}}" + } + ] +} diff --git a/tests/_fixtures/cassettes/r4_recovery/cassette-model-bee84f304abdb716.cassette.json b/tests/_fixtures/cassettes/r4_recovery/cassette-model-bee84f304abdb716.cassette.json new file mode 100644 index 00000000..35f044a1 --- /dev/null +++ b/tests/_fixtures/cassettes/r4_recovery/cassette-model-bee84f304abdb716.cassette.json @@ -0,0 +1,72 @@ +{ + "fingerprint": "bee84f304abdb716bd4769fc32a3a48d69fbab63b3f9199526aedc5815c706d1", + "note": "captured in seed mode", + "request": { + "model": "cassette-model", + "stream": false, + "messages": [ + { + "role": "system", + "content": "You are LeapFlow, an intelligent assistant that can both converse naturally and take real actions on the user's computer.\n\n## Capabilities\nThe tool index below lists **every** registered tool by name and a one-line summary — this is the complete\ncapability contract; nothing else exists. Only a subset is directly callable this turn (via native tool calling,\nnot a JSON block in your reply). If you need a tool from the index that is not yet callable, call\n`capability_expand` with its category name first — the matching tools become callable immediately after.\nWhen the user asks about LeapFlow **itself** — its identity/version/model, what tools, plugins, or capabilities\nit has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of\nself-awareness — answer from live runtime evidence via `self_describe` (facet=capabilities for \"what can you\ndo / do you support X\", identity for version/model, runtime for context/posture, or `all` for a full self-check),\nnever by reading LeapFlow's own source code, documentation, or memory. Report configuration-dependent or\nunavailable capabilities as limitations rather than inferring them.\n- **text_search**(text, pattern): Search for a regex pattern in text.\n- **text_replace**(text, old, new, count) [capability_expand category: write]: Replace occurrences of a substring in text.\n- **time_get**(): Get current date and time.\n- **env_info**(): Get system environment information (OS, Python version, cwd).\n- **skills_list**(query, category, source): List available learned skills. Use when user asks about capabilities or you need a specific skill.\n- **skill_view**(name): View the full content of a specific skill document.\n- **skill_list_files**(name) [capability_expand category: skills]: List all support files in a skill directory (configs, templates, examples). Use before skill_file_read to discover available files.\n- **skill_file_read**(name, file_path) [capability_expand category: skills]: Read an individual support file from a skill directory. Use skill_list_files first to discover available files.\n- **code_intel**(path, operation): Precise document symbols (outline) for a source file: classes, functions, and methods with line ranges. Python uses an exact AST parse; other languages use a keyword-prefix scan. Prefer over file_read mode=symbols for accurate navigation before editing. Read-only.\n- **repo_map**(path): Compact project orientation for a repository root: languages, detected test/lint commands, top-level structure, entry points, manifest, and VCS branch. Call this first when entering an unfamiliar codebase. Read-only.\n- **scm_sync**(action, cwd, remote, pull_ref, push_ref, timeout) [capability_expand category: scm]: Run a typed git SCM action. Use this instead of shell_run for git pull/push/status. For 'pull origin main then push', set action='pull_then_push', remote='origin', pull_ref='main', and omit push_ref so LeapFlow pushes the current local branch.\n- **git_query**(action, cwd, ref, path, staged, max_count, stat): Read-only structured git inspection: action=diff|log|status|branch|show. Prefer over shell_run for reading repo state — output is clipped, redacted, and log/branch are parsed into structured fields. Use scm_sync for pull/push.\n- **git_write**(action, cwd, message, stage_all, name, ref, create) [capability_expand category: scm]: Mutating git actions: action=commit (message, stage_all), branch (create+switch), checkout (switch; create=true for -b). Approval-gated. Use scm_sync for pull/push and git_query for reads.\n- **test_run**(command, cwd, timeout): Run the project's test suite and return structured results (framework, passed/failed counts, failing tests). Auto-detects the runner (pytest/npm/go/cargo) or uses a configured/explicit command; executes via the governed shell. ok=true means the runner executed — see 'success' for pass/fail.\n- **lint_check**(command, cwd, timeout): Run the project's linter and return a structured clean/issue result. Auto-detects the linter (ruff/eslint/go vet/clippy) or uses a configured/explicit command; executes via the governed shell. ok=true means the linter ran — see 'clean'.\n- **file_list**(path, pattern, depth): List files and directories at a given path. Use depth=1 or depth=2 to get a recursive tree in one call instead of listing each sub-directory separately.\n- **file_read**(path, max_lines, start_line, max_chars, mode): Read text file content with adaptive context governance. For large or unfamiliar files, prefer mode='outline' or mode='symbols' first, then use mode='raw' with start_line/max_lines for the specific range you actually need. For LeapFlow's own settings, use config_list / config_get / config_set — its config files are outside the workspace and not readable here.\n- **file_write**(path, content, mode) [capability_expand category: write]: Write content to a file (overwrite or append).\n- **code_search**(pattern, patterns, path, glob, ignore_case, multiline, max_results, context_lines): Search file CONTENTS by regex pattern across a directory tree (ripgrep-backed). Requires a regex pattern. NOT for listing or browsing directory contents — use file_list for that. Prefer this over shell_run grep: faster, skips VCS/dependency/build dirs, and returns structured path:line:column matches. Batch related lookups into ONE call via `patterns` (OR-combined, single pass) instead of issuing several separate searches. Use file_read for the surrounding context of a hit.\n- **file_find**(glob, path, max_results): Find files by a recursive glob pattern under a base path (e.g. '**/test_*.py' or '*.md'). Prefer this over shell_run find; skips VCS/dependency/build dirs.\n- **edit_file**(path, edits, dry_run, diff) [capability_expand category: write]: Apply targeted, anchored search-replace edits to an EXISTING text file (use file_write to create/overwrite). Each edit is {original_text, new_text, replace_all?}; original_text must match exactly and uniquely (or set replace_all) — a non-unique or missing anchor is rejected so files are never corrupted. Set dry_run to preview. Alternatively pass a unified 'diff' to apply its hunks as anchored edits. Far cheaper and safer than rewriting a whole file.\n- **shell_run**(command, cwd, timeout) [capability_expand category: shell]: Execute a one-shot shell command with timeout protection. Runs in the active workspace; paths resolving outside it are refused. Reach for a structured tool first when one fits — web_fetch for anything over HTTP(S), git_query/scm_sync for git, code_search/file_find/file_read for the repo, config_get/config_set for LeapFlow's own settings — because those report typed results, while a failed shell command can only be diagnosed from its exit code and stderr. Every shell run counts as an external side effect, so a failure stops the rest of the batch and is not retried automatically.\n- **terminal_open**(command, cwd, shell) [capability_expand category: terminal]: Open a PERSISTENT shell session (REPL/dev server/watch), returning a session_id for terminal_send/read/close. Disabled unless tools.terminal_session_enabled is set. For one-shot commands use shell_run instead.\n- **terminal_send**(session_id, input, wait) [capability_expand category: terminal]: Send a line of input to a persistent terminal session and return output captured shortly after.\n- **terminal_read**(session_id, wait): Drain buffered output from a persistent terminal session (optionally waiting briefly first).\n- **terminal_close**(session_id) [capability_expand category: terminal]: Terminate a persistent terminal session and release its process group.\n- **terminal_list**(): List active persistent terminal sessions.\n- **config_list**(category, limit): List LeapFlow's own writable settings (LLM model and endpoint, daemon, memory, perception, gateway, …) with current values. Use this to discover the exact key before changing anything. Optionally narrow by `category`. This is the only correct way to inspect LeapFlow configuration — never read config files from disk.\n- **config_get**(key): Read one LeapFlow setting by key (e.g. 'llm.model', 'llm.base_url', 'daemon.log_level'), returning its current value, type, scopes, and whether a change needs a daemon restart. There is no 'llm.provider' key: provider behavior is inferred from the OpenAI-compatible 'llm.base_url'. Never read LeapFlow config files from disk — use this.\n- **config_set**(key, value, scope) [capability_expand category: config]: Change one LeapFlow setting by key, e.g. switch an OpenAI-compatible LLM with key='llm.model' and key='llm.base_url'. There is no 'llm.provider' key: provider behavior is inferred from the endpoint. Values are validated and coerced; credentials are stored in the vault automatically. Call config_list or config_get first if unsure of the key. The result states whether a `leap daemon restart` is required. Never edit LeapFlow config files directly.\n- **web_fetch**(url, select, timeout, max_bytes): Read a URL over HTTP(S) and get back extracted, context-sized content: parsed JSON for API endpoints, readable text plus links for web pages. Use this for anything on the internet — prices, docs, releases, articles — instead of running curl through shell_run: it reports real HTTP status codes, retries rate limits on its own, and is a plain read so a retry is always safe. For JSON APIs pass `select` with a dotted path (e.g. 'chart.result.0.meta') to return just that part instead of the whole payload.\n- **memory_search**(query, limit): Search agent memory for relevant past experiences, observations, and facts.\n- **memory_add**(content, kind) [capability_expand category: write]: Store a new observation or insight in memory for future reference.\n- **research_note**(kind, text): Record a compact, structured note about the current task's state so it survives context compression on long / multi-step tasks. Use for durable findings, open questions still to resolve, decisions / excluded paths, and the immediate next step. One concise sentence per note.\n- **capability_expand**(category): Fetch the full callable schema for every tool in a capability category (e.g. 'hub', 'gateway', 'desktop', 'delegate', 'file', 'memory', 'skill'). The compact tool index always lists every registered tool by name and a one-line summary, but only a static low-risk subset is directly callable each turn. If you need a tool from the index that is not yet callable, call capability_expand with its category first; the matching tools become callable in this turn. Never invent a tool name — expand the category instead.\n- **delegate_task**(goal, context) [capability_expand category: delegate]: Delegate a complex sub-task to an isolated subagent. The subagent gets a fresh context and restricted tool access. Use when a task is self-contained and can be solved independently.\n- **schedule_reentry**(kind, reason, delay_seconds, event_match, max_reentries, deadline_seconds): Register a re-entry so this task can resume later from its current orientation (findings / open questions / next step). Use when work must pause and continue after a delay (kind=time) or when a matching platform event arrives (kind=event), instead of finishing now. The research-ledger state is carried over automatically.\n- **hub_push**(skill_name, visibility, version) [capability_expand category: hub]: Push a local skill to the ModelScope Hub for sharing or backup.\n- **hub_pull**(repo_id, version) [capability_expand category: hub]: Pull a skill from the ModelScope Hub to install locally.\n- **hub_search**(query) [capability_expand category: hub]: Search for skills on the Hub by keyword or description.\n- **hub_sync**(mode, dry_run) [capability_expand category: hub]: Preview or execute sync between local skills and Hub.\n- **platform_action**(platform, action, payload, backend_kind) [capability_expand category: gateway]: Execute an exact registered business action on an external platform through LeapFlow's App Connector layer. Actions must be copied from the App Connector Capability Index and are addressed as domain.operation, e.g. im.send_message or docs.create_markdown. All business fields (chat_id, text, query, etc.) MUST be placed inside `payload`, never at the top level. Example: {\"platform\":\"feishu\",\"action\":\"im.send_message\",\"payload\":{\"chat_id\":\"oc_xxx\",\"text\":\"hello\"}}. Do not invent action names, do not use management actions such as list/guide/connect/status here.\n- **platform_connect**(action, platform, credentials, options, checkpoint) [capability_expand category: gateway]: List, guide, connect, disconnect, remove, or check status for external platforms using the App Connector management namespace. Supports REST and CLI backends. Use this for management actions such as list/guide/preflight/connect/status; use platform_action only for exact registered business actions.\n- **gateway_send**(platform, chat_id, text, thread_id) [capability_expand category: gateway]: Send a message to a connected external platform (Feishu group, Telegram chat, DingTalk conversation, etc.). Requires the platform to be connected via gateway_connect first. Use gateway_connect with action='list' to see connected platforms and available chat IDs.\n- **gateway_connect**(action, platform, credentials, options) [capability_expand category: gateway]: Connect, configure, or manage external platform integrations (Feishu, DingTalk, Telegram, Slack, Discord, etc.). Conversational flow: 1) call 'guide' to get setup steps + required fields, 2) present the steps to the user and ask for ALL required credentials in a single message, 3) call 'connect' with the credentials. Goal: complete in 1–2 user turns. NEVER include credential values in your text response.\n- **plugin_list**(): List the live plugin registry across Tool/Gateway/LLM subsystems for plugin management: which plugins are loaded, their category and fiber lifecycle state. For questions about LeapFlow's own capabilities or whether it supports X, use self_describe instead.\n- **plugin_status**(plugin_id): Get detailed status of a specific plugin: its declared category, runtime dependencies, contributed tools, and fiber lifecycle state.\n- **plugin_versions**(plugin_id): List recorded source versions and active pointer for a profile-scoped plugin.\n- **plugin_propose**(requested_capability, plugin_id, proposed_tools, test_cases, risk_level, evidence): Create a side-effect-free PluginProposal from explicit capability-gap evidence. Use this before plugin_generate when a missing capability should be reviewed. Does not call an LLM, write files, or install anything.\n- **assess_compatibility**(manifest, source_path) [capability_expand category: plugin_management]: Assess whether a foreign plugin manifest or real DSH source bundle is compatible with LeapFlow. A source bundle assessment is static: runtime_ready stays false until restricted Node discovery during plugin_install. Returns component-level verdicts and limitations.\n- **plugin_generate**(plugin_id, description, proposal_id) [capability_expand category: system]: Generate a new ToolPlugin from a natural-language capability description. The LLM produces code that conforms to the ToolPlugin Protocol; it is then rigorously validated (syntax, structure, import, protocol conformance). The generated source is stored in CAS and receives explicit content approval. It DOES NOT install the plugin — installation requires a second, mutation-specific approval via plugin_install.\n- **plugin_install**(plugin_id, code, marketplace_name, source_path, proposal_id, version_label) [capability_expand category: system]: Install a plugin from exactly one source: validated Python code, the configured Python marketplace, or a real DSH source bundle. DSH bundles run restricted Node discovery before registration; only public host tools are installed and client UI limitations are reported. Writes profile-scoped state and mutates the process-global registry. REQUIRES APPROVAL.\n- **plugin_rollback**(plugin_id, version) [capability_expand category: system]: Rollback a profile-scoped plugin to a recorded source version and reload it. REQUIRES APPROVAL.\n- **plugin_reload**(plugin_id, version_label) [capability_expand category: system]: Hot-reload a plugin at runtime. Disposes the old plugin fiber, re-imports its module, and registers a fresh instance. Existing in-flight turns are unaffected (snapshot isolation). REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_disable**(plugin_id) [capability_expand category: system]: Disable a plugin by disposing its fiber, removing its tools from the runtime registry. Cannot disable self_management itself. REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_remove**(plugin_id, delete_source) [capability_expand category: system]: Terminally remove a plugin: dispose its fiber, unregister its tools, remove reload metadata, and optionally delete its profile-scoped source file. Cannot remove self_management itself. REQUIRES APPROVAL.\n- **plugin_unquarantine**(plugin_id) [capability_expand category: plugin_management]: Restore a quarantined plugin to probation status for re-evaluation. Unfreezes the trust ledger, transitions the proposal back to PROBATION, and reloads the plugin. REQUIRES APPROVAL.\n- **plugin_enable**(plugin_id) [capability_expand category: system]: Re-enable a previously disabled plugin by reloading its module and registering a fresh instance. REQUIRES APPROVAL.\n- **self_describe**(facet): Answer questions about LeapFlow ITSELF from live runtime evidence — its identity/version/model, what tools and plugins it has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of self-awareness. Prefer this over reading LeapFlow's own source code. Pick facet=capabilities for 'what can you do / do you support X', identity for version/model, runtime for context/posture, or all for a full self-check when explicitly requested.\n- **runtime_snapshot**(): Lightweight (~150 token) flat snapshot of current runtime state: model, context budget, posture, disclosure level, turn count, cache hit rate, uptime, tool count, and pending approvals.\n- **schedule_create**(trigger_expression, instruction, execution_mode, max_retries, delivery_target) [capability_expand category: scheduler]: Create a new scheduled task. Specify a trigger expression (e.g. '30m', 'every 2h', '0 9 * * *') and the instruction to execute on each trigger. Optionally provide an execution mode and a delivery target to receive notifications.\n- **schedule_list**() [capability_expand category: scheduler]: List all scheduled tasks with their current state, trigger, and next due time.\n- **schedule_status**(task_id) [capability_expand category: scheduler]: Get detailed status and recent execution history for a scheduled task.\n- **schedule_pause**(task_id) [capability_expand category: scheduler]: Pause a scheduled task so it stops firing without being cancelled.\n- **schedule_resume**(task_id) [capability_expand category: scheduler]: Resume a paused scheduled task — re-arms it and recalculates next due time.\n- **schedule_cancel**(task_id) [capability_expand category: scheduler]: Cancel a scheduled task permanently.\n- **tool_search**(query, max_results): Search all registered tools by keyword relevance. Returns a ranked list of matching tools with name, category, and summary. Use this when you need a tool but are unsure of its exact name or category.\n- **tool_describe**(tool_name): Get the full callable schema of a single tool by exact name. Returns the tool's description, parameters, and metadata. Use after tool_search to inspect a specific tool before calling it.\n- **session_search**(query, limit) [capability_expand category: unclassified]: Search past conversation sessions in the current workspace for relevant context.\n- **session_list**(limit): List recent conversation sessions in the current workspace with stable ids, titles, dates, and summaries. Use for browsing past tasks or when user asks to see history without specific search terms. No keywords needed — returns chronological list.\n- **session_detail**(session_id, limit, offset, include_inactive) [capability_expand category: unclassified]: Read a paginated persisted transcript for one past conversation session in the current workspace. Use after session_list or session_search returns a session_id.\n- **click**(element_index) [capability_expand category: desktop]: Click a UI element by its element_index (from the latest observe_ui snapshot)\n- **get_clipboard**() [capability_expand category: desktop]: Read current clipboard text content\n- **list_apps**(filter, running_only) [capability_expand category: desktop]: List available applications on this system. Use to discover correct bundle_id before switch_app.\n- **list_windows**() [capability_expand category: desktop]: List all top-level windows with pid, window_id, title, and per-window state (minimized, on-screen). Call this first to pick the pid and window_id that observe_ui and other window tools require.\n- **observe_ui**(pid, window_id, query) [capability_expand category: desktop]: Snapshot one window's actionable UI elements, each tagged with an element_index for click/right_click/read_text. Re-observe after actions — indices belong to one snapshot. Requires the window's pid and window_id from list_windows.\n- **open_url**(url, app_id) [capability_expand category: desktop]: Open a URL in the default or specified browser\n- **read_text**(element_index) [capability_expand category: desktop]: Read the text content of a specific UI element from the latest snapshot\n- **right_click**(element_index) [capability_expand category: desktop]: Right-click a UI element to open its context menu. Returns visible menu items.\n- **screenshot**(pid, window_id) [capability_expand category: desktop]: Capture a screenshot for visual verification. With pid + window_id captures that window (works across all displays); defaults to the last observed window, or the full desktop when no window has been observed.\n- **scroll**(element_index, direction, amount, pid, window_id) [capability_expand category: desktop]: Scroll a scrollable area of a window. Omit element_index to scroll the window's focused/page scroller; pass one to scroll an exact element from the latest snapshot.\n- **select_text**(element_index) [capability_expand category: desktop]: Select all text in a UI element (focus + select-all, for subsequent copy)\n- **set_clipboard**(text) [capability_expand category: desktop]: Write text to the clipboard\n- **shortcut**(keys) [capability_expand category: desktop]: Execute a keyboard shortcut\n- **switch_app**(app_id) [capability_expand category: desktop]: Switch to an app (launch if needed, activate, verify)\n- **type_text**(text) [capability_expand category: desktop]: Type text into the currently focused element\n- **wait**(seconds) [capability_expand category: desktop]: Wait for a specified duration before continuing\n- **wait_until**(condition, pid, window_id, timeout, poll_interval) [capability_expand category: desktop]: Wait until a UI condition is met (polls UI tree). Returns elements when found or on timeout.\n- **wait_until_stable**(timeout, poll_interval, pid, window_id) [capability_expand category: desktop]: Wait until the UI stops changing (element set stabilizes across polls).\n\n## App Connector Capability Index\nLeapFlow can onboard and manage external apps through `platform_connect` and execute exact registered business actions through `platform_action`.\nFor requests about connecting, setting up, configuring, enabling, or managing a supported app, use `platform_connect` first instead of generating SDK/Webhook sample code.\n`platform_connect.action` is the management namespace: list, guide, preflight, connect, disconnect, remove, status, events_start, events_stop, events_status.\n`platform_action.action` is only for exact registered platform business actions listed below, such as `im.send_message`; never use management actions like `list` or `guide` there.\nAll business fields MUST go inside `payload`; top-level keys are only `platform`, `action`, and `payload`.\nDo not invent platform IDs or platform action names. If the needed action is not listed, ask for discovery/clarification instead of guessing.\nUse `platform_connect` with `action='guide'` and the matching `platform` to start onboarding; use `action='list'` when the app is unclear.\nWhen a pending onboarding state is present, continue from that state with `platform_connect` instead of asking the user to restate the app.\nSupported apps:\n- `api_server`: API Server (OpenAI Compatible) (category=api; backend=adapter; platform_action actions=none registered)\n- `dingtalk`: 钉钉 (DingTalk) (category=im; backend=adapter; platform_action actions=none registered)\n- `feishu`: 飞书 (Feishu/Lark) (category=im; backend=cli)\n - `calendar.create_event` payload={summary*, start_time*, end_time*, attendees} [write/high]\n - `docs.create_markdown` payload={title*, markdown*, folder_token} [write/medium]\n - `drive.search` payload={query*, limit} [read/medium]\n - `im.add_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.download_resource` payload={message_id*, file_key*, type*} [read/low]\n - `im.get_messages` payload={message_ids*} [read/low]\n - `im.list_chats` payload={page_size} [read/low]\n - `im.list_messages` payload={chat_id*} [read/low]\n - `im.list_thread_messages` payload={thread*} [read/low]\n - `im.remove_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.reply_message` payload={message_id*, text*} [send/high]\n - `im.search_chats` payload={query*, page_size} [read/low]\n - `im.search_messages` payload={query*} [read/medium]\n - `im.send_message` payload={chat_id*, text*, thread_id} [send/high]\n - `im.update_card` payload={token*, card*} [write/medium]\n - `im.update_message` payload={message_id*, text*} [write/medium]\n - `mail.search_unread` payload={query, limit} [read/high]\n - `sheets.append_row` payload={spreadsheet_token*, sheet_id*, values*} [write/medium]\n - `task.create` payload={title*, description, due_time} [write/medium]\n- `telegram`: Telegram (category=social; backend=adapter; platform_action actions=none registered)\n- `webhook`: Webhook (Generic) (category=webhook; backend=adapter; platform_action actions=none registered)\n\n## Tool Usage\nTools are normally invoked through the native function-calling mechanism, not by writing JSON in your reply\ntext. Only if the provider signals that native function calling is unavailable for this turn, fall back to a\nsingle JSON code block: `{\"name\": \"tool_name\", \"arguments\": {\"key\": \"value\"}}` — use this fallback format\nonly, never both. Only call a tool whose exact name appears in the tool index above and is currently callable\n(or reachable via `capability_expand`). Never invent, rename, alias, or guess a tool name, platform ID, or\nplatform action from argument shape or wording — if the index does not list it, it does not exist.\n`platform_connect.action` (list/guide/preflight/connect/disconnect/remove/status/events_start/events_stop/\nevents_status) is the App Connector management namespace; `platform_action.action` only accepts exact\nregistered business actions such as `im.send_message` shown in the App Connector Capability Index — never mix\nthe two namespaces. If a tool call returns an unknown/unavailable result, use the returned suggestions or\navailable names for a single retry instead of trying further variations of the same guess.\n\n**Side-effect action rule** (`platform_action` with effect=send/write/execute):\n- Call each unique action+payload **exactly once**. Never include duplicates in the same turn.\n- Once the result returns `\"completed\": true`, that action is DONE for this task. Do NOT call it again\n in any subsequent turn — immediately summarize the result for the user instead.\n- The system enforces idempotency: duplicate calls are blocked and will not execute.\n- If the user explicitly requests sending/writing multiple times, use distinct payloads per call.\n\n**Resource identifier provenance rule**:\n- NEVER fabricate, guess, or infer resource identifiers (chat_id, message_id, file_key, user_id, etc.).\n Every resource ID used in a side-effect action MUST come from a successful API response in this session.\n- If a read/list action fails (e.g. authorization error), you do NOT have valid resource IDs.\n Report the failure to the user — do NOT attempt the dependent write/send action with a guessed ID.\n- When a tool result contains `\"llm_instruction\"`, follow it exactly.\n\n## Guidelines\n1. **Direct answers first**: If you already know the answer, respond directly without tools.\n2. **Avoid redundant tool calls**: Do not call the same tool with the same arguments more than once in the same user turn. When an existing tool result already answers the user's request, stop calling tools and answer directly.\n3. **Use tools proactively**: When the user asks about files, time, system state, or needs actions performed, use the appropriate tool.\n4. **Chain tools when needed**: You can call multiple tools in sequence (e.g., list files → read file → summarize).\n5. **Handle failures gracefully**: If a tool fails, explain what went wrong and suggest alternatives. If it failed because of an unknown tool/platform/action name, retry once with an exact name from the returned suggestions, then explain rather than keep guessing.\n6. **Summarize results naturally**: After tool execution, synthesize the results into a helpful answer rather than dumping raw output.\n7. **Stay conversational**: Maintain a natural, helpful tone. Acknowledge context from earlier in the conversation.\n8. **Recall past work**:\n - Broad queries (\"之前做了什么\", \"列出任务\"): answer from the \"Recent Task History\" section already in your context. If insufficient, call session_list.\n - Specific lookups (\"上次那个配置怎么改的\"): call session_search with relevant phrases (NOT single characters).\n - Do NOT call search tools repeatedly with keyword fragments. One well-phrased call is better than ten fragmented ones.\n\n## Coding & Verification\nWhen working with code, prefer the precise built-in tools over ad-hoc shell: use `repo_map` to orient in an\nunfamiliar project, `code_search`/`file_find` to locate and `code_intel` for symbols, `edit_file` (anchored\nsearch-replace, or a unified `diff`) to change files — never rewrite a whole file to change a few lines — and\n`git_query` to inspect diffs/log. After edits, check `syntax_ok` in the result and run `test_run`/`lint_check`\nbefore declaring the work done. Batch independent read-only calls (search/read/query) into a single turn — they\nrun in parallel.\n\n## Presentation Style\n1. **Polished Markdown only**: Format user-facing answers with clean Markdown headings, short paragraphs, and concise bullets. Use tables only when they improve comparison or scanning.\n2. **Terminal-friendly layout**: Keep lines readable in a TUI; avoid dense walls of text, deeply nested lists, oversized ASCII art, or heavy visual blocks.\n3. **Elegant emphasis**: Use bold text sparingly for key terms and conclusions. Avoid excessive emojis, decorative symbols, repeated separators, or visual noise.\n4. **Theme-safe colors**: Do not emit ANSI escape codes, HTML color tags, Rich markup, or hardcoded color names. Rely on the TUI theme to render Markdown professionally.\n5. **No leaked tool protocol**: Never show tool-call JSON, internal schemas, raw observations, tool result payloads, or hidden reasoning in the final answer unless the user explicitly asks for raw/debug output. Treat any prior `{\"name\": ..., \"arguments\": ...}` blocks and `Tool result (...)` messages as internal execution context only.\n6. **Professional closure**: End with a concise conclusion or next step when helpful; avoid rambling after the useful answer is complete.\n\nWhen finished with all tool calls, respond normally without a JSON block, tool-call transcript, or process log.\n\n## Task Contract\n- Task ID: turn-3\n- Original user request: Keep going with more context.\n- Workspace root: \n- Allowed roots: \n- Treat relative project paths as relative to the workspace root; never infer `.` as the project root when a workspace root is provided.\n- Workspace boundary is enforced by tools: do not read, search, edit, or run commands against paths outside the allowed roots unless the user explicitly requests an external path and the tool/approval policy permits it.\n- LeapFlow workspace config is optional at `/.leapflow/config.yaml`; runtime config is loaded from `~/.leapflow/config/user.yaml` and `~/.leapflow/profiles//config/*.yaml`.\n- Preserve this task contract across summarization, compression, tool loops, and memory retrieval.\n" + }, + { + "role": "system", + "content": "\n## Recent Session Summary\n- [assistant] Recovered after a server error.\n- [user] Keep going with more context.\n" + }, + { + "role": "assistant", + "content": "Recovered after a server error." + }, + { + "role": "user", + "content": "Keep going with more context.\nKeep going with more context." + } + ], + "tools": [ + "capability_expand", + "code_intel", + "code_search", + "config_get", + "config_list", + "env_info", + "file_find", + "file_list", + "file_read", + "git_query", + "lint_check", + "memory_search", + "plugin_list", + "plugin_propose", + "plugin_status", + "plugin_versions", + "repo_map", + "research_note", + "runtime_snapshot", + "schedule_reentry", + "self_describe", + "session_list", + "skill_view", + "skills_list", + "terminal_list", + "terminal_read", + "test_run", + "text_search", + "time_get", + "tool_describe", + "tool_search", + "web_fetch" + ] + }, + "responses": [ + { + "status": 400, + "content_type": "application/json", + "body": "{\"error\": {\"message\": \"This model's maximum context length is 8192 tokens. However, your messages resulted in 9001 tokens.\", \"type\": \"invalid_request_error\", \"code\": \"context_length_exceeded\"}}" + }, + { + "status": 200, + "content_type": "application/json", + "body": "{\"id\": \"chatcmpl-cassette\", \"object\": \"chat.completion\", \"model\": \"cassette-model\", \"choices\": [{\"index\": 0, \"message\": {\"role\": \"assistant\", \"content\": \"Recovered after compressing context.\"}, \"finish_reason\": \"stop\"}], \"usage\": {\"prompt_tokens\": 64, \"completion_tokens\": 16, \"total_tokens\": 80}}" + } + ] +} diff --git a/tests/_fixtures/cassettes/r4_recovery/cassette-model-c55e75b7af1dfc19.cassette.json b/tests/_fixtures/cassettes/r4_recovery/cassette-model-c55e75b7af1dfc19.cassette.json new file mode 100644 index 00000000..46f9bcc6 --- /dev/null +++ b/tests/_fixtures/cassettes/r4_recovery/cassette-model-c55e75b7af1dfc19.cassette.json @@ -0,0 +1,33 @@ +{ + "fingerprint": "c55e75b7af1dfc19c576349236bf58b7b362ab1f4b31fe8028c834f91ba2521e", + "note": "captured in seed mode", + "request": { + "model": "cassette-model", + "stream": false, + "messages": [ + { + "role": "system", + "content": "You are LeapFlow, an intelligent assistant that can both converse naturally and take real actions on the user's computer.\n\n## Capabilities\nThe tool index below lists **every** registered tool by name and a one-line summary — this is the complete\ncapability contract; nothing else exists. Only a subset is directly callable this turn (via native tool calling,\nnot a JSON block in your reply). If you need a tool from the index that is not yet callable, call\n`capability_expand` with its category name first — the matching tools become callable immediately after.\nWhen the user asks about LeapFlow **itself** — its identity/version/model, what tools, plugins, or capabilities\nit has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of\nself-awareness — answer from live runtime evidence via `self_describe` (facet=capabilities for \"what can you\ndo / do you support X\", identity for version/model, runtime for context/posture, or `all` for a full self-check),\nnever by reading LeapFlow's own source code, documentation, or memory. Report configuration-dependent or\nunavailable capabilities as limitations rather than inferring them.\n- **text_search**(text, pattern): Search for a regex pattern in text.\n- **text_replace**(text, old, new, count) [capability_expand category: write]: Replace occurrences of a substring in text.\n- **time_get**(): Get current date and time.\n- **env_info**(): Get system environment information (OS, Python version, cwd).\n- **skills_list**(query, category, source): List available learned skills. Use when user asks about capabilities or you need a specific skill.\n- **skill_view**(name): View the full content of a specific skill document.\n- **skill_list_files**(name) [capability_expand category: skills]: List all support files in a skill directory (configs, templates, examples). Use before skill_file_read to discover available files.\n- **skill_file_read**(name, file_path) [capability_expand category: skills]: Read an individual support file from a skill directory. Use skill_list_files first to discover available files.\n- **code_intel**(path, operation): Precise document symbols (outline) for a source file: classes, functions, and methods with line ranges. Python uses an exact AST parse; other languages use a keyword-prefix scan. Prefer over file_read mode=symbols for accurate navigation before editing. Read-only.\n- **repo_map**(path): Compact project orientation for a repository root: languages, detected test/lint commands, top-level structure, entry points, manifest, and VCS branch. Call this first when entering an unfamiliar codebase. Read-only.\n- **scm_sync**(action, cwd, remote, pull_ref, push_ref, timeout) [capability_expand category: scm]: Run a typed git SCM action. Use this instead of shell_run for git pull/push/status. For 'pull origin main then push', set action='pull_then_push', remote='origin', pull_ref='main', and omit push_ref so LeapFlow pushes the current local branch.\n- **git_query**(action, cwd, ref, path, staged, max_count, stat): Read-only structured git inspection: action=diff|log|status|branch|show. Prefer over shell_run for reading repo state — output is clipped, redacted, and log/branch are parsed into structured fields. Use scm_sync for pull/push.\n- **git_write**(action, cwd, message, stage_all, name, ref, create) [capability_expand category: scm]: Mutating git actions: action=commit (message, stage_all), branch (create+switch), checkout (switch; create=true for -b). Approval-gated. Use scm_sync for pull/push and git_query for reads.\n- **test_run**(command, cwd, timeout): Run the project's test suite and return structured results (framework, passed/failed counts, failing tests). Auto-detects the runner (pytest/npm/go/cargo) or uses a configured/explicit command; executes via the governed shell. ok=true means the runner executed — see 'success' for pass/fail.\n- **lint_check**(command, cwd, timeout): Run the project's linter and return a structured clean/issue result. Auto-detects the linter (ruff/eslint/go vet/clippy) or uses a configured/explicit command; executes via the governed shell. ok=true means the linter ran — see 'clean'.\n- **file_list**(path, pattern, depth): List files and directories at a given path. Use depth=1 or depth=2 to get a recursive tree in one call instead of listing each sub-directory separately.\n- **file_read**(path, max_lines, start_line, max_chars, mode): Read text file content with adaptive context governance. For large or unfamiliar files, prefer mode='outline' or mode='symbols' first, then use mode='raw' with start_line/max_lines for the specific range you actually need. For LeapFlow's own settings, use config_list / config_get / config_set — its config files are outside the workspace and not readable here.\n- **file_write**(path, content, mode) [capability_expand category: write]: Write content to a file (overwrite or append).\n- **code_search**(pattern, patterns, path, glob, ignore_case, multiline, max_results, context_lines): Search file CONTENTS by regex pattern across a directory tree (ripgrep-backed). Requires a regex pattern. NOT for listing or browsing directory contents — use file_list for that. Prefer this over shell_run grep: faster, skips VCS/dependency/build dirs, and returns structured path:line:column matches. Batch related lookups into ONE call via `patterns` (OR-combined, single pass) instead of issuing several separate searches. Use file_read for the surrounding context of a hit.\n- **file_find**(glob, path, max_results): Find files by a recursive glob pattern under a base path (e.g. '**/test_*.py' or '*.md'). Prefer this over shell_run find; skips VCS/dependency/build dirs.\n- **edit_file**(path, edits, dry_run, diff) [capability_expand category: write]: Apply targeted, anchored search-replace edits to an EXISTING text file (use file_write to create/overwrite). Each edit is {original_text, new_text, replace_all?}; original_text must match exactly and uniquely (or set replace_all) — a non-unique or missing anchor is rejected so files are never corrupted. Set dry_run to preview. Alternatively pass a unified 'diff' to apply its hunks as anchored edits. Far cheaper and safer than rewriting a whole file.\n- **shell_run**(command, cwd, timeout) [capability_expand category: shell]: Execute a one-shot shell command with timeout protection. Runs in the active workspace; paths resolving outside it are refused. Reach for a structured tool first when one fits — web_fetch for anything over HTTP(S), git_query/scm_sync for git, code_search/file_find/file_read for the repo, config_get/config_set for LeapFlow's own settings — because those report typed results, while a failed shell command can only be diagnosed from its exit code and stderr. Every shell run counts as an external side effect, so a failure stops the rest of the batch and is not retried automatically.\n- **terminal_open**(command, cwd, shell) [capability_expand category: terminal]: Open a PERSISTENT shell session (REPL/dev server/watch), returning a session_id for terminal_send/read/close. Disabled unless tools.terminal_session_enabled is set. For one-shot commands use shell_run instead.\n- **terminal_send**(session_id, input, wait) [capability_expand category: terminal]: Send a line of input to a persistent terminal session and return output captured shortly after.\n- **terminal_read**(session_id, wait): Drain buffered output from a persistent terminal session (optionally waiting briefly first).\n- **terminal_close**(session_id) [capability_expand category: terminal]: Terminate a persistent terminal session and release its process group.\n- **terminal_list**(): List active persistent terminal sessions.\n- **config_list**(category, limit): List LeapFlow's own writable settings (LLM model and endpoint, daemon, memory, perception, gateway, …) with current values. Use this to discover the exact key before changing anything. Optionally narrow by `category`. This is the only correct way to inspect LeapFlow configuration — never read config files from disk.\n- **config_get**(key): Read one LeapFlow setting by key (e.g. 'llm.model', 'llm.base_url', 'daemon.log_level'), returning its current value, type, scopes, and whether a change needs a daemon restart. There is no 'llm.provider' key: provider behavior is inferred from the OpenAI-compatible 'llm.base_url'. Never read LeapFlow config files from disk — use this.\n- **config_set**(key, value, scope) [capability_expand category: config]: Change one LeapFlow setting by key, e.g. switch an OpenAI-compatible LLM with key='llm.model' and key='llm.base_url'. There is no 'llm.provider' key: provider behavior is inferred from the endpoint. Values are validated and coerced; credentials are stored in the vault automatically. Call config_list or config_get first if unsure of the key. The result states whether a `leap daemon restart` is required. Never edit LeapFlow config files directly.\n- **web_fetch**(url, select, timeout, max_bytes): Read a URL over HTTP(S) and get back extracted, context-sized content: parsed JSON for API endpoints, readable text plus links for web pages. Use this for anything on the internet — prices, docs, releases, articles — instead of running curl through shell_run: it reports real HTTP status codes, retries rate limits on its own, and is a plain read so a retry is always safe. For JSON APIs pass `select` with a dotted path (e.g. 'chart.result.0.meta') to return just that part instead of the whole payload.\n- **memory_search**(query, limit): Search agent memory for relevant past experiences, observations, and facts.\n- **memory_add**(content, kind) [capability_expand category: write]: Store a new observation or insight in memory for future reference.\n- **research_note**(kind, text): Record a compact, structured note about the current task's state so it survives context compression on long / multi-step tasks. Use for durable findings, open questions still to resolve, decisions / excluded paths, and the immediate next step. One concise sentence per note.\n- **capability_expand**(category): Fetch the full callable schema for every tool in a capability category (e.g. 'hub', 'gateway', 'desktop', 'delegate', 'file', 'memory', 'skill'). The compact tool index always lists every registered tool by name and a one-line summary, but only a static low-risk subset is directly callable each turn. If you need a tool from the index that is not yet callable, call capability_expand with its category first; the matching tools become callable in this turn. Never invent a tool name — expand the category instead.\n- **delegate_task**(goal, context) [capability_expand category: delegate]: Delegate a complex sub-task to an isolated subagent. The subagent gets a fresh context and restricted tool access. Use when a task is self-contained and can be solved independently.\n- **schedule_reentry**(kind, reason, delay_seconds, event_match, max_reentries, deadline_seconds): Register a re-entry so this task can resume later from its current orientation (findings / open questions / next step). Use when work must pause and continue after a delay (kind=time) or when a matching platform event arrives (kind=event), instead of finishing now. The research-ledger state is carried over automatically.\n- **hub_push**(skill_name, visibility, version) [capability_expand category: hub]: Push a local skill to the ModelScope Hub for sharing or backup.\n- **hub_pull**(repo_id, version) [capability_expand category: hub]: Pull a skill from the ModelScope Hub to install locally.\n- **hub_search**(query) [capability_expand category: hub]: Search for skills on the Hub by keyword or description.\n- **hub_sync**(mode, dry_run) [capability_expand category: hub]: Preview or execute sync between local skills and Hub.\n- **platform_action**(platform, action, payload, backend_kind) [capability_expand category: gateway]: Execute an exact registered business action on an external platform through LeapFlow's App Connector layer. Actions must be copied from the App Connector Capability Index and are addressed as domain.operation, e.g. im.send_message or docs.create_markdown. All business fields (chat_id, text, query, etc.) MUST be placed inside `payload`, never at the top level. Example: {\"platform\":\"feishu\",\"action\":\"im.send_message\",\"payload\":{\"chat_id\":\"oc_xxx\",\"text\":\"hello\"}}. Do not invent action names, do not use management actions such as list/guide/connect/status here.\n- **platform_connect**(action, platform, credentials, options, checkpoint) [capability_expand category: gateway]: List, guide, connect, disconnect, remove, or check status for external platforms using the App Connector management namespace. Supports REST and CLI backends. Use this for management actions such as list/guide/preflight/connect/status; use platform_action only for exact registered business actions.\n- **gateway_send**(platform, chat_id, text, thread_id) [capability_expand category: gateway]: Send a message to a connected external platform (Feishu group, Telegram chat, DingTalk conversation, etc.). Requires the platform to be connected via gateway_connect first. Use gateway_connect with action='list' to see connected platforms and available chat IDs.\n- **gateway_connect**(action, platform, credentials, options) [capability_expand category: gateway]: Connect, configure, or manage external platform integrations (Feishu, DingTalk, Telegram, Slack, Discord, etc.). Conversational flow: 1) call 'guide' to get setup steps + required fields, 2) present the steps to the user and ask for ALL required credentials in a single message, 3) call 'connect' with the credentials. Goal: complete in 1–2 user turns. NEVER include credential values in your text response.\n- **plugin_list**(): List the live plugin registry across Tool/Gateway/LLM subsystems for plugin management: which plugins are loaded, their category and fiber lifecycle state. For questions about LeapFlow's own capabilities or whether it supports X, use self_describe instead.\n- **plugin_status**(plugin_id): Get detailed status of a specific plugin: its declared category, runtime dependencies, contributed tools, and fiber lifecycle state.\n- **plugin_versions**(plugin_id): List recorded source versions and active pointer for a profile-scoped plugin.\n- **plugin_propose**(requested_capability, plugin_id, proposed_tools, test_cases, risk_level, evidence): Create a side-effect-free PluginProposal from explicit capability-gap evidence. Use this before plugin_generate when a missing capability should be reviewed. Does not call an LLM, write files, or install anything.\n- **assess_compatibility**(manifest, source_path) [capability_expand category: plugin_management]: Assess whether a foreign plugin manifest or real DSH source bundle is compatible with LeapFlow. A source bundle assessment is static: runtime_ready stays false until restricted Node discovery during plugin_install. Returns component-level verdicts and limitations.\n- **plugin_generate**(plugin_id, description, proposal_id) [capability_expand category: system]: Generate a new ToolPlugin from a natural-language capability description. The LLM produces code that conforms to the ToolPlugin Protocol; it is then rigorously validated (syntax, structure, import, protocol conformance). The generated source is stored in CAS and receives explicit content approval. It DOES NOT install the plugin — installation requires a second, mutation-specific approval via plugin_install.\n- **plugin_install**(plugin_id, code, marketplace_name, source_path, proposal_id, version_label) [capability_expand category: system]: Install a plugin from exactly one source: validated Python code, the configured Python marketplace, or a real DSH source bundle. DSH bundles run restricted Node discovery before registration; only public host tools are installed and client UI limitations are reported. Writes profile-scoped state and mutates the process-global registry. REQUIRES APPROVAL.\n- **plugin_rollback**(plugin_id, version) [capability_expand category: system]: Rollback a profile-scoped plugin to a recorded source version and reload it. REQUIRES APPROVAL.\n- **plugin_reload**(plugin_id, version_label) [capability_expand category: system]: Hot-reload a plugin at runtime. Disposes the old plugin fiber, re-imports its module, and registers a fresh instance. Existing in-flight turns are unaffected (snapshot isolation). REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_disable**(plugin_id) [capability_expand category: system]: Disable a plugin by disposing its fiber, removing its tools from the runtime registry. Cannot disable self_management itself. REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_remove**(plugin_id, delete_source) [capability_expand category: system]: Terminally remove a plugin: dispose its fiber, unregister its tools, remove reload metadata, and optionally delete its profile-scoped source file. Cannot remove self_management itself. REQUIRES APPROVAL.\n- **plugin_unquarantine**(plugin_id) [capability_expand category: plugin_management]: Restore a quarantined plugin to probation status for re-evaluation. Unfreezes the trust ledger, transitions the proposal back to PROBATION, and reloads the plugin. REQUIRES APPROVAL.\n- **plugin_enable**(plugin_id) [capability_expand category: system]: Re-enable a previously disabled plugin by reloading its module and registering a fresh instance. REQUIRES APPROVAL.\n- **self_describe**(facet): Answer questions about LeapFlow ITSELF from live runtime evidence — its identity/version/model, what tools and plugins it has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of self-awareness. Prefer this over reading LeapFlow's own source code. Pick facet=capabilities for 'what can you do / do you support X', identity for version/model, runtime for context/posture, or all for a full self-check when explicitly requested.\n- **runtime_snapshot**(): Lightweight (~150 token) flat snapshot of current runtime state: model, context budget, posture, disclosure level, turn count, cache hit rate, uptime, tool count, and pending approvals.\n- **schedule_create**(trigger_expression, instruction, execution_mode, max_retries, delivery_target) [capability_expand category: scheduler]: Create a new scheduled task. Specify a trigger expression (e.g. '30m', 'every 2h', '0 9 * * *') and the instruction to execute on each trigger. Optionally provide an execution mode and a delivery target to receive notifications.\n- **schedule_list**() [capability_expand category: scheduler]: List all scheduled tasks with their current state, trigger, and next due time.\n- **schedule_status**(task_id) [capability_expand category: scheduler]: Get detailed status and recent execution history for a scheduled task.\n- **schedule_pause**(task_id) [capability_expand category: scheduler]: Pause a scheduled task so it stops firing without being cancelled.\n- **schedule_resume**(task_id) [capability_expand category: scheduler]: Resume a paused scheduled task — re-arms it and recalculates next due time.\n- **schedule_cancel**(task_id) [capability_expand category: scheduler]: Cancel a scheduled task permanently.\n- **tool_search**(query, max_results): Search all registered tools by keyword relevance. Returns a ranked list of matching tools with name, category, and summary. Use this when you need a tool but are unsure of its exact name or category.\n- **tool_describe**(tool_name): Get the full callable schema of a single tool by exact name. Returns the tool's description, parameters, and metadata. Use after tool_search to inspect a specific tool before calling it.\n- **session_search**(query, limit) [capability_expand category: unclassified]: Search past conversation sessions in the current workspace for relevant context.\n- **session_list**(limit): List recent conversation sessions in the current workspace with stable ids, titles, dates, and summaries. Use for browsing past tasks or when user asks to see history without specific search terms. No keywords needed — returns chronological list.\n- **session_detail**(session_id, limit, offset, include_inactive) [capability_expand category: unclassified]: Read a paginated persisted transcript for one past conversation session in the current workspace. Use after session_list or session_search returns a session_id.\n- **click**(element_index) [capability_expand category: desktop]: Click a UI element by its element_index (from the latest observe_ui snapshot)\n- **get_clipboard**() [capability_expand category: desktop]: Read current clipboard text content\n- **list_apps**(filter, running_only) [capability_expand category: desktop]: List available applications on this system. Use to discover correct bundle_id before switch_app.\n- **list_windows**() [capability_expand category: desktop]: List all top-level windows with pid, window_id, title, and per-window state (minimized, on-screen). Call this first to pick the pid and window_id that observe_ui and other window tools require.\n- **observe_ui**(pid, window_id, query) [capability_expand category: desktop]: Snapshot one window's actionable UI elements, each tagged with an element_index for click/right_click/read_text. Re-observe after actions — indices belong to one snapshot. Requires the window's pid and window_id from list_windows.\n- **open_url**(url, app_id) [capability_expand category: desktop]: Open a URL in the default or specified browser\n- **read_text**(element_index) [capability_expand category: desktop]: Read the text content of a specific UI element from the latest snapshot\n- **right_click**(element_index) [capability_expand category: desktop]: Right-click a UI element to open its context menu. Returns visible menu items.\n- **screenshot**(pid, window_id) [capability_expand category: desktop]: Capture a screenshot for visual verification. With pid + window_id captures that window (works across all displays); defaults to the last observed window, or the full desktop when no window has been observed.\n- **scroll**(element_index, direction, amount, pid, window_id) [capability_expand category: desktop]: Scroll a scrollable area of a window. Omit element_index to scroll the window's focused/page scroller; pass one to scroll an exact element from the latest snapshot.\n- **select_text**(element_index) [capability_expand category: desktop]: Select all text in a UI element (focus + select-all, for subsequent copy)\n- **set_clipboard**(text) [capability_expand category: desktop]: Write text to the clipboard\n- **shortcut**(keys) [capability_expand category: desktop]: Execute a keyboard shortcut\n- **switch_app**(app_id) [capability_expand category: desktop]: Switch to an app (launch if needed, activate, verify)\n- **type_text**(text) [capability_expand category: desktop]: Type text into the currently focused element\n- **wait**(seconds) [capability_expand category: desktop]: Wait for a specified duration before continuing\n- **wait_until**(condition, pid, window_id, timeout, poll_interval) [capability_expand category: desktop]: Wait until a UI condition is met (polls UI tree). Returns elements when found or on timeout.\n- **wait_until_stable**(timeout, poll_interval, pid, window_id) [capability_expand category: desktop]: Wait until the UI stops changing (element set stabilizes across polls).\n\n## App Connector Capability Index\nLeapFlow can onboard and manage external apps through `platform_connect` and execute exact registered business actions through `platform_action`.\nFor requests about connecting, setting up, configuring, enabling, or managing a supported app, use `platform_connect` first instead of generating SDK/Webhook sample code.\n`platform_connect.action` is the management namespace: list, guide, preflight, connect, disconnect, remove, status, events_start, events_stop, events_status.\n`platform_action.action` is only for exact registered platform business actions listed below, such as `im.send_message`; never use management actions like `list` or `guide` there.\nAll business fields MUST go inside `payload`; top-level keys are only `platform`, `action`, and `payload`.\nDo not invent platform IDs or platform action names. If the needed action is not listed, ask for discovery/clarification instead of guessing.\nUse `platform_connect` with `action='guide'` and the matching `platform` to start onboarding; use `action='list'` when the app is unclear.\nWhen a pending onboarding state is present, continue from that state with `platform_connect` instead of asking the user to restate the app.\nSupported apps:\n- `api_server`: API Server (OpenAI Compatible) (category=api; backend=adapter; platform_action actions=none registered)\n- `dingtalk`: 钉钉 (DingTalk) (category=im; backend=adapter; platform_action actions=none registered)\n- `feishu`: 飞书 (Feishu/Lark) (category=im; backend=cli)\n - `calendar.create_event` payload={summary*, start_time*, end_time*, attendees} [write/high]\n - `docs.create_markdown` payload={title*, markdown*, folder_token} [write/medium]\n - `drive.search` payload={query*, limit} [read/medium]\n - `im.add_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.download_resource` payload={message_id*, file_key*, type*} [read/low]\n - `im.get_messages` payload={message_ids*} [read/low]\n - `im.list_chats` payload={page_size} [read/low]\n - `im.list_messages` payload={chat_id*} [read/low]\n - `im.list_thread_messages` payload={thread*} [read/low]\n - `im.remove_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.reply_message` payload={message_id*, text*} [send/high]\n - `im.search_chats` payload={query*, page_size} [read/low]\n - `im.search_messages` payload={query*} [read/medium]\n - `im.send_message` payload={chat_id*, text*, thread_id} [send/high]\n - `im.update_card` payload={token*, card*} [write/medium]\n - `im.update_message` payload={message_id*, text*} [write/medium]\n - `mail.search_unread` payload={query, limit} [read/high]\n - `sheets.append_row` payload={spreadsheet_token*, sheet_id*, values*} [write/medium]\n - `task.create` payload={title*, description, due_time} [write/medium]\n- `telegram`: Telegram (category=social; backend=adapter; platform_action actions=none registered)\n- `webhook`: Webhook (Generic) (category=webhook; backend=adapter; platform_action actions=none registered)\n\n## Tool Usage\nTools are normally invoked through the native function-calling mechanism, not by writing JSON in your reply\ntext. Only if the provider signals that native function calling is unavailable for this turn, fall back to a\nsingle JSON code block: `{\"name\": \"tool_name\", \"arguments\": {\"key\": \"value\"}}` — use this fallback format\nonly, never both. Only call a tool whose exact name appears in the tool index above and is currently callable\n(or reachable via `capability_expand`). Never invent, rename, alias, or guess a tool name, platform ID, or\nplatform action from argument shape or wording — if the index does not list it, it does not exist.\n`platform_connect.action` (list/guide/preflight/connect/disconnect/remove/status/events_start/events_stop/\nevents_status) is the App Connector management namespace; `platform_action.action` only accepts exact\nregistered business actions such as `im.send_message` shown in the App Connector Capability Index — never mix\nthe two namespaces. If a tool call returns an unknown/unavailable result, use the returned suggestions or\navailable names for a single retry instead of trying further variations of the same guess.\n\n**Side-effect action rule** (`platform_action` with effect=send/write/execute):\n- Call each unique action+payload **exactly once**. Never include duplicates in the same turn.\n- Once the result returns `\"completed\": true`, that action is DONE for this task. Do NOT call it again\n in any subsequent turn — immediately summarize the result for the user instead.\n- The system enforces idempotency: duplicate calls are blocked and will not execute.\n- If the user explicitly requests sending/writing multiple times, use distinct payloads per call.\n\n**Resource identifier provenance rule**:\n- NEVER fabricate, guess, or infer resource identifiers (chat_id, message_id, file_key, user_id, etc.).\n Every resource ID used in a side-effect action MUST come from a successful API response in this session.\n- If a read/list action fails (e.g. authorization error), you do NOT have valid resource IDs.\n Report the failure to the user — do NOT attempt the dependent write/send action with a guessed ID.\n- When a tool result contains `\"llm_instruction\"`, follow it exactly.\n\n## Guidelines\n1. **Direct answers first**: If you already know the answer, respond directly without tools.\n2. **Avoid redundant tool calls**: Do not call the same tool with the same arguments more than once in the same user turn. When an existing tool result already answers the user's request, stop calling tools and answer directly.\n3. **Use tools proactively**: When the user asks about files, time, system state, or needs actions performed, use the appropriate tool.\n4. **Chain tools when needed**: You can call multiple tools in sequence (e.g., list files → read file → summarize).\n5. **Handle failures gracefully**: If a tool fails, explain what went wrong and suggest alternatives. If it failed because of an unknown tool/platform/action name, retry once with an exact name from the returned suggestions, then explain rather than keep guessing.\n6. **Summarize results naturally**: After tool execution, synthesize the results into a helpful answer rather than dumping raw output.\n7. **Stay conversational**: Maintain a natural, helpful tone. Acknowledge context from earlier in the conversation.\n8. **Recall past work**:\n - Broad queries (\"之前做了什么\", \"列出任务\"): answer from the \"Recent Task History\" section already in your context. If insufficient, call session_list.\n - Specific lookups (\"上次那个配置怎么改的\"): call session_search with relevant phrases (NOT single characters).\n - Do NOT call search tools repeatedly with keyword fragments. One well-phrased call is better than ten fragmented ones.\n\n## Coding & Verification\nWhen working with code, prefer the precise built-in tools over ad-hoc shell: use `repo_map` to orient in an\nunfamiliar project, `code_search`/`file_find` to locate and `code_intel` for symbols, `edit_file` (anchored\nsearch-replace, or a unified `diff`) to change files — never rewrite a whole file to change a few lines — and\n`git_query` to inspect diffs/log. After edits, check `syntax_ok` in the result and run `test_run`/`lint_check`\nbefore declaring the work done. Batch independent read-only calls (search/read/query) into a single turn — they\nrun in parallel.\n\n## Presentation Style\n1. **Polished Markdown only**: Format user-facing answers with clean Markdown headings, short paragraphs, and concise bullets. Use tables only when they improve comparison or scanning.\n2. **Terminal-friendly layout**: Keep lines readable in a TUI; avoid dense walls of text, deeply nested lists, oversized ASCII art, or heavy visual blocks.\n3. **Elegant emphasis**: Use bold text sparingly for key terms and conclusions. Avoid excessive emojis, decorative symbols, repeated separators, or visual noise.\n4. **Theme-safe colors**: Do not emit ANSI escape codes, HTML color tags, Rich markup, or hardcoded color names. Rely on the TUI theme to render Markdown professionally.\n5. **No leaked tool protocol**: Never show tool-call JSON, internal schemas, raw observations, tool result payloads, or hidden reasoning in the final answer unless the user explicitly asks for raw/debug output. Treat any prior `{\"name\": ..., \"arguments\": ...}` blocks and `Tool result (...)` messages as internal execution context only.\n6. **Professional closure**: End with a concise conclusion or next step when helpful; avoid rambling after the useful answer is complete.\n\nWhen finished with all tool calls, respond normally without a JSON block, tool-call transcript, or process log.\n\n## Task Contract\n- Task ID: turn-4\n- Original user request: Do the impossible thing.\n- Workspace root: \n- Allowed roots: \n- Treat relative project paths as relative to the workspace root; never infer `.` as the project root when a workspace root is provided.\n- Workspace boundary is enforced by tools: do not read, search, edit, or run commands against paths outside the allowed roots unless the user explicitly requests an external path and the tool/approval policy permits it.\n- LeapFlow workspace config is optional at `/.leapflow/config.yaml`; runtime config is loaded from `~/.leapflow/config/user.yaml` and `~/.leapflow/profiles//config/*.yaml`.\n- Preserve this task contract across summarization, compression, tool loops, and memory retrieval.\n" + }, + { + "role": "system", + "content": "\n## Recent Session Summary\n- [assistant] Recovered after compressing context.\n- [user] Do the impossible thing.\n" + }, + { + "role": "assistant", + "content": "Recovered after compressing context." + }, + { + "role": "user", + "content": "Do the impossible thing.\nDo the impossible thing.\nSYSTEM: No further tool calls are available for this turn. Do not attempt to call any tool. Answer the user's request directly and concisely using the information already gathered above. If part of it cannot be determined from what you have, say so plainly and state what would be needed — do not repeat an earlier tool call." + } + ] + }, + "responses": [ + { + "status": 400, + "content_type": "application/json", + "body": "{\"error\": {\"message\": \"The requested configuration is not supported by this model\", \"type\": \"invalid_request_error\", \"code\": \"unsupported_value\"}}" + } + ] +} diff --git a/tests/_fixtures/cassettes/r5_learning/cassette-model-2431a179bda357c1.cassette.json b/tests/_fixtures/cassettes/r5_learning/cassette-model-2431a179bda357c1.cassette.json new file mode 100644 index 00000000..5d68dd06 --- /dev/null +++ b/tests/_fixtures/cassettes/r5_learning/cassette-model-2431a179bda357c1.cassette.json @@ -0,0 +1,63 @@ +{ + "fingerprint": "2431a179bda357c1007678874d6cb775d2fbb1f1e0d93763bfc78223779bef6e", + "note": "captured in seed mode", + "request": { + "model": "cassette-model", + "stream": false, + "messages": [ + { + "role": "system", + "content": "You are LeapFlow, an intelligent assistant that can both converse naturally and take real actions on the user's computer.\n\n## Capabilities\nThe tool index below lists **every** registered tool by name and a one-line summary — this is the complete\ncapability contract; nothing else exists. Only a subset is directly callable this turn (via native tool calling,\nnot a JSON block in your reply). If you need a tool from the index that is not yet callable, call\n`capability_expand` with its category name first — the matching tools become callable immediately after.\nWhen the user asks about LeapFlow **itself** — its identity/version/model, what tools, plugins, or capabilities\nit has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of\nself-awareness — answer from live runtime evidence via `self_describe` (facet=capabilities for \"what can you\ndo / do you support X\", identity for version/model, runtime for context/posture, or `all` for a full self-check),\nnever by reading LeapFlow's own source code, documentation, or memory. Report configuration-dependent or\nunavailable capabilities as limitations rather than inferring them.\n- **text_search**(text, pattern): Search for a regex pattern in text.\n- **text_replace**(text, old, new, count) [capability_expand category: write]: Replace occurrences of a substring in text.\n- **time_get**(): Get current date and time.\n- **env_info**(): Get system environment information (OS, Python version, cwd).\n- **skills_list**(query, category, source): List available learned skills. Use when user asks about capabilities or you need a specific skill.\n- **skill_view**(name): View the full content of a specific skill document.\n- **skill_list_files**(name) [capability_expand category: skills]: List all support files in a skill directory (configs, templates, examples). Use before skill_file_read to discover available files.\n- **skill_file_read**(name, file_path) [capability_expand category: skills]: Read an individual support file from a skill directory. Use skill_list_files first to discover available files.\n- **code_intel**(path, operation): Precise document symbols (outline) for a source file: classes, functions, and methods with line ranges. Python uses an exact AST parse; other languages use a keyword-prefix scan. Prefer over file_read mode=symbols for accurate navigation before editing. Read-only.\n- **repo_map**(path): Compact project orientation for a repository root: languages, detected test/lint commands, top-level structure, entry points, manifest, and VCS branch. Call this first when entering an unfamiliar codebase. Read-only.\n- **scm_sync**(action, cwd, remote, pull_ref, push_ref, timeout) [capability_expand category: scm]: Run a typed git SCM action. Use this instead of shell_run for git pull/push/status. For 'pull origin main then push', set action='pull_then_push', remote='origin', pull_ref='main', and omit push_ref so LeapFlow pushes the current local branch.\n- **git_query**(action, cwd, ref, path, staged, max_count, stat): Read-only structured git inspection: action=diff|log|status|branch|show. Prefer over shell_run for reading repo state — output is clipped, redacted, and log/branch are parsed into structured fields. Use scm_sync for pull/push.\n- **git_write**(action, cwd, message, stage_all, name, ref, create) [capability_expand category: scm]: Mutating git actions: action=commit (message, stage_all), branch (create+switch), checkout (switch; create=true for -b). Approval-gated. Use scm_sync for pull/push and git_query for reads.\n- **test_run**(command, cwd, timeout): Run the project's test suite and return structured results (framework, passed/failed counts, failing tests). Auto-detects the runner (pytest/npm/go/cargo) or uses a configured/explicit command; executes via the governed shell. ok=true means the runner executed — see 'success' for pass/fail.\n- **lint_check**(command, cwd, timeout): Run the project's linter and return a structured clean/issue result. Auto-detects the linter (ruff/eslint/go vet/clippy) or uses a configured/explicit command; executes via the governed shell. ok=true means the linter ran — see 'clean'.\n- **file_list**(path, pattern, depth): List files and directories at a given path. Use depth=1 or depth=2 to get a recursive tree in one call instead of listing each sub-directory separately.\n- **file_read**(path, max_lines, start_line, max_chars, mode): Read text file content with adaptive context governance. For large or unfamiliar files, prefer mode='outline' or mode='symbols' first, then use mode='raw' with start_line/max_lines for the specific range you actually need. For LeapFlow's own settings, use config_list / config_get / config_set — its config files are outside the workspace and not readable here.\n- **file_write**(path, content, mode) [capability_expand category: write]: Write content to a file (overwrite or append).\n- **code_search**(pattern, patterns, path, glob, ignore_case, multiline, max_results, context_lines): Search file CONTENTS by regex pattern across a directory tree (ripgrep-backed). Requires a regex pattern. NOT for listing or browsing directory contents — use file_list for that. Prefer this over shell_run grep: faster, skips VCS/dependency/build dirs, and returns structured path:line:column matches. Batch related lookups into ONE call via `patterns` (OR-combined, single pass) instead of issuing several separate searches. Use file_read for the surrounding context of a hit.\n- **file_find**(glob, path, max_results): Find files by a recursive glob pattern under a base path (e.g. '**/test_*.py' or '*.md'). Prefer this over shell_run find; skips VCS/dependency/build dirs.\n- **edit_file**(path, edits, dry_run, diff) [capability_expand category: write]: Apply targeted, anchored search-replace edits to an EXISTING text file (use file_write to create/overwrite). Each edit is {original_text, new_text, replace_all?}; original_text must match exactly and uniquely (or set replace_all) — a non-unique or missing anchor is rejected so files are never corrupted. Set dry_run to preview. Alternatively pass a unified 'diff' to apply its hunks as anchored edits. Far cheaper and safer than rewriting a whole file.\n- **shell_run**(command, cwd, timeout) [capability_expand category: shell]: Execute a one-shot shell command with timeout protection. Runs in the active workspace; paths resolving outside it are refused. Reach for a structured tool first when one fits — web_fetch for anything over HTTP(S), git_query/scm_sync for git, code_search/file_find/file_read for the repo, config_get/config_set for LeapFlow's own settings — because those report typed results, while a failed shell command can only be diagnosed from its exit code and stderr. Every shell run counts as an external side effect, so a failure stops the rest of the batch and is not retried automatically.\n- **terminal_open**(command, cwd, shell) [capability_expand category: terminal]: Open a PERSISTENT shell session (REPL/dev server/watch), returning a session_id for terminal_send/read/close. Disabled unless tools.terminal_session_enabled is set. For one-shot commands use shell_run instead.\n- **terminal_send**(session_id, input, wait) [capability_expand category: terminal]: Send a line of input to a persistent terminal session and return output captured shortly after.\n- **terminal_read**(session_id, wait): Drain buffered output from a persistent terminal session (optionally waiting briefly first).\n- **terminal_close**(session_id) [capability_expand category: terminal]: Terminate a persistent terminal session and release its process group.\n- **terminal_list**(): List active persistent terminal sessions.\n- **config_list**(category, limit): List LeapFlow's own writable settings (LLM model and endpoint, daemon, memory, perception, gateway, …) with current values. Use this to discover the exact key before changing anything. Optionally narrow by `category`. This is the only correct way to inspect LeapFlow configuration — never read config files from disk.\n- **config_get**(key): Read one LeapFlow setting by key (e.g. 'llm.model', 'llm.base_url', 'daemon.log_level'), returning its current value, type, scopes, and whether a change needs a daemon restart. There is no 'llm.provider' key: provider behavior is inferred from the OpenAI-compatible 'llm.base_url'. Never read LeapFlow config files from disk — use this.\n- **config_set**(key, value, scope) [capability_expand category: config]: Change one LeapFlow setting by key, e.g. switch an OpenAI-compatible LLM with key='llm.model' and key='llm.base_url'. There is no 'llm.provider' key: provider behavior is inferred from the endpoint. Values are validated and coerced; credentials are stored in the vault automatically. Call config_list or config_get first if unsure of the key. The result states whether a `leap daemon restart` is required. Never edit LeapFlow config files directly.\n- **web_fetch**(url, select, timeout, max_bytes): Read a URL over HTTP(S) and get back extracted, context-sized content: parsed JSON for API endpoints, readable text plus links for web pages. Use this for anything on the internet — prices, docs, releases, articles — instead of running curl through shell_run: it reports real HTTP status codes, retries rate limits on its own, and is a plain read so a retry is always safe. For JSON APIs pass `select` with a dotted path (e.g. 'chart.result.0.meta') to return just that part instead of the whole payload.\n- **memory_search**(query, limit): Search agent memory for relevant past experiences, observations, and facts.\n- **memory_add**(content, kind) [capability_expand category: write]: Store a new observation or insight in memory for future reference.\n- **research_note**(kind, text): Record a compact, structured note about the current task's state so it survives context compression on long / multi-step tasks. Use for durable findings, open questions still to resolve, decisions / excluded paths, and the immediate next step. One concise sentence per note.\n- **capability_expand**(category): Fetch the full callable schema for every tool in a capability category (e.g. 'hub', 'gateway', 'desktop', 'delegate', 'file', 'memory', 'skill'). The compact tool index always lists every registered tool by name and a one-line summary, but only a static low-risk subset is directly callable each turn. If you need a tool from the index that is not yet callable, call capability_expand with its category first; the matching tools become callable in this turn. Never invent a tool name — expand the category instead.\n- **delegate_task**(goal, context) [capability_expand category: delegate]: Delegate a complex sub-task to an isolated subagent. The subagent gets a fresh context and restricted tool access. Use when a task is self-contained and can be solved independently.\n- **schedule_reentry**(kind, reason, delay_seconds, event_match, max_reentries, deadline_seconds): Register a re-entry so this task can resume later from its current orientation (findings / open questions / next step). Use when work must pause and continue after a delay (kind=time) or when a matching platform event arrives (kind=event), instead of finishing now. The research-ledger state is carried over automatically.\n- **hub_push**(skill_name, visibility, version) [capability_expand category: hub]: Push a local skill to the ModelScope Hub for sharing or backup.\n- **hub_pull**(repo_id, version) [capability_expand category: hub]: Pull a skill from the ModelScope Hub to install locally.\n- **hub_search**(query) [capability_expand category: hub]: Search for skills on the Hub by keyword or description.\n- **hub_sync**(mode, dry_run) [capability_expand category: hub]: Preview or execute sync between local skills and Hub.\n- **platform_action**(platform, action, payload, backend_kind) [capability_expand category: gateway]: Execute an exact registered business action on an external platform through LeapFlow's App Connector layer. Actions must be copied from the App Connector Capability Index and are addressed as domain.operation, e.g. im.send_message or docs.create_markdown. All business fields (chat_id, text, query, etc.) MUST be placed inside `payload`, never at the top level. Example: {\"platform\":\"feishu\",\"action\":\"im.send_message\",\"payload\":{\"chat_id\":\"oc_xxx\",\"text\":\"hello\"}}. Do not invent action names, do not use management actions such as list/guide/connect/status here.\n- **platform_connect**(action, platform, credentials, options, checkpoint) [capability_expand category: gateway]: List, guide, connect, disconnect, remove, or check status for external platforms using the App Connector management namespace. Supports REST and CLI backends. Use this for management actions such as list/guide/preflight/connect/status; use platform_action only for exact registered business actions.\n- **gateway_send**(platform, chat_id, text, thread_id) [capability_expand category: gateway]: Send a message to a connected external platform (Feishu group, Telegram chat, DingTalk conversation, etc.). Requires the platform to be connected via gateway_connect first. Use gateway_connect with action='list' to see connected platforms and available chat IDs.\n- **gateway_connect**(action, platform, credentials, options) [capability_expand category: gateway]: Connect, configure, or manage external platform integrations (Feishu, DingTalk, Telegram, Slack, Discord, etc.). Conversational flow: 1) call 'guide' to get setup steps + required fields, 2) present the steps to the user and ask for ALL required credentials in a single message, 3) call 'connect' with the credentials. Goal: complete in 1–2 user turns. NEVER include credential values in your text response.\n- **plugin_list**(): List the live plugin registry across Tool/Gateway/LLM subsystems for plugin management: which plugins are loaded, their category and fiber lifecycle state. For questions about LeapFlow's own capabilities or whether it supports X, use self_describe instead.\n- **plugin_status**(plugin_id): Get detailed status of a specific plugin: its declared category, runtime dependencies, contributed tools, and fiber lifecycle state.\n- **plugin_versions**(plugin_id): List recorded source versions and active pointer for a profile-scoped plugin.\n- **plugin_propose**(requested_capability, plugin_id, proposed_tools, test_cases, risk_level, evidence): Create a side-effect-free PluginProposal from explicit capability-gap evidence. Use this before plugin_generate when a missing capability should be reviewed. Does not call an LLM, write files, or install anything.\n- **assess_compatibility**(manifest, source_path) [capability_expand category: plugin_management]: Assess whether a foreign plugin manifest or real DSH source bundle is compatible with LeapFlow. A source bundle assessment is static: runtime_ready stays false until restricted Node discovery during plugin_install. Returns component-level verdicts and limitations.\n- **plugin_generate**(plugin_id, description, proposal_id) [capability_expand category: system]: Generate a new ToolPlugin from a natural-language capability description. The LLM produces code that conforms to the ToolPlugin Protocol; it is then rigorously validated (syntax, structure, import, protocol conformance). The generated source is stored in CAS and receives explicit content approval. It DOES NOT install the plugin — installation requires a second, mutation-specific approval via plugin_install.\n- **plugin_install**(plugin_id, code, marketplace_name, source_path, proposal_id, version_label) [capability_expand category: system]: Install a plugin from exactly one source: validated Python code, the configured Python marketplace, or a real DSH source bundle. DSH bundles run restricted Node discovery before registration; only public host tools are installed and client UI limitations are reported. Writes profile-scoped state and mutates the process-global registry. REQUIRES APPROVAL.\n- **plugin_rollback**(plugin_id, version) [capability_expand category: system]: Rollback a profile-scoped plugin to a recorded source version and reload it. REQUIRES APPROVAL.\n- **plugin_reload**(plugin_id, version_label) [capability_expand category: system]: Hot-reload a plugin at runtime. Disposes the old plugin fiber, re-imports its module, and registers a fresh instance. Existing in-flight turns are unaffected (snapshot isolation). REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_disable**(plugin_id) [capability_expand category: system]: Disable a plugin by disposing its fiber, removing its tools from the runtime registry. Cannot disable self_management itself. REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_remove**(plugin_id, delete_source) [capability_expand category: system]: Terminally remove a plugin: dispose its fiber, unregister its tools, remove reload metadata, and optionally delete its profile-scoped source file. Cannot remove self_management itself. REQUIRES APPROVAL.\n- **plugin_unquarantine**(plugin_id) [capability_expand category: plugin_management]: Restore a quarantined plugin to probation status for re-evaluation. Unfreezes the trust ledger, transitions the proposal back to PROBATION, and reloads the plugin. REQUIRES APPROVAL.\n- **plugin_enable**(plugin_id) [capability_expand category: system]: Re-enable a previously disabled plugin by reloading its module and registering a fresh instance. REQUIRES APPROVAL.\n- **self_describe**(facet): Answer questions about LeapFlow ITSELF from live runtime evidence — its identity/version/model, what tools and plugins it has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of self-awareness. Prefer this over reading LeapFlow's own source code. Pick facet=capabilities for 'what can you do / do you support X', identity for version/model, runtime for context/posture, or all for a full self-check when explicitly requested.\n- **runtime_snapshot**(): Lightweight (~150 token) flat snapshot of current runtime state: model, context budget, posture, disclosure level, turn count, cache hit rate, uptime, tool count, and pending approvals.\n- **schedule_create**(trigger_expression, instruction, execution_mode, max_retries, delivery_target) [capability_expand category: scheduler]: Create a new scheduled task. Specify a trigger expression (e.g. '30m', 'every 2h', '0 9 * * *') and the instruction to execute on each trigger. Optionally provide an execution mode and a delivery target to receive notifications.\n- **schedule_list**() [capability_expand category: scheduler]: List all scheduled tasks with their current state, trigger, and next due time.\n- **schedule_status**(task_id) [capability_expand category: scheduler]: Get detailed status and recent execution history for a scheduled task.\n- **schedule_pause**(task_id) [capability_expand category: scheduler]: Pause a scheduled task so it stops firing without being cancelled.\n- **schedule_resume**(task_id) [capability_expand category: scheduler]: Resume a paused scheduled task — re-arms it and recalculates next due time.\n- **schedule_cancel**(task_id) [capability_expand category: scheduler]: Cancel a scheduled task permanently.\n- **tool_search**(query, max_results): Search all registered tools by keyword relevance. Returns a ranked list of matching tools with name, category, and summary. Use this when you need a tool but are unsure of its exact name or category.\n- **tool_describe**(tool_name): Get the full callable schema of a single tool by exact name. Returns the tool's description, parameters, and metadata. Use after tool_search to inspect a specific tool before calling it.\n- **session_search**(query, limit) [capability_expand category: unclassified]: Search past conversation sessions in the current workspace for relevant context.\n- **session_list**(limit): List recent conversation sessions in the current workspace with stable ids, titles, dates, and summaries. Use for browsing past tasks or when user asks to see history without specific search terms. No keywords needed — returns chronological list.\n- **session_detail**(session_id, limit, offset, include_inactive) [capability_expand category: unclassified]: Read a paginated persisted transcript for one past conversation session in the current workspace. Use after session_list or session_search returns a session_id.\n- **click**(element_index) [capability_expand category: desktop]: Click a UI element by its element_index (from the latest observe_ui snapshot)\n- **get_clipboard**() [capability_expand category: desktop]: Read current clipboard text content\n- **list_apps**(filter, running_only) [capability_expand category: desktop]: List available applications on this system. Use to discover correct bundle_id before switch_app.\n- **list_windows**() [capability_expand category: desktop]: List all top-level windows with pid, window_id, title, and per-window state (minimized, on-screen). Call this first to pick the pid and window_id that observe_ui and other window tools require.\n- **observe_ui**(pid, window_id, query) [capability_expand category: desktop]: Snapshot one window's actionable UI elements, each tagged with an element_index for click/right_click/read_text. Re-observe after actions — indices belong to one snapshot. Requires the window's pid and window_id from list_windows.\n- **open_url**(url, app_id) [capability_expand category: desktop]: Open a URL in the default or specified browser\n- **read_text**(element_index) [capability_expand category: desktop]: Read the text content of a specific UI element from the latest snapshot\n- **right_click**(element_index) [capability_expand category: desktop]: Right-click a UI element to open its context menu. Returns visible menu items.\n- **screenshot**(pid, window_id) [capability_expand category: desktop]: Capture a screenshot for visual verification. With pid + window_id captures that window (works across all displays); defaults to the last observed window, or the full desktop when no window has been observed.\n- **scroll**(element_index, direction, amount, pid, window_id) [capability_expand category: desktop]: Scroll a scrollable area of a window. Omit element_index to scroll the window's focused/page scroller; pass one to scroll an exact element from the latest snapshot.\n- **select_text**(element_index) [capability_expand category: desktop]: Select all text in a UI element (focus + select-all, for subsequent copy)\n- **set_clipboard**(text) [capability_expand category: desktop]: Write text to the clipboard\n- **shortcut**(keys) [capability_expand category: desktop]: Execute a keyboard shortcut\n- **switch_app**(app_id) [capability_expand category: desktop]: Switch to an app (launch if needed, activate, verify)\n- **type_text**(text) [capability_expand category: desktop]: Type text into the currently focused element\n- **wait**(seconds) [capability_expand category: desktop]: Wait for a specified duration before continuing\n- **wait_until**(condition, pid, window_id, timeout, poll_interval) [capability_expand category: desktop]: Wait until a UI condition is met (polls UI tree). Returns elements when found or on timeout.\n- **wait_until_stable**(timeout, poll_interval, pid, window_id) [capability_expand category: desktop]: Wait until the UI stops changing (element set stabilizes across polls).\n\n## App Connector Capability Index\nLeapFlow can onboard and manage external apps through `platform_connect` and execute exact registered business actions through `platform_action`.\nFor requests about connecting, setting up, configuring, enabling, or managing a supported app, use `platform_connect` first instead of generating SDK/Webhook sample code.\n`platform_connect.action` is the management namespace: list, guide, preflight, connect, disconnect, remove, status, events_start, events_stop, events_status.\n`platform_action.action` is only for exact registered platform business actions listed below, such as `im.send_message`; never use management actions like `list` or `guide` there.\nAll business fields MUST go inside `payload`; top-level keys are only `platform`, `action`, and `payload`.\nDo not invent platform IDs or platform action names. If the needed action is not listed, ask for discovery/clarification instead of guessing.\nUse `platform_connect` with `action='guide'` and the matching `platform` to start onboarding; use `action='list'` when the app is unclear.\nWhen a pending onboarding state is present, continue from that state with `platform_connect` instead of asking the user to restate the app.\nSupported apps:\n- `api_server`: API Server (OpenAI Compatible) (category=api; backend=adapter; platform_action actions=none registered)\n- `dingtalk`: 钉钉 (DingTalk) (category=im; backend=adapter; platform_action actions=none registered)\n- `feishu`: 飞书 (Feishu/Lark) (category=im; backend=cli)\n - `calendar.create_event` payload={summary*, start_time*, end_time*, attendees} [write/high]\n - `docs.create_markdown` payload={title*, markdown*, folder_token} [write/medium]\n - `drive.search` payload={query*, limit} [read/medium]\n - `im.add_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.download_resource` payload={message_id*, file_key*, type*} [read/low]\n - `im.get_messages` payload={message_ids*} [read/low]\n - `im.list_chats` payload={page_size} [read/low]\n - `im.list_messages` payload={chat_id*} [read/low]\n - `im.list_thread_messages` payload={thread*} [read/low]\n - `im.remove_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.reply_message` payload={message_id*, text*} [send/high]\n - `im.search_chats` payload={query*, page_size} [read/low]\n - `im.search_messages` payload={query*} [read/medium]\n - `im.send_message` payload={chat_id*, text*, thread_id} [send/high]\n - `im.update_card` payload={token*, card*} [write/medium]\n - `im.update_message` payload={message_id*, text*} [write/medium]\n - `mail.search_unread` payload={query, limit} [read/high]\n - `sheets.append_row` payload={spreadsheet_token*, sheet_id*, values*} [write/medium]\n - `task.create` payload={title*, description, due_time} [write/medium]\n- `telegram`: Telegram (category=social; backend=adapter; platform_action actions=none registered)\n- `webhook`: Webhook (Generic) (category=webhook; backend=adapter; platform_action actions=none registered)\n\n## Tool Usage\nTools are normally invoked through the native function-calling mechanism, not by writing JSON in your reply\ntext. Only if the provider signals that native function calling is unavailable for this turn, fall back to a\nsingle JSON code block: `{\"name\": \"tool_name\", \"arguments\": {\"key\": \"value\"}}` — use this fallback format\nonly, never both. Only call a tool whose exact name appears in the tool index above and is currently callable\n(or reachable via `capability_expand`). Never invent, rename, alias, or guess a tool name, platform ID, or\nplatform action from argument shape or wording — if the index does not list it, it does not exist.\n`platform_connect.action` (list/guide/preflight/connect/disconnect/remove/status/events_start/events_stop/\nevents_status) is the App Connector management namespace; `platform_action.action` only accepts exact\nregistered business actions such as `im.send_message` shown in the App Connector Capability Index — never mix\nthe two namespaces. If a tool call returns an unknown/unavailable result, use the returned suggestions or\navailable names for a single retry instead of trying further variations of the same guess.\n\n**Side-effect action rule** (`platform_action` with effect=send/write/execute):\n- Call each unique action+payload **exactly once**. Never include duplicates in the same turn.\n- Once the result returns `\"completed\": true`, that action is DONE for this task. Do NOT call it again\n in any subsequent turn — immediately summarize the result for the user instead.\n- The system enforces idempotency: duplicate calls are blocked and will not execute.\n- If the user explicitly requests sending/writing multiple times, use distinct payloads per call.\n\n**Resource identifier provenance rule**:\n- NEVER fabricate, guess, or infer resource identifiers (chat_id, message_id, file_key, user_id, etc.).\n Every resource ID used in a side-effect action MUST come from a successful API response in this session.\n- If a read/list action fails (e.g. authorization error), you do NOT have valid resource IDs.\n Report the failure to the user — do NOT attempt the dependent write/send action with a guessed ID.\n- When a tool result contains `\"llm_instruction\"`, follow it exactly.\n\n## Guidelines\n1. **Direct answers first**: If you already know the answer, respond directly without tools.\n2. **Avoid redundant tool calls**: Do not call the same tool with the same arguments more than once in the same user turn. When an existing tool result already answers the user's request, stop calling tools and answer directly.\n3. **Use tools proactively**: When the user asks about files, time, system state, or needs actions performed, use the appropriate tool.\n4. **Chain tools when needed**: You can call multiple tools in sequence (e.g., list files → read file → summarize).\n5. **Handle failures gracefully**: If a tool fails, explain what went wrong and suggest alternatives. If it failed because of an unknown tool/platform/action name, retry once with an exact name from the returned suggestions, then explain rather than keep guessing.\n6. **Summarize results naturally**: After tool execution, synthesize the results into a helpful answer rather than dumping raw output.\n7. **Stay conversational**: Maintain a natural, helpful tone. Acknowledge context from earlier in the conversation.\n8. **Recall past work**:\n - Broad queries (\"之前做了什么\", \"列出任务\"): answer from the \"Recent Task History\" section already in your context. If insufficient, call session_list.\n - Specific lookups (\"上次那个配置怎么改的\"): call session_search with relevant phrases (NOT single characters).\n - Do NOT call search tools repeatedly with keyword fragments. One well-phrased call is better than ten fragmented ones.\n\n## Coding & Verification\nWhen working with code, prefer the precise built-in tools over ad-hoc shell: use `repo_map` to orient in an\nunfamiliar project, `code_search`/`file_find` to locate and `code_intel` for symbols, `edit_file` (anchored\nsearch-replace, or a unified `diff`) to change files — never rewrite a whole file to change a few lines — and\n`git_query` to inspect diffs/log. After edits, check `syntax_ok` in the result and run `test_run`/`lint_check`\nbefore declaring the work done. Batch independent read-only calls (search/read/query) into a single turn — they\nrun in parallel.\n\n## Presentation Style\n1. **Polished Markdown only**: Format user-facing answers with clean Markdown headings, short paragraphs, and concise bullets. Use tables only when they improve comparison or scanning.\n2. **Terminal-friendly layout**: Keep lines readable in a TUI; avoid dense walls of text, deeply nested lists, oversized ASCII art, or heavy visual blocks.\n3. **Elegant emphasis**: Use bold text sparingly for key terms and conclusions. Avoid excessive emojis, decorative symbols, repeated separators, or visual noise.\n4. **Theme-safe colors**: Do not emit ANSI escape codes, HTML color tags, Rich markup, or hardcoded color names. Rely on the TUI theme to render Markdown professionally.\n5. **No leaked tool protocol**: Never show tool-call JSON, internal schemas, raw observations, tool result payloads, or hidden reasoning in the final answer unless the user explicitly asks for raw/debug output. Treat any prior `{\"name\": ..., \"arguments\": ...}` blocks and `Tool result (...)` messages as internal execution context only.\n6. **Professional closure**: End with a concise conclusion or next step when helpful; avoid rambling after the useful answer is complete.\n\nWhen finished with all tool calls, respond normally without a JSON block, tool-call transcript, or process log.\n\n## Task Contract\n- Task ID: turn-1\n- Original user request: Let me show you something.\n- Workspace root: \n- Allowed roots: \n- Treat relative project paths as relative to the workspace root; never infer `.` as the project root when a workspace root is provided.\n- Workspace boundary is enforced by tools: do not read, search, edit, or run commands against paths outside the allowed roots unless the user explicitly requests an external path and the tool/approval policy permits it.\n- LeapFlow workspace config is optional at `/.leapflow/config.yaml`; runtime config is loaded from `~/.leapflow/config/user.yaml` and `~/.leapflow/profiles//config/*.yaml`.\n- Preserve this task contract across summarization, compression, tool loops, and memory retrieval.\n" + }, + { + "role": "system", + "content": "\n## Recent Session Summary\n- [user] Let me show you something.\n" + }, + { + "role": "user", + "content": "Let me show you something.\nLet me show you something." + } + ], + "tools": [ + "capability_expand", + "code_intel", + "code_search", + "config_get", + "config_list", + "env_info", + "file_find", + "file_list", + "file_read", + "git_query", + "lint_check", + "memory_search", + "plugin_list", + "plugin_propose", + "plugin_status", + "plugin_versions", + "repo_map", + "research_note", + "runtime_snapshot", + "schedule_reentry", + "self_describe", + "session_list", + "skill_view", + "skills_list", + "terminal_list", + "terminal_read", + "test_run", + "text_search", + "time_get", + "tool_describe", + "tool_search", + "web_fetch" + ] + }, + "responses": [ + { + "status": 200, + "content_type": "application/json", + "body": "{\"id\": \"chatcmpl-cassette\", \"object\": \"chat.completion\", \"model\": \"cassette-model\", \"choices\": [{\"index\": 0, \"message\": {\"role\": \"assistant\", \"content\": \"Noted the first step.\"}, \"finish_reason\": \"stop\"}], \"usage\": {\"prompt_tokens\": 64, \"completion_tokens\": 16, \"total_tokens\": 80}}" + } + ] +} diff --git a/tests/_fixtures/cassettes/r5_learning/cassette-model-4be3dad592bb7457.cassette.json b/tests/_fixtures/cassettes/r5_learning/cassette-model-4be3dad592bb7457.cassette.json deleted file mode 100644 index 0cdd1338..00000000 --- a/tests/_fixtures/cassettes/r5_learning/cassette-model-4be3dad592bb7457.cassette.json +++ /dev/null @@ -1,67 +0,0 @@ -{ - "fingerprint": "4be3dad592bb74572d86d07ec58737e5b26a4a32c7f0d725dd3e1a979f00b077", - "note": "captured in seed mode", - "request": { - "model": "cassette-model", - "stream": false, - "messages": [ - { - "role": "system", - "content": "You are LeapFlow, an intelligent assistant that can both converse naturally and take real actions on the user's computer.\n\n## Capabilities\nThe tool index below lists **every** registered tool by name and a one-line summary — this is the complete\ncapability contract; nothing else exists. Only a subset is directly callable this turn (via native tool calling,\nnot a JSON block in your reply). If you need a tool from the index that is not yet callable, call\n`capability_expand` with its category name first — the matching tools become callable immediately after.\nWhen the user asks what LeapFlow itself supports, whether it supports plugins, or which runtime capabilities\nare available, use the live capability evidence exposed by `plugin_list` before making capability claims; report\nconfiguration-dependent or unavailable capabilities as limitations instead of inferring from documentation.\n- **text_search**(text, pattern): Search for a regex pattern in text.\n- **text_replace**(text, old, new, count) [capability_expand category: write]: Replace occurrences of a substring in text.\n- **time_get**(): Get current date and time.\n- **env_info**(): Get system environment information (OS, Python version, cwd).\n- **skills_list**(query, category, source): List available learned skills. Use when user asks about capabilities or you need a specific skill.\n- **skill_view**(name): View the full content of a specific skill document.\n- **code_intel**(path, operation): Precise document symbols (outline) for a source file: classes, functions, and methods with line ranges. Python uses an exact AST parse; other languages use a keyword-prefix scan. Prefer over file_read mode=symbols for accurate navigation before editing. Read-only.\n- **repo_map**(path): Compact project orientation for a repository root: languages, detected test/lint commands, top-level structure, entry points, manifest, and VCS branch. Call this first when entering an unfamiliar codebase. Read-only.\n- **scm_sync**(action, cwd, remote, pull_ref, push_ref, timeout) [capability_expand category: scm]: Run a typed git SCM action. Use this instead of shell_run for git pull/push/status. For 'pull origin main then push', set action='pull_then_push', remote='origin', pull_ref='main', and omit push_ref so LeapFlow pushes the current local branch.\n- **git_query**(action, cwd, ref, path, staged, max_count, stat): Read-only structured git inspection: action=diff|log|status|branch|show. Prefer over shell_run for reading repo state — output is clipped, redacted, and log/branch are parsed into structured fields. Use scm_sync for pull/push.\n- **git_write**(action, cwd, message, stage_all, name, ref, create) [capability_expand category: scm]: Mutating git actions: action=commit (message, stage_all), branch (create+switch), checkout (switch; create=true for -b). Approval-gated. Use scm_sync for pull/push and git_query for reads.\n- **test_run**(command, cwd, timeout): Run the project's test suite and return structured results (framework, passed/failed counts, failing tests). Auto-detects the runner (pytest/npm/go/cargo) or uses a configured/explicit command; executes via the governed shell. ok=true means the runner executed — see 'success' for pass/fail.\n- **lint_check**(command, cwd, timeout): Run the project's linter and return a structured clean/issue result. Auto-detects the linter (ruff/eslint/go vet/clippy) or uses a configured/explicit command; executes via the governed shell. ok=true means the linter ran — see 'clean'.\n- **file_list**(path, pattern, depth): List files and directories at a given path. Use depth=1 or depth=2 to get a recursive tree in one call instead of listing each sub-directory separately.\n- **file_read**(path, max_lines, start_line, max_chars, mode): Read text file content with adaptive context governance. For large or unfamiliar files, prefer mode='outline' or mode='symbols' first, then use mode='raw' with start_line/max_lines for the specific range you actually need. For LeapFlow's own settings, use config_list / config_get / config_set — its config files are outside the workspace and not readable here.\n- **file_write**(path, content, mode) [capability_expand category: write]: Write content to a file (overwrite or append).\n- **code_search**(pattern, patterns, path, glob, ignore_case, multiline, max_results, context_lines): Search file CONTENTS by regex pattern across a directory tree (ripgrep-backed). Requires a regex pattern. NOT for listing or browsing directory contents — use file_list for that. Prefer this over shell_run grep: faster, skips VCS/dependency/build dirs, and returns structured path:line:column matches. Batch related lookups into ONE call via `patterns` (OR-combined, single pass) instead of issuing several separate searches. Use file_read for the surrounding context of a hit.\n- **file_find**(glob, path, max_results): Find files by a recursive glob pattern under a base path (e.g. '**/test_*.py' or '*.md'). Prefer this over shell_run find; skips VCS/dependency/build dirs.\n- **edit_file**(path, edits, dry_run, diff) [capability_expand category: write]: Apply targeted, anchored search-replace edits to an EXISTING text file (use file_write to create/overwrite). Each edit is {original_text, new_text, replace_all?}; original_text must match exactly and uniquely (or set replace_all) — a non-unique or missing anchor is rejected so files are never corrupted. Set dry_run to preview. Alternatively pass a unified 'diff' to apply its hunks as anchored edits. Far cheaper and safer than rewriting a whole file.\n- **shell_run**(command, cwd, timeout) [capability_expand category: shell]: Execute a one-shot shell command with timeout protection. Runs in the active workspace; paths resolving outside it are refused. Reach for a structured tool first when one fits — web_fetch for anything over HTTP(S), git_query/scm_sync for git, code_search/file_find/file_read for the repo, config_get/config_set for LeapFlow's own settings — because those report typed results, while a failed shell command can only be diagnosed from its exit code and stderr. Every shell run counts as an external side effect, so a failure stops the rest of the batch and is not retried automatically.\n- **terminal_open**(command, cwd, shell) [capability_expand category: terminal]: Open a PERSISTENT shell session (REPL/dev server/watch), returning a session_id for terminal_send/read/close. Disabled unless tools.terminal_session_enabled is set. For one-shot commands use shell_run instead.\n- **terminal_send**(session_id, input, wait) [capability_expand category: terminal]: Send a line of input to a persistent terminal session and return output captured shortly after.\n- **terminal_read**(session_id, wait): Drain buffered output from a persistent terminal session (optionally waiting briefly first).\n- **terminal_close**(session_id) [capability_expand category: terminal]: Terminate a persistent terminal session and release its process group.\n- **terminal_list**(): List active persistent terminal sessions.\n- **config_list**(category, limit): List LeapFlow's own writable settings (LLM model and endpoint, daemon, memory, perception, gateway, …) with current values. Use this to discover the exact key before changing anything. Optionally narrow by `category`. This is the only correct way to inspect LeapFlow configuration — never read config files from disk.\n- **config_get**(key): Read one LeapFlow setting by key (e.g. 'llm.model', 'llm.base_url', 'daemon.log_level'), returning its current value, type, scopes, and whether a change needs a daemon restart. There is no 'llm.provider' key: provider behavior is inferred from the OpenAI-compatible 'llm.base_url'. Never read LeapFlow config files from disk — use this.\n- **config_set**(key, value, scope) [capability_expand category: config]: Change one LeapFlow setting by key, e.g. switch an OpenAI-compatible LLM with key='llm.model' and key='llm.base_url'. There is no 'llm.provider' key: provider behavior is inferred from the endpoint. Values are validated and coerced; credentials are stored in the vault automatically. Call config_list or config_get first if unsure of the key. The result states whether a `leap daemon restart` is required. Never edit LeapFlow config files directly.\n- **web_fetch**(url, select, timeout, max_bytes): Read a URL over HTTP(S) and get back extracted, context-sized content: parsed JSON for API endpoints, readable text plus links for web pages. Use this for anything on the internet — prices, docs, releases, articles — instead of running curl through shell_run: it reports real HTTP status codes, retries rate limits on its own, and is a plain read so a retry is always safe. For JSON APIs pass `select` with a dotted path (e.g. 'chart.result.0.meta') to return just that part instead of the whole payload.\n- **memory_search**(query, limit): Search agent memory for relevant past experiences, observations, and facts.\n- **memory_add**(content, kind) [capability_expand category: write]: Store a new observation or insight in memory for future reference.\n- **research_note**(kind, text): Record a compact, structured note about the current task's state so it survives context compression on long / multi-step tasks. Use for durable findings, open questions still to resolve, decisions / excluded paths, and the immediate next step. One concise sentence per note.\n- **capability_expand**(category): Fetch the full callable schema for every tool in a capability category (e.g. 'hub', 'gateway', 'desktop', 'delegate', 'file', 'memory', 'skill'). The compact tool index always lists every registered tool by name and a one-line summary, but only a static low-risk subset is directly callable each turn. If you need a tool from the index that is not yet callable, call capability_expand with its category first; the matching tools become callable in this turn. Never invent a tool name — expand the category instead.\n- **delegate_task**(goal, context) [capability_expand category: delegate]: Delegate a complex sub-task to an isolated subagent. The subagent gets a fresh context and restricted tool access. Use when a task is self-contained and can be solved independently.\n- **schedule_reentry**(kind, reason, delay_seconds, event_match, max_reentries, deadline_seconds): Register a re-entry so this task can resume later from its current orientation (findings / open questions / next step). Use when work must pause and continue after a delay (kind=time) or when a matching platform event arrives (kind=event), instead of finishing now. The research-ledger state is carried over automatically.\n- **hub_push**(skill_name, visibility, version) [capability_expand category: hub]: Push a local skill to the ModelScope Hub for sharing or backup.\n- **hub_pull**(repo_id, version) [capability_expand category: hub]: Pull a skill from the ModelScope Hub to install locally.\n- **hub_search**(query) [capability_expand category: hub]: Search for skills on the Hub by keyword or description.\n- **hub_sync**(mode, dry_run) [capability_expand category: hub]: Preview or execute sync between local skills and Hub.\n- **platform_action**(platform, action, payload, backend_kind) [capability_expand category: gateway]: Execute an exact registered business action on an external platform through LeapFlow's App Connector layer. Actions must be copied from the App Connector Capability Index and are addressed as domain.operation, e.g. im.send_message or docs.create_markdown. All business fields (chat_id, text, query, etc.) MUST be placed inside `payload`, never at the top level. Example: {\"platform\":\"feishu\",\"action\":\"im.send_message\",\"payload\":{\"chat_id\":\"oc_xxx\",\"text\":\"hello\"}}. Do not invent action names, do not use management actions such as list/guide/connect/status here.\n- **platform_connect**(action, platform, credentials, options, checkpoint) [capability_expand category: gateway]: List, guide, connect, disconnect, remove, or check status for external platforms using the App Connector management namespace. Supports REST and CLI backends. Use this for management actions such as list/guide/preflight/connect/status; use platform_action only for exact registered business actions.\n- **gateway_send**(platform, chat_id, text, thread_id) [capability_expand category: gateway]: Send a message to a connected external platform (Feishu group, Telegram chat, DingTalk conversation, etc.). Requires the platform to be connected via gateway_connect first. Use gateway_connect with action='list' to see connected platforms and available chat IDs.\n- **gateway_connect**(action, platform, credentials, options) [capability_expand category: gateway]: Connect, configure, or manage external platform integrations (Feishu, DingTalk, Telegram, Slack, Discord, etc.). Conversational flow: 1) call 'guide' to get setup steps + required fields, 2) present the steps to the user and ask for ALL required credentials in a single message, 3) call 'connect' with the credentials. Goal: complete in 1–2 user turns. NEVER include credential values in your text response.\n- **plugin_list**(): List the live plugin registry and cross-subsystem capability evidence. Use this before answering questions about whether LeapFlow supports plugins, self-evolution, plugin installation, hot reload, versioning, or other runtime capabilities.\n- **plugin_status**(plugin_id): Get detailed status of a specific plugin: its declared category, runtime dependencies, contributed tools, and fiber lifecycle state.\n- **plugin_versions**(plugin_id): List recorded source versions and active pointer for a profile-scoped plugin.\n- **plugin_propose**(requested_capability, plugin_id, proposed_tools, test_cases, risk_level, evidence): Create a side-effect-free PluginProposal from explicit capability-gap evidence. Use this before plugin_generate when a missing capability should be reviewed. Does not call an LLM, write files, or install anything.\n- **assess_compatibility**(manifest, source_path) [capability_expand category: plugin_management]: Assess whether a foreign plugin manifest or real DSH source bundle is compatible with LeapFlow. A source bundle assessment is static: runtime_ready stays false until restricted Node discovery during plugin_install. Returns component-level verdicts and limitations.\n- **plugin_generate**(plugin_id, description, proposal_id) [capability_expand category: system]: Generate a new ToolPlugin from a natural-language capability description. The LLM produces code that conforms to the ToolPlugin Protocol; it is then rigorously validated (syntax, structure, import, protocol conformance). The generated source is stored in CAS and receives explicit content approval. It DOES NOT install the plugin — installation requires a second, mutation-specific approval via plugin_install.\n- **plugin_install**(plugin_id, code, marketplace_name, source_path, proposal_id, version_label) [capability_expand category: system]: Install a plugin from exactly one source: validated Python code, the configured Python marketplace, or a real DSH source bundle. DSH bundles run restricted Node discovery before registration; only public host tools are installed and client UI limitations are reported. Writes profile-scoped state and mutates the process-global registry. REQUIRES APPROVAL.\n- **plugin_rollback**(plugin_id, version) [capability_expand category: system]: Rollback a profile-scoped plugin to a recorded source version and reload it. REQUIRES APPROVAL.\n- **plugin_reload**(plugin_id, version_label) [capability_expand category: system]: Hot-reload a plugin at runtime. Disposes the old plugin fiber, re-imports its module, and registers a fresh instance. Existing in-flight turns are unaffected (snapshot isolation). REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_disable**(plugin_id) [capability_expand category: system]: Disable a plugin by disposing its fiber, removing its tools from the runtime registry. Cannot disable self_management itself. REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_remove**(plugin_id, delete_source) [capability_expand category: system]: Terminally remove a plugin: dispose its fiber, unregister its tools, remove reload metadata, and optionally delete its profile-scoped source file. Cannot remove self_management itself. REQUIRES APPROVAL.\n- **plugin_unquarantine**(plugin_id) [capability_expand category: plugin_management]: Restore a quarantined plugin to probation status for re-evaluation. Unfreezes the trust ledger, transitions the proposal back to PROBATION, and reloads the plugin. REQUIRES APPROVAL.\n- **plugin_enable**(plugin_id) [capability_expand category: system]: Re-enable a previously disabled plugin by reloading its module and registering a fresh instance. REQUIRES APPROVAL.\n- **self_describe**(facet): Introspect LeapFlow's own identity, capabilities, runtime state, evolution metrics, or platform connections. Use facet='all' only when a comprehensive self-check is explicitly requested.\n- **runtime_snapshot**(): Lightweight (~150 token) flat snapshot of current runtime state: model, context budget, posture, disclosure level, turn count, cache hit rate, uptime, tool count, and pending approvals.\n- **schedule_create**(trigger_expression, instruction, execution_mode, max_retries, delivery_target) [capability_expand category: scheduler]: Create a new scheduled task. Specify a trigger expression (e.g. '30m', 'every 2h', '0 9 * * *') and the instruction to execute on each trigger. Optionally provide an execution mode and a delivery target to receive notifications.\n- **schedule_list**() [capability_expand category: scheduler]: List all scheduled tasks with their current state, trigger, and next due time.\n- **schedule_status**(task_id) [capability_expand category: scheduler]: Get detailed status and recent execution history for a scheduled task.\n- **schedule_pause**(task_id) [capability_expand category: scheduler]: Pause a scheduled task so it stops firing without being cancelled.\n- **schedule_resume**(task_id) [capability_expand category: scheduler]: Resume a paused scheduled task — re-arms it and recalculates next due time.\n- **schedule_cancel**(task_id) [capability_expand category: scheduler]: Cancel a scheduled task permanently.\n- **tool_search**(query, max_results): Search all registered tools by keyword relevance. Returns a ranked list of matching tools with name, category, and summary. Use this when you need a tool but are unsure of its exact name or category.\n- **tool_describe**(tool_name): Get the full callable schema of a single tool by exact name. Returns the tool's description, parameters, and metadata. Use after tool_search to inspect a specific tool before calling it.\n- **session_search**(query, limit) [capability_expand category: unclassified]: Search past conversation sessions in the current workspace for relevant context.\n- **session_list**(limit): List recent conversation sessions in the current workspace with stable ids, titles, dates, and summaries. Use for browsing past tasks or when user asks to see history without specific search terms. No keywords needed — returns chronological list.\n- **session_detail**(session_id, limit, offset, include_inactive) [capability_expand category: unclassified]: Read a paginated persisted transcript for one past conversation session in the current workspace. Use after session_list or session_search returns a session_id.\n- **click**(element_index) [capability_expand category: desktop]: Click a UI element by its element_index (from the latest observe_ui snapshot)\n- **get_clipboard**() [capability_expand category: desktop]: Read current clipboard text content\n- **list_apps**(filter, running_only) [capability_expand category: desktop]: List available applications on this system. Use to discover correct bundle_id before switch_app.\n- **list_windows**() [capability_expand category: desktop]: List all top-level windows with pid, window_id, title, and per-window state (minimized, on-screen). Call this first to pick the pid and window_id that observe_ui and other window tools require.\n- **observe_ui**(pid, window_id, query) [capability_expand category: desktop]: Snapshot one window's actionable UI elements, each tagged with an element_index for click/right_click/read_text. Re-observe after actions — indices belong to one snapshot. Requires the window's pid and window_id from list_windows.\n- **open_url**(url, app_id) [capability_expand category: desktop]: Open a URL in the default or specified browser\n- **read_text**(element_index) [capability_expand category: desktop]: Read the text content of a specific UI element from the latest snapshot\n- **right_click**(element_index) [capability_expand category: desktop]: Right-click a UI element to open its context menu. Returns visible menu items.\n- **screenshot**(pid, window_id) [capability_expand category: desktop]: Capture a screenshot for visual verification. With pid + window_id captures that window (works across all displays); defaults to the last observed window, or the full desktop when no window has been observed.\n- **scroll**(element_index, direction, amount, pid, window_id) [capability_expand category: desktop]: Scroll a scrollable area of a window. Omit element_index to scroll the window's focused/page scroller; pass one to scroll an exact element from the latest snapshot.\n- **select_text**(element_index) [capability_expand category: desktop]: Select all text in a UI element (focus + select-all, for subsequent copy)\n- **set_clipboard**(text) [capability_expand category: desktop]: Write text to the clipboard\n- **shortcut**(keys) [capability_expand category: desktop]: Execute a keyboard shortcut\n- **switch_app**(app_id) [capability_expand category: desktop]: Switch to an app (launch if needed, activate, verify)\n- **type_text**(text) [capability_expand category: desktop]: Type text into the currently focused element\n- **wait**(seconds) [capability_expand category: desktop]: Wait for a specified duration before continuing\n- **wait_until**(condition, pid, window_id, timeout, poll_interval) [capability_expand category: desktop]: Wait until a UI condition is met (polls UI tree). Returns elements when found or on timeout.\n- **wait_until_stable**(timeout, poll_interval, pid, window_id) [capability_expand category: desktop]: Wait until the UI stops changing (element set stabilizes across polls).\n\n## App Connector Capability Index\nLeapFlow can onboard and manage external apps through `platform_connect` and execute exact registered business actions through `platform_action`.\nFor requests about connecting, setting up, configuring, enabling, or managing a supported app, use `platform_connect` first instead of generating SDK/Webhook sample code.\n`platform_connect.action` is the management namespace: list, guide, preflight, connect, disconnect, remove, status, events_start, events_stop, events_status.\n`platform_action.action` is only for exact registered platform business actions listed below, such as `im.send_message`; never use management actions like `list` or `guide` there.\nAll business fields MUST go inside `payload`; top-level keys are only `platform`, `action`, and `payload`.\nDo not invent platform IDs or platform action names. If the needed action is not listed, ask for discovery/clarification instead of guessing.\nUse `platform_connect` with `action='guide'` and the matching `platform` to start onboarding; use `action='list'` when the app is unclear.\nWhen a pending onboarding state is present, continue from that state with `platform_connect` instead of asking the user to restate the app.\nSupported apps:\n- `api_server`: API Server (OpenAI Compatible) (category=api; backend=adapter; platform_action actions=none registered)\n- `dingtalk`: 钉钉 (DingTalk) (category=im; backend=adapter; platform_action actions=none registered)\n- `feishu`: 飞书 (Feishu/Lark) (category=im; backend=cli)\n - `calendar.create_event` payload={summary*, start_time*, end_time*, attendees} [write/high]\n - `docs.create_markdown` payload={title*, markdown*, folder_token} [write/medium]\n - `drive.search` payload={query*, limit} [read/medium]\n - `im.add_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.download_resource` payload={message_id*, file_key*, type*} [read/low]\n - `im.get_messages` payload={message_ids*} [read/low]\n - `im.list_chats` payload={page_size} [read/low]\n - `im.list_messages` payload={chat_id*} [read/low]\n - `im.list_thread_messages` payload={thread*} [read/low]\n - `im.remove_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.reply_message` payload={message_id*, text*} [send/high]\n - `im.search_chats` payload={query*, page_size} [read/low]\n - `im.search_messages` payload={query*} [read/medium]\n - `im.send_message` payload={chat_id*, text*, thread_id} [send/high]\n - `im.update_card` payload={token*, card*} [write/medium]\n - `im.update_message` payload={message_id*, text*} [write/medium]\n - `mail.search_unread` payload={query, limit} [read/high]\n - `sheets.append_row` payload={spreadsheet_token*, sheet_id*, values*} [write/medium]\n - `task.create` payload={title*, description, due_time} [write/medium]\n- `telegram`: Telegram (category=social; backend=adapter; platform_action actions=none registered)\n- `webhook`: Webhook (Generic) (category=webhook; backend=adapter; platform_action actions=none registered)\n\n## Tool Usage\nTools are normally invoked through the native function-calling mechanism, not by writing JSON in your reply\ntext. Only if the provider signals that native function calling is unavailable for this turn, fall back to a\nsingle JSON code block: `{\"name\": \"tool_name\", \"arguments\": {\"key\": \"value\"}}` — use this fallback format\nonly, never both. Only call a tool whose exact name appears in the tool index above and is currently callable\n(or reachable via `capability_expand`). Never invent, rename, alias, or guess a tool name, platform ID, or\nplatform action from argument shape or wording — if the index does not list it, it does not exist.\n`platform_connect.action` (list/guide/preflight/connect/disconnect/remove/status/events_start/events_stop/\nevents_status) is the App Connector management namespace; `platform_action.action` only accepts exact\nregistered business actions such as `im.send_message` shown in the App Connector Capability Index — never mix\nthe two namespaces. If a tool call returns an unknown/unavailable result, use the returned suggestions or\navailable names for a single retry instead of trying further variations of the same guess.\n\n**Side-effect action rule** (`platform_action` with effect=send/write/execute):\n- Call each unique action+payload **exactly once**. Never include duplicates in the same turn.\n- Once the result returns `\"completed\": true`, that action is DONE for this task. Do NOT call it again\n in any subsequent turn — immediately summarize the result for the user instead.\n- The system enforces idempotency: duplicate calls are blocked and will not execute.\n- If the user explicitly requests sending/writing multiple times, use distinct payloads per call.\n\n**Resource identifier provenance rule**:\n- NEVER fabricate, guess, or infer resource identifiers (chat_id, message_id, file_key, user_id, etc.).\n Every resource ID used in a side-effect action MUST come from a successful API response in this session.\n- If a read/list action fails (e.g. authorization error), you do NOT have valid resource IDs.\n Report the failure to the user — do NOT attempt the dependent write/send action with a guessed ID.\n- When a tool result contains `\"llm_instruction\"`, follow it exactly.\n\n## Guidelines\n1. **Direct answers first**: If you already know the answer, respond directly without tools.\n2. **Avoid redundant tool calls**: Do not call the same tool with the same arguments more than once in the same user turn. When an existing tool result already answers the user's request, stop calling tools and answer directly.\n3. **Use tools proactively**: When the user asks about files, time, system state, or needs actions performed, use the appropriate tool.\n4. **Chain tools when needed**: You can call multiple tools in sequence (e.g., list files → read file → summarize).\n5. **Handle failures gracefully**: If a tool fails, explain what went wrong and suggest alternatives. If it failed because of an unknown tool/platform/action name, retry once with an exact name from the returned suggestions, then explain rather than keep guessing.\n6. **Summarize results naturally**: After tool execution, synthesize the results into a helpful answer rather than dumping raw output.\n7. **Stay conversational**: Maintain a natural, helpful tone. Acknowledge context from earlier in the conversation.\n8. **Recall past work**:\n - Broad queries (\"之前做了什么\", \"列出任务\"): answer from the \"Recent Task History\" section already in your context. If insufficient, call session_list.\n - Specific lookups (\"上次那个配置怎么改的\"): call session_search with relevant phrases (NOT single characters).\n - Do NOT call search tools repeatedly with keyword fragments. One well-phrased call is better than ten fragmented ones.\n\n## Coding & Verification\nWhen working with code, prefer the precise built-in tools over ad-hoc shell: use `repo_map` to orient in an\nunfamiliar project, `code_search`/`file_find` to locate and `code_intel` for symbols, `edit_file` (anchored\nsearch-replace, or a unified `diff`) to change files — never rewrite a whole file to change a few lines — and\n`git_query` to inspect diffs/log. After edits, check `syntax_ok` in the result and run `test_run`/`lint_check`\nbefore declaring the work done. Batch independent read-only calls (search/read/query) into a single turn — they\nrun in parallel.\n\n## Presentation Style\n1. **Polished Markdown only**: Format user-facing answers with clean Markdown headings, short paragraphs, and concise bullets. Use tables only when they improve comparison or scanning.\n2. **Terminal-friendly layout**: Keep lines readable in a TUI; avoid dense walls of text, deeply nested lists, oversized ASCII art, or heavy visual blocks.\n3. **Elegant emphasis**: Use bold text sparingly for key terms and conclusions. Avoid excessive emojis, decorative symbols, repeated separators, or visual noise.\n4. **Theme-safe colors**: Do not emit ANSI escape codes, HTML color tags, Rich markup, or hardcoded color names. Rely on the TUI theme to render Markdown professionally.\n5. **No leaked tool protocol**: Never show tool-call JSON, internal schemas, raw observations, tool result payloads, or hidden reasoning in the final answer unless the user explicitly asks for raw/debug output. Treat any prior `{\"name\": ..., \"arguments\": ...}` blocks and `Tool result (...)` messages as internal execution context only.\n6. **Professional closure**: End with a concise conclusion or next step when helpful; avoid rambling after the useful answer is complete.\n\nWhen finished with all tool calls, respond normally without a JSON block, tool-call transcript, or process log.\n\n## Task Contract\n- Task ID: turn-2\n- Original user request: Now sort them by month.\n- Workspace root: \n- Allowed roots: \n- Treat relative project paths as relative to the workspace root; never infer `.` as the project root when a workspace root is provided.\n- Workspace boundary is enforced by tools: do not read, search, edit, or run commands against paths outside the allowed roots unless the user explicitly requests an external path and the tool/approval policy permits it.\n- LeapFlow workspace config is optional at `/.leapflow/config.yaml`; runtime config is loaded from `~/.leapflow/config/user.yaml` and `~/.leapflow/profiles//config/*.yaml`.\n- Preserve this task contract across summarization, compression, tool loops, and memory retrieval.\n" - }, - { - "role": "system", - "content": "\n## Recent Session Summary\n- [assistant] Noted the first step.\n- [user] Now sort them by month.\n" - }, - { - "role": "assistant", - "content": "Noted the first step." - }, - { - "role": "user", - "content": "Now sort them by month.\nNow sort them by month." - } - ], - "tools": [ - "capability_expand", - "code_intel", - "code_search", - "config_get", - "config_list", - "env_info", - "file_find", - "file_list", - "file_read", - "git_query", - "lint_check", - "memory_search", - "plugin_list", - "plugin_propose", - "plugin_status", - "plugin_versions", - "repo_map", - "research_note", - "runtime_snapshot", - "schedule_reentry", - "self_describe", - "session_list", - "skill_view", - "skills_list", - "terminal_list", - "terminal_read", - "test_run", - "text_search", - "time_get", - "tool_describe", - "tool_search", - "web_fetch" - ] - }, - "responses": [ - { - "status": 200, - "content_type": "application/json", - "body": "{\"id\": \"chatcmpl-cassette\", \"object\": \"chat.completion\", \"model\": \"cassette-model\", \"choices\": [{\"index\": 0, \"message\": {\"role\": \"assistant\", \"content\": \"Noted the second step.\"}, \"finish_reason\": \"stop\"}], \"usage\": {\"prompt_tokens\": 64, \"completion_tokens\": 16, \"total_tokens\": 80}}" - } - ] -} diff --git a/tests/_fixtures/cassettes/r5_learning/cassette-model-4c8af80689cd92d0.cassette.json b/tests/_fixtures/cassettes/r5_learning/cassette-model-4c8af80689cd92d0.cassette.json new file mode 100644 index 00000000..4da59609 --- /dev/null +++ b/tests/_fixtures/cassettes/r5_learning/cassette-model-4c8af80689cd92d0.cassette.json @@ -0,0 +1,67 @@ +{ + "fingerprint": "4c8af80689cd92d0dd47838f862a258141413d063a90e8c79b30c1e4c5caef4c", + "note": "captured in seed mode", + "request": { + "model": "cassette-model", + "stream": false, + "messages": [ + { + "role": "system", + "content": "You are LeapFlow, an intelligent assistant that can both converse naturally and take real actions on the user's computer.\n\n## Capabilities\nThe tool index below lists **every** registered tool by name and a one-line summary — this is the complete\ncapability contract; nothing else exists. Only a subset is directly callable this turn (via native tool calling,\nnot a JSON block in your reply). If you need a tool from the index that is not yet callable, call\n`capability_expand` with its category name first — the matching tools become callable immediately after.\nWhen the user asks about LeapFlow **itself** — its identity/version/model, what tools, plugins, or capabilities\nit has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of\nself-awareness — answer from live runtime evidence via `self_describe` (facet=capabilities for \"what can you\ndo / do you support X\", identity for version/model, runtime for context/posture, or `all` for a full self-check),\nnever by reading LeapFlow's own source code, documentation, or memory. Report configuration-dependent or\nunavailable capabilities as limitations rather than inferring them.\n- **text_search**(text, pattern): Search for a regex pattern in text.\n- **text_replace**(text, old, new, count) [capability_expand category: write]: Replace occurrences of a substring in text.\n- **time_get**(): Get current date and time.\n- **env_info**(): Get system environment information (OS, Python version, cwd).\n- **skills_list**(query, category, source): List available learned skills. Use when user asks about capabilities or you need a specific skill.\n- **skill_view**(name): View the full content of a specific skill document.\n- **skill_list_files**(name) [capability_expand category: skills]: List all support files in a skill directory (configs, templates, examples). Use before skill_file_read to discover available files.\n- **skill_file_read**(name, file_path) [capability_expand category: skills]: Read an individual support file from a skill directory. Use skill_list_files first to discover available files.\n- **code_intel**(path, operation): Precise document symbols (outline) for a source file: classes, functions, and methods with line ranges. Python uses an exact AST parse; other languages use a keyword-prefix scan. Prefer over file_read mode=symbols for accurate navigation before editing. Read-only.\n- **repo_map**(path): Compact project orientation for a repository root: languages, detected test/lint commands, top-level structure, entry points, manifest, and VCS branch. Call this first when entering an unfamiliar codebase. Read-only.\n- **scm_sync**(action, cwd, remote, pull_ref, push_ref, timeout) [capability_expand category: scm]: Run a typed git SCM action. Use this instead of shell_run for git pull/push/status. For 'pull origin main then push', set action='pull_then_push', remote='origin', pull_ref='main', and omit push_ref so LeapFlow pushes the current local branch.\n- **git_query**(action, cwd, ref, path, staged, max_count, stat): Read-only structured git inspection: action=diff|log|status|branch|show. Prefer over shell_run for reading repo state — output is clipped, redacted, and log/branch are parsed into structured fields. Use scm_sync for pull/push.\n- **git_write**(action, cwd, message, stage_all, name, ref, create) [capability_expand category: scm]: Mutating git actions: action=commit (message, stage_all), branch (create+switch), checkout (switch; create=true for -b). Approval-gated. Use scm_sync for pull/push and git_query for reads.\n- **test_run**(command, cwd, timeout): Run the project's test suite and return structured results (framework, passed/failed counts, failing tests). Auto-detects the runner (pytest/npm/go/cargo) or uses a configured/explicit command; executes via the governed shell. ok=true means the runner executed — see 'success' for pass/fail.\n- **lint_check**(command, cwd, timeout): Run the project's linter and return a structured clean/issue result. Auto-detects the linter (ruff/eslint/go vet/clippy) or uses a configured/explicit command; executes via the governed shell. ok=true means the linter ran — see 'clean'.\n- **file_list**(path, pattern, depth): List files and directories at a given path. Use depth=1 or depth=2 to get a recursive tree in one call instead of listing each sub-directory separately.\n- **file_read**(path, max_lines, start_line, max_chars, mode): Read text file content with adaptive context governance. For large or unfamiliar files, prefer mode='outline' or mode='symbols' first, then use mode='raw' with start_line/max_lines for the specific range you actually need. For LeapFlow's own settings, use config_list / config_get / config_set — its config files are outside the workspace and not readable here.\n- **file_write**(path, content, mode) [capability_expand category: write]: Write content to a file (overwrite or append).\n- **code_search**(pattern, patterns, path, glob, ignore_case, multiline, max_results, context_lines): Search file CONTENTS by regex pattern across a directory tree (ripgrep-backed). Requires a regex pattern. NOT for listing or browsing directory contents — use file_list for that. Prefer this over shell_run grep: faster, skips VCS/dependency/build dirs, and returns structured path:line:column matches. Batch related lookups into ONE call via `patterns` (OR-combined, single pass) instead of issuing several separate searches. Use file_read for the surrounding context of a hit.\n- **file_find**(glob, path, max_results): Find files by a recursive glob pattern under a base path (e.g. '**/test_*.py' or '*.md'). Prefer this over shell_run find; skips VCS/dependency/build dirs.\n- **edit_file**(path, edits, dry_run, diff) [capability_expand category: write]: Apply targeted, anchored search-replace edits to an EXISTING text file (use file_write to create/overwrite). Each edit is {original_text, new_text, replace_all?}; original_text must match exactly and uniquely (or set replace_all) — a non-unique or missing anchor is rejected so files are never corrupted. Set dry_run to preview. Alternatively pass a unified 'diff' to apply its hunks as anchored edits. Far cheaper and safer than rewriting a whole file.\n- **shell_run**(command, cwd, timeout) [capability_expand category: shell]: Execute a one-shot shell command with timeout protection. Runs in the active workspace; paths resolving outside it are refused. Reach for a structured tool first when one fits — web_fetch for anything over HTTP(S), git_query/scm_sync for git, code_search/file_find/file_read for the repo, config_get/config_set for LeapFlow's own settings — because those report typed results, while a failed shell command can only be diagnosed from its exit code and stderr. Every shell run counts as an external side effect, so a failure stops the rest of the batch and is not retried automatically.\n- **terminal_open**(command, cwd, shell) [capability_expand category: terminal]: Open a PERSISTENT shell session (REPL/dev server/watch), returning a session_id for terminal_send/read/close. Disabled unless tools.terminal_session_enabled is set. For one-shot commands use shell_run instead.\n- **terminal_send**(session_id, input, wait) [capability_expand category: terminal]: Send a line of input to a persistent terminal session and return output captured shortly after.\n- **terminal_read**(session_id, wait): Drain buffered output from a persistent terminal session (optionally waiting briefly first).\n- **terminal_close**(session_id) [capability_expand category: terminal]: Terminate a persistent terminal session and release its process group.\n- **terminal_list**(): List active persistent terminal sessions.\n- **config_list**(category, limit): List LeapFlow's own writable settings (LLM model and endpoint, daemon, memory, perception, gateway, …) with current values. Use this to discover the exact key before changing anything. Optionally narrow by `category`. This is the only correct way to inspect LeapFlow configuration — never read config files from disk.\n- **config_get**(key): Read one LeapFlow setting by key (e.g. 'llm.model', 'llm.base_url', 'daemon.log_level'), returning its current value, type, scopes, and whether a change needs a daemon restart. There is no 'llm.provider' key: provider behavior is inferred from the OpenAI-compatible 'llm.base_url'. Never read LeapFlow config files from disk — use this.\n- **config_set**(key, value, scope) [capability_expand category: config]: Change one LeapFlow setting by key, e.g. switch an OpenAI-compatible LLM with key='llm.model' and key='llm.base_url'. There is no 'llm.provider' key: provider behavior is inferred from the endpoint. Values are validated and coerced; credentials are stored in the vault automatically. Call config_list or config_get first if unsure of the key. The result states whether a `leap daemon restart` is required. Never edit LeapFlow config files directly.\n- **web_fetch**(url, select, timeout, max_bytes): Read a URL over HTTP(S) and get back extracted, context-sized content: parsed JSON for API endpoints, readable text plus links for web pages. Use this for anything on the internet — prices, docs, releases, articles — instead of running curl through shell_run: it reports real HTTP status codes, retries rate limits on its own, and is a plain read so a retry is always safe. For JSON APIs pass `select` with a dotted path (e.g. 'chart.result.0.meta') to return just that part instead of the whole payload.\n- **memory_search**(query, limit): Search agent memory for relevant past experiences, observations, and facts.\n- **memory_add**(content, kind) [capability_expand category: write]: Store a new observation or insight in memory for future reference.\n- **research_note**(kind, text): Record a compact, structured note about the current task's state so it survives context compression on long / multi-step tasks. Use for durable findings, open questions still to resolve, decisions / excluded paths, and the immediate next step. One concise sentence per note.\n- **capability_expand**(category): Fetch the full callable schema for every tool in a capability category (e.g. 'hub', 'gateway', 'desktop', 'delegate', 'file', 'memory', 'skill'). The compact tool index always lists every registered tool by name and a one-line summary, but only a static low-risk subset is directly callable each turn. If you need a tool from the index that is not yet callable, call capability_expand with its category first; the matching tools become callable in this turn. Never invent a tool name — expand the category instead.\n- **delegate_task**(goal, context) [capability_expand category: delegate]: Delegate a complex sub-task to an isolated subagent. The subagent gets a fresh context and restricted tool access. Use when a task is self-contained and can be solved independently.\n- **schedule_reentry**(kind, reason, delay_seconds, event_match, max_reentries, deadline_seconds): Register a re-entry so this task can resume later from its current orientation (findings / open questions / next step). Use when work must pause and continue after a delay (kind=time) or when a matching platform event arrives (kind=event), instead of finishing now. The research-ledger state is carried over automatically.\n- **hub_push**(skill_name, visibility, version) [capability_expand category: hub]: Push a local skill to the ModelScope Hub for sharing or backup.\n- **hub_pull**(repo_id, version) [capability_expand category: hub]: Pull a skill from the ModelScope Hub to install locally.\n- **hub_search**(query) [capability_expand category: hub]: Search for skills on the Hub by keyword or description.\n- **hub_sync**(mode, dry_run) [capability_expand category: hub]: Preview or execute sync between local skills and Hub.\n- **platform_action**(platform, action, payload, backend_kind) [capability_expand category: gateway]: Execute an exact registered business action on an external platform through LeapFlow's App Connector layer. Actions must be copied from the App Connector Capability Index and are addressed as domain.operation, e.g. im.send_message or docs.create_markdown. All business fields (chat_id, text, query, etc.) MUST be placed inside `payload`, never at the top level. Example: {\"platform\":\"feishu\",\"action\":\"im.send_message\",\"payload\":{\"chat_id\":\"oc_xxx\",\"text\":\"hello\"}}. Do not invent action names, do not use management actions such as list/guide/connect/status here.\n- **platform_connect**(action, platform, credentials, options, checkpoint) [capability_expand category: gateway]: List, guide, connect, disconnect, remove, or check status for external platforms using the App Connector management namespace. Supports REST and CLI backends. Use this for management actions such as list/guide/preflight/connect/status; use platform_action only for exact registered business actions.\n- **gateway_send**(platform, chat_id, text, thread_id) [capability_expand category: gateway]: Send a message to a connected external platform (Feishu group, Telegram chat, DingTalk conversation, etc.). Requires the platform to be connected via gateway_connect first. Use gateway_connect with action='list' to see connected platforms and available chat IDs.\n- **gateway_connect**(action, platform, credentials, options) [capability_expand category: gateway]: Connect, configure, or manage external platform integrations (Feishu, DingTalk, Telegram, Slack, Discord, etc.). Conversational flow: 1) call 'guide' to get setup steps + required fields, 2) present the steps to the user and ask for ALL required credentials in a single message, 3) call 'connect' with the credentials. Goal: complete in 1–2 user turns. NEVER include credential values in your text response.\n- **plugin_list**(): List the live plugin registry across Tool/Gateway/LLM subsystems for plugin management: which plugins are loaded, their category and fiber lifecycle state. For questions about LeapFlow's own capabilities or whether it supports X, use self_describe instead.\n- **plugin_status**(plugin_id): Get detailed status of a specific plugin: its declared category, runtime dependencies, contributed tools, and fiber lifecycle state.\n- **plugin_versions**(plugin_id): List recorded source versions and active pointer for a profile-scoped plugin.\n- **plugin_propose**(requested_capability, plugin_id, proposed_tools, test_cases, risk_level, evidence): Create a side-effect-free PluginProposal from explicit capability-gap evidence. Use this before plugin_generate when a missing capability should be reviewed. Does not call an LLM, write files, or install anything.\n- **assess_compatibility**(manifest, source_path) [capability_expand category: plugin_management]: Assess whether a foreign plugin manifest or real DSH source bundle is compatible with LeapFlow. A source bundle assessment is static: runtime_ready stays false until restricted Node discovery during plugin_install. Returns component-level verdicts and limitations.\n- **plugin_generate**(plugin_id, description, proposal_id) [capability_expand category: system]: Generate a new ToolPlugin from a natural-language capability description. The LLM produces code that conforms to the ToolPlugin Protocol; it is then rigorously validated (syntax, structure, import, protocol conformance). The generated source is stored in CAS and receives explicit content approval. It DOES NOT install the plugin — installation requires a second, mutation-specific approval via plugin_install.\n- **plugin_install**(plugin_id, code, marketplace_name, source_path, proposal_id, version_label) [capability_expand category: system]: Install a plugin from exactly one source: validated Python code, the configured Python marketplace, or a real DSH source bundle. DSH bundles run restricted Node discovery before registration; only public host tools are installed and client UI limitations are reported. Writes profile-scoped state and mutates the process-global registry. REQUIRES APPROVAL.\n- **plugin_rollback**(plugin_id, version) [capability_expand category: system]: Rollback a profile-scoped plugin to a recorded source version and reload it. REQUIRES APPROVAL.\n- **plugin_reload**(plugin_id, version_label) [capability_expand category: system]: Hot-reload a plugin at runtime. Disposes the old plugin fiber, re-imports its module, and registers a fresh instance. Existing in-flight turns are unaffected (snapshot isolation). REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_disable**(plugin_id) [capability_expand category: system]: Disable a plugin by disposing its fiber, removing its tools from the runtime registry. Cannot disable self_management itself. REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_remove**(plugin_id, delete_source) [capability_expand category: system]: Terminally remove a plugin: dispose its fiber, unregister its tools, remove reload metadata, and optionally delete its profile-scoped source file. Cannot remove self_management itself. REQUIRES APPROVAL.\n- **plugin_unquarantine**(plugin_id) [capability_expand category: plugin_management]: Restore a quarantined plugin to probation status for re-evaluation. Unfreezes the trust ledger, transitions the proposal back to PROBATION, and reloads the plugin. REQUIRES APPROVAL.\n- **plugin_enable**(plugin_id) [capability_expand category: system]: Re-enable a previously disabled plugin by reloading its module and registering a fresh instance. REQUIRES APPROVAL.\n- **self_describe**(facet): Answer questions about LeapFlow ITSELF from live runtime evidence — its identity/version/model, what tools and plugins it has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of self-awareness. Prefer this over reading LeapFlow's own source code. Pick facet=capabilities for 'what can you do / do you support X', identity for version/model, runtime for context/posture, or all for a full self-check when explicitly requested.\n- **runtime_snapshot**(): Lightweight (~150 token) flat snapshot of current runtime state: model, context budget, posture, disclosure level, turn count, cache hit rate, uptime, tool count, and pending approvals.\n- **schedule_create**(trigger_expression, instruction, execution_mode, max_retries, delivery_target) [capability_expand category: scheduler]: Create a new scheduled task. Specify a trigger expression (e.g. '30m', 'every 2h', '0 9 * * *') and the instruction to execute on each trigger. Optionally provide an execution mode and a delivery target to receive notifications.\n- **schedule_list**() [capability_expand category: scheduler]: List all scheduled tasks with their current state, trigger, and next due time.\n- **schedule_status**(task_id) [capability_expand category: scheduler]: Get detailed status and recent execution history for a scheduled task.\n- **schedule_pause**(task_id) [capability_expand category: scheduler]: Pause a scheduled task so it stops firing without being cancelled.\n- **schedule_resume**(task_id) [capability_expand category: scheduler]: Resume a paused scheduled task — re-arms it and recalculates next due time.\n- **schedule_cancel**(task_id) [capability_expand category: scheduler]: Cancel a scheduled task permanently.\n- **tool_search**(query, max_results): Search all registered tools by keyword relevance. Returns a ranked list of matching tools with name, category, and summary. Use this when you need a tool but are unsure of its exact name or category.\n- **tool_describe**(tool_name): Get the full callable schema of a single tool by exact name. Returns the tool's description, parameters, and metadata. Use after tool_search to inspect a specific tool before calling it.\n- **session_search**(query, limit) [capability_expand category: unclassified]: Search past conversation sessions in the current workspace for relevant context.\n- **session_list**(limit): List recent conversation sessions in the current workspace with stable ids, titles, dates, and summaries. Use for browsing past tasks or when user asks to see history without specific search terms. No keywords needed — returns chronological list.\n- **session_detail**(session_id, limit, offset, include_inactive) [capability_expand category: unclassified]: Read a paginated persisted transcript for one past conversation session in the current workspace. Use after session_list or session_search returns a session_id.\n- **click**(element_index) [capability_expand category: desktop]: Click a UI element by its element_index (from the latest observe_ui snapshot)\n- **get_clipboard**() [capability_expand category: desktop]: Read current clipboard text content\n- **list_apps**(filter, running_only) [capability_expand category: desktop]: List available applications on this system. Use to discover correct bundle_id before switch_app.\n- **list_windows**() [capability_expand category: desktop]: List all top-level windows with pid, window_id, title, and per-window state (minimized, on-screen). Call this first to pick the pid and window_id that observe_ui and other window tools require.\n- **observe_ui**(pid, window_id, query) [capability_expand category: desktop]: Snapshot one window's actionable UI elements, each tagged with an element_index for click/right_click/read_text. Re-observe after actions — indices belong to one snapshot. Requires the window's pid and window_id from list_windows.\n- **open_url**(url, app_id) [capability_expand category: desktop]: Open a URL in the default or specified browser\n- **read_text**(element_index) [capability_expand category: desktop]: Read the text content of a specific UI element from the latest snapshot\n- **right_click**(element_index) [capability_expand category: desktop]: Right-click a UI element to open its context menu. Returns visible menu items.\n- **screenshot**(pid, window_id) [capability_expand category: desktop]: Capture a screenshot for visual verification. With pid + window_id captures that window (works across all displays); defaults to the last observed window, or the full desktop when no window has been observed.\n- **scroll**(element_index, direction, amount, pid, window_id) [capability_expand category: desktop]: Scroll a scrollable area of a window. Omit element_index to scroll the window's focused/page scroller; pass one to scroll an exact element from the latest snapshot.\n- **select_text**(element_index) [capability_expand category: desktop]: Select all text in a UI element (focus + select-all, for subsequent copy)\n- **set_clipboard**(text) [capability_expand category: desktop]: Write text to the clipboard\n- **shortcut**(keys) [capability_expand category: desktop]: Execute a keyboard shortcut\n- **switch_app**(app_id) [capability_expand category: desktop]: Switch to an app (launch if needed, activate, verify)\n- **type_text**(text) [capability_expand category: desktop]: Type text into the currently focused element\n- **wait**(seconds) [capability_expand category: desktop]: Wait for a specified duration before continuing\n- **wait_until**(condition, pid, window_id, timeout, poll_interval) [capability_expand category: desktop]: Wait until a UI condition is met (polls UI tree). Returns elements when found or on timeout.\n- **wait_until_stable**(timeout, poll_interval, pid, window_id) [capability_expand category: desktop]: Wait until the UI stops changing (element set stabilizes across polls).\n\n## App Connector Capability Index\nLeapFlow can onboard and manage external apps through `platform_connect` and execute exact registered business actions through `platform_action`.\nFor requests about connecting, setting up, configuring, enabling, or managing a supported app, use `platform_connect` first instead of generating SDK/Webhook sample code.\n`platform_connect.action` is the management namespace: list, guide, preflight, connect, disconnect, remove, status, events_start, events_stop, events_status.\n`platform_action.action` is only for exact registered platform business actions listed below, such as `im.send_message`; never use management actions like `list` or `guide` there.\nAll business fields MUST go inside `payload`; top-level keys are only `platform`, `action`, and `payload`.\nDo not invent platform IDs or platform action names. If the needed action is not listed, ask for discovery/clarification instead of guessing.\nUse `platform_connect` with `action='guide'` and the matching `platform` to start onboarding; use `action='list'` when the app is unclear.\nWhen a pending onboarding state is present, continue from that state with `platform_connect` instead of asking the user to restate the app.\nSupported apps:\n- `api_server`: API Server (OpenAI Compatible) (category=api; backend=adapter; platform_action actions=none registered)\n- `dingtalk`: 钉钉 (DingTalk) (category=im; backend=adapter; platform_action actions=none registered)\n- `feishu`: 飞书 (Feishu/Lark) (category=im; backend=cli)\n - `calendar.create_event` payload={summary*, start_time*, end_time*, attendees} [write/high]\n - `docs.create_markdown` payload={title*, markdown*, folder_token} [write/medium]\n - `drive.search` payload={query*, limit} [read/medium]\n - `im.add_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.download_resource` payload={message_id*, file_key*, type*} [read/low]\n - `im.get_messages` payload={message_ids*} [read/low]\n - `im.list_chats` payload={page_size} [read/low]\n - `im.list_messages` payload={chat_id*} [read/low]\n - `im.list_thread_messages` payload={thread*} [read/low]\n - `im.remove_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.reply_message` payload={message_id*, text*} [send/high]\n - `im.search_chats` payload={query*, page_size} [read/low]\n - `im.search_messages` payload={query*} [read/medium]\n - `im.send_message` payload={chat_id*, text*, thread_id} [send/high]\n - `im.update_card` payload={token*, card*} [write/medium]\n - `im.update_message` payload={message_id*, text*} [write/medium]\n - `mail.search_unread` payload={query, limit} [read/high]\n - `sheets.append_row` payload={spreadsheet_token*, sheet_id*, values*} [write/medium]\n - `task.create` payload={title*, description, due_time} [write/medium]\n- `telegram`: Telegram (category=social; backend=adapter; platform_action actions=none registered)\n- `webhook`: Webhook (Generic) (category=webhook; backend=adapter; platform_action actions=none registered)\n\n## Tool Usage\nTools are normally invoked through the native function-calling mechanism, not by writing JSON in your reply\ntext. Only if the provider signals that native function calling is unavailable for this turn, fall back to a\nsingle JSON code block: `{\"name\": \"tool_name\", \"arguments\": {\"key\": \"value\"}}` — use this fallback format\nonly, never both. Only call a tool whose exact name appears in the tool index above and is currently callable\n(or reachable via `capability_expand`). Never invent, rename, alias, or guess a tool name, platform ID, or\nplatform action from argument shape or wording — if the index does not list it, it does not exist.\n`platform_connect.action` (list/guide/preflight/connect/disconnect/remove/status/events_start/events_stop/\nevents_status) is the App Connector management namespace; `platform_action.action` only accepts exact\nregistered business actions such as `im.send_message` shown in the App Connector Capability Index — never mix\nthe two namespaces. If a tool call returns an unknown/unavailable result, use the returned suggestions or\navailable names for a single retry instead of trying further variations of the same guess.\n\n**Side-effect action rule** (`platform_action` with effect=send/write/execute):\n- Call each unique action+payload **exactly once**. Never include duplicates in the same turn.\n- Once the result returns `\"completed\": true`, that action is DONE for this task. Do NOT call it again\n in any subsequent turn — immediately summarize the result for the user instead.\n- The system enforces idempotency: duplicate calls are blocked and will not execute.\n- If the user explicitly requests sending/writing multiple times, use distinct payloads per call.\n\n**Resource identifier provenance rule**:\n- NEVER fabricate, guess, or infer resource identifiers (chat_id, message_id, file_key, user_id, etc.).\n Every resource ID used in a side-effect action MUST come from a successful API response in this session.\n- If a read/list action fails (e.g. authorization error), you do NOT have valid resource IDs.\n Report the failure to the user — do NOT attempt the dependent write/send action with a guessed ID.\n- When a tool result contains `\"llm_instruction\"`, follow it exactly.\n\n## Guidelines\n1. **Direct answers first**: If you already know the answer, respond directly without tools.\n2. **Avoid redundant tool calls**: Do not call the same tool with the same arguments more than once in the same user turn. When an existing tool result already answers the user's request, stop calling tools and answer directly.\n3. **Use tools proactively**: When the user asks about files, time, system state, or needs actions performed, use the appropriate tool.\n4. **Chain tools when needed**: You can call multiple tools in sequence (e.g., list files → read file → summarize).\n5. **Handle failures gracefully**: If a tool fails, explain what went wrong and suggest alternatives. If it failed because of an unknown tool/platform/action name, retry once with an exact name from the returned suggestions, then explain rather than keep guessing.\n6. **Summarize results naturally**: After tool execution, synthesize the results into a helpful answer rather than dumping raw output.\n7. **Stay conversational**: Maintain a natural, helpful tone. Acknowledge context from earlier in the conversation.\n8. **Recall past work**:\n - Broad queries (\"之前做了什么\", \"列出任务\"): answer from the \"Recent Task History\" section already in your context. If insufficient, call session_list.\n - Specific lookups (\"上次那个配置怎么改的\"): call session_search with relevant phrases (NOT single characters).\n - Do NOT call search tools repeatedly with keyword fragments. One well-phrased call is better than ten fragmented ones.\n\n## Coding & Verification\nWhen working with code, prefer the precise built-in tools over ad-hoc shell: use `repo_map` to orient in an\nunfamiliar project, `code_search`/`file_find` to locate and `code_intel` for symbols, `edit_file` (anchored\nsearch-replace, or a unified `diff`) to change files — never rewrite a whole file to change a few lines — and\n`git_query` to inspect diffs/log. After edits, check `syntax_ok` in the result and run `test_run`/`lint_check`\nbefore declaring the work done. Batch independent read-only calls (search/read/query) into a single turn — they\nrun in parallel.\n\n## Presentation Style\n1. **Polished Markdown only**: Format user-facing answers with clean Markdown headings, short paragraphs, and concise bullets. Use tables only when they improve comparison or scanning.\n2. **Terminal-friendly layout**: Keep lines readable in a TUI; avoid dense walls of text, deeply nested lists, oversized ASCII art, or heavy visual blocks.\n3. **Elegant emphasis**: Use bold text sparingly for key terms and conclusions. Avoid excessive emojis, decorative symbols, repeated separators, or visual noise.\n4. **Theme-safe colors**: Do not emit ANSI escape codes, HTML color tags, Rich markup, or hardcoded color names. Rely on the TUI theme to render Markdown professionally.\n5. **No leaked tool protocol**: Never show tool-call JSON, internal schemas, raw observations, tool result payloads, or hidden reasoning in the final answer unless the user explicitly asks for raw/debug output. Treat any prior `{\"name\": ..., \"arguments\": ...}` blocks and `Tool result (...)` messages as internal execution context only.\n6. **Professional closure**: End with a concise conclusion or next step when helpful; avoid rambling after the useful answer is complete.\n\nWhen finished with all tool calls, respond normally without a JSON block, tool-call transcript, or process log.\n\n## Task Contract\n- Task ID: turn-2\n- Original user request: Now sort them by month.\n- Workspace root: \n- Allowed roots: \n- Treat relative project paths as relative to the workspace root; never infer `.` as the project root when a workspace root is provided.\n- Workspace boundary is enforced by tools: do not read, search, edit, or run commands against paths outside the allowed roots unless the user explicitly requests an external path and the tool/approval policy permits it.\n- LeapFlow workspace config is optional at `/.leapflow/config.yaml`; runtime config is loaded from `~/.leapflow/config/user.yaml` and `~/.leapflow/profiles//config/*.yaml`.\n- Preserve this task contract across summarization, compression, tool loops, and memory retrieval.\n" + }, + { + "role": "system", + "content": "\n## Recent Session Summary\n- [assistant] Noted the first step.\n- [user] Now sort them by month.\n" + }, + { + "role": "assistant", + "content": "Noted the first step." + }, + { + "role": "user", + "content": "Now sort them by month.\nNow sort them by month." + } + ], + "tools": [ + "capability_expand", + "code_intel", + "code_search", + "config_get", + "config_list", + "env_info", + "file_find", + "file_list", + "file_read", + "git_query", + "lint_check", + "memory_search", + "plugin_list", + "plugin_propose", + "plugin_status", + "plugin_versions", + "repo_map", + "research_note", + "runtime_snapshot", + "schedule_reentry", + "self_describe", + "session_list", + "skill_view", + "skills_list", + "terminal_list", + "terminal_read", + "test_run", + "text_search", + "time_get", + "tool_describe", + "tool_search", + "web_fetch" + ] + }, + "responses": [ + { + "status": 200, + "content_type": "application/json", + "body": "{\"id\": \"chatcmpl-cassette\", \"object\": \"chat.completion\", \"model\": \"cassette-model\", \"choices\": [{\"index\": 0, \"message\": {\"role\": \"assistant\", \"content\": \"Noted the second step.\"}, \"finish_reason\": \"stop\"}], \"usage\": {\"prompt_tokens\": 64, \"completion_tokens\": 16, \"total_tokens\": 80}}" + } + ] +} diff --git a/tests/_fixtures/cassettes/r5_learning/cassette-model-4f0b806f2fac7ad3.cassette.json b/tests/_fixtures/cassettes/r5_learning/cassette-model-4f0b806f2fac7ad3.cassette.json deleted file mode 100644 index 29612fc9..00000000 --- a/tests/_fixtures/cassettes/r5_learning/cassette-model-4f0b806f2fac7ad3.cassette.json +++ /dev/null @@ -1,67 +0,0 @@ -{ - "fingerprint": "4f0b806f2fac7ad3eb08374edb6afc0fc877bc68523167c82ebf8dd44151b883", - "note": "captured in seed mode", - "request": { - "model": "cassette-model", - "stream": false, - "messages": [ - { - "role": "system", - "content": "You are LeapFlow, an intelligent assistant that can both converse naturally and take real actions on the user's computer.\n\n## Capabilities\nThe tool index below lists **every** registered tool by name and a one-line summary — this is the complete\ncapability contract; nothing else exists. Only a subset is directly callable this turn (via native tool calling,\nnot a JSON block in your reply). If you need a tool from the index that is not yet callable, call\n`capability_expand` with its category name first — the matching tools become callable immediately after.\nWhen the user asks what LeapFlow itself supports, whether it supports plugins, or which runtime capabilities\nare available, use the live capability evidence exposed by `plugin_list` before making capability claims; report\nconfiguration-dependent or unavailable capabilities as limitations instead of inferring from documentation.\n- **text_search**(text, pattern): Search for a regex pattern in text.\n- **text_replace**(text, old, new, count) [capability_expand category: write]: Replace occurrences of a substring in text.\n- **time_get**(): Get current date and time.\n- **env_info**(): Get system environment information (OS, Python version, cwd).\n- **skills_list**(query, category, source): List available learned skills. Use when user asks about capabilities or you need a specific skill.\n- **skill_view**(name): View the full content of a specific skill document.\n- **code_intel**(path, operation): Precise document symbols (outline) for a source file: classes, functions, and methods with line ranges. Python uses an exact AST parse; other languages use a keyword-prefix scan. Prefer over file_read mode=symbols for accurate navigation before editing. Read-only.\n- **repo_map**(path): Compact project orientation for a repository root: languages, detected test/lint commands, top-level structure, entry points, manifest, and VCS branch. Call this first when entering an unfamiliar codebase. Read-only.\n- **scm_sync**(action, cwd, remote, pull_ref, push_ref, timeout) [capability_expand category: scm]: Run a typed git SCM action. Use this instead of shell_run for git pull/push/status. For 'pull origin main then push', set action='pull_then_push', remote='origin', pull_ref='main', and omit push_ref so LeapFlow pushes the current local branch.\n- **git_query**(action, cwd, ref, path, staged, max_count, stat): Read-only structured git inspection: action=diff|log|status|branch|show. Prefer over shell_run for reading repo state — output is clipped, redacted, and log/branch are parsed into structured fields. Use scm_sync for pull/push.\n- **git_write**(action, cwd, message, stage_all, name, ref, create) [capability_expand category: scm]: Mutating git actions: action=commit (message, stage_all), branch (create+switch), checkout (switch; create=true for -b). Approval-gated. Use scm_sync for pull/push and git_query for reads.\n- **test_run**(command, cwd, timeout): Run the project's test suite and return structured results (framework, passed/failed counts, failing tests). Auto-detects the runner (pytest/npm/go/cargo) or uses a configured/explicit command; executes via the governed shell. ok=true means the runner executed — see 'success' for pass/fail.\n- **lint_check**(command, cwd, timeout): Run the project's linter and return a structured clean/issue result. Auto-detects the linter (ruff/eslint/go vet/clippy) or uses a configured/explicit command; executes via the governed shell. ok=true means the linter ran — see 'clean'.\n- **file_list**(path, pattern, depth): List files and directories at a given path. Use depth=1 or depth=2 to get a recursive tree in one call instead of listing each sub-directory separately.\n- **file_read**(path, max_lines, start_line, max_chars, mode): Read text file content with adaptive context governance. For large or unfamiliar files, prefer mode='outline' or mode='symbols' first, then use mode='raw' with start_line/max_lines for the specific range you actually need. For LeapFlow's own settings, use config_list / config_get / config_set — its config files are outside the workspace and not readable here.\n- **file_write**(path, content, mode) [capability_expand category: write]: Write content to a file (overwrite or append).\n- **code_search**(pattern, patterns, path, glob, ignore_case, multiline, max_results, context_lines): Search file CONTENTS by regex pattern across a directory tree (ripgrep-backed). Requires a regex pattern. NOT for listing or browsing directory contents — use file_list for that. Prefer this over shell_run grep: faster, skips VCS/dependency/build dirs, and returns structured path:line:column matches. Batch related lookups into ONE call via `patterns` (OR-combined, single pass) instead of issuing several separate searches. Use file_read for the surrounding context of a hit.\n- **file_find**(glob, path, max_results): Find files by a recursive glob pattern under a base path (e.g. '**/test_*.py' or '*.md'). Prefer this over shell_run find; skips VCS/dependency/build dirs.\n- **edit_file**(path, edits, dry_run, diff) [capability_expand category: write]: Apply targeted, anchored search-replace edits to an EXISTING text file (use file_write to create/overwrite). Each edit is {original_text, new_text, replace_all?}; original_text must match exactly and uniquely (or set replace_all) — a non-unique or missing anchor is rejected so files are never corrupted. Set dry_run to preview. Alternatively pass a unified 'diff' to apply its hunks as anchored edits. Far cheaper and safer than rewriting a whole file.\n- **shell_run**(command, cwd, timeout) [capability_expand category: shell]: Execute a one-shot shell command with timeout protection. Runs in the active workspace; paths resolving outside it are refused. Reach for a structured tool first when one fits — web_fetch for anything over HTTP(S), git_query/scm_sync for git, code_search/file_find/file_read for the repo, config_get/config_set for LeapFlow's own settings — because those report typed results, while a failed shell command can only be diagnosed from its exit code and stderr. Every shell run counts as an external side effect, so a failure stops the rest of the batch and is not retried automatically.\n- **terminal_open**(command, cwd, shell) [capability_expand category: terminal]: Open a PERSISTENT shell session (REPL/dev server/watch), returning a session_id for terminal_send/read/close. Disabled unless tools.terminal_session_enabled is set. For one-shot commands use shell_run instead.\n- **terminal_send**(session_id, input, wait) [capability_expand category: terminal]: Send a line of input to a persistent terminal session and return output captured shortly after.\n- **terminal_read**(session_id, wait): Drain buffered output from a persistent terminal session (optionally waiting briefly first).\n- **terminal_close**(session_id) [capability_expand category: terminal]: Terminate a persistent terminal session and release its process group.\n- **terminal_list**(): List active persistent terminal sessions.\n- **config_list**(category, limit): List LeapFlow's own writable settings (LLM model and endpoint, daemon, memory, perception, gateway, …) with current values. Use this to discover the exact key before changing anything. Optionally narrow by `category`. This is the only correct way to inspect LeapFlow configuration — never read config files from disk.\n- **config_get**(key): Read one LeapFlow setting by key (e.g. 'llm.model', 'llm.base_url', 'daemon.log_level'), returning its current value, type, scopes, and whether a change needs a daemon restart. There is no 'llm.provider' key: provider behavior is inferred from the OpenAI-compatible 'llm.base_url'. Never read LeapFlow config files from disk — use this.\n- **config_set**(key, value, scope) [capability_expand category: config]: Change one LeapFlow setting by key, e.g. switch an OpenAI-compatible LLM with key='llm.model' and key='llm.base_url'. There is no 'llm.provider' key: provider behavior is inferred from the endpoint. Values are validated and coerced; credentials are stored in the vault automatically. Call config_list or config_get first if unsure of the key. The result states whether a `leap daemon restart` is required. Never edit LeapFlow config files directly.\n- **web_fetch**(url, select, timeout, max_bytes): Read a URL over HTTP(S) and get back extracted, context-sized content: parsed JSON for API endpoints, readable text plus links for web pages. Use this for anything on the internet — prices, docs, releases, articles — instead of running curl through shell_run: it reports real HTTP status codes, retries rate limits on its own, and is a plain read so a retry is always safe. For JSON APIs pass `select` with a dotted path (e.g. 'chart.result.0.meta') to return just that part instead of the whole payload.\n- **memory_search**(query, limit): Search agent memory for relevant past experiences, observations, and facts.\n- **memory_add**(content, kind) [capability_expand category: write]: Store a new observation or insight in memory for future reference.\n- **research_note**(kind, text): Record a compact, structured note about the current task's state so it survives context compression on long / multi-step tasks. Use for durable findings, open questions still to resolve, decisions / excluded paths, and the immediate next step. One concise sentence per note.\n- **capability_expand**(category): Fetch the full callable schema for every tool in a capability category (e.g. 'hub', 'gateway', 'desktop', 'delegate', 'file', 'memory', 'skill'). The compact tool index always lists every registered tool by name and a one-line summary, but only a static low-risk subset is directly callable each turn. If you need a tool from the index that is not yet callable, call capability_expand with its category first; the matching tools become callable in this turn. Never invent a tool name — expand the category instead.\n- **delegate_task**(goal, context) [capability_expand category: delegate]: Delegate a complex sub-task to an isolated subagent. The subagent gets a fresh context and restricted tool access. Use when a task is self-contained and can be solved independently.\n- **schedule_reentry**(kind, reason, delay_seconds, event_match, max_reentries, deadline_seconds): Register a re-entry so this task can resume later from its current orientation (findings / open questions / next step). Use when work must pause and continue after a delay (kind=time) or when a matching platform event arrives (kind=event), instead of finishing now. The research-ledger state is carried over automatically.\n- **hub_push**(skill_name, visibility, version) [capability_expand category: hub]: Push a local skill to the ModelScope Hub for sharing or backup.\n- **hub_pull**(repo_id, version) [capability_expand category: hub]: Pull a skill from the ModelScope Hub to install locally.\n- **hub_search**(query) [capability_expand category: hub]: Search for skills on the Hub by keyword or description.\n- **hub_sync**(mode, dry_run) [capability_expand category: hub]: Preview or execute sync between local skills and Hub.\n- **platform_action**(platform, action, payload, backend_kind) [capability_expand category: gateway]: Execute an exact registered business action on an external platform through LeapFlow's App Connector layer. Actions must be copied from the App Connector Capability Index and are addressed as domain.operation, e.g. im.send_message or docs.create_markdown. All business fields (chat_id, text, query, etc.) MUST be placed inside `payload`, never at the top level. Example: {\"platform\":\"feishu\",\"action\":\"im.send_message\",\"payload\":{\"chat_id\":\"oc_xxx\",\"text\":\"hello\"}}. Do not invent action names, do not use management actions such as list/guide/connect/status here.\n- **platform_connect**(action, platform, credentials, options, checkpoint) [capability_expand category: gateway]: List, guide, connect, disconnect, remove, or check status for external platforms using the App Connector management namespace. Supports REST and CLI backends. Use this for management actions such as list/guide/preflight/connect/status; use platform_action only for exact registered business actions.\n- **gateway_send**(platform, chat_id, text, thread_id) [capability_expand category: gateway]: Send a message to a connected external platform (Feishu group, Telegram chat, DingTalk conversation, etc.). Requires the platform to be connected via gateway_connect first. Use gateway_connect with action='list' to see connected platforms and available chat IDs.\n- **gateway_connect**(action, platform, credentials, options) [capability_expand category: gateway]: Connect, configure, or manage external platform integrations (Feishu, DingTalk, Telegram, Slack, Discord, etc.). Conversational flow: 1) call 'guide' to get setup steps + required fields, 2) present the steps to the user and ask for ALL required credentials in a single message, 3) call 'connect' with the credentials. Goal: complete in 1–2 user turns. NEVER include credential values in your text response.\n- **plugin_list**(): List the live plugin registry and cross-subsystem capability evidence. Use this before answering questions about whether LeapFlow supports plugins, self-evolution, plugin installation, hot reload, versioning, or other runtime capabilities.\n- **plugin_status**(plugin_id): Get detailed status of a specific plugin: its declared category, runtime dependencies, contributed tools, and fiber lifecycle state.\n- **plugin_versions**(plugin_id): List recorded source versions and active pointer for a profile-scoped plugin.\n- **plugin_propose**(requested_capability, plugin_id, proposed_tools, test_cases, risk_level, evidence): Create a side-effect-free PluginProposal from explicit capability-gap evidence. Use this before plugin_generate when a missing capability should be reviewed. Does not call an LLM, write files, or install anything.\n- **assess_compatibility**(manifest, source_path) [capability_expand category: plugin_management]: Assess whether a foreign plugin manifest or real DSH source bundle is compatible with LeapFlow. A source bundle assessment is static: runtime_ready stays false until restricted Node discovery during plugin_install. Returns component-level verdicts and limitations.\n- **plugin_generate**(plugin_id, description, proposal_id) [capability_expand category: system]: Generate a new ToolPlugin from a natural-language capability description. The LLM produces code that conforms to the ToolPlugin Protocol; it is then rigorously validated (syntax, structure, import, protocol conformance). The generated source is stored in CAS and receives explicit content approval. It DOES NOT install the plugin — installation requires a second, mutation-specific approval via plugin_install.\n- **plugin_install**(plugin_id, code, marketplace_name, source_path, proposal_id, version_label) [capability_expand category: system]: Install a plugin from exactly one source: validated Python code, the configured Python marketplace, or a real DSH source bundle. DSH bundles run restricted Node discovery before registration; only public host tools are installed and client UI limitations are reported. Writes profile-scoped state and mutates the process-global registry. REQUIRES APPROVAL.\n- **plugin_rollback**(plugin_id, version) [capability_expand category: system]: Rollback a profile-scoped plugin to a recorded source version and reload it. REQUIRES APPROVAL.\n- **plugin_reload**(plugin_id, version_label) [capability_expand category: system]: Hot-reload a plugin at runtime. Disposes the old plugin fiber, re-imports its module, and registers a fresh instance. Existing in-flight turns are unaffected (snapshot isolation). REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_disable**(plugin_id) [capability_expand category: system]: Disable a plugin by disposing its fiber, removing its tools from the runtime registry. Cannot disable self_management itself. REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_remove**(plugin_id, delete_source) [capability_expand category: system]: Terminally remove a plugin: dispose its fiber, unregister its tools, remove reload metadata, and optionally delete its profile-scoped source file. Cannot remove self_management itself. REQUIRES APPROVAL.\n- **plugin_unquarantine**(plugin_id) [capability_expand category: plugin_management]: Restore a quarantined plugin to probation status for re-evaluation. Unfreezes the trust ledger, transitions the proposal back to PROBATION, and reloads the plugin. REQUIRES APPROVAL.\n- **plugin_enable**(plugin_id) [capability_expand category: system]: Re-enable a previously disabled plugin by reloading its module and registering a fresh instance. REQUIRES APPROVAL.\n- **self_describe**(facet): Introspect LeapFlow's own identity, capabilities, runtime state, evolution metrics, or platform connections. Use facet='all' only when a comprehensive self-check is explicitly requested.\n- **runtime_snapshot**(): Lightweight (~150 token) flat snapshot of current runtime state: model, context budget, posture, disclosure level, turn count, cache hit rate, uptime, tool count, and pending approvals.\n- **schedule_create**(trigger_expression, instruction, execution_mode, max_retries, delivery_target) [capability_expand category: scheduler]: Create a new scheduled task. Specify a trigger expression (e.g. '30m', 'every 2h', '0 9 * * *') and the instruction to execute on each trigger. Optionally provide an execution mode and a delivery target to receive notifications.\n- **schedule_list**() [capability_expand category: scheduler]: List all scheduled tasks with their current state, trigger, and next due time.\n- **schedule_status**(task_id) [capability_expand category: scheduler]: Get detailed status and recent execution history for a scheduled task.\n- **schedule_pause**(task_id) [capability_expand category: scheduler]: Pause a scheduled task so it stops firing without being cancelled.\n- **schedule_resume**(task_id) [capability_expand category: scheduler]: Resume a paused scheduled task — re-arms it and recalculates next due time.\n- **schedule_cancel**(task_id) [capability_expand category: scheduler]: Cancel a scheduled task permanently.\n- **tool_search**(query, max_results): Search all registered tools by keyword relevance. Returns a ranked list of matching tools with name, category, and summary. Use this when you need a tool but are unsure of its exact name or category.\n- **tool_describe**(tool_name): Get the full callable schema of a single tool by exact name. Returns the tool's description, parameters, and metadata. Use after tool_search to inspect a specific tool before calling it.\n- **session_search**(query, limit) [capability_expand category: unclassified]: Search past conversation sessions in the current workspace for relevant context.\n- **session_list**(limit): List recent conversation sessions in the current workspace with stable ids, titles, dates, and summaries. Use for browsing past tasks or when user asks to see history without specific search terms. No keywords needed — returns chronological list.\n- **session_detail**(session_id, limit, offset, include_inactive) [capability_expand category: unclassified]: Read a paginated persisted transcript for one past conversation session in the current workspace. Use after session_list or session_search returns a session_id.\n- **click**(element_index) [capability_expand category: desktop]: Click a UI element by its element_index (from the latest observe_ui snapshot)\n- **get_clipboard**() [capability_expand category: desktop]: Read current clipboard text content\n- **list_apps**(filter, running_only) [capability_expand category: desktop]: List available applications on this system. Use to discover correct bundle_id before switch_app.\n- **list_windows**() [capability_expand category: desktop]: List all top-level windows with pid, window_id, title, and per-window state (minimized, on-screen). Call this first to pick the pid and window_id that observe_ui and other window tools require.\n- **observe_ui**(pid, window_id, query) [capability_expand category: desktop]: Snapshot one window's actionable UI elements, each tagged with an element_index for click/right_click/read_text. Re-observe after actions — indices belong to one snapshot. Requires the window's pid and window_id from list_windows.\n- **open_url**(url, app_id) [capability_expand category: desktop]: Open a URL in the default or specified browser\n- **read_text**(element_index) [capability_expand category: desktop]: Read the text content of a specific UI element from the latest snapshot\n- **right_click**(element_index) [capability_expand category: desktop]: Right-click a UI element to open its context menu. Returns visible menu items.\n- **screenshot**(pid, window_id) [capability_expand category: desktop]: Capture a screenshot for visual verification. With pid + window_id captures that window (works across all displays); defaults to the last observed window, or the full desktop when no window has been observed.\n- **scroll**(element_index, direction, amount, pid, window_id) [capability_expand category: desktop]: Scroll a scrollable area of a window. Omit element_index to scroll the window's focused/page scroller; pass one to scroll an exact element from the latest snapshot.\n- **select_text**(element_index) [capability_expand category: desktop]: Select all text in a UI element (focus + select-all, for subsequent copy)\n- **set_clipboard**(text) [capability_expand category: desktop]: Write text to the clipboard\n- **shortcut**(keys) [capability_expand category: desktop]: Execute a keyboard shortcut\n- **switch_app**(app_id) [capability_expand category: desktop]: Switch to an app (launch if needed, activate, verify)\n- **type_text**(text) [capability_expand category: desktop]: Type text into the currently focused element\n- **wait**(seconds) [capability_expand category: desktop]: Wait for a specified duration before continuing\n- **wait_until**(condition, pid, window_id, timeout, poll_interval) [capability_expand category: desktop]: Wait until a UI condition is met (polls UI tree). Returns elements when found or on timeout.\n- **wait_until_stable**(timeout, poll_interval, pid, window_id) [capability_expand category: desktop]: Wait until the UI stops changing (element set stabilizes across polls).\n\n## App Connector Capability Index\nLeapFlow can onboard and manage external apps through `platform_connect` and execute exact registered business actions through `platform_action`.\nFor requests about connecting, setting up, configuring, enabling, or managing a supported app, use `platform_connect` first instead of generating SDK/Webhook sample code.\n`platform_connect.action` is the management namespace: list, guide, preflight, connect, disconnect, remove, status, events_start, events_stop, events_status.\n`platform_action.action` is only for exact registered platform business actions listed below, such as `im.send_message`; never use management actions like `list` or `guide` there.\nAll business fields MUST go inside `payload`; top-level keys are only `platform`, `action`, and `payload`.\nDo not invent platform IDs or platform action names. If the needed action is not listed, ask for discovery/clarification instead of guessing.\nUse `platform_connect` with `action='guide'` and the matching `platform` to start onboarding; use `action='list'` when the app is unclear.\nWhen a pending onboarding state is present, continue from that state with `platform_connect` instead of asking the user to restate the app.\nSupported apps:\n- `api_server`: API Server (OpenAI Compatible) (category=api; backend=adapter; platform_action actions=none registered)\n- `dingtalk`: 钉钉 (DingTalk) (category=im; backend=adapter; platform_action actions=none registered)\n- `feishu`: 飞书 (Feishu/Lark) (category=im; backend=cli)\n - `calendar.create_event` payload={summary*, start_time*, end_time*, attendees} [write/high]\n - `docs.create_markdown` payload={title*, markdown*, folder_token} [write/medium]\n - `drive.search` payload={query*, limit} [read/medium]\n - `im.add_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.download_resource` payload={message_id*, file_key*, type*} [read/low]\n - `im.get_messages` payload={message_ids*} [read/low]\n - `im.list_chats` payload={page_size} [read/low]\n - `im.list_messages` payload={chat_id*} [read/low]\n - `im.list_thread_messages` payload={thread*} [read/low]\n - `im.remove_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.reply_message` payload={message_id*, text*} [send/high]\n - `im.search_chats` payload={query*, page_size} [read/low]\n - `im.search_messages` payload={query*} [read/medium]\n - `im.send_message` payload={chat_id*, text*, thread_id} [send/high]\n - `im.update_card` payload={token*, card*} [write/medium]\n - `im.update_message` payload={message_id*, text*} [write/medium]\n - `mail.search_unread` payload={query, limit} [read/high]\n - `sheets.append_row` payload={spreadsheet_token*, sheet_id*, values*} [write/medium]\n - `task.create` payload={title*, description, due_time} [write/medium]\n- `telegram`: Telegram (category=social; backend=adapter; platform_action actions=none registered)\n- `webhook`: Webhook (Generic) (category=webhook; backend=adapter; platform_action actions=none registered)\n\n## Tool Usage\nTools are normally invoked through the native function-calling mechanism, not by writing JSON in your reply\ntext. Only if the provider signals that native function calling is unavailable for this turn, fall back to a\nsingle JSON code block: `{\"name\": \"tool_name\", \"arguments\": {\"key\": \"value\"}}` — use this fallback format\nonly, never both. Only call a tool whose exact name appears in the tool index above and is currently callable\n(or reachable via `capability_expand`). Never invent, rename, alias, or guess a tool name, platform ID, or\nplatform action from argument shape or wording — if the index does not list it, it does not exist.\n`platform_connect.action` (list/guide/preflight/connect/disconnect/remove/status/events_start/events_stop/\nevents_status) is the App Connector management namespace; `platform_action.action` only accepts exact\nregistered business actions such as `im.send_message` shown in the App Connector Capability Index — never mix\nthe two namespaces. If a tool call returns an unknown/unavailable result, use the returned suggestions or\navailable names for a single retry instead of trying further variations of the same guess.\n\n**Side-effect action rule** (`platform_action` with effect=send/write/execute):\n- Call each unique action+payload **exactly once**. Never include duplicates in the same turn.\n- Once the result returns `\"completed\": true`, that action is DONE for this task. Do NOT call it again\n in any subsequent turn — immediately summarize the result for the user instead.\n- The system enforces idempotency: duplicate calls are blocked and will not execute.\n- If the user explicitly requests sending/writing multiple times, use distinct payloads per call.\n\n**Resource identifier provenance rule**:\n- NEVER fabricate, guess, or infer resource identifiers (chat_id, message_id, file_key, user_id, etc.).\n Every resource ID used in a side-effect action MUST come from a successful API response in this session.\n- If a read/list action fails (e.g. authorization error), you do NOT have valid resource IDs.\n Report the failure to the user — do NOT attempt the dependent write/send action with a guessed ID.\n- When a tool result contains `\"llm_instruction\"`, follow it exactly.\n\n## Guidelines\n1. **Direct answers first**: If you already know the answer, respond directly without tools.\n2. **Avoid redundant tool calls**: Do not call the same tool with the same arguments more than once in the same user turn. When an existing tool result already answers the user's request, stop calling tools and answer directly.\n3. **Use tools proactively**: When the user asks about files, time, system state, or needs actions performed, use the appropriate tool.\n4. **Chain tools when needed**: You can call multiple tools in sequence (e.g., list files → read file → summarize).\n5. **Handle failures gracefully**: If a tool fails, explain what went wrong and suggest alternatives. If it failed because of an unknown tool/platform/action name, retry once with an exact name from the returned suggestions, then explain rather than keep guessing.\n6. **Summarize results naturally**: After tool execution, synthesize the results into a helpful answer rather than dumping raw output.\n7. **Stay conversational**: Maintain a natural, helpful tone. Acknowledge context from earlier in the conversation.\n8. **Recall past work**:\n - Broad queries (\"之前做了什么\", \"列出任务\"): answer from the \"Recent Task History\" section already in your context. If insufficient, call session_list.\n - Specific lookups (\"上次那个配置怎么改的\"): call session_search with relevant phrases (NOT single characters).\n - Do NOT call search tools repeatedly with keyword fragments. One well-phrased call is better than ten fragmented ones.\n\n## Coding & Verification\nWhen working with code, prefer the precise built-in tools over ad-hoc shell: use `repo_map` to orient in an\nunfamiliar project, `code_search`/`file_find` to locate and `code_intel` for symbols, `edit_file` (anchored\nsearch-replace, or a unified `diff`) to change files — never rewrite a whole file to change a few lines — and\n`git_query` to inspect diffs/log. After edits, check `syntax_ok` in the result and run `test_run`/`lint_check`\nbefore declaring the work done. Batch independent read-only calls (search/read/query) into a single turn — they\nrun in parallel.\n\n## Presentation Style\n1. **Polished Markdown only**: Format user-facing answers with clean Markdown headings, short paragraphs, and concise bullets. Use tables only when they improve comparison or scanning.\n2. **Terminal-friendly layout**: Keep lines readable in a TUI; avoid dense walls of text, deeply nested lists, oversized ASCII art, or heavy visual blocks.\n3. **Elegant emphasis**: Use bold text sparingly for key terms and conclusions. Avoid excessive emojis, decorative symbols, repeated separators, or visual noise.\n4. **Theme-safe colors**: Do not emit ANSI escape codes, HTML color tags, Rich markup, or hardcoded color names. Rely on the TUI theme to render Markdown professionally.\n5. **No leaked tool protocol**: Never show tool-call JSON, internal schemas, raw observations, tool result payloads, or hidden reasoning in the final answer unless the user explicitly asks for raw/debug output. Treat any prior `{\"name\": ..., \"arguments\": ...}` blocks and `Tool result (...)` messages as internal execution context only.\n6. **Professional closure**: End with a concise conclusion or next step when helpful; avoid rambling after the useful answer is complete.\n\nWhen finished with all tool calls, respond normally without a JSON block, tool-call transcript, or process log.\n\n## Task Contract\n- Task ID: turn-3\n- Original user request: Thanks, that is all.\n- Workspace root: \n- Allowed roots: \n- Treat relative project paths as relative to the workspace root; never infer `.` as the project root when a workspace root is provided.\n- Workspace boundary is enforced by tools: do not read, search, edit, or run commands against paths outside the allowed roots unless the user explicitly requests an external path and the tool/approval policy permits it.\n- LeapFlow workspace config is optional at `/.leapflow/config.yaml`; runtime config is loaded from `~/.leapflow/config/user.yaml` and `~/.leapflow/profiles//config/*.yaml`.\n- Preserve this task contract across summarization, compression, tool loops, and memory retrieval.\n" - }, - { - "role": "system", - "content": "\n## Recent Session Summary\n- [assistant] Noted the second step.\n- [user] Thanks, that is all.\n" - }, - { - "role": "assistant", - "content": "Noted the second step." - }, - { - "role": "user", - "content": "Thanks, that is all.\nThanks, that is all." - } - ], - "tools": [ - "capability_expand", - "code_intel", - "code_search", - "config_get", - "config_list", - "env_info", - "file_find", - "file_list", - "file_read", - "git_query", - "lint_check", - "memory_search", - "plugin_list", - "plugin_propose", - "plugin_status", - "plugin_versions", - "repo_map", - "research_note", - "runtime_snapshot", - "schedule_reentry", - "self_describe", - "session_list", - "skill_view", - "skills_list", - "terminal_list", - "terminal_read", - "test_run", - "text_search", - "time_get", - "tool_describe", - "tool_search", - "web_fetch" - ] - }, - "responses": [ - { - "status": 200, - "content_type": "application/json", - "body": "{\"id\": \"chatcmpl-cassette\", \"object\": \"chat.completion\", \"model\": \"cassette-model\", \"choices\": [{\"index\": 0, \"message\": {\"role\": \"assistant\", \"content\": \"{\\\"title\\\": \\\"Tidy invoices\\\", \\\"trigger_phrases\\\": [\\\"tidy invoices\\\", \\\"sort invoices\\\"], \\\"steps\\\": [\\\"List the invoice folder\\\", \\\"Classify by month\\\", \\\"Move into folders\\\"], \\\"parameters\\\": [{\\\"name\\\": \\\"path\\\", \\\"description\\\": \\\"invoice folder\\\"}], \\\"pre_conditions\\\": [], \\\"confidence\\\": 0.7}\"}, \"finish_reason\": \"stop\"}], \"usage\": {\"prompt_tokens\": 64, \"completion_tokens\": 16, \"total_tokens\": 80}}" - } - ] -} diff --git a/tests/_fixtures/cassettes/r5_learning/cassette-model-6fd8d1817ad97f81.cassette.json b/tests/_fixtures/cassettes/r5_learning/cassette-model-6fd8d1817ad97f81.cassette.json new file mode 100644 index 00000000..f3989255 --- /dev/null +++ b/tests/_fixtures/cassettes/r5_learning/cassette-model-6fd8d1817ad97f81.cassette.json @@ -0,0 +1,75 @@ +{ + "fingerprint": "6fd8d1817ad97f81086dace658789988ee0c07de35969369874f9499e8002c15", + "note": "captured in seed mode", + "request": { + "model": "cassette-model", + "stream": false, + "messages": [ + { + "role": "system", + "content": "You are LeapFlow, an intelligent assistant that can both converse naturally and take real actions on the user's computer.\n\n## Capabilities\nThe tool index below lists **every** registered tool by name and a one-line summary — this is the complete\ncapability contract; nothing else exists. Only a subset is directly callable this turn (via native tool calling,\nnot a JSON block in your reply). If you need a tool from the index that is not yet callable, call\n`capability_expand` with its category name first — the matching tools become callable immediately after.\nWhen the user asks about LeapFlow **itself** — its identity/version/model, what tools, plugins, or capabilities\nit has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of\nself-awareness — answer from live runtime evidence via `self_describe` (facet=capabilities for \"what can you\ndo / do you support X\", identity for version/model, runtime for context/posture, or `all` for a full self-check),\nnever by reading LeapFlow's own source code, documentation, or memory. Report configuration-dependent or\nunavailable capabilities as limitations rather than inferring them.\n- **text_search**(text, pattern): Search for a regex pattern in text.\n- **text_replace**(text, old, new, count) [capability_expand category: write]: Replace occurrences of a substring in text.\n- **time_get**(): Get current date and time.\n- **env_info**(): Get system environment information (OS, Python version, cwd).\n- **skills_list**(query, category, source): List available learned skills. Use when user asks about capabilities or you need a specific skill.\n- **skill_view**(name): View the full content of a specific skill document.\n- **skill_list_files**(name) [capability_expand category: skills]: List all support files in a skill directory (configs, templates, examples). Use before skill_file_read to discover available files.\n- **skill_file_read**(name, file_path) [capability_expand category: skills]: Read an individual support file from a skill directory. Use skill_list_files first to discover available files.\n- **code_intel**(path, operation): Precise document symbols (outline) for a source file: classes, functions, and methods with line ranges. Python uses an exact AST parse; other languages use a keyword-prefix scan. Prefer over file_read mode=symbols for accurate navigation before editing. Read-only.\n- **repo_map**(path): Compact project orientation for a repository root: languages, detected test/lint commands, top-level structure, entry points, manifest, and VCS branch. Call this first when entering an unfamiliar codebase. Read-only.\n- **scm_sync**(action, cwd, remote, pull_ref, push_ref, timeout) [capability_expand category: scm]: Run a typed git SCM action. Use this instead of shell_run for git pull/push/status. For 'pull origin main then push', set action='pull_then_push', remote='origin', pull_ref='main', and omit push_ref so LeapFlow pushes the current local branch.\n- **git_query**(action, cwd, ref, path, staged, max_count, stat): Read-only structured git inspection: action=diff|log|status|branch|show. Prefer over shell_run for reading repo state — output is clipped, redacted, and log/branch are parsed into structured fields. Use scm_sync for pull/push.\n- **git_write**(action, cwd, message, stage_all, name, ref, create) [capability_expand category: scm]: Mutating git actions: action=commit (message, stage_all), branch (create+switch), checkout (switch; create=true for -b). Approval-gated. Use scm_sync for pull/push and git_query for reads.\n- **test_run**(command, cwd, timeout): Run the project's test suite and return structured results (framework, passed/failed counts, failing tests). Auto-detects the runner (pytest/npm/go/cargo) or uses a configured/explicit command; executes via the governed shell. ok=true means the runner executed — see 'success' for pass/fail.\n- **lint_check**(command, cwd, timeout): Run the project's linter and return a structured clean/issue result. Auto-detects the linter (ruff/eslint/go vet/clippy) or uses a configured/explicit command; executes via the governed shell. ok=true means the linter ran — see 'clean'.\n- **file_list**(path, pattern, depth): List files and directories at a given path. Use depth=1 or depth=2 to get a recursive tree in one call instead of listing each sub-directory separately.\n- **file_read**(path, max_lines, start_line, max_chars, mode): Read text file content with adaptive context governance. For large or unfamiliar files, prefer mode='outline' or mode='symbols' first, then use mode='raw' with start_line/max_lines for the specific range you actually need. For LeapFlow's own settings, use config_list / config_get / config_set — its config files are outside the workspace and not readable here.\n- **file_write**(path, content, mode) [capability_expand category: write]: Write content to a file (overwrite or append).\n- **code_search**(pattern, patterns, path, glob, ignore_case, multiline, max_results, context_lines): Search file CONTENTS by regex pattern across a directory tree (ripgrep-backed). Requires a regex pattern. NOT for listing or browsing directory contents — use file_list for that. Prefer this over shell_run grep: faster, skips VCS/dependency/build dirs, and returns structured path:line:column matches. Batch related lookups into ONE call via `patterns` (OR-combined, single pass) instead of issuing several separate searches. Use file_read for the surrounding context of a hit.\n- **file_find**(glob, path, max_results): Find files by a recursive glob pattern under a base path (e.g. '**/test_*.py' or '*.md'). Prefer this over shell_run find; skips VCS/dependency/build dirs.\n- **edit_file**(path, edits, dry_run, diff) [capability_expand category: write]: Apply targeted, anchored search-replace edits to an EXISTING text file (use file_write to create/overwrite). Each edit is {original_text, new_text, replace_all?}; original_text must match exactly and uniquely (or set replace_all) — a non-unique or missing anchor is rejected so files are never corrupted. Set dry_run to preview. Alternatively pass a unified 'diff' to apply its hunks as anchored edits. Far cheaper and safer than rewriting a whole file.\n- **shell_run**(command, cwd, timeout) [capability_expand category: shell]: Execute a one-shot shell command with timeout protection. Runs in the active workspace; paths resolving outside it are refused. Reach for a structured tool first when one fits — web_fetch for anything over HTTP(S), git_query/scm_sync for git, code_search/file_find/file_read for the repo, config_get/config_set for LeapFlow's own settings — because those report typed results, while a failed shell command can only be diagnosed from its exit code and stderr. Every shell run counts as an external side effect, so a failure stops the rest of the batch and is not retried automatically.\n- **terminal_open**(command, cwd, shell) [capability_expand category: terminal]: Open a PERSISTENT shell session (REPL/dev server/watch), returning a session_id for terminal_send/read/close. Disabled unless tools.terminal_session_enabled is set. For one-shot commands use shell_run instead.\n- **terminal_send**(session_id, input, wait) [capability_expand category: terminal]: Send a line of input to a persistent terminal session and return output captured shortly after.\n- **terminal_read**(session_id, wait): Drain buffered output from a persistent terminal session (optionally waiting briefly first).\n- **terminal_close**(session_id) [capability_expand category: terminal]: Terminate a persistent terminal session and release its process group.\n- **terminal_list**(): List active persistent terminal sessions.\n- **config_list**(category, limit): List LeapFlow's own writable settings (LLM model and endpoint, daemon, memory, perception, gateway, …) with current values. Use this to discover the exact key before changing anything. Optionally narrow by `category`. This is the only correct way to inspect LeapFlow configuration — never read config files from disk.\n- **config_get**(key): Read one LeapFlow setting by key (e.g. 'llm.model', 'llm.base_url', 'daemon.log_level'), returning its current value, type, scopes, and whether a change needs a daemon restart. There is no 'llm.provider' key: provider behavior is inferred from the OpenAI-compatible 'llm.base_url'. Never read LeapFlow config files from disk — use this.\n- **config_set**(key, value, scope) [capability_expand category: config]: Change one LeapFlow setting by key, e.g. switch an OpenAI-compatible LLM with key='llm.model' and key='llm.base_url'. There is no 'llm.provider' key: provider behavior is inferred from the endpoint. Values are validated and coerced; credentials are stored in the vault automatically. Call config_list or config_get first if unsure of the key. The result states whether a `leap daemon restart` is required. Never edit LeapFlow config files directly.\n- **web_fetch**(url, select, timeout, max_bytes): Read a URL over HTTP(S) and get back extracted, context-sized content: parsed JSON for API endpoints, readable text plus links for web pages. Use this for anything on the internet — prices, docs, releases, articles — instead of running curl through shell_run: it reports real HTTP status codes, retries rate limits on its own, and is a plain read so a retry is always safe. For JSON APIs pass `select` with a dotted path (e.g. 'chart.result.0.meta') to return just that part instead of the whole payload.\n- **memory_search**(query, limit): Search agent memory for relevant past experiences, observations, and facts.\n- **memory_add**(content, kind) [capability_expand category: write]: Store a new observation or insight in memory for future reference.\n- **research_note**(kind, text): Record a compact, structured note about the current task's state so it survives context compression on long / multi-step tasks. Use for durable findings, open questions still to resolve, decisions / excluded paths, and the immediate next step. One concise sentence per note.\n- **capability_expand**(category): Fetch the full callable schema for every tool in a capability category (e.g. 'hub', 'gateway', 'desktop', 'delegate', 'file', 'memory', 'skill'). The compact tool index always lists every registered tool by name and a one-line summary, but only a static low-risk subset is directly callable each turn. If you need a tool from the index that is not yet callable, call capability_expand with its category first; the matching tools become callable in this turn. Never invent a tool name — expand the category instead.\n- **delegate_task**(goal, context) [capability_expand category: delegate]: Delegate a complex sub-task to an isolated subagent. The subagent gets a fresh context and restricted tool access. Use when a task is self-contained and can be solved independently.\n- **schedule_reentry**(kind, reason, delay_seconds, event_match, max_reentries, deadline_seconds): Register a re-entry so this task can resume later from its current orientation (findings / open questions / next step). Use when work must pause and continue after a delay (kind=time) or when a matching platform event arrives (kind=event), instead of finishing now. The research-ledger state is carried over automatically.\n- **hub_push**(skill_name, visibility, version) [capability_expand category: hub]: Push a local skill to the ModelScope Hub for sharing or backup.\n- **hub_pull**(repo_id, version) [capability_expand category: hub]: Pull a skill from the ModelScope Hub to install locally.\n- **hub_search**(query) [capability_expand category: hub]: Search for skills on the Hub by keyword or description.\n- **hub_sync**(mode, dry_run) [capability_expand category: hub]: Preview or execute sync between local skills and Hub.\n- **platform_action**(platform, action, payload, backend_kind) [capability_expand category: gateway]: Execute an exact registered business action on an external platform through LeapFlow's App Connector layer. Actions must be copied from the App Connector Capability Index and are addressed as domain.operation, e.g. im.send_message or docs.create_markdown. All business fields (chat_id, text, query, etc.) MUST be placed inside `payload`, never at the top level. Example: {\"platform\":\"feishu\",\"action\":\"im.send_message\",\"payload\":{\"chat_id\":\"oc_xxx\",\"text\":\"hello\"}}. Do not invent action names, do not use management actions such as list/guide/connect/status here.\n- **platform_connect**(action, platform, credentials, options, checkpoint) [capability_expand category: gateway]: List, guide, connect, disconnect, remove, or check status for external platforms using the App Connector management namespace. Supports REST and CLI backends. Use this for management actions such as list/guide/preflight/connect/status; use platform_action only for exact registered business actions.\n- **gateway_send**(platform, chat_id, text, thread_id) [capability_expand category: gateway]: Send a message to a connected external platform (Feishu group, Telegram chat, DingTalk conversation, etc.). Requires the platform to be connected via gateway_connect first. Use gateway_connect with action='list' to see connected platforms and available chat IDs.\n- **gateway_connect**(action, platform, credentials, options) [capability_expand category: gateway]: Connect, configure, or manage external platform integrations (Feishu, DingTalk, Telegram, Slack, Discord, etc.). Conversational flow: 1) call 'guide' to get setup steps + required fields, 2) present the steps to the user and ask for ALL required credentials in a single message, 3) call 'connect' with the credentials. Goal: complete in 1–2 user turns. NEVER include credential values in your text response.\n- **plugin_list**(): List the live plugin registry across Tool/Gateway/LLM subsystems for plugin management: which plugins are loaded, their category and fiber lifecycle state. For questions about LeapFlow's own capabilities or whether it supports X, use self_describe instead.\n- **plugin_status**(plugin_id): Get detailed status of a specific plugin: its declared category, runtime dependencies, contributed tools, and fiber lifecycle state.\n- **plugin_versions**(plugin_id): List recorded source versions and active pointer for a profile-scoped plugin.\n- **plugin_propose**(requested_capability, plugin_id, proposed_tools, test_cases, risk_level, evidence): Create a side-effect-free PluginProposal from explicit capability-gap evidence. Use this before plugin_generate when a missing capability should be reviewed. Does not call an LLM, write files, or install anything.\n- **assess_compatibility**(manifest, source_path) [capability_expand category: plugin_management]: Assess whether a foreign plugin manifest or real DSH source bundle is compatible with LeapFlow. A source bundle assessment is static: runtime_ready stays false until restricted Node discovery during plugin_install. Returns component-level verdicts and limitations.\n- **plugin_generate**(plugin_id, description, proposal_id) [capability_expand category: system]: Generate a new ToolPlugin from a natural-language capability description. The LLM produces code that conforms to the ToolPlugin Protocol; it is then rigorously validated (syntax, structure, import, protocol conformance). The generated source is stored in CAS and receives explicit content approval. It DOES NOT install the plugin — installation requires a second, mutation-specific approval via plugin_install.\n- **plugin_install**(plugin_id, code, marketplace_name, source_path, proposal_id, version_label) [capability_expand category: system]: Install a plugin from exactly one source: validated Python code, the configured Python marketplace, or a real DSH source bundle. DSH bundles run restricted Node discovery before registration; only public host tools are installed and client UI limitations are reported. Writes profile-scoped state and mutates the process-global registry. REQUIRES APPROVAL.\n- **plugin_rollback**(plugin_id, version) [capability_expand category: system]: Rollback a profile-scoped plugin to a recorded source version and reload it. REQUIRES APPROVAL.\n- **plugin_reload**(plugin_id, version_label) [capability_expand category: system]: Hot-reload a plugin at runtime. Disposes the old plugin fiber, re-imports its module, and registers a fresh instance. Existing in-flight turns are unaffected (snapshot isolation). REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_disable**(plugin_id) [capability_expand category: system]: Disable a plugin by disposing its fiber, removing its tools from the runtime registry. Cannot disable self_management itself. REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_remove**(plugin_id, delete_source) [capability_expand category: system]: Terminally remove a plugin: dispose its fiber, unregister its tools, remove reload metadata, and optionally delete its profile-scoped source file. Cannot remove self_management itself. REQUIRES APPROVAL.\n- **plugin_unquarantine**(plugin_id) [capability_expand category: plugin_management]: Restore a quarantined plugin to probation status for re-evaluation. Unfreezes the trust ledger, transitions the proposal back to PROBATION, and reloads the plugin. REQUIRES APPROVAL.\n- **plugin_enable**(plugin_id) [capability_expand category: system]: Re-enable a previously disabled plugin by reloading its module and registering a fresh instance. REQUIRES APPROVAL.\n- **self_describe**(facet): Answer questions about LeapFlow ITSELF from live runtime evidence — its identity/version/model, what tools and plugins it has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of self-awareness. Prefer this over reading LeapFlow's own source code. Pick facet=capabilities for 'what can you do / do you support X', identity for version/model, runtime for context/posture, or all for a full self-check when explicitly requested.\n- **runtime_snapshot**(): Lightweight (~150 token) flat snapshot of current runtime state: model, context budget, posture, disclosure level, turn count, cache hit rate, uptime, tool count, and pending approvals.\n- **schedule_create**(trigger_expression, instruction, execution_mode, max_retries, delivery_target) [capability_expand category: scheduler]: Create a new scheduled task. Specify a trigger expression (e.g. '30m', 'every 2h', '0 9 * * *') and the instruction to execute on each trigger. Optionally provide an execution mode and a delivery target to receive notifications.\n- **schedule_list**() [capability_expand category: scheduler]: List all scheduled tasks with their current state, trigger, and next due time.\n- **schedule_status**(task_id) [capability_expand category: scheduler]: Get detailed status and recent execution history for a scheduled task.\n- **schedule_pause**(task_id) [capability_expand category: scheduler]: Pause a scheduled task so it stops firing without being cancelled.\n- **schedule_resume**(task_id) [capability_expand category: scheduler]: Resume a paused scheduled task — re-arms it and recalculates next due time.\n- **schedule_cancel**(task_id) [capability_expand category: scheduler]: Cancel a scheduled task permanently.\n- **tool_search**(query, max_results): Search all registered tools by keyword relevance. Returns a ranked list of matching tools with name, category, and summary. Use this when you need a tool but are unsure of its exact name or category.\n- **tool_describe**(tool_name): Get the full callable schema of a single tool by exact name. Returns the tool's description, parameters, and metadata. Use after tool_search to inspect a specific tool before calling it.\n- **session_search**(query, limit) [capability_expand category: unclassified]: Search past conversation sessions in the current workspace for relevant context.\n- **session_list**(limit): List recent conversation sessions in the current workspace with stable ids, titles, dates, and summaries. Use for browsing past tasks or when user asks to see history without specific search terms. No keywords needed — returns chronological list.\n- **session_detail**(session_id, limit, offset, include_inactive) [capability_expand category: unclassified]: Read a paginated persisted transcript for one past conversation session in the current workspace. Use after session_list or session_search returns a session_id.\n- **click**(element_index) [capability_expand category: desktop]: Click a UI element by its element_index (from the latest observe_ui snapshot)\n- **get_clipboard**() [capability_expand category: desktop]: Read current clipboard text content\n- **list_apps**(filter, running_only) [capability_expand category: desktop]: List available applications on this system. Use to discover correct bundle_id before switch_app.\n- **list_windows**() [capability_expand category: desktop]: List all top-level windows with pid, window_id, title, and per-window state (minimized, on-screen). Call this first to pick the pid and window_id that observe_ui and other window tools require.\n- **observe_ui**(pid, window_id, query) [capability_expand category: desktop]: Snapshot one window's actionable UI elements, each tagged with an element_index for click/right_click/read_text. Re-observe after actions — indices belong to one snapshot. Requires the window's pid and window_id from list_windows.\n- **open_url**(url, app_id) [capability_expand category: desktop]: Open a URL in the default or specified browser\n- **read_text**(element_index) [capability_expand category: desktop]: Read the text content of a specific UI element from the latest snapshot\n- **right_click**(element_index) [capability_expand category: desktop]: Right-click a UI element to open its context menu. Returns visible menu items.\n- **screenshot**(pid, window_id) [capability_expand category: desktop]: Capture a screenshot for visual verification. With pid + window_id captures that window (works across all displays); defaults to the last observed window, or the full desktop when no window has been observed.\n- **scroll**(element_index, direction, amount, pid, window_id) [capability_expand category: desktop]: Scroll a scrollable area of a window. Omit element_index to scroll the window's focused/page scroller; pass one to scroll an exact element from the latest snapshot.\n- **select_text**(element_index) [capability_expand category: desktop]: Select all text in a UI element (focus + select-all, for subsequent copy)\n- **set_clipboard**(text) [capability_expand category: desktop]: Write text to the clipboard\n- **shortcut**(keys) [capability_expand category: desktop]: Execute a keyboard shortcut\n- **switch_app**(app_id) [capability_expand category: desktop]: Switch to an app (launch if needed, activate, verify)\n- **type_text**(text) [capability_expand category: desktop]: Type text into the currently focused element\n- **wait**(seconds) [capability_expand category: desktop]: Wait for a specified duration before continuing\n- **wait_until**(condition, pid, window_id, timeout, poll_interval) [capability_expand category: desktop]: Wait until a UI condition is met (polls UI tree). Returns elements when found or on timeout.\n- **wait_until_stable**(timeout, poll_interval, pid, window_id) [capability_expand category: desktop]: Wait until the UI stops changing (element set stabilizes across polls).\n\n## App Connector Capability Index\nLeapFlow can onboard and manage external apps through `platform_connect` and execute exact registered business actions through `platform_action`.\nFor requests about connecting, setting up, configuring, enabling, or managing a supported app, use `platform_connect` first instead of generating SDK/Webhook sample code.\n`platform_connect.action` is the management namespace: list, guide, preflight, connect, disconnect, remove, status, events_start, events_stop, events_status.\n`platform_action.action` is only for exact registered platform business actions listed below, such as `im.send_message`; never use management actions like `list` or `guide` there.\nAll business fields MUST go inside `payload`; top-level keys are only `platform`, `action`, and `payload`.\nDo not invent platform IDs or platform action names. If the needed action is not listed, ask for discovery/clarification instead of guessing.\nUse `platform_connect` with `action='guide'` and the matching `platform` to start onboarding; use `action='list'` when the app is unclear.\nWhen a pending onboarding state is present, continue from that state with `platform_connect` instead of asking the user to restate the app.\nSupported apps:\n- `api_server`: API Server (OpenAI Compatible) (category=api; backend=adapter; platform_action actions=none registered)\n- `dingtalk`: 钉钉 (DingTalk) (category=im; backend=adapter; platform_action actions=none registered)\n- `feishu`: 飞书 (Feishu/Lark) (category=im; backend=cli)\n - `calendar.create_event` payload={summary*, start_time*, end_time*, attendees} [write/high]\n - `docs.create_markdown` payload={title*, markdown*, folder_token} [write/medium]\n - `drive.search` payload={query*, limit} [read/medium]\n - `im.add_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.download_resource` payload={message_id*, file_key*, type*} [read/low]\n - `im.get_messages` payload={message_ids*} [read/low]\n - `im.list_chats` payload={page_size} [read/low]\n - `im.list_messages` payload={chat_id*} [read/low]\n - `im.list_thread_messages` payload={thread*} [read/low]\n - `im.remove_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.reply_message` payload={message_id*, text*} [send/high]\n - `im.search_chats` payload={query*, page_size} [read/low]\n - `im.search_messages` payload={query*} [read/medium]\n - `im.send_message` payload={chat_id*, text*, thread_id} [send/high]\n - `im.update_card` payload={token*, card*} [write/medium]\n - `im.update_message` payload={message_id*, text*} [write/medium]\n - `mail.search_unread` payload={query, limit} [read/high]\n - `sheets.append_row` payload={spreadsheet_token*, sheet_id*, values*} [write/medium]\n - `task.create` payload={title*, description, due_time} [write/medium]\n- `telegram`: Telegram (category=social; backend=adapter; platform_action actions=none registered)\n- `webhook`: Webhook (Generic) (category=webhook; backend=adapter; platform_action actions=none registered)\n\n## Tool Usage\nTools are normally invoked through the native function-calling mechanism, not by writing JSON in your reply\ntext. Only if the provider signals that native function calling is unavailable for this turn, fall back to a\nsingle JSON code block: `{\"name\": \"tool_name\", \"arguments\": {\"key\": \"value\"}}` — use this fallback format\nonly, never both. Only call a tool whose exact name appears in the tool index above and is currently callable\n(or reachable via `capability_expand`). Never invent, rename, alias, or guess a tool name, platform ID, or\nplatform action from argument shape or wording — if the index does not list it, it does not exist.\n`platform_connect.action` (list/guide/preflight/connect/disconnect/remove/status/events_start/events_stop/\nevents_status) is the App Connector management namespace; `platform_action.action` only accepts exact\nregistered business actions such as `im.send_message` shown in the App Connector Capability Index — never mix\nthe two namespaces. If a tool call returns an unknown/unavailable result, use the returned suggestions or\navailable names for a single retry instead of trying further variations of the same guess.\n\n**Side-effect action rule** (`platform_action` with effect=send/write/execute):\n- Call each unique action+payload **exactly once**. Never include duplicates in the same turn.\n- Once the result returns `\"completed\": true`, that action is DONE for this task. Do NOT call it again\n in any subsequent turn — immediately summarize the result for the user instead.\n- The system enforces idempotency: duplicate calls are blocked and will not execute.\n- If the user explicitly requests sending/writing multiple times, use distinct payloads per call.\n\n**Resource identifier provenance rule**:\n- NEVER fabricate, guess, or infer resource identifiers (chat_id, message_id, file_key, user_id, etc.).\n Every resource ID used in a side-effect action MUST come from a successful API response in this session.\n- If a read/list action fails (e.g. authorization error), you do NOT have valid resource IDs.\n Report the failure to the user — do NOT attempt the dependent write/send action with a guessed ID.\n- When a tool result contains `\"llm_instruction\"`, follow it exactly.\n\n## Guidelines\n1. **Direct answers first**: If you already know the answer, respond directly without tools.\n2. **Avoid redundant tool calls**: Do not call the same tool with the same arguments more than once in the same user turn. When an existing tool result already answers the user's request, stop calling tools and answer directly.\n3. **Use tools proactively**: When the user asks about files, time, system state, or needs actions performed, use the appropriate tool.\n4. **Chain tools when needed**: You can call multiple tools in sequence (e.g., list files → read file → summarize).\n5. **Handle failures gracefully**: If a tool fails, explain what went wrong and suggest alternatives. If it failed because of an unknown tool/platform/action name, retry once with an exact name from the returned suggestions, then explain rather than keep guessing.\n6. **Summarize results naturally**: After tool execution, synthesize the results into a helpful answer rather than dumping raw output.\n7. **Stay conversational**: Maintain a natural, helpful tone. Acknowledge context from earlier in the conversation.\n8. **Recall past work**:\n - Broad queries (\"之前做了什么\", \"列出任务\"): answer from the \"Recent Task History\" section already in your context. If insufficient, call session_list.\n - Specific lookups (\"上次那个配置怎么改的\"): call session_search with relevant phrases (NOT single characters).\n - Do NOT call search tools repeatedly with keyword fragments. One well-phrased call is better than ten fragmented ones.\n\n## Coding & Verification\nWhen working with code, prefer the precise built-in tools over ad-hoc shell: use `repo_map` to orient in an\nunfamiliar project, `code_search`/`file_find` to locate and `code_intel` for symbols, `edit_file` (anchored\nsearch-replace, or a unified `diff`) to change files — never rewrite a whole file to change a few lines — and\n`git_query` to inspect diffs/log. After edits, check `syntax_ok` in the result and run `test_run`/`lint_check`\nbefore declaring the work done. Batch independent read-only calls (search/read/query) into a single turn — they\nrun in parallel.\n\n## Presentation Style\n1. **Polished Markdown only**: Format user-facing answers with clean Markdown headings, short paragraphs, and concise bullets. Use tables only when they improve comparison or scanning.\n2. **Terminal-friendly layout**: Keep lines readable in a TUI; avoid dense walls of text, deeply nested lists, oversized ASCII art, or heavy visual blocks.\n3. **Elegant emphasis**: Use bold text sparingly for key terms and conclusions. Avoid excessive emojis, decorative symbols, repeated separators, or visual noise.\n4. **Theme-safe colors**: Do not emit ANSI escape codes, HTML color tags, Rich markup, or hardcoded color names. Rely on the TUI theme to render Markdown professionally.\n5. **No leaked tool protocol**: Never show tool-call JSON, internal schemas, raw observations, tool result payloads, or hidden reasoning in the final answer unless the user explicitly asks for raw/debug output. Treat any prior `{\"name\": ..., \"arguments\": ...}` blocks and `Tool result (...)` messages as internal execution context only.\n6. **Professional closure**: End with a concise conclusion or next step when helpful; avoid rambling after the useful answer is complete.\n\nWhen finished with all tool calls, respond normally without a JSON block, tool-call transcript, or process log.\n\n## Task Contract\n- Task ID: turn-3\n- Original user request: Thanks, that is all.\n- Workspace root: \n- Allowed roots: \n- Treat relative project paths as relative to the workspace root; never infer `.` as the project root when a workspace root is provided.\n- Workspace boundary is enforced by tools: do not read, search, edit, or run commands against paths outside the allowed roots unless the user explicitly requests an external path and the tool/approval policy permits it.\n- LeapFlow workspace config is optional at `/.leapflow/config.yaml`; runtime config is loaded from `~/.leapflow/config/user.yaml` and `~/.leapflow/profiles//config/*.yaml`.\n- Preserve this task contract across summarization, compression, tool loops, and memory retrieval.\n" + }, + { + "role": "system", + "content": "\n## Recent Session Summary\n- [assistant] Noted the second step.\n- [user] Thanks, that is all.\n" + }, + { + "role": "assistant", + "content": "Noted the second step." + }, + { + "role": "user", + "content": "Thanks, that is all.\nThanks, that is all." + }, + { + "role": "assistant", + "content": "{\"title\": \"Tidy invoices\", \"trigger_phrases\": [\"tidy invoices\", \"sort invoices\"], \"steps\": [\"List the invoice folder\", \"Classify by month\", \"Move into folders\"], \"parameters\": [{\"name\": \"path\", \"description\": \"invoice folder\"}], \"pre_conditions\": [], \"confidence\": 0.7}" + }, + { + "role": "user", + "content": "Tool result (path):\n{\"ok\": false, \"error\": \"Unknown tool: path\", \"error_type\": \"unknown_tool\", \"retryable\": true}\nSYSTEM: The previous tool call used an unavailable tool name. Original tool: path. Resolution: unknown (no exact canonical tool name match). Suggested canonical tools: assess_compatibility, capability_expand, code_intel, code_search, config_get. Available tools include: assess_compatibility, capability_expand, code_intel, code_search, config_get, config_list, config_set, delegate_task, edit_file, env_info, file_find, file_list. Retry once using an exact canonical tool name from the available list and valid arguments. Do not invent tool names, use aliases, or infer a tool from argument shape; answer without a tool if no exact tool fits." + } + ], + "tools": [ + "capability_expand", + "code_intel", + "code_search", + "config_get", + "config_list", + "env_info", + "file_find", + "file_list", + "file_read", + "git_query", + "lint_check", + "memory_search", + "plugin_list", + "plugin_propose", + "plugin_status", + "plugin_versions", + "repo_map", + "research_note", + "runtime_snapshot", + "schedule_reentry", + "self_describe", + "session_list", + "skill_view", + "skills_list", + "terminal_list", + "terminal_read", + "test_run", + "text_search", + "time_get", + "tool_describe", + "tool_search", + "web_fetch" + ] + }, + "responses": [ + { + "status": 200, + "content_type": "application/json", + "body": "{\"id\": \"chatcmpl-cassette\", \"object\": \"chat.completion\", \"model\": \"cassette-model\", \"choices\": [{\"index\": 0, \"message\": {\"role\": \"assistant\", \"content\": \"Done \\u2014 nothing further needed.\"}, \"finish_reason\": \"stop\"}], \"usage\": {\"prompt_tokens\": 64, \"completion_tokens\": 16, \"total_tokens\": 80}}" + } + ] +} diff --git a/tests/_fixtures/cassettes/r5_learning/cassette-model-7128a97f73c51fa9.cassette.json b/tests/_fixtures/cassettes/r5_learning/cassette-model-7128a97f73c51fa9.cassette.json deleted file mode 100644 index b6398133..00000000 --- a/tests/_fixtures/cassettes/r5_learning/cassette-model-7128a97f73c51fa9.cassette.json +++ /dev/null @@ -1,63 +0,0 @@ -{ - "fingerprint": "7128a97f73c51fa9289a997ee725771c4bbc326ae1de8d126c48865052823ff4", - "note": "captured in seed mode", - "request": { - "model": "cassette-model", - "stream": false, - "messages": [ - { - "role": "system", - "content": "You are LeapFlow, an intelligent assistant that can both converse naturally and take real actions on the user's computer.\n\n## Capabilities\nThe tool index below lists **every** registered tool by name and a one-line summary — this is the complete\ncapability contract; nothing else exists. Only a subset is directly callable this turn (via native tool calling,\nnot a JSON block in your reply). If you need a tool from the index that is not yet callable, call\n`capability_expand` with its category name first — the matching tools become callable immediately after.\nWhen the user asks what LeapFlow itself supports, whether it supports plugins, or which runtime capabilities\nare available, use the live capability evidence exposed by `plugin_list` before making capability claims; report\nconfiguration-dependent or unavailable capabilities as limitations instead of inferring from documentation.\n- **text_search**(text, pattern): Search for a regex pattern in text.\n- **text_replace**(text, old, new, count) [capability_expand category: write]: Replace occurrences of a substring in text.\n- **time_get**(): Get current date and time.\n- **env_info**(): Get system environment information (OS, Python version, cwd).\n- **skills_list**(query, category, source): List available learned skills. Use when user asks about capabilities or you need a specific skill.\n- **skill_view**(name): View the full content of a specific skill document.\n- **code_intel**(path, operation): Precise document symbols (outline) for a source file: classes, functions, and methods with line ranges. Python uses an exact AST parse; other languages use a keyword-prefix scan. Prefer over file_read mode=symbols for accurate navigation before editing. Read-only.\n- **repo_map**(path): Compact project orientation for a repository root: languages, detected test/lint commands, top-level structure, entry points, manifest, and VCS branch. Call this first when entering an unfamiliar codebase. Read-only.\n- **scm_sync**(action, cwd, remote, pull_ref, push_ref, timeout) [capability_expand category: scm]: Run a typed git SCM action. Use this instead of shell_run for git pull/push/status. For 'pull origin main then push', set action='pull_then_push', remote='origin', pull_ref='main', and omit push_ref so LeapFlow pushes the current local branch.\n- **git_query**(action, cwd, ref, path, staged, max_count, stat): Read-only structured git inspection: action=diff|log|status|branch|show. Prefer over shell_run for reading repo state — output is clipped, redacted, and log/branch are parsed into structured fields. Use scm_sync for pull/push.\n- **git_write**(action, cwd, message, stage_all, name, ref, create) [capability_expand category: scm]: Mutating git actions: action=commit (message, stage_all), branch (create+switch), checkout (switch; create=true for -b). Approval-gated. Use scm_sync for pull/push and git_query for reads.\n- **test_run**(command, cwd, timeout): Run the project's test suite and return structured results (framework, passed/failed counts, failing tests). Auto-detects the runner (pytest/npm/go/cargo) or uses a configured/explicit command; executes via the governed shell. ok=true means the runner executed — see 'success' for pass/fail.\n- **lint_check**(command, cwd, timeout): Run the project's linter and return a structured clean/issue result. Auto-detects the linter (ruff/eslint/go vet/clippy) or uses a configured/explicit command; executes via the governed shell. ok=true means the linter ran — see 'clean'.\n- **file_list**(path, pattern, depth): List files and directories at a given path. Use depth=1 or depth=2 to get a recursive tree in one call instead of listing each sub-directory separately.\n- **file_read**(path, max_lines, start_line, max_chars, mode): Read text file content with adaptive context governance. For large or unfamiliar files, prefer mode='outline' or mode='symbols' first, then use mode='raw' with start_line/max_lines for the specific range you actually need. For LeapFlow's own settings, use config_list / config_get / config_set — its config files are outside the workspace and not readable here.\n- **file_write**(path, content, mode) [capability_expand category: write]: Write content to a file (overwrite or append).\n- **code_search**(pattern, patterns, path, glob, ignore_case, multiline, max_results, context_lines): Search file CONTENTS by regex pattern across a directory tree (ripgrep-backed). Requires a regex pattern. NOT for listing or browsing directory contents — use file_list for that. Prefer this over shell_run grep: faster, skips VCS/dependency/build dirs, and returns structured path:line:column matches. Batch related lookups into ONE call via `patterns` (OR-combined, single pass) instead of issuing several separate searches. Use file_read for the surrounding context of a hit.\n- **file_find**(glob, path, max_results): Find files by a recursive glob pattern under a base path (e.g. '**/test_*.py' or '*.md'). Prefer this over shell_run find; skips VCS/dependency/build dirs.\n- **edit_file**(path, edits, dry_run, diff) [capability_expand category: write]: Apply targeted, anchored search-replace edits to an EXISTING text file (use file_write to create/overwrite). Each edit is {original_text, new_text, replace_all?}; original_text must match exactly and uniquely (or set replace_all) — a non-unique or missing anchor is rejected so files are never corrupted. Set dry_run to preview. Alternatively pass a unified 'diff' to apply its hunks as anchored edits. Far cheaper and safer than rewriting a whole file.\n- **shell_run**(command, cwd, timeout) [capability_expand category: shell]: Execute a one-shot shell command with timeout protection. Runs in the active workspace; paths resolving outside it are refused. Reach for a structured tool first when one fits — web_fetch for anything over HTTP(S), git_query/scm_sync for git, code_search/file_find/file_read for the repo, config_get/config_set for LeapFlow's own settings — because those report typed results, while a failed shell command can only be diagnosed from its exit code and stderr. Every shell run counts as an external side effect, so a failure stops the rest of the batch and is not retried automatically.\n- **terminal_open**(command, cwd, shell) [capability_expand category: terminal]: Open a PERSISTENT shell session (REPL/dev server/watch), returning a session_id for terminal_send/read/close. Disabled unless tools.terminal_session_enabled is set. For one-shot commands use shell_run instead.\n- **terminal_send**(session_id, input, wait) [capability_expand category: terminal]: Send a line of input to a persistent terminal session and return output captured shortly after.\n- **terminal_read**(session_id, wait): Drain buffered output from a persistent terminal session (optionally waiting briefly first).\n- **terminal_close**(session_id) [capability_expand category: terminal]: Terminate a persistent terminal session and release its process group.\n- **terminal_list**(): List active persistent terminal sessions.\n- **config_list**(category, limit): List LeapFlow's own writable settings (LLM model and endpoint, daemon, memory, perception, gateway, …) with current values. Use this to discover the exact key before changing anything. Optionally narrow by `category`. This is the only correct way to inspect LeapFlow configuration — never read config files from disk.\n- **config_get**(key): Read one LeapFlow setting by key (e.g. 'llm.model', 'llm.base_url', 'daemon.log_level'), returning its current value, type, scopes, and whether a change needs a daemon restart. There is no 'llm.provider' key: provider behavior is inferred from the OpenAI-compatible 'llm.base_url'. Never read LeapFlow config files from disk — use this.\n- **config_set**(key, value, scope) [capability_expand category: config]: Change one LeapFlow setting by key, e.g. switch an OpenAI-compatible LLM with key='llm.model' and key='llm.base_url'. There is no 'llm.provider' key: provider behavior is inferred from the endpoint. Values are validated and coerced; credentials are stored in the vault automatically. Call config_list or config_get first if unsure of the key. The result states whether a `leap daemon restart` is required. Never edit LeapFlow config files directly.\n- **web_fetch**(url, select, timeout, max_bytes): Read a URL over HTTP(S) and get back extracted, context-sized content: parsed JSON for API endpoints, readable text plus links for web pages. Use this for anything on the internet — prices, docs, releases, articles — instead of running curl through shell_run: it reports real HTTP status codes, retries rate limits on its own, and is a plain read so a retry is always safe. For JSON APIs pass `select` with a dotted path (e.g. 'chart.result.0.meta') to return just that part instead of the whole payload.\n- **memory_search**(query, limit): Search agent memory for relevant past experiences, observations, and facts.\n- **memory_add**(content, kind) [capability_expand category: write]: Store a new observation or insight in memory for future reference.\n- **research_note**(kind, text): Record a compact, structured note about the current task's state so it survives context compression on long / multi-step tasks. Use for durable findings, open questions still to resolve, decisions / excluded paths, and the immediate next step. One concise sentence per note.\n- **capability_expand**(category): Fetch the full callable schema for every tool in a capability category (e.g. 'hub', 'gateway', 'desktop', 'delegate', 'file', 'memory', 'skill'). The compact tool index always lists every registered tool by name and a one-line summary, but only a static low-risk subset is directly callable each turn. If you need a tool from the index that is not yet callable, call capability_expand with its category first; the matching tools become callable in this turn. Never invent a tool name — expand the category instead.\n- **delegate_task**(goal, context) [capability_expand category: delegate]: Delegate a complex sub-task to an isolated subagent. The subagent gets a fresh context and restricted tool access. Use when a task is self-contained and can be solved independently.\n- **schedule_reentry**(kind, reason, delay_seconds, event_match, max_reentries, deadline_seconds): Register a re-entry so this task can resume later from its current orientation (findings / open questions / next step). Use when work must pause and continue after a delay (kind=time) or when a matching platform event arrives (kind=event), instead of finishing now. The research-ledger state is carried over automatically.\n- **hub_push**(skill_name, visibility, version) [capability_expand category: hub]: Push a local skill to the ModelScope Hub for sharing or backup.\n- **hub_pull**(repo_id, version) [capability_expand category: hub]: Pull a skill from the ModelScope Hub to install locally.\n- **hub_search**(query) [capability_expand category: hub]: Search for skills on the Hub by keyword or description.\n- **hub_sync**(mode, dry_run) [capability_expand category: hub]: Preview or execute sync between local skills and Hub.\n- **platform_action**(platform, action, payload, backend_kind) [capability_expand category: gateway]: Execute an exact registered business action on an external platform through LeapFlow's App Connector layer. Actions must be copied from the App Connector Capability Index and are addressed as domain.operation, e.g. im.send_message or docs.create_markdown. All business fields (chat_id, text, query, etc.) MUST be placed inside `payload`, never at the top level. Example: {\"platform\":\"feishu\",\"action\":\"im.send_message\",\"payload\":{\"chat_id\":\"oc_xxx\",\"text\":\"hello\"}}. Do not invent action names, do not use management actions such as list/guide/connect/status here.\n- **platform_connect**(action, platform, credentials, options, checkpoint) [capability_expand category: gateway]: List, guide, connect, disconnect, remove, or check status for external platforms using the App Connector management namespace. Supports REST and CLI backends. Use this for management actions such as list/guide/preflight/connect/status; use platform_action only for exact registered business actions.\n- **gateway_send**(platform, chat_id, text, thread_id) [capability_expand category: gateway]: Send a message to a connected external platform (Feishu group, Telegram chat, DingTalk conversation, etc.). Requires the platform to be connected via gateway_connect first. Use gateway_connect with action='list' to see connected platforms and available chat IDs.\n- **gateway_connect**(action, platform, credentials, options) [capability_expand category: gateway]: Connect, configure, or manage external platform integrations (Feishu, DingTalk, Telegram, Slack, Discord, etc.). Conversational flow: 1) call 'guide' to get setup steps + required fields, 2) present the steps to the user and ask for ALL required credentials in a single message, 3) call 'connect' with the credentials. Goal: complete in 1–2 user turns. NEVER include credential values in your text response.\n- **plugin_list**(): List the live plugin registry and cross-subsystem capability evidence. Use this before answering questions about whether LeapFlow supports plugins, self-evolution, plugin installation, hot reload, versioning, or other runtime capabilities.\n- **plugin_status**(plugin_id): Get detailed status of a specific plugin: its declared category, runtime dependencies, contributed tools, and fiber lifecycle state.\n- **plugin_versions**(plugin_id): List recorded source versions and active pointer for a profile-scoped plugin.\n- **plugin_propose**(requested_capability, plugin_id, proposed_tools, test_cases, risk_level, evidence): Create a side-effect-free PluginProposal from explicit capability-gap evidence. Use this before plugin_generate when a missing capability should be reviewed. Does not call an LLM, write files, or install anything.\n- **assess_compatibility**(manifest, source_path) [capability_expand category: plugin_management]: Assess whether a foreign plugin manifest or real DSH source bundle is compatible with LeapFlow. A source bundle assessment is static: runtime_ready stays false until restricted Node discovery during plugin_install. Returns component-level verdicts and limitations.\n- **plugin_generate**(plugin_id, description, proposal_id) [capability_expand category: system]: Generate a new ToolPlugin from a natural-language capability description. The LLM produces code that conforms to the ToolPlugin Protocol; it is then rigorously validated (syntax, structure, import, protocol conformance). The generated source is stored in CAS and receives explicit content approval. It DOES NOT install the plugin — installation requires a second, mutation-specific approval via plugin_install.\n- **plugin_install**(plugin_id, code, marketplace_name, source_path, proposal_id, version_label) [capability_expand category: system]: Install a plugin from exactly one source: validated Python code, the configured Python marketplace, or a real DSH source bundle. DSH bundles run restricted Node discovery before registration; only public host tools are installed and client UI limitations are reported. Writes profile-scoped state and mutates the process-global registry. REQUIRES APPROVAL.\n- **plugin_rollback**(plugin_id, version) [capability_expand category: system]: Rollback a profile-scoped plugin to a recorded source version and reload it. REQUIRES APPROVAL.\n- **plugin_reload**(plugin_id, version_label) [capability_expand category: system]: Hot-reload a plugin at runtime. Disposes the old plugin fiber, re-imports its module, and registers a fresh instance. Existing in-flight turns are unaffected (snapshot isolation). REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_disable**(plugin_id) [capability_expand category: system]: Disable a plugin by disposing its fiber, removing its tools from the runtime registry. Cannot disable self_management itself. REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_remove**(plugin_id, delete_source) [capability_expand category: system]: Terminally remove a plugin: dispose its fiber, unregister its tools, remove reload metadata, and optionally delete its profile-scoped source file. Cannot remove self_management itself. REQUIRES APPROVAL.\n- **plugin_unquarantine**(plugin_id) [capability_expand category: plugin_management]: Restore a quarantined plugin to probation status for re-evaluation. Unfreezes the trust ledger, transitions the proposal back to PROBATION, and reloads the plugin. REQUIRES APPROVAL.\n- **plugin_enable**(plugin_id) [capability_expand category: system]: Re-enable a previously disabled plugin by reloading its module and registering a fresh instance. REQUIRES APPROVAL.\n- **self_describe**(facet): Introspect LeapFlow's own identity, capabilities, runtime state, evolution metrics, or platform connections. Use facet='all' only when a comprehensive self-check is explicitly requested.\n- **runtime_snapshot**(): Lightweight (~150 token) flat snapshot of current runtime state: model, context budget, posture, disclosure level, turn count, cache hit rate, uptime, tool count, and pending approvals.\n- **schedule_create**(trigger_expression, instruction, execution_mode, max_retries, delivery_target) [capability_expand category: scheduler]: Create a new scheduled task. Specify a trigger expression (e.g. '30m', 'every 2h', '0 9 * * *') and the instruction to execute on each trigger. Optionally provide an execution mode and a delivery target to receive notifications.\n- **schedule_list**() [capability_expand category: scheduler]: List all scheduled tasks with their current state, trigger, and next due time.\n- **schedule_status**(task_id) [capability_expand category: scheduler]: Get detailed status and recent execution history for a scheduled task.\n- **schedule_pause**(task_id) [capability_expand category: scheduler]: Pause a scheduled task so it stops firing without being cancelled.\n- **schedule_resume**(task_id) [capability_expand category: scheduler]: Resume a paused scheduled task — re-arms it and recalculates next due time.\n- **schedule_cancel**(task_id) [capability_expand category: scheduler]: Cancel a scheduled task permanently.\n- **tool_search**(query, max_results): Search all registered tools by keyword relevance. Returns a ranked list of matching tools with name, category, and summary. Use this when you need a tool but are unsure of its exact name or category.\n- **tool_describe**(tool_name): Get the full callable schema of a single tool by exact name. Returns the tool's description, parameters, and metadata. Use after tool_search to inspect a specific tool before calling it.\n- **session_search**(query, limit) [capability_expand category: unclassified]: Search past conversation sessions in the current workspace for relevant context.\n- **session_list**(limit): List recent conversation sessions in the current workspace with stable ids, titles, dates, and summaries. Use for browsing past tasks or when user asks to see history without specific search terms. No keywords needed — returns chronological list.\n- **session_detail**(session_id, limit, offset, include_inactive) [capability_expand category: unclassified]: Read a paginated persisted transcript for one past conversation session in the current workspace. Use after session_list or session_search returns a session_id.\n- **click**(element_index) [capability_expand category: desktop]: Click a UI element by its element_index (from the latest observe_ui snapshot)\n- **get_clipboard**() [capability_expand category: desktop]: Read current clipboard text content\n- **list_apps**(filter, running_only) [capability_expand category: desktop]: List available applications on this system. Use to discover correct bundle_id before switch_app.\n- **list_windows**() [capability_expand category: desktop]: List all top-level windows with pid, window_id, title, and per-window state (minimized, on-screen). Call this first to pick the pid and window_id that observe_ui and other window tools require.\n- **observe_ui**(pid, window_id, query) [capability_expand category: desktop]: Snapshot one window's actionable UI elements, each tagged with an element_index for click/right_click/read_text. Re-observe after actions — indices belong to one snapshot. Requires the window's pid and window_id from list_windows.\n- **open_url**(url, app_id) [capability_expand category: desktop]: Open a URL in the default or specified browser\n- **read_text**(element_index) [capability_expand category: desktop]: Read the text content of a specific UI element from the latest snapshot\n- **right_click**(element_index) [capability_expand category: desktop]: Right-click a UI element to open its context menu. Returns visible menu items.\n- **screenshot**(pid, window_id) [capability_expand category: desktop]: Capture a screenshot for visual verification. With pid + window_id captures that window (works across all displays); defaults to the last observed window, or the full desktop when no window has been observed.\n- **scroll**(element_index, direction, amount, pid, window_id) [capability_expand category: desktop]: Scroll a scrollable area of a window. Omit element_index to scroll the window's focused/page scroller; pass one to scroll an exact element from the latest snapshot.\n- **select_text**(element_index) [capability_expand category: desktop]: Select all text in a UI element (focus + select-all, for subsequent copy)\n- **set_clipboard**(text) [capability_expand category: desktop]: Write text to the clipboard\n- **shortcut**(keys) [capability_expand category: desktop]: Execute a keyboard shortcut\n- **switch_app**(app_id) [capability_expand category: desktop]: Switch to an app (launch if needed, activate, verify)\n- **type_text**(text) [capability_expand category: desktop]: Type text into the currently focused element\n- **wait**(seconds) [capability_expand category: desktop]: Wait for a specified duration before continuing\n- **wait_until**(condition, pid, window_id, timeout, poll_interval) [capability_expand category: desktop]: Wait until a UI condition is met (polls UI tree). Returns elements when found or on timeout.\n- **wait_until_stable**(timeout, poll_interval, pid, window_id) [capability_expand category: desktop]: Wait until the UI stops changing (element set stabilizes across polls).\n\n## App Connector Capability Index\nLeapFlow can onboard and manage external apps through `platform_connect` and execute exact registered business actions through `platform_action`.\nFor requests about connecting, setting up, configuring, enabling, or managing a supported app, use `platform_connect` first instead of generating SDK/Webhook sample code.\n`platform_connect.action` is the management namespace: list, guide, preflight, connect, disconnect, remove, status, events_start, events_stop, events_status.\n`platform_action.action` is only for exact registered platform business actions listed below, such as `im.send_message`; never use management actions like `list` or `guide` there.\nAll business fields MUST go inside `payload`; top-level keys are only `platform`, `action`, and `payload`.\nDo not invent platform IDs or platform action names. If the needed action is not listed, ask for discovery/clarification instead of guessing.\nUse `platform_connect` with `action='guide'` and the matching `platform` to start onboarding; use `action='list'` when the app is unclear.\nWhen a pending onboarding state is present, continue from that state with `platform_connect` instead of asking the user to restate the app.\nSupported apps:\n- `api_server`: API Server (OpenAI Compatible) (category=api; backend=adapter; platform_action actions=none registered)\n- `dingtalk`: 钉钉 (DingTalk) (category=im; backend=adapter; platform_action actions=none registered)\n- `feishu`: 飞书 (Feishu/Lark) (category=im; backend=cli)\n - `calendar.create_event` payload={summary*, start_time*, end_time*, attendees} [write/high]\n - `docs.create_markdown` payload={title*, markdown*, folder_token} [write/medium]\n - `drive.search` payload={query*, limit} [read/medium]\n - `im.add_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.download_resource` payload={message_id*, file_key*, type*} [read/low]\n - `im.get_messages` payload={message_ids*} [read/low]\n - `im.list_chats` payload={page_size} [read/low]\n - `im.list_messages` payload={chat_id*} [read/low]\n - `im.list_thread_messages` payload={thread*} [read/low]\n - `im.remove_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.reply_message` payload={message_id*, text*} [send/high]\n - `im.search_chats` payload={query*, page_size} [read/low]\n - `im.search_messages` payload={query*} [read/medium]\n - `im.send_message` payload={chat_id*, text*, thread_id} [send/high]\n - `im.update_card` payload={token*, card*} [write/medium]\n - `im.update_message` payload={message_id*, text*} [write/medium]\n - `mail.search_unread` payload={query, limit} [read/high]\n - `sheets.append_row` payload={spreadsheet_token*, sheet_id*, values*} [write/medium]\n - `task.create` payload={title*, description, due_time} [write/medium]\n- `telegram`: Telegram (category=social; backend=adapter; platform_action actions=none registered)\n- `webhook`: Webhook (Generic) (category=webhook; backend=adapter; platform_action actions=none registered)\n\n## Tool Usage\nTools are normally invoked through the native function-calling mechanism, not by writing JSON in your reply\ntext. Only if the provider signals that native function calling is unavailable for this turn, fall back to a\nsingle JSON code block: `{\"name\": \"tool_name\", \"arguments\": {\"key\": \"value\"}}` — use this fallback format\nonly, never both. Only call a tool whose exact name appears in the tool index above and is currently callable\n(or reachable via `capability_expand`). Never invent, rename, alias, or guess a tool name, platform ID, or\nplatform action from argument shape or wording — if the index does not list it, it does not exist.\n`platform_connect.action` (list/guide/preflight/connect/disconnect/remove/status/events_start/events_stop/\nevents_status) is the App Connector management namespace; `platform_action.action` only accepts exact\nregistered business actions such as `im.send_message` shown in the App Connector Capability Index — never mix\nthe two namespaces. If a tool call returns an unknown/unavailable result, use the returned suggestions or\navailable names for a single retry instead of trying further variations of the same guess.\n\n**Side-effect action rule** (`platform_action` with effect=send/write/execute):\n- Call each unique action+payload **exactly once**. Never include duplicates in the same turn.\n- Once the result returns `\"completed\": true`, that action is DONE for this task. Do NOT call it again\n in any subsequent turn — immediately summarize the result for the user instead.\n- The system enforces idempotency: duplicate calls are blocked and will not execute.\n- If the user explicitly requests sending/writing multiple times, use distinct payloads per call.\n\n**Resource identifier provenance rule**:\n- NEVER fabricate, guess, or infer resource identifiers (chat_id, message_id, file_key, user_id, etc.).\n Every resource ID used in a side-effect action MUST come from a successful API response in this session.\n- If a read/list action fails (e.g. authorization error), you do NOT have valid resource IDs.\n Report the failure to the user — do NOT attempt the dependent write/send action with a guessed ID.\n- When a tool result contains `\"llm_instruction\"`, follow it exactly.\n\n## Guidelines\n1. **Direct answers first**: If you already know the answer, respond directly without tools.\n2. **Avoid redundant tool calls**: Do not call the same tool with the same arguments more than once in the same user turn. When an existing tool result already answers the user's request, stop calling tools and answer directly.\n3. **Use tools proactively**: When the user asks about files, time, system state, or needs actions performed, use the appropriate tool.\n4. **Chain tools when needed**: You can call multiple tools in sequence (e.g., list files → read file → summarize).\n5. **Handle failures gracefully**: If a tool fails, explain what went wrong and suggest alternatives. If it failed because of an unknown tool/platform/action name, retry once with an exact name from the returned suggestions, then explain rather than keep guessing.\n6. **Summarize results naturally**: After tool execution, synthesize the results into a helpful answer rather than dumping raw output.\n7. **Stay conversational**: Maintain a natural, helpful tone. Acknowledge context from earlier in the conversation.\n8. **Recall past work**:\n - Broad queries (\"之前做了什么\", \"列出任务\"): answer from the \"Recent Task History\" section already in your context. If insufficient, call session_list.\n - Specific lookups (\"上次那个配置怎么改的\"): call session_search with relevant phrases (NOT single characters).\n - Do NOT call search tools repeatedly with keyword fragments. One well-phrased call is better than ten fragmented ones.\n\n## Coding & Verification\nWhen working with code, prefer the precise built-in tools over ad-hoc shell: use `repo_map` to orient in an\nunfamiliar project, `code_search`/`file_find` to locate and `code_intel` for symbols, `edit_file` (anchored\nsearch-replace, or a unified `diff`) to change files — never rewrite a whole file to change a few lines — and\n`git_query` to inspect diffs/log. After edits, check `syntax_ok` in the result and run `test_run`/`lint_check`\nbefore declaring the work done. Batch independent read-only calls (search/read/query) into a single turn — they\nrun in parallel.\n\n## Presentation Style\n1. **Polished Markdown only**: Format user-facing answers with clean Markdown headings, short paragraphs, and concise bullets. Use tables only when they improve comparison or scanning.\n2. **Terminal-friendly layout**: Keep lines readable in a TUI; avoid dense walls of text, deeply nested lists, oversized ASCII art, or heavy visual blocks.\n3. **Elegant emphasis**: Use bold text sparingly for key terms and conclusions. Avoid excessive emojis, decorative symbols, repeated separators, or visual noise.\n4. **Theme-safe colors**: Do not emit ANSI escape codes, HTML color tags, Rich markup, or hardcoded color names. Rely on the TUI theme to render Markdown professionally.\n5. **No leaked tool protocol**: Never show tool-call JSON, internal schemas, raw observations, tool result payloads, or hidden reasoning in the final answer unless the user explicitly asks for raw/debug output. Treat any prior `{\"name\": ..., \"arguments\": ...}` blocks and `Tool result (...)` messages as internal execution context only.\n6. **Professional closure**: End with a concise conclusion or next step when helpful; avoid rambling after the useful answer is complete.\n\nWhen finished with all tool calls, respond normally without a JSON block, tool-call transcript, or process log.\n\n## Task Contract\n- Task ID: turn-1\n- Original user request: Let me show you something.\n- Workspace root: \n- Allowed roots: \n- Treat relative project paths as relative to the workspace root; never infer `.` as the project root when a workspace root is provided.\n- Workspace boundary is enforced by tools: do not read, search, edit, or run commands against paths outside the allowed roots unless the user explicitly requests an external path and the tool/approval policy permits it.\n- LeapFlow workspace config is optional at `/.leapflow/config.yaml`; runtime config is loaded from `~/.leapflow/config/user.yaml` and `~/.leapflow/profiles//config/*.yaml`.\n- Preserve this task contract across summarization, compression, tool loops, and memory retrieval.\n" - }, - { - "role": "system", - "content": "\n## Recent Session Summary\n- [user] Let me show you something.\n" - }, - { - "role": "user", - "content": "Let me show you something.\nLet me show you something." - } - ], - "tools": [ - "capability_expand", - "code_intel", - "code_search", - "config_get", - "config_list", - "env_info", - "file_find", - "file_list", - "file_read", - "git_query", - "lint_check", - "memory_search", - "plugin_list", - "plugin_propose", - "plugin_status", - "plugin_versions", - "repo_map", - "research_note", - "runtime_snapshot", - "schedule_reentry", - "self_describe", - "session_list", - "skill_view", - "skills_list", - "terminal_list", - "terminal_read", - "test_run", - "text_search", - "time_get", - "tool_describe", - "tool_search", - "web_fetch" - ] - }, - "responses": [ - { - "status": 200, - "content_type": "application/json", - "body": "{\"id\": \"chatcmpl-cassette\", \"object\": \"chat.completion\", \"model\": \"cassette-model\", \"choices\": [{\"index\": 0, \"message\": {\"role\": \"assistant\", \"content\": \"Noted the first step.\"}, \"finish_reason\": \"stop\"}], \"usage\": {\"prompt_tokens\": 64, \"completion_tokens\": 16, \"total_tokens\": 80}}" - } - ] -} diff --git a/tests/_fixtures/cassettes/r5_learning/cassette-model-97b715bc47cd3386.cassette.json b/tests/_fixtures/cassettes/r5_learning/cassette-model-97b715bc47cd3386.cassette.json new file mode 100644 index 00000000..b6f96318 --- /dev/null +++ b/tests/_fixtures/cassettes/r5_learning/cassette-model-97b715bc47cd3386.cassette.json @@ -0,0 +1,67 @@ +{ + "fingerprint": "97b715bc47cd3386a956cbb6c98c5658ecaf8df5bd2a915a92969726a3e54449", + "note": "captured in seed mode", + "request": { + "model": "cassette-model", + "stream": false, + "messages": [ + { + "role": "system", + "content": "You are LeapFlow, an intelligent assistant that can both converse naturally and take real actions on the user's computer.\n\n## Capabilities\nThe tool index below lists **every** registered tool by name and a one-line summary — this is the complete\ncapability contract; nothing else exists. Only a subset is directly callable this turn (via native tool calling,\nnot a JSON block in your reply). If you need a tool from the index that is not yet callable, call\n`capability_expand` with its category name first — the matching tools become callable immediately after.\nWhen the user asks about LeapFlow **itself** — its identity/version/model, what tools, plugins, or capabilities\nit has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of\nself-awareness — answer from live runtime evidence via `self_describe` (facet=capabilities for \"what can you\ndo / do you support X\", identity for version/model, runtime for context/posture, or `all` for a full self-check),\nnever by reading LeapFlow's own source code, documentation, or memory. Report configuration-dependent or\nunavailable capabilities as limitations rather than inferring them.\n- **text_search**(text, pattern): Search for a regex pattern in text.\n- **text_replace**(text, old, new, count) [capability_expand category: write]: Replace occurrences of a substring in text.\n- **time_get**(): Get current date and time.\n- **env_info**(): Get system environment information (OS, Python version, cwd).\n- **skills_list**(query, category, source): List available learned skills. Use when user asks about capabilities or you need a specific skill.\n- **skill_view**(name): View the full content of a specific skill document.\n- **skill_list_files**(name) [capability_expand category: skills]: List all support files in a skill directory (configs, templates, examples). Use before skill_file_read to discover available files.\n- **skill_file_read**(name, file_path) [capability_expand category: skills]: Read an individual support file from a skill directory. Use skill_list_files first to discover available files.\n- **code_intel**(path, operation): Precise document symbols (outline) for a source file: classes, functions, and methods with line ranges. Python uses an exact AST parse; other languages use a keyword-prefix scan. Prefer over file_read mode=symbols for accurate navigation before editing. Read-only.\n- **repo_map**(path): Compact project orientation for a repository root: languages, detected test/lint commands, top-level structure, entry points, manifest, and VCS branch. Call this first when entering an unfamiliar codebase. Read-only.\n- **scm_sync**(action, cwd, remote, pull_ref, push_ref, timeout) [capability_expand category: scm]: Run a typed git SCM action. Use this instead of shell_run for git pull/push/status. For 'pull origin main then push', set action='pull_then_push', remote='origin', pull_ref='main', and omit push_ref so LeapFlow pushes the current local branch.\n- **git_query**(action, cwd, ref, path, staged, max_count, stat): Read-only structured git inspection: action=diff|log|status|branch|show. Prefer over shell_run for reading repo state — output is clipped, redacted, and log/branch are parsed into structured fields. Use scm_sync for pull/push.\n- **git_write**(action, cwd, message, stage_all, name, ref, create) [capability_expand category: scm]: Mutating git actions: action=commit (message, stage_all), branch (create+switch), checkout (switch; create=true for -b). Approval-gated. Use scm_sync for pull/push and git_query for reads.\n- **test_run**(command, cwd, timeout): Run the project's test suite and return structured results (framework, passed/failed counts, failing tests). Auto-detects the runner (pytest/npm/go/cargo) or uses a configured/explicit command; executes via the governed shell. ok=true means the runner executed — see 'success' for pass/fail.\n- **lint_check**(command, cwd, timeout): Run the project's linter and return a structured clean/issue result. Auto-detects the linter (ruff/eslint/go vet/clippy) or uses a configured/explicit command; executes via the governed shell. ok=true means the linter ran — see 'clean'.\n- **file_list**(path, pattern, depth): List files and directories at a given path. Use depth=1 or depth=2 to get a recursive tree in one call instead of listing each sub-directory separately.\n- **file_read**(path, max_lines, start_line, max_chars, mode): Read text file content with adaptive context governance. For large or unfamiliar files, prefer mode='outline' or mode='symbols' first, then use mode='raw' with start_line/max_lines for the specific range you actually need. For LeapFlow's own settings, use config_list / config_get / config_set — its config files are outside the workspace and not readable here.\n- **file_write**(path, content, mode) [capability_expand category: write]: Write content to a file (overwrite or append).\n- **code_search**(pattern, patterns, path, glob, ignore_case, multiline, max_results, context_lines): Search file CONTENTS by regex pattern across a directory tree (ripgrep-backed). Requires a regex pattern. NOT for listing or browsing directory contents — use file_list for that. Prefer this over shell_run grep: faster, skips VCS/dependency/build dirs, and returns structured path:line:column matches. Batch related lookups into ONE call via `patterns` (OR-combined, single pass) instead of issuing several separate searches. Use file_read for the surrounding context of a hit.\n- **file_find**(glob, path, max_results): Find files by a recursive glob pattern under a base path (e.g. '**/test_*.py' or '*.md'). Prefer this over shell_run find; skips VCS/dependency/build dirs.\n- **edit_file**(path, edits, dry_run, diff) [capability_expand category: write]: Apply targeted, anchored search-replace edits to an EXISTING text file (use file_write to create/overwrite). Each edit is {original_text, new_text, replace_all?}; original_text must match exactly and uniquely (or set replace_all) — a non-unique or missing anchor is rejected so files are never corrupted. Set dry_run to preview. Alternatively pass a unified 'diff' to apply its hunks as anchored edits. Far cheaper and safer than rewriting a whole file.\n- **shell_run**(command, cwd, timeout) [capability_expand category: shell]: Execute a one-shot shell command with timeout protection. Runs in the active workspace; paths resolving outside it are refused. Reach for a structured tool first when one fits — web_fetch for anything over HTTP(S), git_query/scm_sync for git, code_search/file_find/file_read for the repo, config_get/config_set for LeapFlow's own settings — because those report typed results, while a failed shell command can only be diagnosed from its exit code and stderr. Every shell run counts as an external side effect, so a failure stops the rest of the batch and is not retried automatically.\n- **terminal_open**(command, cwd, shell) [capability_expand category: terminal]: Open a PERSISTENT shell session (REPL/dev server/watch), returning a session_id for terminal_send/read/close. Disabled unless tools.terminal_session_enabled is set. For one-shot commands use shell_run instead.\n- **terminal_send**(session_id, input, wait) [capability_expand category: terminal]: Send a line of input to a persistent terminal session and return output captured shortly after.\n- **terminal_read**(session_id, wait): Drain buffered output from a persistent terminal session (optionally waiting briefly first).\n- **terminal_close**(session_id) [capability_expand category: terminal]: Terminate a persistent terminal session and release its process group.\n- **terminal_list**(): List active persistent terminal sessions.\n- **config_list**(category, limit): List LeapFlow's own writable settings (LLM model and endpoint, daemon, memory, perception, gateway, …) with current values. Use this to discover the exact key before changing anything. Optionally narrow by `category`. This is the only correct way to inspect LeapFlow configuration — never read config files from disk.\n- **config_get**(key): Read one LeapFlow setting by key (e.g. 'llm.model', 'llm.base_url', 'daemon.log_level'), returning its current value, type, scopes, and whether a change needs a daemon restart. There is no 'llm.provider' key: provider behavior is inferred from the OpenAI-compatible 'llm.base_url'. Never read LeapFlow config files from disk — use this.\n- **config_set**(key, value, scope) [capability_expand category: config]: Change one LeapFlow setting by key, e.g. switch an OpenAI-compatible LLM with key='llm.model' and key='llm.base_url'. There is no 'llm.provider' key: provider behavior is inferred from the endpoint. Values are validated and coerced; credentials are stored in the vault automatically. Call config_list or config_get first if unsure of the key. The result states whether a `leap daemon restart` is required. Never edit LeapFlow config files directly.\n- **web_fetch**(url, select, timeout, max_bytes): Read a URL over HTTP(S) and get back extracted, context-sized content: parsed JSON for API endpoints, readable text plus links for web pages. Use this for anything on the internet — prices, docs, releases, articles — instead of running curl through shell_run: it reports real HTTP status codes, retries rate limits on its own, and is a plain read so a retry is always safe. For JSON APIs pass `select` with a dotted path (e.g. 'chart.result.0.meta') to return just that part instead of the whole payload.\n- **memory_search**(query, limit): Search agent memory for relevant past experiences, observations, and facts.\n- **memory_add**(content, kind) [capability_expand category: write]: Store a new observation or insight in memory for future reference.\n- **research_note**(kind, text): Record a compact, structured note about the current task's state so it survives context compression on long / multi-step tasks. Use for durable findings, open questions still to resolve, decisions / excluded paths, and the immediate next step. One concise sentence per note.\n- **capability_expand**(category): Fetch the full callable schema for every tool in a capability category (e.g. 'hub', 'gateway', 'desktop', 'delegate', 'file', 'memory', 'skill'). The compact tool index always lists every registered tool by name and a one-line summary, but only a static low-risk subset is directly callable each turn. If you need a tool from the index that is not yet callable, call capability_expand with its category first; the matching tools become callable in this turn. Never invent a tool name — expand the category instead.\n- **delegate_task**(goal, context) [capability_expand category: delegate]: Delegate a complex sub-task to an isolated subagent. The subagent gets a fresh context and restricted tool access. Use when a task is self-contained and can be solved independently.\n- **schedule_reentry**(kind, reason, delay_seconds, event_match, max_reentries, deadline_seconds): Register a re-entry so this task can resume later from its current orientation (findings / open questions / next step). Use when work must pause and continue after a delay (kind=time) or when a matching platform event arrives (kind=event), instead of finishing now. The research-ledger state is carried over automatically.\n- **hub_push**(skill_name, visibility, version) [capability_expand category: hub]: Push a local skill to the ModelScope Hub for sharing or backup.\n- **hub_pull**(repo_id, version) [capability_expand category: hub]: Pull a skill from the ModelScope Hub to install locally.\n- **hub_search**(query) [capability_expand category: hub]: Search for skills on the Hub by keyword or description.\n- **hub_sync**(mode, dry_run) [capability_expand category: hub]: Preview or execute sync between local skills and Hub.\n- **platform_action**(platform, action, payload, backend_kind) [capability_expand category: gateway]: Execute an exact registered business action on an external platform through LeapFlow's App Connector layer. Actions must be copied from the App Connector Capability Index and are addressed as domain.operation, e.g. im.send_message or docs.create_markdown. All business fields (chat_id, text, query, etc.) MUST be placed inside `payload`, never at the top level. Example: {\"platform\":\"feishu\",\"action\":\"im.send_message\",\"payload\":{\"chat_id\":\"oc_xxx\",\"text\":\"hello\"}}. Do not invent action names, do not use management actions such as list/guide/connect/status here.\n- **platform_connect**(action, platform, credentials, options, checkpoint) [capability_expand category: gateway]: List, guide, connect, disconnect, remove, or check status for external platforms using the App Connector management namespace. Supports REST and CLI backends. Use this for management actions such as list/guide/preflight/connect/status; use platform_action only for exact registered business actions.\n- **gateway_send**(platform, chat_id, text, thread_id) [capability_expand category: gateway]: Send a message to a connected external platform (Feishu group, Telegram chat, DingTalk conversation, etc.). Requires the platform to be connected via gateway_connect first. Use gateway_connect with action='list' to see connected platforms and available chat IDs.\n- **gateway_connect**(action, platform, credentials, options) [capability_expand category: gateway]: Connect, configure, or manage external platform integrations (Feishu, DingTalk, Telegram, Slack, Discord, etc.). Conversational flow: 1) call 'guide' to get setup steps + required fields, 2) present the steps to the user and ask for ALL required credentials in a single message, 3) call 'connect' with the credentials. Goal: complete in 1–2 user turns. NEVER include credential values in your text response.\n- **plugin_list**(): List the live plugin registry across Tool/Gateway/LLM subsystems for plugin management: which plugins are loaded, their category and fiber lifecycle state. For questions about LeapFlow's own capabilities or whether it supports X, use self_describe instead.\n- **plugin_status**(plugin_id): Get detailed status of a specific plugin: its declared category, runtime dependencies, contributed tools, and fiber lifecycle state.\n- **plugin_versions**(plugin_id): List recorded source versions and active pointer for a profile-scoped plugin.\n- **plugin_propose**(requested_capability, plugin_id, proposed_tools, test_cases, risk_level, evidence): Create a side-effect-free PluginProposal from explicit capability-gap evidence. Use this before plugin_generate when a missing capability should be reviewed. Does not call an LLM, write files, or install anything.\n- **assess_compatibility**(manifest, source_path) [capability_expand category: plugin_management]: Assess whether a foreign plugin manifest or real DSH source bundle is compatible with LeapFlow. A source bundle assessment is static: runtime_ready stays false until restricted Node discovery during plugin_install. Returns component-level verdicts and limitations.\n- **plugin_generate**(plugin_id, description, proposal_id) [capability_expand category: system]: Generate a new ToolPlugin from a natural-language capability description. The LLM produces code that conforms to the ToolPlugin Protocol; it is then rigorously validated (syntax, structure, import, protocol conformance). The generated source is stored in CAS and receives explicit content approval. It DOES NOT install the plugin — installation requires a second, mutation-specific approval via plugin_install.\n- **plugin_install**(plugin_id, code, marketplace_name, source_path, proposal_id, version_label) [capability_expand category: system]: Install a plugin from exactly one source: validated Python code, the configured Python marketplace, or a real DSH source bundle. DSH bundles run restricted Node discovery before registration; only public host tools are installed and client UI limitations are reported. Writes profile-scoped state and mutates the process-global registry. REQUIRES APPROVAL.\n- **plugin_rollback**(plugin_id, version) [capability_expand category: system]: Rollback a profile-scoped plugin to a recorded source version and reload it. REQUIRES APPROVAL.\n- **plugin_reload**(plugin_id, version_label) [capability_expand category: system]: Hot-reload a plugin at runtime. Disposes the old plugin fiber, re-imports its module, and registers a fresh instance. Existing in-flight turns are unaffected (snapshot isolation). REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_disable**(plugin_id) [capability_expand category: system]: Disable a plugin by disposing its fiber, removing its tools from the runtime registry. Cannot disable self_management itself. REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_remove**(plugin_id, delete_source) [capability_expand category: system]: Terminally remove a plugin: dispose its fiber, unregister its tools, remove reload metadata, and optionally delete its profile-scoped source file. Cannot remove self_management itself. REQUIRES APPROVAL.\n- **plugin_unquarantine**(plugin_id) [capability_expand category: plugin_management]: Restore a quarantined plugin to probation status for re-evaluation. Unfreezes the trust ledger, transitions the proposal back to PROBATION, and reloads the plugin. REQUIRES APPROVAL.\n- **plugin_enable**(plugin_id) [capability_expand category: system]: Re-enable a previously disabled plugin by reloading its module and registering a fresh instance. REQUIRES APPROVAL.\n- **self_describe**(facet): Answer questions about LeapFlow ITSELF from live runtime evidence — its identity/version/model, what tools and plugins it has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of self-awareness. Prefer this over reading LeapFlow's own source code. Pick facet=capabilities for 'what can you do / do you support X', identity for version/model, runtime for context/posture, or all for a full self-check when explicitly requested.\n- **runtime_snapshot**(): Lightweight (~150 token) flat snapshot of current runtime state: model, context budget, posture, disclosure level, turn count, cache hit rate, uptime, tool count, and pending approvals.\n- **schedule_create**(trigger_expression, instruction, execution_mode, max_retries, delivery_target) [capability_expand category: scheduler]: Create a new scheduled task. Specify a trigger expression (e.g. '30m', 'every 2h', '0 9 * * *') and the instruction to execute on each trigger. Optionally provide an execution mode and a delivery target to receive notifications.\n- **schedule_list**() [capability_expand category: scheduler]: List all scheduled tasks with their current state, trigger, and next due time.\n- **schedule_status**(task_id) [capability_expand category: scheduler]: Get detailed status and recent execution history for a scheduled task.\n- **schedule_pause**(task_id) [capability_expand category: scheduler]: Pause a scheduled task so it stops firing without being cancelled.\n- **schedule_resume**(task_id) [capability_expand category: scheduler]: Resume a paused scheduled task — re-arms it and recalculates next due time.\n- **schedule_cancel**(task_id) [capability_expand category: scheduler]: Cancel a scheduled task permanently.\n- **tool_search**(query, max_results): Search all registered tools by keyword relevance. Returns a ranked list of matching tools with name, category, and summary. Use this when you need a tool but are unsure of its exact name or category.\n- **tool_describe**(tool_name): Get the full callable schema of a single tool by exact name. Returns the tool's description, parameters, and metadata. Use after tool_search to inspect a specific tool before calling it.\n- **session_search**(query, limit) [capability_expand category: unclassified]: Search past conversation sessions in the current workspace for relevant context.\n- **session_list**(limit): List recent conversation sessions in the current workspace with stable ids, titles, dates, and summaries. Use for browsing past tasks or when user asks to see history without specific search terms. No keywords needed — returns chronological list.\n- **session_detail**(session_id, limit, offset, include_inactive) [capability_expand category: unclassified]: Read a paginated persisted transcript for one past conversation session in the current workspace. Use after session_list or session_search returns a session_id.\n- **click**(element_index) [capability_expand category: desktop]: Click a UI element by its element_index (from the latest observe_ui snapshot)\n- **get_clipboard**() [capability_expand category: desktop]: Read current clipboard text content\n- **list_apps**(filter, running_only) [capability_expand category: desktop]: List available applications on this system. Use to discover correct bundle_id before switch_app.\n- **list_windows**() [capability_expand category: desktop]: List all top-level windows with pid, window_id, title, and per-window state (minimized, on-screen). Call this first to pick the pid and window_id that observe_ui and other window tools require.\n- **observe_ui**(pid, window_id, query) [capability_expand category: desktop]: Snapshot one window's actionable UI elements, each tagged with an element_index for click/right_click/read_text. Re-observe after actions — indices belong to one snapshot. Requires the window's pid and window_id from list_windows.\n- **open_url**(url, app_id) [capability_expand category: desktop]: Open a URL in the default or specified browser\n- **read_text**(element_index) [capability_expand category: desktop]: Read the text content of a specific UI element from the latest snapshot\n- **right_click**(element_index) [capability_expand category: desktop]: Right-click a UI element to open its context menu. Returns visible menu items.\n- **screenshot**(pid, window_id) [capability_expand category: desktop]: Capture a screenshot for visual verification. With pid + window_id captures that window (works across all displays); defaults to the last observed window, or the full desktop when no window has been observed.\n- **scroll**(element_index, direction, amount, pid, window_id) [capability_expand category: desktop]: Scroll a scrollable area of a window. Omit element_index to scroll the window's focused/page scroller; pass one to scroll an exact element from the latest snapshot.\n- **select_text**(element_index) [capability_expand category: desktop]: Select all text in a UI element (focus + select-all, for subsequent copy)\n- **set_clipboard**(text) [capability_expand category: desktop]: Write text to the clipboard\n- **shortcut**(keys) [capability_expand category: desktop]: Execute a keyboard shortcut\n- **switch_app**(app_id) [capability_expand category: desktop]: Switch to an app (launch if needed, activate, verify)\n- **type_text**(text) [capability_expand category: desktop]: Type text into the currently focused element\n- **wait**(seconds) [capability_expand category: desktop]: Wait for a specified duration before continuing\n- **wait_until**(condition, pid, window_id, timeout, poll_interval) [capability_expand category: desktop]: Wait until a UI condition is met (polls UI tree). Returns elements when found or on timeout.\n- **wait_until_stable**(timeout, poll_interval, pid, window_id) [capability_expand category: desktop]: Wait until the UI stops changing (element set stabilizes across polls).\n\n## App Connector Capability Index\nLeapFlow can onboard and manage external apps through `platform_connect` and execute exact registered business actions through `platform_action`.\nFor requests about connecting, setting up, configuring, enabling, or managing a supported app, use `platform_connect` first instead of generating SDK/Webhook sample code.\n`platform_connect.action` is the management namespace: list, guide, preflight, connect, disconnect, remove, status, events_start, events_stop, events_status.\n`platform_action.action` is only for exact registered platform business actions listed below, such as `im.send_message`; never use management actions like `list` or `guide` there.\nAll business fields MUST go inside `payload`; top-level keys are only `platform`, `action`, and `payload`.\nDo not invent platform IDs or platform action names. If the needed action is not listed, ask for discovery/clarification instead of guessing.\nUse `platform_connect` with `action='guide'` and the matching `platform` to start onboarding; use `action='list'` when the app is unclear.\nWhen a pending onboarding state is present, continue from that state with `platform_connect` instead of asking the user to restate the app.\nSupported apps:\n- `api_server`: API Server (OpenAI Compatible) (category=api; backend=adapter; platform_action actions=none registered)\n- `dingtalk`: 钉钉 (DingTalk) (category=im; backend=adapter; platform_action actions=none registered)\n- `feishu`: 飞书 (Feishu/Lark) (category=im; backend=cli)\n - `calendar.create_event` payload={summary*, start_time*, end_time*, attendees} [write/high]\n - `docs.create_markdown` payload={title*, markdown*, folder_token} [write/medium]\n - `drive.search` payload={query*, limit} [read/medium]\n - `im.add_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.download_resource` payload={message_id*, file_key*, type*} [read/low]\n - `im.get_messages` payload={message_ids*} [read/low]\n - `im.list_chats` payload={page_size} [read/low]\n - `im.list_messages` payload={chat_id*} [read/low]\n - `im.list_thread_messages` payload={thread*} [read/low]\n - `im.remove_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.reply_message` payload={message_id*, text*} [send/high]\n - `im.search_chats` payload={query*, page_size} [read/low]\n - `im.search_messages` payload={query*} [read/medium]\n - `im.send_message` payload={chat_id*, text*, thread_id} [send/high]\n - `im.update_card` payload={token*, card*} [write/medium]\n - `im.update_message` payload={message_id*, text*} [write/medium]\n - `mail.search_unread` payload={query, limit} [read/high]\n - `sheets.append_row` payload={spreadsheet_token*, sheet_id*, values*} [write/medium]\n - `task.create` payload={title*, description, due_time} [write/medium]\n- `telegram`: Telegram (category=social; backend=adapter; platform_action actions=none registered)\n- `webhook`: Webhook (Generic) (category=webhook; backend=adapter; platform_action actions=none registered)\n\n## Tool Usage\nTools are normally invoked through the native function-calling mechanism, not by writing JSON in your reply\ntext. Only if the provider signals that native function calling is unavailable for this turn, fall back to a\nsingle JSON code block: `{\"name\": \"tool_name\", \"arguments\": {\"key\": \"value\"}}` — use this fallback format\nonly, never both. Only call a tool whose exact name appears in the tool index above and is currently callable\n(or reachable via `capability_expand`). Never invent, rename, alias, or guess a tool name, platform ID, or\nplatform action from argument shape or wording — if the index does not list it, it does not exist.\n`platform_connect.action` (list/guide/preflight/connect/disconnect/remove/status/events_start/events_stop/\nevents_status) is the App Connector management namespace; `platform_action.action` only accepts exact\nregistered business actions such as `im.send_message` shown in the App Connector Capability Index — never mix\nthe two namespaces. If a tool call returns an unknown/unavailable result, use the returned suggestions or\navailable names for a single retry instead of trying further variations of the same guess.\n\n**Side-effect action rule** (`platform_action` with effect=send/write/execute):\n- Call each unique action+payload **exactly once**. Never include duplicates in the same turn.\n- Once the result returns `\"completed\": true`, that action is DONE for this task. Do NOT call it again\n in any subsequent turn — immediately summarize the result for the user instead.\n- The system enforces idempotency: duplicate calls are blocked and will not execute.\n- If the user explicitly requests sending/writing multiple times, use distinct payloads per call.\n\n**Resource identifier provenance rule**:\n- NEVER fabricate, guess, or infer resource identifiers (chat_id, message_id, file_key, user_id, etc.).\n Every resource ID used in a side-effect action MUST come from a successful API response in this session.\n- If a read/list action fails (e.g. authorization error), you do NOT have valid resource IDs.\n Report the failure to the user — do NOT attempt the dependent write/send action with a guessed ID.\n- When a tool result contains `\"llm_instruction\"`, follow it exactly.\n\n## Guidelines\n1. **Direct answers first**: If you already know the answer, respond directly without tools.\n2. **Avoid redundant tool calls**: Do not call the same tool with the same arguments more than once in the same user turn. When an existing tool result already answers the user's request, stop calling tools and answer directly.\n3. **Use tools proactively**: When the user asks about files, time, system state, or needs actions performed, use the appropriate tool.\n4. **Chain tools when needed**: You can call multiple tools in sequence (e.g., list files → read file → summarize).\n5. **Handle failures gracefully**: If a tool fails, explain what went wrong and suggest alternatives. If it failed because of an unknown tool/platform/action name, retry once with an exact name from the returned suggestions, then explain rather than keep guessing.\n6. **Summarize results naturally**: After tool execution, synthesize the results into a helpful answer rather than dumping raw output.\n7. **Stay conversational**: Maintain a natural, helpful tone. Acknowledge context from earlier in the conversation.\n8. **Recall past work**:\n - Broad queries (\"之前做了什么\", \"列出任务\"): answer from the \"Recent Task History\" section already in your context. If insufficient, call session_list.\n - Specific lookups (\"上次那个配置怎么改的\"): call session_search with relevant phrases (NOT single characters).\n - Do NOT call search tools repeatedly with keyword fragments. One well-phrased call is better than ten fragmented ones.\n\n## Coding & Verification\nWhen working with code, prefer the precise built-in tools over ad-hoc shell: use `repo_map` to orient in an\nunfamiliar project, `code_search`/`file_find` to locate and `code_intel` for symbols, `edit_file` (anchored\nsearch-replace, or a unified `diff`) to change files — never rewrite a whole file to change a few lines — and\n`git_query` to inspect diffs/log. After edits, check `syntax_ok` in the result and run `test_run`/`lint_check`\nbefore declaring the work done. Batch independent read-only calls (search/read/query) into a single turn — they\nrun in parallel.\n\n## Presentation Style\n1. **Polished Markdown only**: Format user-facing answers with clean Markdown headings, short paragraphs, and concise bullets. Use tables only when they improve comparison or scanning.\n2. **Terminal-friendly layout**: Keep lines readable in a TUI; avoid dense walls of text, deeply nested lists, oversized ASCII art, or heavy visual blocks.\n3. **Elegant emphasis**: Use bold text sparingly for key terms and conclusions. Avoid excessive emojis, decorative symbols, repeated separators, or visual noise.\n4. **Theme-safe colors**: Do not emit ANSI escape codes, HTML color tags, Rich markup, or hardcoded color names. Rely on the TUI theme to render Markdown professionally.\n5. **No leaked tool protocol**: Never show tool-call JSON, internal schemas, raw observations, tool result payloads, or hidden reasoning in the final answer unless the user explicitly asks for raw/debug output. Treat any prior `{\"name\": ..., \"arguments\": ...}` blocks and `Tool result (...)` messages as internal execution context only.\n6. **Professional closure**: End with a concise conclusion or next step when helpful; avoid rambling after the useful answer is complete.\n\nWhen finished with all tool calls, respond normally without a JSON block, tool-call transcript, or process log.\n\n## Task Contract\n- Task ID: turn-3\n- Original user request: Thanks, that is all.\n- Workspace root: \n- Allowed roots: \n- Treat relative project paths as relative to the workspace root; never infer `.` as the project root when a workspace root is provided.\n- Workspace boundary is enforced by tools: do not read, search, edit, or run commands against paths outside the allowed roots unless the user explicitly requests an external path and the tool/approval policy permits it.\n- LeapFlow workspace config is optional at `/.leapflow/config.yaml`; runtime config is loaded from `~/.leapflow/config/user.yaml` and `~/.leapflow/profiles//config/*.yaml`.\n- Preserve this task contract across summarization, compression, tool loops, and memory retrieval.\n" + }, + { + "role": "system", + "content": "\n## Recent Session Summary\n- [assistant] Noted the second step.\n- [user] Thanks, that is all.\n" + }, + { + "role": "assistant", + "content": "Noted the second step." + }, + { + "role": "user", + "content": "Thanks, that is all.\nThanks, that is all." + } + ], + "tools": [ + "capability_expand", + "code_intel", + "code_search", + "config_get", + "config_list", + "env_info", + "file_find", + "file_list", + "file_read", + "git_query", + "lint_check", + "memory_search", + "plugin_list", + "plugin_propose", + "plugin_status", + "plugin_versions", + "repo_map", + "research_note", + "runtime_snapshot", + "schedule_reentry", + "self_describe", + "session_list", + "skill_view", + "skills_list", + "terminal_list", + "terminal_read", + "test_run", + "text_search", + "time_get", + "tool_describe", + "tool_search", + "web_fetch" + ] + }, + "responses": [ + { + "status": 200, + "content_type": "application/json", + "body": "{\"id\": \"chatcmpl-cassette\", \"object\": \"chat.completion\", \"model\": \"cassette-model\", \"choices\": [{\"index\": 0, \"message\": {\"role\": \"assistant\", \"content\": \"{\\\"title\\\": \\\"Tidy invoices\\\", \\\"trigger_phrases\\\": [\\\"tidy invoices\\\", \\\"sort invoices\\\"], \\\"steps\\\": [\\\"List the invoice folder\\\", \\\"Classify by month\\\", \\\"Move into folders\\\"], \\\"parameters\\\": [{\\\"name\\\": \\\"path\\\", \\\"description\\\": \\\"invoice folder\\\"}], \\\"pre_conditions\\\": [], \\\"confidence\\\": 0.7}\"}, \"finish_reason\": \"stop\"}], \"usage\": {\"prompt_tokens\": 64, \"completion_tokens\": 16, \"total_tokens\": 80}}" + } + ] +} diff --git a/tests/_fixtures/cassettes/r5_learning/cassette-model-f0f9b74778416194.cassette.json b/tests/_fixtures/cassettes/r5_learning/cassette-model-f0f9b74778416194.cassette.json deleted file mode 100644 index 634b084a..00000000 --- a/tests/_fixtures/cassettes/r5_learning/cassette-model-f0f9b74778416194.cassette.json +++ /dev/null @@ -1,75 +0,0 @@ -{ - "fingerprint": "f0f9b74778416194446625877be18c9f6b75ef07c0d8655516dbf8d351d16673", - "note": "captured in seed mode", - "request": { - "model": "cassette-model", - "stream": false, - "messages": [ - { - "role": "system", - "content": "You are LeapFlow, an intelligent assistant that can both converse naturally and take real actions on the user's computer.\n\n## Capabilities\nThe tool index below lists **every** registered tool by name and a one-line summary — this is the complete\ncapability contract; nothing else exists. Only a subset is directly callable this turn (via native tool calling,\nnot a JSON block in your reply). If you need a tool from the index that is not yet callable, call\n`capability_expand` with its category name first — the matching tools become callable immediately after.\nWhen the user asks what LeapFlow itself supports, whether it supports plugins, or which runtime capabilities\nare available, use the live capability evidence exposed by `plugin_list` before making capability claims; report\nconfiguration-dependent or unavailable capabilities as limitations instead of inferring from documentation.\n- **text_search**(text, pattern): Search for a regex pattern in text.\n- **text_replace**(text, old, new, count) [capability_expand category: write]: Replace occurrences of a substring in text.\n- **time_get**(): Get current date and time.\n- **env_info**(): Get system environment information (OS, Python version, cwd).\n- **skills_list**(query, category, source): List available learned skills. Use when user asks about capabilities or you need a specific skill.\n- **skill_view**(name): View the full content of a specific skill document.\n- **code_intel**(path, operation): Precise document symbols (outline) for a source file: classes, functions, and methods with line ranges. Python uses an exact AST parse; other languages use a keyword-prefix scan. Prefer over file_read mode=symbols for accurate navigation before editing. Read-only.\n- **repo_map**(path): Compact project orientation for a repository root: languages, detected test/lint commands, top-level structure, entry points, manifest, and VCS branch. Call this first when entering an unfamiliar codebase. Read-only.\n- **scm_sync**(action, cwd, remote, pull_ref, push_ref, timeout) [capability_expand category: scm]: Run a typed git SCM action. Use this instead of shell_run for git pull/push/status. For 'pull origin main then push', set action='pull_then_push', remote='origin', pull_ref='main', and omit push_ref so LeapFlow pushes the current local branch.\n- **git_query**(action, cwd, ref, path, staged, max_count, stat): Read-only structured git inspection: action=diff|log|status|branch|show. Prefer over shell_run for reading repo state — output is clipped, redacted, and log/branch are parsed into structured fields. Use scm_sync for pull/push.\n- **git_write**(action, cwd, message, stage_all, name, ref, create) [capability_expand category: scm]: Mutating git actions: action=commit (message, stage_all), branch (create+switch), checkout (switch; create=true for -b). Approval-gated. Use scm_sync for pull/push and git_query for reads.\n- **test_run**(command, cwd, timeout): Run the project's test suite and return structured results (framework, passed/failed counts, failing tests). Auto-detects the runner (pytest/npm/go/cargo) or uses a configured/explicit command; executes via the governed shell. ok=true means the runner executed — see 'success' for pass/fail.\n- **lint_check**(command, cwd, timeout): Run the project's linter and return a structured clean/issue result. Auto-detects the linter (ruff/eslint/go vet/clippy) or uses a configured/explicit command; executes via the governed shell. ok=true means the linter ran — see 'clean'.\n- **file_list**(path, pattern, depth): List files and directories at a given path. Use depth=1 or depth=2 to get a recursive tree in one call instead of listing each sub-directory separately.\n- **file_read**(path, max_lines, start_line, max_chars, mode): Read text file content with adaptive context governance. For large or unfamiliar files, prefer mode='outline' or mode='symbols' first, then use mode='raw' with start_line/max_lines for the specific range you actually need. For LeapFlow's own settings, use config_list / config_get / config_set — its config files are outside the workspace and not readable here.\n- **file_write**(path, content, mode) [capability_expand category: write]: Write content to a file (overwrite or append).\n- **code_search**(pattern, patterns, path, glob, ignore_case, multiline, max_results, context_lines): Search file CONTENTS by regex pattern across a directory tree (ripgrep-backed). Requires a regex pattern. NOT for listing or browsing directory contents — use file_list for that. Prefer this over shell_run grep: faster, skips VCS/dependency/build dirs, and returns structured path:line:column matches. Batch related lookups into ONE call via `patterns` (OR-combined, single pass) instead of issuing several separate searches. Use file_read for the surrounding context of a hit.\n- **file_find**(glob, path, max_results): Find files by a recursive glob pattern under a base path (e.g. '**/test_*.py' or '*.md'). Prefer this over shell_run find; skips VCS/dependency/build dirs.\n- **edit_file**(path, edits, dry_run, diff) [capability_expand category: write]: Apply targeted, anchored search-replace edits to an EXISTING text file (use file_write to create/overwrite). Each edit is {original_text, new_text, replace_all?}; original_text must match exactly and uniquely (or set replace_all) — a non-unique or missing anchor is rejected so files are never corrupted. Set dry_run to preview. Alternatively pass a unified 'diff' to apply its hunks as anchored edits. Far cheaper and safer than rewriting a whole file.\n- **shell_run**(command, cwd, timeout) [capability_expand category: shell]: Execute a one-shot shell command with timeout protection. Runs in the active workspace; paths resolving outside it are refused. Reach for a structured tool first when one fits — web_fetch for anything over HTTP(S), git_query/scm_sync for git, code_search/file_find/file_read for the repo, config_get/config_set for LeapFlow's own settings — because those report typed results, while a failed shell command can only be diagnosed from its exit code and stderr. Every shell run counts as an external side effect, so a failure stops the rest of the batch and is not retried automatically.\n- **terminal_open**(command, cwd, shell) [capability_expand category: terminal]: Open a PERSISTENT shell session (REPL/dev server/watch), returning a session_id for terminal_send/read/close. Disabled unless tools.terminal_session_enabled is set. For one-shot commands use shell_run instead.\n- **terminal_send**(session_id, input, wait) [capability_expand category: terminal]: Send a line of input to a persistent terminal session and return output captured shortly after.\n- **terminal_read**(session_id, wait): Drain buffered output from a persistent terminal session (optionally waiting briefly first).\n- **terminal_close**(session_id) [capability_expand category: terminal]: Terminate a persistent terminal session and release its process group.\n- **terminal_list**(): List active persistent terminal sessions.\n- **config_list**(category, limit): List LeapFlow's own writable settings (LLM model and endpoint, daemon, memory, perception, gateway, …) with current values. Use this to discover the exact key before changing anything. Optionally narrow by `category`. This is the only correct way to inspect LeapFlow configuration — never read config files from disk.\n- **config_get**(key): Read one LeapFlow setting by key (e.g. 'llm.model', 'llm.base_url', 'daemon.log_level'), returning its current value, type, scopes, and whether a change needs a daemon restart. There is no 'llm.provider' key: provider behavior is inferred from the OpenAI-compatible 'llm.base_url'. Never read LeapFlow config files from disk — use this.\n- **config_set**(key, value, scope) [capability_expand category: config]: Change one LeapFlow setting by key, e.g. switch an OpenAI-compatible LLM with key='llm.model' and key='llm.base_url'. There is no 'llm.provider' key: provider behavior is inferred from the endpoint. Values are validated and coerced; credentials are stored in the vault automatically. Call config_list or config_get first if unsure of the key. The result states whether a `leap daemon restart` is required. Never edit LeapFlow config files directly.\n- **web_fetch**(url, select, timeout, max_bytes): Read a URL over HTTP(S) and get back extracted, context-sized content: parsed JSON for API endpoints, readable text plus links for web pages. Use this for anything on the internet — prices, docs, releases, articles — instead of running curl through shell_run: it reports real HTTP status codes, retries rate limits on its own, and is a plain read so a retry is always safe. For JSON APIs pass `select` with a dotted path (e.g. 'chart.result.0.meta') to return just that part instead of the whole payload.\n- **memory_search**(query, limit): Search agent memory for relevant past experiences, observations, and facts.\n- **memory_add**(content, kind) [capability_expand category: write]: Store a new observation or insight in memory for future reference.\n- **research_note**(kind, text): Record a compact, structured note about the current task's state so it survives context compression on long / multi-step tasks. Use for durable findings, open questions still to resolve, decisions / excluded paths, and the immediate next step. One concise sentence per note.\n- **capability_expand**(category): Fetch the full callable schema for every tool in a capability category (e.g. 'hub', 'gateway', 'desktop', 'delegate', 'file', 'memory', 'skill'). The compact tool index always lists every registered tool by name and a one-line summary, but only a static low-risk subset is directly callable each turn. If you need a tool from the index that is not yet callable, call capability_expand with its category first; the matching tools become callable in this turn. Never invent a tool name — expand the category instead.\n- **delegate_task**(goal, context) [capability_expand category: delegate]: Delegate a complex sub-task to an isolated subagent. The subagent gets a fresh context and restricted tool access. Use when a task is self-contained and can be solved independently.\n- **schedule_reentry**(kind, reason, delay_seconds, event_match, max_reentries, deadline_seconds): Register a re-entry so this task can resume later from its current orientation (findings / open questions / next step). Use when work must pause and continue after a delay (kind=time) or when a matching platform event arrives (kind=event), instead of finishing now. The research-ledger state is carried over automatically.\n- **hub_push**(skill_name, visibility, version) [capability_expand category: hub]: Push a local skill to the ModelScope Hub for sharing or backup.\n- **hub_pull**(repo_id, version) [capability_expand category: hub]: Pull a skill from the ModelScope Hub to install locally.\n- **hub_search**(query) [capability_expand category: hub]: Search for skills on the Hub by keyword or description.\n- **hub_sync**(mode, dry_run) [capability_expand category: hub]: Preview or execute sync between local skills and Hub.\n- **platform_action**(platform, action, payload, backend_kind) [capability_expand category: gateway]: Execute an exact registered business action on an external platform through LeapFlow's App Connector layer. Actions must be copied from the App Connector Capability Index and are addressed as domain.operation, e.g. im.send_message or docs.create_markdown. All business fields (chat_id, text, query, etc.) MUST be placed inside `payload`, never at the top level. Example: {\"platform\":\"feishu\",\"action\":\"im.send_message\",\"payload\":{\"chat_id\":\"oc_xxx\",\"text\":\"hello\"}}. Do not invent action names, do not use management actions such as list/guide/connect/status here.\n- **platform_connect**(action, platform, credentials, options, checkpoint) [capability_expand category: gateway]: List, guide, connect, disconnect, remove, or check status for external platforms using the App Connector management namespace. Supports REST and CLI backends. Use this for management actions such as list/guide/preflight/connect/status; use platform_action only for exact registered business actions.\n- **gateway_send**(platform, chat_id, text, thread_id) [capability_expand category: gateway]: Send a message to a connected external platform (Feishu group, Telegram chat, DingTalk conversation, etc.). Requires the platform to be connected via gateway_connect first. Use gateway_connect with action='list' to see connected platforms and available chat IDs.\n- **gateway_connect**(action, platform, credentials, options) [capability_expand category: gateway]: Connect, configure, or manage external platform integrations (Feishu, DingTalk, Telegram, Slack, Discord, etc.). Conversational flow: 1) call 'guide' to get setup steps + required fields, 2) present the steps to the user and ask for ALL required credentials in a single message, 3) call 'connect' with the credentials. Goal: complete in 1–2 user turns. NEVER include credential values in your text response.\n- **plugin_list**(): List the live plugin registry and cross-subsystem capability evidence. Use this before answering questions about whether LeapFlow supports plugins, self-evolution, plugin installation, hot reload, versioning, or other runtime capabilities.\n- **plugin_status**(plugin_id): Get detailed status of a specific plugin: its declared category, runtime dependencies, contributed tools, and fiber lifecycle state.\n- **plugin_versions**(plugin_id): List recorded source versions and active pointer for a profile-scoped plugin.\n- **plugin_propose**(requested_capability, plugin_id, proposed_tools, test_cases, risk_level, evidence): Create a side-effect-free PluginProposal from explicit capability-gap evidence. Use this before plugin_generate when a missing capability should be reviewed. Does not call an LLM, write files, or install anything.\n- **assess_compatibility**(manifest, source_path) [capability_expand category: plugin_management]: Assess whether a foreign plugin manifest or real DSH source bundle is compatible with LeapFlow. A source bundle assessment is static: runtime_ready stays false until restricted Node discovery during plugin_install. Returns component-level verdicts and limitations.\n- **plugin_generate**(plugin_id, description, proposal_id) [capability_expand category: system]: Generate a new ToolPlugin from a natural-language capability description. The LLM produces code that conforms to the ToolPlugin Protocol; it is then rigorously validated (syntax, structure, import, protocol conformance). The generated source is stored in CAS and receives explicit content approval. It DOES NOT install the plugin — installation requires a second, mutation-specific approval via plugin_install.\n- **plugin_install**(plugin_id, code, marketplace_name, source_path, proposal_id, version_label) [capability_expand category: system]: Install a plugin from exactly one source: validated Python code, the configured Python marketplace, or a real DSH source bundle. DSH bundles run restricted Node discovery before registration; only public host tools are installed and client UI limitations are reported. Writes profile-scoped state and mutates the process-global registry. REQUIRES APPROVAL.\n- **plugin_rollback**(plugin_id, version) [capability_expand category: system]: Rollback a profile-scoped plugin to a recorded source version and reload it. REQUIRES APPROVAL.\n- **plugin_reload**(plugin_id, version_label) [capability_expand category: system]: Hot-reload a plugin at runtime. Disposes the old plugin fiber, re-imports its module, and registers a fresh instance. Existing in-flight turns are unaffected (snapshot isolation). REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_disable**(plugin_id) [capability_expand category: system]: Disable a plugin by disposing its fiber, removing its tools from the runtime registry. Cannot disable self_management itself. REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_remove**(plugin_id, delete_source) [capability_expand category: system]: Terminally remove a plugin: dispose its fiber, unregister its tools, remove reload metadata, and optionally delete its profile-scoped source file. Cannot remove self_management itself. REQUIRES APPROVAL.\n- **plugin_unquarantine**(plugin_id) [capability_expand category: plugin_management]: Restore a quarantined plugin to probation status for re-evaluation. Unfreezes the trust ledger, transitions the proposal back to PROBATION, and reloads the plugin. REQUIRES APPROVAL.\n- **plugin_enable**(plugin_id) [capability_expand category: system]: Re-enable a previously disabled plugin by reloading its module and registering a fresh instance. REQUIRES APPROVAL.\n- **self_describe**(facet): Introspect LeapFlow's own identity, capabilities, runtime state, evolution metrics, or platform connections. Use facet='all' only when a comprehensive self-check is explicitly requested.\n- **runtime_snapshot**(): Lightweight (~150 token) flat snapshot of current runtime state: model, context budget, posture, disclosure level, turn count, cache hit rate, uptime, tool count, and pending approvals.\n- **schedule_create**(trigger_expression, instruction, execution_mode, max_retries, delivery_target) [capability_expand category: scheduler]: Create a new scheduled task. Specify a trigger expression (e.g. '30m', 'every 2h', '0 9 * * *') and the instruction to execute on each trigger. Optionally provide an execution mode and a delivery target to receive notifications.\n- **schedule_list**() [capability_expand category: scheduler]: List all scheduled tasks with their current state, trigger, and next due time.\n- **schedule_status**(task_id) [capability_expand category: scheduler]: Get detailed status and recent execution history for a scheduled task.\n- **schedule_pause**(task_id) [capability_expand category: scheduler]: Pause a scheduled task so it stops firing without being cancelled.\n- **schedule_resume**(task_id) [capability_expand category: scheduler]: Resume a paused scheduled task — re-arms it and recalculates next due time.\n- **schedule_cancel**(task_id) [capability_expand category: scheduler]: Cancel a scheduled task permanently.\n- **tool_search**(query, max_results): Search all registered tools by keyword relevance. Returns a ranked list of matching tools with name, category, and summary. Use this when you need a tool but are unsure of its exact name or category.\n- **tool_describe**(tool_name): Get the full callable schema of a single tool by exact name. Returns the tool's description, parameters, and metadata. Use after tool_search to inspect a specific tool before calling it.\n- **session_search**(query, limit) [capability_expand category: unclassified]: Search past conversation sessions in the current workspace for relevant context.\n- **session_list**(limit): List recent conversation sessions in the current workspace with stable ids, titles, dates, and summaries. Use for browsing past tasks or when user asks to see history without specific search terms. No keywords needed — returns chronological list.\n- **session_detail**(session_id, limit, offset, include_inactive) [capability_expand category: unclassified]: Read a paginated persisted transcript for one past conversation session in the current workspace. Use after session_list or session_search returns a session_id.\n- **click**(element_index) [capability_expand category: desktop]: Click a UI element by its element_index (from the latest observe_ui snapshot)\n- **get_clipboard**() [capability_expand category: desktop]: Read current clipboard text content\n- **list_apps**(filter, running_only) [capability_expand category: desktop]: List available applications on this system. Use to discover correct bundle_id before switch_app.\n- **list_windows**() [capability_expand category: desktop]: List all top-level windows with pid, window_id, title, and per-window state (minimized, on-screen). Call this first to pick the pid and window_id that observe_ui and other window tools require.\n- **observe_ui**(pid, window_id, query) [capability_expand category: desktop]: Snapshot one window's actionable UI elements, each tagged with an element_index for click/right_click/read_text. Re-observe after actions — indices belong to one snapshot. Requires the window's pid and window_id from list_windows.\n- **open_url**(url, app_id) [capability_expand category: desktop]: Open a URL in the default or specified browser\n- **read_text**(element_index) [capability_expand category: desktop]: Read the text content of a specific UI element from the latest snapshot\n- **right_click**(element_index) [capability_expand category: desktop]: Right-click a UI element to open its context menu. Returns visible menu items.\n- **screenshot**(pid, window_id) [capability_expand category: desktop]: Capture a screenshot for visual verification. With pid + window_id captures that window (works across all displays); defaults to the last observed window, or the full desktop when no window has been observed.\n- **scroll**(element_index, direction, amount, pid, window_id) [capability_expand category: desktop]: Scroll a scrollable area of a window. Omit element_index to scroll the window's focused/page scroller; pass one to scroll an exact element from the latest snapshot.\n- **select_text**(element_index) [capability_expand category: desktop]: Select all text in a UI element (focus + select-all, for subsequent copy)\n- **set_clipboard**(text) [capability_expand category: desktop]: Write text to the clipboard\n- **shortcut**(keys) [capability_expand category: desktop]: Execute a keyboard shortcut\n- **switch_app**(app_id) [capability_expand category: desktop]: Switch to an app (launch if needed, activate, verify)\n- **type_text**(text) [capability_expand category: desktop]: Type text into the currently focused element\n- **wait**(seconds) [capability_expand category: desktop]: Wait for a specified duration before continuing\n- **wait_until**(condition, pid, window_id, timeout, poll_interval) [capability_expand category: desktop]: Wait until a UI condition is met (polls UI tree). Returns elements when found or on timeout.\n- **wait_until_stable**(timeout, poll_interval, pid, window_id) [capability_expand category: desktop]: Wait until the UI stops changing (element set stabilizes across polls).\n\n## App Connector Capability Index\nLeapFlow can onboard and manage external apps through `platform_connect` and execute exact registered business actions through `platform_action`.\nFor requests about connecting, setting up, configuring, enabling, or managing a supported app, use `platform_connect` first instead of generating SDK/Webhook sample code.\n`platform_connect.action` is the management namespace: list, guide, preflight, connect, disconnect, remove, status, events_start, events_stop, events_status.\n`platform_action.action` is only for exact registered platform business actions listed below, such as `im.send_message`; never use management actions like `list` or `guide` there.\nAll business fields MUST go inside `payload`; top-level keys are only `platform`, `action`, and `payload`.\nDo not invent platform IDs or platform action names. If the needed action is not listed, ask for discovery/clarification instead of guessing.\nUse `platform_connect` with `action='guide'` and the matching `platform` to start onboarding; use `action='list'` when the app is unclear.\nWhen a pending onboarding state is present, continue from that state with `platform_connect` instead of asking the user to restate the app.\nSupported apps:\n- `api_server`: API Server (OpenAI Compatible) (category=api; backend=adapter; platform_action actions=none registered)\n- `dingtalk`: 钉钉 (DingTalk) (category=im; backend=adapter; platform_action actions=none registered)\n- `feishu`: 飞书 (Feishu/Lark) (category=im; backend=cli)\n - `calendar.create_event` payload={summary*, start_time*, end_time*, attendees} [write/high]\n - `docs.create_markdown` payload={title*, markdown*, folder_token} [write/medium]\n - `drive.search` payload={query*, limit} [read/medium]\n - `im.add_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.download_resource` payload={message_id*, file_key*, type*} [read/low]\n - `im.get_messages` payload={message_ids*} [read/low]\n - `im.list_chats` payload={page_size} [read/low]\n - `im.list_messages` payload={chat_id*} [read/low]\n - `im.list_thread_messages` payload={thread*} [read/low]\n - `im.remove_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.reply_message` payload={message_id*, text*} [send/high]\n - `im.search_chats` payload={query*, page_size} [read/low]\n - `im.search_messages` payload={query*} [read/medium]\n - `im.send_message` payload={chat_id*, text*, thread_id} [send/high]\n - `im.update_card` payload={token*, card*} [write/medium]\n - `im.update_message` payload={message_id*, text*} [write/medium]\n - `mail.search_unread` payload={query, limit} [read/high]\n - `sheets.append_row` payload={spreadsheet_token*, sheet_id*, values*} [write/medium]\n - `task.create` payload={title*, description, due_time} [write/medium]\n- `telegram`: Telegram (category=social; backend=adapter; platform_action actions=none registered)\n- `webhook`: Webhook (Generic) (category=webhook; backend=adapter; platform_action actions=none registered)\n\n## Tool Usage\nTools are normally invoked through the native function-calling mechanism, not by writing JSON in your reply\ntext. Only if the provider signals that native function calling is unavailable for this turn, fall back to a\nsingle JSON code block: `{\"name\": \"tool_name\", \"arguments\": {\"key\": \"value\"}}` — use this fallback format\nonly, never both. Only call a tool whose exact name appears in the tool index above and is currently callable\n(or reachable via `capability_expand`). Never invent, rename, alias, or guess a tool name, platform ID, or\nplatform action from argument shape or wording — if the index does not list it, it does not exist.\n`platform_connect.action` (list/guide/preflight/connect/disconnect/remove/status/events_start/events_stop/\nevents_status) is the App Connector management namespace; `platform_action.action` only accepts exact\nregistered business actions such as `im.send_message` shown in the App Connector Capability Index — never mix\nthe two namespaces. If a tool call returns an unknown/unavailable result, use the returned suggestions or\navailable names for a single retry instead of trying further variations of the same guess.\n\n**Side-effect action rule** (`platform_action` with effect=send/write/execute):\n- Call each unique action+payload **exactly once**. Never include duplicates in the same turn.\n- Once the result returns `\"completed\": true`, that action is DONE for this task. Do NOT call it again\n in any subsequent turn — immediately summarize the result for the user instead.\n- The system enforces idempotency: duplicate calls are blocked and will not execute.\n- If the user explicitly requests sending/writing multiple times, use distinct payloads per call.\n\n**Resource identifier provenance rule**:\n- NEVER fabricate, guess, or infer resource identifiers (chat_id, message_id, file_key, user_id, etc.).\n Every resource ID used in a side-effect action MUST come from a successful API response in this session.\n- If a read/list action fails (e.g. authorization error), you do NOT have valid resource IDs.\n Report the failure to the user — do NOT attempt the dependent write/send action with a guessed ID.\n- When a tool result contains `\"llm_instruction\"`, follow it exactly.\n\n## Guidelines\n1. **Direct answers first**: If you already know the answer, respond directly without tools.\n2. **Avoid redundant tool calls**: Do not call the same tool with the same arguments more than once in the same user turn. When an existing tool result already answers the user's request, stop calling tools and answer directly.\n3. **Use tools proactively**: When the user asks about files, time, system state, or needs actions performed, use the appropriate tool.\n4. **Chain tools when needed**: You can call multiple tools in sequence (e.g., list files → read file → summarize).\n5. **Handle failures gracefully**: If a tool fails, explain what went wrong and suggest alternatives. If it failed because of an unknown tool/platform/action name, retry once with an exact name from the returned suggestions, then explain rather than keep guessing.\n6. **Summarize results naturally**: After tool execution, synthesize the results into a helpful answer rather than dumping raw output.\n7. **Stay conversational**: Maintain a natural, helpful tone. Acknowledge context from earlier in the conversation.\n8. **Recall past work**:\n - Broad queries (\"之前做了什么\", \"列出任务\"): answer from the \"Recent Task History\" section already in your context. If insufficient, call session_list.\n - Specific lookups (\"上次那个配置怎么改的\"): call session_search with relevant phrases (NOT single characters).\n - Do NOT call search tools repeatedly with keyword fragments. One well-phrased call is better than ten fragmented ones.\n\n## Coding & Verification\nWhen working with code, prefer the precise built-in tools over ad-hoc shell: use `repo_map` to orient in an\nunfamiliar project, `code_search`/`file_find` to locate and `code_intel` for symbols, `edit_file` (anchored\nsearch-replace, or a unified `diff`) to change files — never rewrite a whole file to change a few lines — and\n`git_query` to inspect diffs/log. After edits, check `syntax_ok` in the result and run `test_run`/`lint_check`\nbefore declaring the work done. Batch independent read-only calls (search/read/query) into a single turn — they\nrun in parallel.\n\n## Presentation Style\n1. **Polished Markdown only**: Format user-facing answers with clean Markdown headings, short paragraphs, and concise bullets. Use tables only when they improve comparison or scanning.\n2. **Terminal-friendly layout**: Keep lines readable in a TUI; avoid dense walls of text, deeply nested lists, oversized ASCII art, or heavy visual blocks.\n3. **Elegant emphasis**: Use bold text sparingly for key terms and conclusions. Avoid excessive emojis, decorative symbols, repeated separators, or visual noise.\n4. **Theme-safe colors**: Do not emit ANSI escape codes, HTML color tags, Rich markup, or hardcoded color names. Rely on the TUI theme to render Markdown professionally.\n5. **No leaked tool protocol**: Never show tool-call JSON, internal schemas, raw observations, tool result payloads, or hidden reasoning in the final answer unless the user explicitly asks for raw/debug output. Treat any prior `{\"name\": ..., \"arguments\": ...}` blocks and `Tool result (...)` messages as internal execution context only.\n6. **Professional closure**: End with a concise conclusion or next step when helpful; avoid rambling after the useful answer is complete.\n\nWhen finished with all tool calls, respond normally without a JSON block, tool-call transcript, or process log.\n\n## Task Contract\n- Task ID: turn-3\n- Original user request: Thanks, that is all.\n- Workspace root: \n- Allowed roots: \n- Treat relative project paths as relative to the workspace root; never infer `.` as the project root when a workspace root is provided.\n- Workspace boundary is enforced by tools: do not read, search, edit, or run commands against paths outside the allowed roots unless the user explicitly requests an external path and the tool/approval policy permits it.\n- LeapFlow workspace config is optional at `/.leapflow/config.yaml`; runtime config is loaded from `~/.leapflow/config/user.yaml` and `~/.leapflow/profiles//config/*.yaml`.\n- Preserve this task contract across summarization, compression, tool loops, and memory retrieval.\n" - }, - { - "role": "system", - "content": "\n## Recent Session Summary\n- [assistant] Noted the second step.\n- [user] Thanks, that is all.\n" - }, - { - "role": "assistant", - "content": "Noted the second step." - }, - { - "role": "user", - "content": "Thanks, that is all.\nThanks, that is all." - }, - { - "role": "assistant", - "content": "{\"title\": \"Tidy invoices\", \"trigger_phrases\": [\"tidy invoices\", \"sort invoices\"], \"steps\": [\"List the invoice folder\", \"Classify by month\", \"Move into folders\"], \"parameters\": [{\"name\": \"path\", \"description\": \"invoice folder\"}], \"pre_conditions\": [], \"confidence\": 0.7}" - }, - { - "role": "user", - "content": "Tool result (path):\n{\"ok\": false, \"error\": \"Unknown tool: path\", \"error_type\": \"unknown_tool\", \"retryable\": true}\nSYSTEM: The previous tool call used an unavailable tool name. Original tool: path. Resolution: unknown (no exact canonical tool name match). Suggested canonical tools: assess_compatibility, capability_expand, code_intel, code_search, config_get. Available tools include: assess_compatibility, capability_expand, code_intel, code_search, config_get, config_list, config_set, delegate_task, edit_file, env_info, file_find, file_list. Retry once using an exact canonical tool name from the available list and valid arguments. Do not invent tool names, use aliases, or infer a tool from argument shape; answer without a tool if no exact tool fits." - } - ], - "tools": [ - "capability_expand", - "code_intel", - "code_search", - "config_get", - "config_list", - "env_info", - "file_find", - "file_list", - "file_read", - "git_query", - "lint_check", - "memory_search", - "plugin_list", - "plugin_propose", - "plugin_status", - "plugin_versions", - "repo_map", - "research_note", - "runtime_snapshot", - "schedule_reentry", - "self_describe", - "session_list", - "skill_view", - "skills_list", - "terminal_list", - "terminal_read", - "test_run", - "text_search", - "time_get", - "tool_describe", - "tool_search", - "web_fetch" - ] - }, - "responses": [ - { - "status": 200, - "content_type": "application/json", - "body": "{\"id\": \"chatcmpl-cassette\", \"object\": \"chat.completion\", \"model\": \"cassette-model\", \"choices\": [{\"index\": 0, \"message\": {\"role\": \"assistant\", \"content\": \"Done \\u2014 nothing further needed.\"}, \"finish_reason\": \"stop\"}], \"usage\": {\"prompt_tokens\": 64, \"completion_tokens\": 16, \"total_tokens\": 80}}" - } - ] -} diff --git a/tests/_fixtures/cassettes/r6_lifecycle/cassette-model-00d40eb3a49fcc37.cassette.json b/tests/_fixtures/cassettes/r6_lifecycle/cassette-model-00d40eb3a49fcc37.cassette.json deleted file mode 100644 index 9cf56b60..00000000 --- a/tests/_fixtures/cassettes/r6_lifecycle/cassette-model-00d40eb3a49fcc37.cassette.json +++ /dev/null @@ -1,80 +0,0 @@ -{ - "fingerprint": "00d40eb3a49fcc3757f5790ed394e157b0a9e7da6149aa7b9c681ce7ad14a638", - "note": "captured in seed mode", - "request": { - "model": "cassette-model", - "stream": false, - "messages": [ - { - "role": "system", - "content": "You are LeapFlow, an intelligent assistant that can both converse naturally and take real actions on the user's computer.\n\n## Capabilities\nThe tool index below lists **every** registered tool by name and a one-line summary — this is the complete\ncapability contract; nothing else exists. Only a subset is directly callable this turn (via native tool calling,\nnot a JSON block in your reply). If you need a tool from the index that is not yet callable, call\n`capability_expand` with its category name first — the matching tools become callable immediately after.\nWhen the user asks what LeapFlow itself supports, whether it supports plugins, or which runtime capabilities\nare available, use the live capability evidence exposed by `plugin_list` before making capability claims; report\nconfiguration-dependent or unavailable capabilities as limitations instead of inferring from documentation.\n- **text_search**(text, pattern): Search for a regex pattern in text.\n- **text_replace**(text, old, new, count) [capability_expand category: write]: Replace occurrences of a substring in text.\n- **time_get**(): Get current date and time.\n- **env_info**(): Get system environment information (OS, Python version, cwd).\n- **skills_list**(query, category, source): List available learned skills. Use when user asks about capabilities or you need a specific skill.\n- **skill_view**(name): View the full content of a specific skill document.\n- **code_intel**(path, operation): Precise document symbols (outline) for a source file: classes, functions, and methods with line ranges. Python uses an exact AST parse; other languages use a keyword-prefix scan. Prefer over file_read mode=symbols for accurate navigation before editing. Read-only.\n- **repo_map**(path): Compact project orientation for a repository root: languages, detected test/lint commands, top-level structure, entry points, manifest, and VCS branch. Call this first when entering an unfamiliar codebase. Read-only.\n- **scm_sync**(action, cwd, remote, pull_ref, push_ref, timeout) [capability_expand category: scm]: Run a typed git SCM action. Use this instead of shell_run for git pull/push/status. For 'pull origin main then push', set action='pull_then_push', remote='origin', pull_ref='main', and omit push_ref so LeapFlow pushes the current local branch.\n- **git_query**(action, cwd, ref, path, staged, max_count, stat): Read-only structured git inspection: action=diff|log|status|branch|show. Prefer over shell_run for reading repo state — output is clipped, redacted, and log/branch are parsed into structured fields. Use scm_sync for pull/push.\n- **git_write**(action, cwd, message, stage_all, name, ref, create) [capability_expand category: scm]: Mutating git actions: action=commit (message, stage_all), branch (create+switch), checkout (switch; create=true for -b). Approval-gated. Use scm_sync for pull/push and git_query for reads.\n- **test_run**(command, cwd, timeout): Run the project's test suite and return structured results (framework, passed/failed counts, failing tests). Auto-detects the runner (pytest/npm/go/cargo) or uses a configured/explicit command; executes via the governed shell. ok=true means the runner executed — see 'success' for pass/fail.\n- **lint_check**(command, cwd, timeout): Run the project's linter and return a structured clean/issue result. Auto-detects the linter (ruff/eslint/go vet/clippy) or uses a configured/explicit command; executes via the governed shell. ok=true means the linter ran — see 'clean'.\n- **file_list**(path, pattern, depth): List files and directories at a given path. Use depth=1 or depth=2 to get a recursive tree in one call instead of listing each sub-directory separately.\n- **file_read**(path, max_lines, start_line, max_chars, mode): Read text file content with adaptive context governance. For large or unfamiliar files, prefer mode='outline' or mode='symbols' first, then use mode='raw' with start_line/max_lines for the specific range you actually need. For LeapFlow's own settings, use config_list / config_get / config_set — its config files are outside the workspace and not readable here.\n- **file_write**(path, content, mode) [capability_expand category: write]: Write content to a file (overwrite or append).\n- **code_search**(pattern, patterns, path, glob, ignore_case, multiline, max_results, context_lines): Search file CONTENTS by regex pattern across a directory tree (ripgrep-backed). Requires a regex pattern. NOT for listing or browsing directory contents — use file_list for that. Prefer this over shell_run grep: faster, skips VCS/dependency/build dirs, and returns structured path:line:column matches. Batch related lookups into ONE call via `patterns` (OR-combined, single pass) instead of issuing several separate searches. Use file_read for the surrounding context of a hit.\n- **file_find**(glob, path, max_results): Find files by a recursive glob pattern under a base path (e.g. '**/test_*.py' or '*.md'). Prefer this over shell_run find; skips VCS/dependency/build dirs.\n- **edit_file**(path, edits, dry_run, diff) [capability_expand category: write]: Apply targeted, anchored search-replace edits to an EXISTING text file (use file_write to create/overwrite). Each edit is {original_text, new_text, replace_all?}; original_text must match exactly and uniquely (or set replace_all) — a non-unique or missing anchor is rejected so files are never corrupted. Set dry_run to preview. Alternatively pass a unified 'diff' to apply its hunks as anchored edits. Far cheaper and safer than rewriting a whole file.\n- **shell_run**(command, cwd, timeout) [capability_expand category: shell]: Execute a one-shot shell command with timeout protection. Runs in the active workspace; paths resolving outside it are refused. Reach for a structured tool first when one fits — web_fetch for anything over HTTP(S), git_query/scm_sync for git, code_search/file_find/file_read for the repo, config_get/config_set for LeapFlow's own settings — because those report typed results, while a failed shell command can only be diagnosed from its exit code and stderr. Every shell run counts as an external side effect, so a failure stops the rest of the batch and is not retried automatically.\n- **terminal_open**(command, cwd, shell) [capability_expand category: terminal]: Open a PERSISTENT shell session (REPL/dev server/watch), returning a session_id for terminal_send/read/close. Disabled unless tools.terminal_session_enabled is set. For one-shot commands use shell_run instead.\n- **terminal_send**(session_id, input, wait) [capability_expand category: terminal]: Send a line of input to a persistent terminal session and return output captured shortly after.\n- **terminal_read**(session_id, wait): Drain buffered output from a persistent terminal session (optionally waiting briefly first).\n- **terminal_close**(session_id) [capability_expand category: terminal]: Terminate a persistent terminal session and release its process group.\n- **terminal_list**(): List active persistent terminal sessions.\n- **config_list**(category, limit): List LeapFlow's own writable settings (LLM model and endpoint, daemon, memory, perception, gateway, …) with current values. Use this to discover the exact key before changing anything. Optionally narrow by `category`. This is the only correct way to inspect LeapFlow configuration — never read config files from disk.\n- **config_get**(key): Read one LeapFlow setting by key (e.g. 'llm.model', 'llm.base_url', 'daemon.log_level'), returning its current value, type, scopes, and whether a change needs a daemon restart. There is no 'llm.provider' key: provider behavior is inferred from the OpenAI-compatible 'llm.base_url'. Never read LeapFlow config files from disk — use this.\n- **config_set**(key, value, scope) [capability_expand category: config]: Change one LeapFlow setting by key, e.g. switch an OpenAI-compatible LLM with key='llm.model' and key='llm.base_url'. There is no 'llm.provider' key: provider behavior is inferred from the endpoint. Values are validated and coerced; credentials are stored in the vault automatically. Call config_list or config_get first if unsure of the key. The result states whether a `leap daemon restart` is required. Never edit LeapFlow config files directly.\n- **web_fetch**(url, select, timeout, max_bytes): Read a URL over HTTP(S) and get back extracted, context-sized content: parsed JSON for API endpoints, readable text plus links for web pages. Use this for anything on the internet — prices, docs, releases, articles — instead of running curl through shell_run: it reports real HTTP status codes, retries rate limits on its own, and is a plain read so a retry is always safe. For JSON APIs pass `select` with a dotted path (e.g. 'chart.result.0.meta') to return just that part instead of the whole payload.\n- **memory_search**(query, limit): Search agent memory for relevant past experiences, observations, and facts.\n- **memory_add**(content, kind) [capability_expand category: write]: Store a new observation or insight in memory for future reference.\n- **research_note**(kind, text): Record a compact, structured note about the current task's state so it survives context compression on long / multi-step tasks. Use for durable findings, open questions still to resolve, decisions / excluded paths, and the immediate next step. One concise sentence per note.\n- **capability_expand**(category): Fetch the full callable schema for every tool in a capability category (e.g. 'hub', 'gateway', 'desktop', 'delegate', 'file', 'memory', 'skill'). The compact tool index always lists every registered tool by name and a one-line summary, but only a static low-risk subset is directly callable each turn. If you need a tool from the index that is not yet callable, call capability_expand with its category first; the matching tools become callable in this turn. Never invent a tool name — expand the category instead.\n- **delegate_task**(goal, context) [capability_expand category: delegate]: Delegate a complex sub-task to an isolated subagent. The subagent gets a fresh context and restricted tool access. Use when a task is self-contained and can be solved independently.\n- **schedule_reentry**(kind, reason, delay_seconds, event_match, max_reentries, deadline_seconds): Register a re-entry so this task can resume later from its current orientation (findings / open questions / next step). Use when work must pause and continue after a delay (kind=time) or when a matching platform event arrives (kind=event), instead of finishing now. The research-ledger state is carried over automatically.\n- **hub_push**(skill_name, visibility, version) [capability_expand category: hub]: Push a local skill to the ModelScope Hub for sharing or backup.\n- **hub_pull**(repo_id, version) [capability_expand category: hub]: Pull a skill from the ModelScope Hub to install locally.\n- **hub_search**(query) [capability_expand category: hub]: Search for skills on the Hub by keyword or description.\n- **hub_sync**(mode, dry_run) [capability_expand category: hub]: Preview or execute sync between local skills and Hub.\n- **platform_action**(platform, action, payload, backend_kind) [capability_expand category: gateway]: Execute an exact registered business action on an external platform through LeapFlow's App Connector layer. Actions must be copied from the App Connector Capability Index and are addressed as domain.operation, e.g. im.send_message or docs.create_markdown. All business fields (chat_id, text, query, etc.) MUST be placed inside `payload`, never at the top level. Example: {\"platform\":\"feishu\",\"action\":\"im.send_message\",\"payload\":{\"chat_id\":\"oc_xxx\",\"text\":\"hello\"}}. Do not invent action names, do not use management actions such as list/guide/connect/status here.\n- **platform_connect**(action, platform, credentials, options, checkpoint) [capability_expand category: gateway]: List, guide, connect, disconnect, remove, or check status for external platforms using the App Connector management namespace. Supports REST and CLI backends. Use this for management actions such as list/guide/preflight/connect/status; use platform_action only for exact registered business actions.\n- **gateway_send**(platform, chat_id, text, thread_id) [capability_expand category: gateway]: Send a message to a connected external platform (Feishu group, Telegram chat, DingTalk conversation, etc.). Requires the platform to be connected via gateway_connect first. Use gateway_connect with action='list' to see connected platforms and available chat IDs.\n- **gateway_connect**(action, platform, credentials, options) [capability_expand category: gateway]: Connect, configure, or manage external platform integrations (Feishu, DingTalk, Telegram, Slack, Discord, etc.). Conversational flow: 1) call 'guide' to get setup steps + required fields, 2) present the steps to the user and ask for ALL required credentials in a single message, 3) call 'connect' with the credentials. Goal: complete in 1–2 user turns. NEVER include credential values in your text response.\n- **plugin_list**(): List the live plugin registry and cross-subsystem capability evidence. Use this before answering questions about whether LeapFlow supports plugins, self-evolution, plugin installation, hot reload, versioning, or other runtime capabilities.\n- **plugin_status**(plugin_id): Get detailed status of a specific plugin: its declared category, runtime dependencies, contributed tools, and fiber lifecycle state.\n- **plugin_versions**(plugin_id): List recorded source versions and active pointer for a profile-scoped plugin.\n- **plugin_propose**(requested_capability, plugin_id, proposed_tools, test_cases, risk_level, evidence): Create a side-effect-free PluginProposal from explicit capability-gap evidence. Use this before plugin_generate when a missing capability should be reviewed. Does not call an LLM, write files, or install anything.\n- **assess_compatibility**(manifest, source_path) [capability_expand category: plugin_management]: Assess whether a foreign plugin manifest or real DSH source bundle is compatible with LeapFlow. A source bundle assessment is static: runtime_ready stays false until restricted Node discovery during plugin_install. Returns component-level verdicts and limitations.\n- **plugin_generate**(plugin_id, description, proposal_id) [capability_expand category: system]: Generate a new ToolPlugin from a natural-language capability description. The LLM produces code that conforms to the ToolPlugin Protocol; it is then rigorously validated (syntax, structure, import, protocol conformance). The generated source is stored in CAS and receives explicit content approval. It DOES NOT install the plugin — installation requires a second, mutation-specific approval via plugin_install.\n- **plugin_install**(plugin_id, code, marketplace_name, source_path, proposal_id, version_label) [capability_expand category: system]: Install a plugin from exactly one source: validated Python code, the configured Python marketplace, or a real DSH source bundle. DSH bundles run restricted Node discovery before registration; only public host tools are installed and client UI limitations are reported. Writes profile-scoped state and mutates the process-global registry. REQUIRES APPROVAL.\n- **plugin_rollback**(plugin_id, version) [capability_expand category: system]: Rollback a profile-scoped plugin to a recorded source version and reload it. REQUIRES APPROVAL.\n- **plugin_reload**(plugin_id, version_label) [capability_expand category: system]: Hot-reload a plugin at runtime. Disposes the old plugin fiber, re-imports its module, and registers a fresh instance. Existing in-flight turns are unaffected (snapshot isolation). REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_disable**(plugin_id) [capability_expand category: system]: Disable a plugin by disposing its fiber, removing its tools from the runtime registry. Cannot disable self_management itself. REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_remove**(plugin_id, delete_source) [capability_expand category: system]: Terminally remove a plugin: dispose its fiber, unregister its tools, remove reload metadata, and optionally delete its profile-scoped source file. Cannot remove self_management itself. REQUIRES APPROVAL.\n- **plugin_unquarantine**(plugin_id) [capability_expand category: plugin_management]: Restore a quarantined plugin to probation status for re-evaluation. Unfreezes the trust ledger, transitions the proposal back to PROBATION, and reloads the plugin. REQUIRES APPROVAL.\n- **plugin_enable**(plugin_id) [capability_expand category: system]: Re-enable a previously disabled plugin by reloading its module and registering a fresh instance. REQUIRES APPROVAL.\n- **self_describe**(facet): Introspect LeapFlow's own identity, capabilities, runtime state, evolution metrics, or platform connections. Use facet='all' only when a comprehensive self-check is explicitly requested.\n- **runtime_snapshot**(): Lightweight (~150 token) flat snapshot of current runtime state: model, context budget, posture, disclosure level, turn count, cache hit rate, uptime, tool count, and pending approvals.\n- **schedule_create**(trigger_expression, instruction, execution_mode, max_retries, delivery_target) [capability_expand category: scheduler]: Create a new scheduled task. Specify a trigger expression (e.g. '30m', 'every 2h', '0 9 * * *') and the instruction to execute on each trigger. Optionally provide an execution mode and a delivery target to receive notifications.\n- **schedule_list**() [capability_expand category: scheduler]: List all scheduled tasks with their current state, trigger, and next due time.\n- **schedule_status**(task_id) [capability_expand category: scheduler]: Get detailed status and recent execution history for a scheduled task.\n- **schedule_pause**(task_id) [capability_expand category: scheduler]: Pause a scheduled task so it stops firing without being cancelled.\n- **schedule_resume**(task_id) [capability_expand category: scheduler]: Resume a paused scheduled task — re-arms it and recalculates next due time.\n- **schedule_cancel**(task_id) [capability_expand category: scheduler]: Cancel a scheduled task permanently.\n- **tool_search**(query, max_results): Search all registered tools by keyword relevance. Returns a ranked list of matching tools with name, category, and summary. Use this when you need a tool but are unsure of its exact name or category.\n- **tool_describe**(tool_name): Get the full callable schema of a single tool by exact name. Returns the tool's description, parameters, and metadata. Use after tool_search to inspect a specific tool before calling it.\n- **session_search**(query, limit) [capability_expand category: unclassified]: Search past conversation sessions in the current workspace for relevant context.\n- **session_list**(limit): List recent conversation sessions in the current workspace with stable ids, titles, dates, and summaries. Use for browsing past tasks or when user asks to see history without specific search terms. No keywords needed — returns chronological list.\n- **session_detail**(session_id, limit, offset, include_inactive) [capability_expand category: unclassified]: Read a paginated persisted transcript for one past conversation session in the current workspace. Use after session_list or session_search returns a session_id.\n- **fixture_echo**(text): Return the supplied text from a pre-built DSH package.\n- **click**(element_index) [capability_expand category: desktop]: Click a UI element by its element_index (from the latest observe_ui snapshot)\n- **get_clipboard**() [capability_expand category: desktop]: Read current clipboard text content\n- **list_apps**(filter, running_only) [capability_expand category: desktop]: List available applications on this system. Use to discover correct bundle_id before switch_app.\n- **list_windows**() [capability_expand category: desktop]: List all top-level windows with pid, window_id, title, and per-window state (minimized, on-screen). Call this first to pick the pid and window_id that observe_ui and other window tools require.\n- **observe_ui**(pid, window_id, query) [capability_expand category: desktop]: Snapshot one window's actionable UI elements, each tagged with an element_index for click/right_click/read_text. Re-observe after actions — indices belong to one snapshot. Requires the window's pid and window_id from list_windows.\n- **open_url**(url, app_id) [capability_expand category: desktop]: Open a URL in the default or specified browser\n- **read_text**(element_index) [capability_expand category: desktop]: Read the text content of a specific UI element from the latest snapshot\n- **right_click**(element_index) [capability_expand category: desktop]: Right-click a UI element to open its context menu. Returns visible menu items.\n- **screenshot**(pid, window_id) [capability_expand category: desktop]: Capture a screenshot for visual verification. With pid + window_id captures that window (works across all displays); defaults to the last observed window, or the full desktop when no window has been observed.\n- **scroll**(element_index, direction, amount, pid, window_id) [capability_expand category: desktop]: Scroll a scrollable area of a window. Omit element_index to scroll the window's focused/page scroller; pass one to scroll an exact element from the latest snapshot.\n- **select_text**(element_index) [capability_expand category: desktop]: Select all text in a UI element (focus + select-all, for subsequent copy)\n- **set_clipboard**(text) [capability_expand category: desktop]: Write text to the clipboard\n- **shortcut**(keys) [capability_expand category: desktop]: Execute a keyboard shortcut\n- **switch_app**(app_id) [capability_expand category: desktop]: Switch to an app (launch if needed, activate, verify)\n- **type_text**(text) [capability_expand category: desktop]: Type text into the currently focused element\n- **wait**(seconds) [capability_expand category: desktop]: Wait for a specified duration before continuing\n- **wait_until**(condition, pid, window_id, timeout, poll_interval) [capability_expand category: desktop]: Wait until a UI condition is met (polls UI tree). Returns elements when found or on timeout.\n- **wait_until_stable**(timeout, poll_interval, pid, window_id) [capability_expand category: desktop]: Wait until the UI stops changing (element set stabilizes across polls).\n\n## App Connector Capability Index\nLeapFlow can onboard and manage external apps through `platform_connect` and execute exact registered business actions through `platform_action`.\nFor requests about connecting, setting up, configuring, enabling, or managing a supported app, use `platform_connect` first instead of generating SDK/Webhook sample code.\n`platform_connect.action` is the management namespace: list, guide, preflight, connect, disconnect, remove, status, events_start, events_stop, events_status.\n`platform_action.action` is only for exact registered platform business actions listed below, such as `im.send_message`; never use management actions like `list` or `guide` there.\nAll business fields MUST go inside `payload`; top-level keys are only `platform`, `action`, and `payload`.\nDo not invent platform IDs or platform action names. If the needed action is not listed, ask for discovery/clarification instead of guessing.\nUse `platform_connect` with `action='guide'` and the matching `platform` to start onboarding; use `action='list'` when the app is unclear.\nWhen a pending onboarding state is present, continue from that state with `platform_connect` instead of asking the user to restate the app.\nSupported apps:\n- `api_server`: API Server (OpenAI Compatible) (category=api; backend=adapter; platform_action actions=none registered)\n- `dingtalk`: 钉钉 (DingTalk) (category=im; backend=adapter; platform_action actions=none registered)\n- `feishu`: 飞书 (Feishu/Lark) (category=im; backend=cli)\n - `calendar.create_event` payload={summary*, start_time*, end_time*, attendees} [write/high]\n - `docs.create_markdown` payload={title*, markdown*, folder_token} [write/medium]\n - `drive.search` payload={query*, limit} [read/medium]\n - `im.add_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.download_resource` payload={message_id*, file_key*, type*} [read/low]\n - `im.get_messages` payload={message_ids*} [read/low]\n - `im.list_chats` payload={page_size} [read/low]\n - `im.list_messages` payload={chat_id*} [read/low]\n - `im.list_thread_messages` payload={thread*} [read/low]\n - `im.remove_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.reply_message` payload={message_id*, text*} [send/high]\n - `im.search_chats` payload={query*, page_size} [read/low]\n - `im.search_messages` payload={query*} [read/medium]\n - `im.send_message` payload={chat_id*, text*, thread_id} [send/high]\n - `im.update_card` payload={token*, card*} [write/medium]\n - `im.update_message` payload={message_id*, text*} [write/medium]\n - `mail.search_unread` payload={query, limit} [read/high]\n - `sheets.append_row` payload={spreadsheet_token*, sheet_id*, values*} [write/medium]\n - `task.create` payload={title*, description, due_time} [write/medium]\n- `telegram`: Telegram (category=social; backend=adapter; platform_action actions=none registered)\n- `webhook`: Webhook (Generic) (category=webhook; backend=adapter; platform_action actions=none registered)\n\n## Tool Usage\nTools are normally invoked through the native function-calling mechanism, not by writing JSON in your reply\ntext. Only if the provider signals that native function calling is unavailable for this turn, fall back to a\nsingle JSON code block: `{\"name\": \"tool_name\", \"arguments\": {\"key\": \"value\"}}` — use this fallback format\nonly, never both. Only call a tool whose exact name appears in the tool index above and is currently callable\n(or reachable via `capability_expand`). Never invent, rename, alias, or guess a tool name, platform ID, or\nplatform action from argument shape or wording — if the index does not list it, it does not exist.\n`platform_connect.action` (list/guide/preflight/connect/disconnect/remove/status/events_start/events_stop/\nevents_status) is the App Connector management namespace; `platform_action.action` only accepts exact\nregistered business actions such as `im.send_message` shown in the App Connector Capability Index — never mix\nthe two namespaces. If a tool call returns an unknown/unavailable result, use the returned suggestions or\navailable names for a single retry instead of trying further variations of the same guess.\n\n**Side-effect action rule** (`platform_action` with effect=send/write/execute):\n- Call each unique action+payload **exactly once**. Never include duplicates in the same turn.\n- Once the result returns `\"completed\": true`, that action is DONE for this task. Do NOT call it again\n in any subsequent turn — immediately summarize the result for the user instead.\n- The system enforces idempotency: duplicate calls are blocked and will not execute.\n- If the user explicitly requests sending/writing multiple times, use distinct payloads per call.\n\n**Resource identifier provenance rule**:\n- NEVER fabricate, guess, or infer resource identifiers (chat_id, message_id, file_key, user_id, etc.).\n Every resource ID used in a side-effect action MUST come from a successful API response in this session.\n- If a read/list action fails (e.g. authorization error), you do NOT have valid resource IDs.\n Report the failure to the user — do NOT attempt the dependent write/send action with a guessed ID.\n- When a tool result contains `\"llm_instruction\"`, follow it exactly.\n\n## Guidelines\n1. **Direct answers first**: If you already know the answer, respond directly without tools.\n2. **Avoid redundant tool calls**: Do not call the same tool with the same arguments more than once in the same user turn. When an existing tool result already answers the user's request, stop calling tools and answer directly.\n3. **Use tools proactively**: When the user asks about files, time, system state, or needs actions performed, use the appropriate tool.\n4. **Chain tools when needed**: You can call multiple tools in sequence (e.g., list files → read file → summarize).\n5. **Handle failures gracefully**: If a tool fails, explain what went wrong and suggest alternatives. If it failed because of an unknown tool/platform/action name, retry once with an exact name from the returned suggestions, then explain rather than keep guessing.\n6. **Summarize results naturally**: After tool execution, synthesize the results into a helpful answer rather than dumping raw output.\n7. **Stay conversational**: Maintain a natural, helpful tone. Acknowledge context from earlier in the conversation.\n8. **Recall past work**:\n - Broad queries (\"之前做了什么\", \"列出任务\"): answer from the \"Recent Task History\" section already in your context. If insufficient, call session_list.\n - Specific lookups (\"上次那个配置怎么改的\"): call session_search with relevant phrases (NOT single characters).\n - Do NOT call search tools repeatedly with keyword fragments. One well-phrased call is better than ten fragmented ones.\n\n## Coding & Verification\nWhen working with code, prefer the precise built-in tools over ad-hoc shell: use `repo_map` to orient in an\nunfamiliar project, `code_search`/`file_find` to locate and `code_intel` for symbols, `edit_file` (anchored\nsearch-replace, or a unified `diff`) to change files — never rewrite a whole file to change a few lines — and\n`git_query` to inspect diffs/log. After edits, check `syntax_ok` in the result and run `test_run`/`lint_check`\nbefore declaring the work done. Batch independent read-only calls (search/read/query) into a single turn — they\nrun in parallel.\n\n## Presentation Style\n1. **Polished Markdown only**: Format user-facing answers with clean Markdown headings, short paragraphs, and concise bullets. Use tables only when they improve comparison or scanning.\n2. **Terminal-friendly layout**: Keep lines readable in a TUI; avoid dense walls of text, deeply nested lists, oversized ASCII art, or heavy visual blocks.\n3. **Elegant emphasis**: Use bold text sparingly for key terms and conclusions. Avoid excessive emojis, decorative symbols, repeated separators, or visual noise.\n4. **Theme-safe colors**: Do not emit ANSI escape codes, HTML color tags, Rich markup, or hardcoded color names. Rely on the TUI theme to render Markdown professionally.\n5. **No leaked tool protocol**: Never show tool-call JSON, internal schemas, raw observations, tool result payloads, or hidden reasoning in the final answer unless the user explicitly asks for raw/debug output. Treat any prior `{\"name\": ..., \"arguments\": ...}` blocks and `Tool result (...)` messages as internal execution context only.\n6. **Professional closure**: End with a concise conclusion or next step when helpful; avoid rambling after the useful answer is complete.\n\nWhen finished with all tool calls, respond normally without a JSON block, tool-call transcript, or process log.\n\n## Task Contract\n- Task ID: turn-2\n- Original user request: Invoke fixture_echo with the text before restart.\n- Workspace root: \n- Allowed roots: \n- Treat relative project paths as relative to the workspace root; never infer `.` as the project root when a workspace root is provided.\n- Workspace boundary is enforced by tools: do not read, search, edit, or run commands against paths outside the allowed roots unless the user explicitly requests an external path and the tool/approval policy permits it.\n- LeapFlow workspace config is optional at `/.leapflow/config.yaml`; runtime config is loaded from `~/.leapflow/config/user.yaml` and `~/.leapflow/profiles//config/*.yaml`.\n- Preserve this task contract across summarization, compression, tool loops, and memory retrieval.\n" - }, - { - "role": "system", - "content": "\n## Recent Session Summary\n- [assistant] Installed the hermetic DSH plugin.\n- [user] Invoke fixture_echo with the text before restart.\n" - }, - { - "role": "assistant", - "content": "Installed the hermetic DSH plugin." - }, - { - "role": "user", - "content": "Invoke fixture_echo with the text before restart.\nInvoke fixture_echo with the text before restart." - }, - { - "role": "assistant", - "content": "", - "tool_calls": [ - "fixture_echo" - ] - }, - { - "role": "tool", - "content": "{\"ok\": true, \"echo\": \"before restart\", \"execution_id\": \"\", \"idempotency_key\": \"\", \"execution_policy\": \"read_only\", \"tool_call_id\": \"call_1\", \"execution_status\": \"completed\"}", - "tool_result": true - } - ], - "tools": [ - "capability_expand", - "code_intel", - "code_search", - "config_get", - "config_list", - "env_info", - "file_find", - "file_list", - "file_read", - "fixture_echo", - "git_query", - "lint_check", - "memory_search", - "plugin_list", - "plugin_propose", - "plugin_status", - "plugin_versions", - "repo_map", - "research_note", - "runtime_snapshot", - "schedule_reentry", - "self_describe", - "session_list", - "skill_view", - "skills_list", - "terminal_list", - "terminal_read", - "test_run", - "text_search", - "time_get", - "tool_describe", - "tool_search", - "web_fetch" - ] - }, - "responses": [ - { - "status": 200, - "content_type": "application/json", - "body": "{\"id\": \"chatcmpl-cassette\", \"object\": \"chat.completion\", \"model\": \"cassette-model\", \"choices\": [{\"index\": 0, \"message\": {\"role\": \"assistant\", \"content\": \"The DSH tool ran before restart.\"}, \"finish_reason\": \"stop\"}], \"usage\": {\"prompt_tokens\": 64, \"completion_tokens\": 16, \"total_tokens\": 80}}" - } - ] -} diff --git a/tests/_fixtures/cassettes/r6_lifecycle/cassette-model-1453ebd57d44e98b.cassette.json b/tests/_fixtures/cassettes/r6_lifecycle/cassette-model-1453ebd57d44e98b.cassette.json new file mode 100644 index 00000000..e8490ce7 --- /dev/null +++ b/tests/_fixtures/cassettes/r6_lifecycle/cassette-model-1453ebd57d44e98b.cassette.json @@ -0,0 +1,80 @@ +{ + "fingerprint": "1453ebd57d44e98bb74b37f3edcf28406dc355031e5bda732bd676e76a709b88", + "note": "captured in seed mode", + "request": { + "model": "cassette-model", + "stream": false, + "messages": [ + { + "role": "system", + "content": "You are LeapFlow, an intelligent assistant that can both converse naturally and take real actions on the user's computer.\n\n## Capabilities\nThe tool index below lists **every** registered tool by name and a one-line summary — this is the complete\ncapability contract; nothing else exists. Only a subset is directly callable this turn (via native tool calling,\nnot a JSON block in your reply). If you need a tool from the index that is not yet callable, call\n`capability_expand` with its category name first — the matching tools become callable immediately after.\nWhen the user asks about LeapFlow **itself** — its identity/version/model, what tools, plugins, or capabilities\nit has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of\nself-awareness — answer from live runtime evidence via `self_describe` (facet=capabilities for \"what can you\ndo / do you support X\", identity for version/model, runtime for context/posture, or `all` for a full self-check),\nnever by reading LeapFlow's own source code, documentation, or memory. Report configuration-dependent or\nunavailable capabilities as limitations rather than inferring them.\n- **text_search**(text, pattern): Search for a regex pattern in text.\n- **text_replace**(text, old, new, count) [capability_expand category: write]: Replace occurrences of a substring in text.\n- **time_get**(): Get current date and time.\n- **env_info**(): Get system environment information (OS, Python version, cwd).\n- **skills_list**(query, category, source): List available learned skills. Use when user asks about capabilities or you need a specific skill.\n- **skill_view**(name): View the full content of a specific skill document.\n- **skill_list_files**(name) [capability_expand category: skills]: List all support files in a skill directory (configs, templates, examples). Use before skill_file_read to discover available files.\n- **skill_file_read**(name, file_path) [capability_expand category: skills]: Read an individual support file from a skill directory. Use skill_list_files first to discover available files.\n- **code_intel**(path, operation): Precise document symbols (outline) for a source file: classes, functions, and methods with line ranges. Python uses an exact AST parse; other languages use a keyword-prefix scan. Prefer over file_read mode=symbols for accurate navigation before editing. Read-only.\n- **repo_map**(path): Compact project orientation for a repository root: languages, detected test/lint commands, top-level structure, entry points, manifest, and VCS branch. Call this first when entering an unfamiliar codebase. Read-only.\n- **scm_sync**(action, cwd, remote, pull_ref, push_ref, timeout) [capability_expand category: scm]: Run a typed git SCM action. Use this instead of shell_run for git pull/push/status. For 'pull origin main then push', set action='pull_then_push', remote='origin', pull_ref='main', and omit push_ref so LeapFlow pushes the current local branch.\n- **git_query**(action, cwd, ref, path, staged, max_count, stat): Read-only structured git inspection: action=diff|log|status|branch|show. Prefer over shell_run for reading repo state — output is clipped, redacted, and log/branch are parsed into structured fields. Use scm_sync for pull/push.\n- **git_write**(action, cwd, message, stage_all, name, ref, create) [capability_expand category: scm]: Mutating git actions: action=commit (message, stage_all), branch (create+switch), checkout (switch; create=true for -b). Approval-gated. Use scm_sync for pull/push and git_query for reads.\n- **test_run**(command, cwd, timeout): Run the project's test suite and return structured results (framework, passed/failed counts, failing tests). Auto-detects the runner (pytest/npm/go/cargo) or uses a configured/explicit command; executes via the governed shell. ok=true means the runner executed — see 'success' for pass/fail.\n- **lint_check**(command, cwd, timeout): Run the project's linter and return a structured clean/issue result. Auto-detects the linter (ruff/eslint/go vet/clippy) or uses a configured/explicit command; executes via the governed shell. ok=true means the linter ran — see 'clean'.\n- **file_list**(path, pattern, depth): List files and directories at a given path. Use depth=1 or depth=2 to get a recursive tree in one call instead of listing each sub-directory separately.\n- **file_read**(path, max_lines, start_line, max_chars, mode): Read text file content with adaptive context governance. For large or unfamiliar files, prefer mode='outline' or mode='symbols' first, then use mode='raw' with start_line/max_lines for the specific range you actually need. For LeapFlow's own settings, use config_list / config_get / config_set — its config files are outside the workspace and not readable here.\n- **file_write**(path, content, mode) [capability_expand category: write]: Write content to a file (overwrite or append).\n- **code_search**(pattern, patterns, path, glob, ignore_case, multiline, max_results, context_lines): Search file CONTENTS by regex pattern across a directory tree (ripgrep-backed). Requires a regex pattern. NOT for listing or browsing directory contents — use file_list for that. Prefer this over shell_run grep: faster, skips VCS/dependency/build dirs, and returns structured path:line:column matches. Batch related lookups into ONE call via `patterns` (OR-combined, single pass) instead of issuing several separate searches. Use file_read for the surrounding context of a hit.\n- **file_find**(glob, path, max_results): Find files by a recursive glob pattern under a base path (e.g. '**/test_*.py' or '*.md'). Prefer this over shell_run find; skips VCS/dependency/build dirs.\n- **edit_file**(path, edits, dry_run, diff) [capability_expand category: write]: Apply targeted, anchored search-replace edits to an EXISTING text file (use file_write to create/overwrite). Each edit is {original_text, new_text, replace_all?}; original_text must match exactly and uniquely (or set replace_all) — a non-unique or missing anchor is rejected so files are never corrupted. Set dry_run to preview. Alternatively pass a unified 'diff' to apply its hunks as anchored edits. Far cheaper and safer than rewriting a whole file.\n- **shell_run**(command, cwd, timeout) [capability_expand category: shell]: Execute a one-shot shell command with timeout protection. Runs in the active workspace; paths resolving outside it are refused. Reach for a structured tool first when one fits — web_fetch for anything over HTTP(S), git_query/scm_sync for git, code_search/file_find/file_read for the repo, config_get/config_set for LeapFlow's own settings — because those report typed results, while a failed shell command can only be diagnosed from its exit code and stderr. Every shell run counts as an external side effect, so a failure stops the rest of the batch and is not retried automatically.\n- **terminal_open**(command, cwd, shell) [capability_expand category: terminal]: Open a PERSISTENT shell session (REPL/dev server/watch), returning a session_id for terminal_send/read/close. Disabled unless tools.terminal_session_enabled is set. For one-shot commands use shell_run instead.\n- **terminal_send**(session_id, input, wait) [capability_expand category: terminal]: Send a line of input to a persistent terminal session and return output captured shortly after.\n- **terminal_read**(session_id, wait): Drain buffered output from a persistent terminal session (optionally waiting briefly first).\n- **terminal_close**(session_id) [capability_expand category: terminal]: Terminate a persistent terminal session and release its process group.\n- **terminal_list**(): List active persistent terminal sessions.\n- **config_list**(category, limit): List LeapFlow's own writable settings (LLM model and endpoint, daemon, memory, perception, gateway, …) with current values. Use this to discover the exact key before changing anything. Optionally narrow by `category`. This is the only correct way to inspect LeapFlow configuration — never read config files from disk.\n- **config_get**(key): Read one LeapFlow setting by key (e.g. 'llm.model', 'llm.base_url', 'daemon.log_level'), returning its current value, type, scopes, and whether a change needs a daemon restart. There is no 'llm.provider' key: provider behavior is inferred from the OpenAI-compatible 'llm.base_url'. Never read LeapFlow config files from disk — use this.\n- **config_set**(key, value, scope) [capability_expand category: config]: Change one LeapFlow setting by key, e.g. switch an OpenAI-compatible LLM with key='llm.model' and key='llm.base_url'. There is no 'llm.provider' key: provider behavior is inferred from the endpoint. Values are validated and coerced; credentials are stored in the vault automatically. Call config_list or config_get first if unsure of the key. The result states whether a `leap daemon restart` is required. Never edit LeapFlow config files directly.\n- **web_fetch**(url, select, timeout, max_bytes): Read a URL over HTTP(S) and get back extracted, context-sized content: parsed JSON for API endpoints, readable text plus links for web pages. Use this for anything on the internet — prices, docs, releases, articles — instead of running curl through shell_run: it reports real HTTP status codes, retries rate limits on its own, and is a plain read so a retry is always safe. For JSON APIs pass `select` with a dotted path (e.g. 'chart.result.0.meta') to return just that part instead of the whole payload.\n- **memory_search**(query, limit): Search agent memory for relevant past experiences, observations, and facts.\n- **memory_add**(content, kind) [capability_expand category: write]: Store a new observation or insight in memory for future reference.\n- **research_note**(kind, text): Record a compact, structured note about the current task's state so it survives context compression on long / multi-step tasks. Use for durable findings, open questions still to resolve, decisions / excluded paths, and the immediate next step. One concise sentence per note.\n- **capability_expand**(category): Fetch the full callable schema for every tool in a capability category (e.g. 'hub', 'gateway', 'desktop', 'delegate', 'file', 'memory', 'skill'). The compact tool index always lists every registered tool by name and a one-line summary, but only a static low-risk subset is directly callable each turn. If you need a tool from the index that is not yet callable, call capability_expand with its category first; the matching tools become callable in this turn. Never invent a tool name — expand the category instead.\n- **delegate_task**(goal, context) [capability_expand category: delegate]: Delegate a complex sub-task to an isolated subagent. The subagent gets a fresh context and restricted tool access. Use when a task is self-contained and can be solved independently.\n- **schedule_reentry**(kind, reason, delay_seconds, event_match, max_reentries, deadline_seconds): Register a re-entry so this task can resume later from its current orientation (findings / open questions / next step). Use when work must pause and continue after a delay (kind=time) or when a matching platform event arrives (kind=event), instead of finishing now. The research-ledger state is carried over automatically.\n- **hub_push**(skill_name, visibility, version) [capability_expand category: hub]: Push a local skill to the ModelScope Hub for sharing or backup.\n- **hub_pull**(repo_id, version) [capability_expand category: hub]: Pull a skill from the ModelScope Hub to install locally.\n- **hub_search**(query) [capability_expand category: hub]: Search for skills on the Hub by keyword or description.\n- **hub_sync**(mode, dry_run) [capability_expand category: hub]: Preview or execute sync between local skills and Hub.\n- **platform_action**(platform, action, payload, backend_kind) [capability_expand category: gateway]: Execute an exact registered business action on an external platform through LeapFlow's App Connector layer. Actions must be copied from the App Connector Capability Index and are addressed as domain.operation, e.g. im.send_message or docs.create_markdown. All business fields (chat_id, text, query, etc.) MUST be placed inside `payload`, never at the top level. Example: {\"platform\":\"feishu\",\"action\":\"im.send_message\",\"payload\":{\"chat_id\":\"oc_xxx\",\"text\":\"hello\"}}. Do not invent action names, do not use management actions such as list/guide/connect/status here.\n- **platform_connect**(action, platform, credentials, options, checkpoint) [capability_expand category: gateway]: List, guide, connect, disconnect, remove, or check status for external platforms using the App Connector management namespace. Supports REST and CLI backends. Use this for management actions such as list/guide/preflight/connect/status; use platform_action only for exact registered business actions.\n- **gateway_send**(platform, chat_id, text, thread_id) [capability_expand category: gateway]: Send a message to a connected external platform (Feishu group, Telegram chat, DingTalk conversation, etc.). Requires the platform to be connected via gateway_connect first. Use gateway_connect with action='list' to see connected platforms and available chat IDs.\n- **gateway_connect**(action, platform, credentials, options) [capability_expand category: gateway]: Connect, configure, or manage external platform integrations (Feishu, DingTalk, Telegram, Slack, Discord, etc.). Conversational flow: 1) call 'guide' to get setup steps + required fields, 2) present the steps to the user and ask for ALL required credentials in a single message, 3) call 'connect' with the credentials. Goal: complete in 1–2 user turns. NEVER include credential values in your text response.\n- **plugin_list**(): List the live plugin registry across Tool/Gateway/LLM subsystems for plugin management: which plugins are loaded, their category and fiber lifecycle state. For questions about LeapFlow's own capabilities or whether it supports X, use self_describe instead.\n- **plugin_status**(plugin_id): Get detailed status of a specific plugin: its declared category, runtime dependencies, contributed tools, and fiber lifecycle state.\n- **plugin_versions**(plugin_id): List recorded source versions and active pointer for a profile-scoped plugin.\n- **plugin_propose**(requested_capability, plugin_id, proposed_tools, test_cases, risk_level, evidence): Create a side-effect-free PluginProposal from explicit capability-gap evidence. Use this before plugin_generate when a missing capability should be reviewed. Does not call an LLM, write files, or install anything.\n- **assess_compatibility**(manifest, source_path) [capability_expand category: plugin_management]: Assess whether a foreign plugin manifest or real DSH source bundle is compatible with LeapFlow. A source bundle assessment is static: runtime_ready stays false until restricted Node discovery during plugin_install. Returns component-level verdicts and limitations.\n- **plugin_generate**(plugin_id, description, proposal_id) [capability_expand category: system]: Generate a new ToolPlugin from a natural-language capability description. The LLM produces code that conforms to the ToolPlugin Protocol; it is then rigorously validated (syntax, structure, import, protocol conformance). The generated source is stored in CAS and receives explicit content approval. It DOES NOT install the plugin — installation requires a second, mutation-specific approval via plugin_install.\n- **plugin_install**(plugin_id, code, marketplace_name, source_path, proposal_id, version_label) [capability_expand category: system]: Install a plugin from exactly one source: validated Python code, the configured Python marketplace, or a real DSH source bundle. DSH bundles run restricted Node discovery before registration; only public host tools are installed and client UI limitations are reported. Writes profile-scoped state and mutates the process-global registry. REQUIRES APPROVAL.\n- **plugin_rollback**(plugin_id, version) [capability_expand category: system]: Rollback a profile-scoped plugin to a recorded source version and reload it. REQUIRES APPROVAL.\n- **plugin_reload**(plugin_id, version_label) [capability_expand category: system]: Hot-reload a plugin at runtime. Disposes the old plugin fiber, re-imports its module, and registers a fresh instance. Existing in-flight turns are unaffected (snapshot isolation). REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_disable**(plugin_id) [capability_expand category: system]: Disable a plugin by disposing its fiber, removing its tools from the runtime registry. Cannot disable self_management itself. REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_remove**(plugin_id, delete_source) [capability_expand category: system]: Terminally remove a plugin: dispose its fiber, unregister its tools, remove reload metadata, and optionally delete its profile-scoped source file. Cannot remove self_management itself. REQUIRES APPROVAL.\n- **plugin_unquarantine**(plugin_id) [capability_expand category: plugin_management]: Restore a quarantined plugin to probation status for re-evaluation. Unfreezes the trust ledger, transitions the proposal back to PROBATION, and reloads the plugin. REQUIRES APPROVAL.\n- **plugin_enable**(plugin_id) [capability_expand category: system]: Re-enable a previously disabled plugin by reloading its module and registering a fresh instance. REQUIRES APPROVAL.\n- **self_describe**(facet): Answer questions about LeapFlow ITSELF from live runtime evidence — its identity/version/model, what tools and plugins it has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of self-awareness. Prefer this over reading LeapFlow's own source code. Pick facet=capabilities for 'what can you do / do you support X', identity for version/model, runtime for context/posture, or all for a full self-check when explicitly requested.\n- **runtime_snapshot**(): Lightweight (~150 token) flat snapshot of current runtime state: model, context budget, posture, disclosure level, turn count, cache hit rate, uptime, tool count, and pending approvals.\n- **schedule_create**(trigger_expression, instruction, execution_mode, max_retries, delivery_target) [capability_expand category: scheduler]: Create a new scheduled task. Specify a trigger expression (e.g. '30m', 'every 2h', '0 9 * * *') and the instruction to execute on each trigger. Optionally provide an execution mode and a delivery target to receive notifications.\n- **schedule_list**() [capability_expand category: scheduler]: List all scheduled tasks with their current state, trigger, and next due time.\n- **schedule_status**(task_id) [capability_expand category: scheduler]: Get detailed status and recent execution history for a scheduled task.\n- **schedule_pause**(task_id) [capability_expand category: scheduler]: Pause a scheduled task so it stops firing without being cancelled.\n- **schedule_resume**(task_id) [capability_expand category: scheduler]: Resume a paused scheduled task — re-arms it and recalculates next due time.\n- **schedule_cancel**(task_id) [capability_expand category: scheduler]: Cancel a scheduled task permanently.\n- **tool_search**(query, max_results): Search all registered tools by keyword relevance. Returns a ranked list of matching tools with name, category, and summary. Use this when you need a tool but are unsure of its exact name or category.\n- **tool_describe**(tool_name): Get the full callable schema of a single tool by exact name. Returns the tool's description, parameters, and metadata. Use after tool_search to inspect a specific tool before calling it.\n- **session_search**(query, limit) [capability_expand category: unclassified]: Search past conversation sessions in the current workspace for relevant context.\n- **session_list**(limit): List recent conversation sessions in the current workspace with stable ids, titles, dates, and summaries. Use for browsing past tasks or when user asks to see history without specific search terms. No keywords needed — returns chronological list.\n- **session_detail**(session_id, limit, offset, include_inactive) [capability_expand category: unclassified]: Read a paginated persisted transcript for one past conversation session in the current workspace. Use after session_list or session_search returns a session_id.\n- **fixture_echo**(text): Return the supplied text from a pre-built DSH package.\n- **click**(element_index) [capability_expand category: desktop]: Click a UI element by its element_index (from the latest observe_ui snapshot)\n- **get_clipboard**() [capability_expand category: desktop]: Read current clipboard text content\n- **list_apps**(filter, running_only) [capability_expand category: desktop]: List available applications on this system. Use to discover correct bundle_id before switch_app.\n- **list_windows**() [capability_expand category: desktop]: List all top-level windows with pid, window_id, title, and per-window state (minimized, on-screen). Call this first to pick the pid and window_id that observe_ui and other window tools require.\n- **observe_ui**(pid, window_id, query) [capability_expand category: desktop]: Snapshot one window's actionable UI elements, each tagged with an element_index for click/right_click/read_text. Re-observe after actions — indices belong to one snapshot. Requires the window's pid and window_id from list_windows.\n- **open_url**(url, app_id) [capability_expand category: desktop]: Open a URL in the default or specified browser\n- **read_text**(element_index) [capability_expand category: desktop]: Read the text content of a specific UI element from the latest snapshot\n- **right_click**(element_index) [capability_expand category: desktop]: Right-click a UI element to open its context menu. Returns visible menu items.\n- **screenshot**(pid, window_id) [capability_expand category: desktop]: Capture a screenshot for visual verification. With pid + window_id captures that window (works across all displays); defaults to the last observed window, or the full desktop when no window has been observed.\n- **scroll**(element_index, direction, amount, pid, window_id) [capability_expand category: desktop]: Scroll a scrollable area of a window. Omit element_index to scroll the window's focused/page scroller; pass one to scroll an exact element from the latest snapshot.\n- **select_text**(element_index) [capability_expand category: desktop]: Select all text in a UI element (focus + select-all, for subsequent copy)\n- **set_clipboard**(text) [capability_expand category: desktop]: Write text to the clipboard\n- **shortcut**(keys) [capability_expand category: desktop]: Execute a keyboard shortcut\n- **switch_app**(app_id) [capability_expand category: desktop]: Switch to an app (launch if needed, activate, verify)\n- **type_text**(text) [capability_expand category: desktop]: Type text into the currently focused element\n- **wait**(seconds) [capability_expand category: desktop]: Wait for a specified duration before continuing\n- **wait_until**(condition, pid, window_id, timeout, poll_interval) [capability_expand category: desktop]: Wait until a UI condition is met (polls UI tree). Returns elements when found or on timeout.\n- **wait_until_stable**(timeout, poll_interval, pid, window_id) [capability_expand category: desktop]: Wait until the UI stops changing (element set stabilizes across polls).\n\n## App Connector Capability Index\nLeapFlow can onboard and manage external apps through `platform_connect` and execute exact registered business actions through `platform_action`.\nFor requests about connecting, setting up, configuring, enabling, or managing a supported app, use `platform_connect` first instead of generating SDK/Webhook sample code.\n`platform_connect.action` is the management namespace: list, guide, preflight, connect, disconnect, remove, status, events_start, events_stop, events_status.\n`platform_action.action` is only for exact registered platform business actions listed below, such as `im.send_message`; never use management actions like `list` or `guide` there.\nAll business fields MUST go inside `payload`; top-level keys are only `platform`, `action`, and `payload`.\nDo not invent platform IDs or platform action names. If the needed action is not listed, ask for discovery/clarification instead of guessing.\nUse `platform_connect` with `action='guide'` and the matching `platform` to start onboarding; use `action='list'` when the app is unclear.\nWhen a pending onboarding state is present, continue from that state with `platform_connect` instead of asking the user to restate the app.\nSupported apps:\n- `api_server`: API Server (OpenAI Compatible) (category=api; backend=adapter; platform_action actions=none registered)\n- `dingtalk`: 钉钉 (DingTalk) (category=im; backend=adapter; platform_action actions=none registered)\n- `feishu`: 飞书 (Feishu/Lark) (category=im; backend=cli)\n - `calendar.create_event` payload={summary*, start_time*, end_time*, attendees} [write/high]\n - `docs.create_markdown` payload={title*, markdown*, folder_token} [write/medium]\n - `drive.search` payload={query*, limit} [read/medium]\n - `im.add_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.download_resource` payload={message_id*, file_key*, type*} [read/low]\n - `im.get_messages` payload={message_ids*} [read/low]\n - `im.list_chats` payload={page_size} [read/low]\n - `im.list_messages` payload={chat_id*} [read/low]\n - `im.list_thread_messages` payload={thread*} [read/low]\n - `im.remove_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.reply_message` payload={message_id*, text*} [send/high]\n - `im.search_chats` payload={query*, page_size} [read/low]\n - `im.search_messages` payload={query*} [read/medium]\n - `im.send_message` payload={chat_id*, text*, thread_id} [send/high]\n - `im.update_card` payload={token*, card*} [write/medium]\n - `im.update_message` payload={message_id*, text*} [write/medium]\n - `mail.search_unread` payload={query, limit} [read/high]\n - `sheets.append_row` payload={spreadsheet_token*, sheet_id*, values*} [write/medium]\n - `task.create` payload={title*, description, due_time} [write/medium]\n- `telegram`: Telegram (category=social; backend=adapter; platform_action actions=none registered)\n- `webhook`: Webhook (Generic) (category=webhook; backend=adapter; platform_action actions=none registered)\n\n## Tool Usage\nTools are normally invoked through the native function-calling mechanism, not by writing JSON in your reply\ntext. Only if the provider signals that native function calling is unavailable for this turn, fall back to a\nsingle JSON code block: `{\"name\": \"tool_name\", \"arguments\": {\"key\": \"value\"}}` — use this fallback format\nonly, never both. Only call a tool whose exact name appears in the tool index above and is currently callable\n(or reachable via `capability_expand`). Never invent, rename, alias, or guess a tool name, platform ID, or\nplatform action from argument shape or wording — if the index does not list it, it does not exist.\n`platform_connect.action` (list/guide/preflight/connect/disconnect/remove/status/events_start/events_stop/\nevents_status) is the App Connector management namespace; `platform_action.action` only accepts exact\nregistered business actions such as `im.send_message` shown in the App Connector Capability Index — never mix\nthe two namespaces. If a tool call returns an unknown/unavailable result, use the returned suggestions or\navailable names for a single retry instead of trying further variations of the same guess.\n\n**Side-effect action rule** (`platform_action` with effect=send/write/execute):\n- Call each unique action+payload **exactly once**. Never include duplicates in the same turn.\n- Once the result returns `\"completed\": true`, that action is DONE for this task. Do NOT call it again\n in any subsequent turn — immediately summarize the result for the user instead.\n- The system enforces idempotency: duplicate calls are blocked and will not execute.\n- If the user explicitly requests sending/writing multiple times, use distinct payloads per call.\n\n**Resource identifier provenance rule**:\n- NEVER fabricate, guess, or infer resource identifiers (chat_id, message_id, file_key, user_id, etc.).\n Every resource ID used in a side-effect action MUST come from a successful API response in this session.\n- If a read/list action fails (e.g. authorization error), you do NOT have valid resource IDs.\n Report the failure to the user — do NOT attempt the dependent write/send action with a guessed ID.\n- When a tool result contains `\"llm_instruction\"`, follow it exactly.\n\n## Guidelines\n1. **Direct answers first**: If you already know the answer, respond directly without tools.\n2. **Avoid redundant tool calls**: Do not call the same tool with the same arguments more than once in the same user turn. When an existing tool result already answers the user's request, stop calling tools and answer directly.\n3. **Use tools proactively**: When the user asks about files, time, system state, or needs actions performed, use the appropriate tool.\n4. **Chain tools when needed**: You can call multiple tools in sequence (e.g., list files → read file → summarize).\n5. **Handle failures gracefully**: If a tool fails, explain what went wrong and suggest alternatives. If it failed because of an unknown tool/platform/action name, retry once with an exact name from the returned suggestions, then explain rather than keep guessing.\n6. **Summarize results naturally**: After tool execution, synthesize the results into a helpful answer rather than dumping raw output.\n7. **Stay conversational**: Maintain a natural, helpful tone. Acknowledge context from earlier in the conversation.\n8. **Recall past work**:\n - Broad queries (\"之前做了什么\", \"列出任务\"): answer from the \"Recent Task History\" section already in your context. If insufficient, call session_list.\n - Specific lookups (\"上次那个配置怎么改的\"): call session_search with relevant phrases (NOT single characters).\n - Do NOT call search tools repeatedly with keyword fragments. One well-phrased call is better than ten fragmented ones.\n\n## Coding & Verification\nWhen working with code, prefer the precise built-in tools over ad-hoc shell: use `repo_map` to orient in an\nunfamiliar project, `code_search`/`file_find` to locate and `code_intel` for symbols, `edit_file` (anchored\nsearch-replace, or a unified `diff`) to change files — never rewrite a whole file to change a few lines — and\n`git_query` to inspect diffs/log. After edits, check `syntax_ok` in the result and run `test_run`/`lint_check`\nbefore declaring the work done. Batch independent read-only calls (search/read/query) into a single turn — they\nrun in parallel.\n\n## Presentation Style\n1. **Polished Markdown only**: Format user-facing answers with clean Markdown headings, short paragraphs, and concise bullets. Use tables only when they improve comparison or scanning.\n2. **Terminal-friendly layout**: Keep lines readable in a TUI; avoid dense walls of text, deeply nested lists, oversized ASCII art, or heavy visual blocks.\n3. **Elegant emphasis**: Use bold text sparingly for key terms and conclusions. Avoid excessive emojis, decorative symbols, repeated separators, or visual noise.\n4. **Theme-safe colors**: Do not emit ANSI escape codes, HTML color tags, Rich markup, or hardcoded color names. Rely on the TUI theme to render Markdown professionally.\n5. **No leaked tool protocol**: Never show tool-call JSON, internal schemas, raw observations, tool result payloads, or hidden reasoning in the final answer unless the user explicitly asks for raw/debug output. Treat any prior `{\"name\": ..., \"arguments\": ...}` blocks and `Tool result (...)` messages as internal execution context only.\n6. **Professional closure**: End with a concise conclusion or next step when helpful; avoid rambling after the useful answer is complete.\n\nWhen finished with all tool calls, respond normally without a JSON block, tool-call transcript, or process log.\n\n## Task Contract\n- Task ID: turn-2\n- Original user request: Invoke fixture_echo with the text before restart.\n- Workspace root: \n- Allowed roots: \n- Treat relative project paths as relative to the workspace root; never infer `.` as the project root when a workspace root is provided.\n- Workspace boundary is enforced by tools: do not read, search, edit, or run commands against paths outside the allowed roots unless the user explicitly requests an external path and the tool/approval policy permits it.\n- LeapFlow workspace config is optional at `/.leapflow/config.yaml`; runtime config is loaded from `~/.leapflow/config/user.yaml` and `~/.leapflow/profiles//config/*.yaml`.\n- Preserve this task contract across summarization, compression, tool loops, and memory retrieval.\n" + }, + { + "role": "system", + "content": "\n## Recent Session Summary\n- [assistant] Installed the hermetic DSH plugin.\n- [user] Invoke fixture_echo with the text before restart.\n" + }, + { + "role": "assistant", + "content": "Installed the hermetic DSH plugin." + }, + { + "role": "user", + "content": "Invoke fixture_echo with the text before restart.\nInvoke fixture_echo with the text before restart." + }, + { + "role": "assistant", + "content": "", + "tool_calls": [ + "fixture_echo" + ] + }, + { + "role": "tool", + "content": "{\"ok\": true, \"echo\": \"before restart\", \"execution_id\": \"\", \"idempotency_key\": \"\", \"execution_policy\": \"read_only\", \"tool_call_id\": \"call_1\", \"execution_status\": \"completed\"}", + "tool_result": true + } + ], + "tools": [ + "capability_expand", + "code_intel", + "code_search", + "config_get", + "config_list", + "env_info", + "file_find", + "file_list", + "file_read", + "fixture_echo", + "git_query", + "lint_check", + "memory_search", + "plugin_list", + "plugin_propose", + "plugin_status", + "plugin_versions", + "repo_map", + "research_note", + "runtime_snapshot", + "schedule_reentry", + "self_describe", + "session_list", + "skill_view", + "skills_list", + "terminal_list", + "terminal_read", + "test_run", + "text_search", + "time_get", + "tool_describe", + "tool_search", + "web_fetch" + ] + }, + "responses": [ + { + "status": 200, + "content_type": "application/json", + "body": "{\"id\": \"chatcmpl-cassette\", \"object\": \"chat.completion\", \"model\": \"cassette-model\", \"choices\": [{\"index\": 0, \"message\": {\"role\": \"assistant\", \"content\": \"The DSH tool ran before restart.\"}, \"finish_reason\": \"stop\"}], \"usage\": {\"prompt_tokens\": 64, \"completion_tokens\": 16, \"total_tokens\": 80}}" + } + ] +} diff --git a/tests/_fixtures/cassettes/r6_lifecycle/cassette-model-150ecbf4f218a6f3.cassette.json b/tests/_fixtures/cassettes/r6_lifecycle/cassette-model-150ecbf4f218a6f3.cassette.json deleted file mode 100644 index 6aabc24f..00000000 --- a/tests/_fixtures/cassettes/r6_lifecycle/cassette-model-150ecbf4f218a6f3.cassette.json +++ /dev/null @@ -1,76 +0,0 @@ -{ - "fingerprint": "150ecbf4f218a6f349cc91cceb16e754095358d9e4948a83839f4ad698e58f7c", - "note": "captured in seed mode", - "request": { - "model": "cassette-model", - "stream": false, - "messages": [ - { - "role": "system", - "content": "You are LeapFlow, an intelligent assistant that can both converse naturally and take real actions on the user's computer.\n\n## Capabilities\nThe tool index below lists **every** registered tool by name and a one-line summary — this is the complete\ncapability contract; nothing else exists. Only a subset is directly callable this turn (via native tool calling,\nnot a JSON block in your reply). If you need a tool from the index that is not yet callable, call\n`capability_expand` with its category name first — the matching tools become callable immediately after.\nWhen the user asks what LeapFlow itself supports, whether it supports plugins, or which runtime capabilities\nare available, use the live capability evidence exposed by `plugin_list` before making capability claims; report\nconfiguration-dependent or unavailable capabilities as limitations instead of inferring from documentation.\n- **text_search**(text, pattern): Search for a regex pattern in text.\n- **text_replace**(text, old, new, count) [capability_expand category: write]: Replace occurrences of a substring in text.\n- **time_get**(): Get current date and time.\n- **env_info**(): Get system environment information (OS, Python version, cwd).\n- **skills_list**(query, category, source): List available learned skills. Use when user asks about capabilities or you need a specific skill.\n- **skill_view**(name): View the full content of a specific skill document.\n- **code_intel**(path, operation): Precise document symbols (outline) for a source file: classes, functions, and methods with line ranges. Python uses an exact AST parse; other languages use a keyword-prefix scan. Prefer over file_read mode=symbols for accurate navigation before editing. Read-only.\n- **repo_map**(path): Compact project orientation for a repository root: languages, detected test/lint commands, top-level structure, entry points, manifest, and VCS branch. Call this first when entering an unfamiliar codebase. Read-only.\n- **scm_sync**(action, cwd, remote, pull_ref, push_ref, timeout) [capability_expand category: scm]: Run a typed git SCM action. Use this instead of shell_run for git pull/push/status. For 'pull origin main then push', set action='pull_then_push', remote='origin', pull_ref='main', and omit push_ref so LeapFlow pushes the current local branch.\n- **git_query**(action, cwd, ref, path, staged, max_count, stat): Read-only structured git inspection: action=diff|log|status|branch|show. Prefer over shell_run for reading repo state — output is clipped, redacted, and log/branch are parsed into structured fields. Use scm_sync for pull/push.\n- **git_write**(action, cwd, message, stage_all, name, ref, create) [capability_expand category: scm]: Mutating git actions: action=commit (message, stage_all), branch (create+switch), checkout (switch; create=true for -b). Approval-gated. Use scm_sync for pull/push and git_query for reads.\n- **test_run**(command, cwd, timeout): Run the project's test suite and return structured results (framework, passed/failed counts, failing tests). Auto-detects the runner (pytest/npm/go/cargo) or uses a configured/explicit command; executes via the governed shell. ok=true means the runner executed — see 'success' for pass/fail.\n- **lint_check**(command, cwd, timeout): Run the project's linter and return a structured clean/issue result. Auto-detects the linter (ruff/eslint/go vet/clippy) or uses a configured/explicit command; executes via the governed shell. ok=true means the linter ran — see 'clean'.\n- **file_list**(path, pattern, depth): List files and directories at a given path. Use depth=1 or depth=2 to get a recursive tree in one call instead of listing each sub-directory separately.\n- **file_read**(path, max_lines, start_line, max_chars, mode): Read text file content with adaptive context governance. For large or unfamiliar files, prefer mode='outline' or mode='symbols' first, then use mode='raw' with start_line/max_lines for the specific range you actually need. For LeapFlow's own settings, use config_list / config_get / config_set — its config files are outside the workspace and not readable here.\n- **file_write**(path, content, mode) [capability_expand category: write]: Write content to a file (overwrite or append).\n- **code_search**(pattern, patterns, path, glob, ignore_case, multiline, max_results, context_lines): Search file CONTENTS by regex pattern across a directory tree (ripgrep-backed). Requires a regex pattern. NOT for listing or browsing directory contents — use file_list for that. Prefer this over shell_run grep: faster, skips VCS/dependency/build dirs, and returns structured path:line:column matches. Batch related lookups into ONE call via `patterns` (OR-combined, single pass) instead of issuing several separate searches. Use file_read for the surrounding context of a hit.\n- **file_find**(glob, path, max_results): Find files by a recursive glob pattern under a base path (e.g. '**/test_*.py' or '*.md'). Prefer this over shell_run find; skips VCS/dependency/build dirs.\n- **edit_file**(path, edits, dry_run, diff) [capability_expand category: write]: Apply targeted, anchored search-replace edits to an EXISTING text file (use file_write to create/overwrite). Each edit is {original_text, new_text, replace_all?}; original_text must match exactly and uniquely (or set replace_all) — a non-unique or missing anchor is rejected so files are never corrupted. Set dry_run to preview. Alternatively pass a unified 'diff' to apply its hunks as anchored edits. Far cheaper and safer than rewriting a whole file.\n- **shell_run**(command, cwd, timeout) [capability_expand category: shell]: Execute a one-shot shell command with timeout protection. Runs in the active workspace; paths resolving outside it are refused. Reach for a structured tool first when one fits — web_fetch for anything over HTTP(S), git_query/scm_sync for git, code_search/file_find/file_read for the repo, config_get/config_set for LeapFlow's own settings — because those report typed results, while a failed shell command can only be diagnosed from its exit code and stderr. Every shell run counts as an external side effect, so a failure stops the rest of the batch and is not retried automatically.\n- **terminal_open**(command, cwd, shell) [capability_expand category: terminal]: Open a PERSISTENT shell session (REPL/dev server/watch), returning a session_id for terminal_send/read/close. Disabled unless tools.terminal_session_enabled is set. For one-shot commands use shell_run instead.\n- **terminal_send**(session_id, input, wait) [capability_expand category: terminal]: Send a line of input to a persistent terminal session and return output captured shortly after.\n- **terminal_read**(session_id, wait): Drain buffered output from a persistent terminal session (optionally waiting briefly first).\n- **terminal_close**(session_id) [capability_expand category: terminal]: Terminate a persistent terminal session and release its process group.\n- **terminal_list**(): List active persistent terminal sessions.\n- **config_list**(category, limit): List LeapFlow's own writable settings (LLM model and endpoint, daemon, memory, perception, gateway, …) with current values. Use this to discover the exact key before changing anything. Optionally narrow by `category`. This is the only correct way to inspect LeapFlow configuration — never read config files from disk.\n- **config_get**(key): Read one LeapFlow setting by key (e.g. 'llm.model', 'llm.base_url', 'daemon.log_level'), returning its current value, type, scopes, and whether a change needs a daemon restart. There is no 'llm.provider' key: provider behavior is inferred from the OpenAI-compatible 'llm.base_url'. Never read LeapFlow config files from disk — use this.\n- **config_set**(key, value, scope) [capability_expand category: config]: Change one LeapFlow setting by key, e.g. switch an OpenAI-compatible LLM with key='llm.model' and key='llm.base_url'. There is no 'llm.provider' key: provider behavior is inferred from the endpoint. Values are validated and coerced; credentials are stored in the vault automatically. Call config_list or config_get first if unsure of the key. The result states whether a `leap daemon restart` is required. Never edit LeapFlow config files directly.\n- **web_fetch**(url, select, timeout, max_bytes): Read a URL over HTTP(S) and get back extracted, context-sized content: parsed JSON for API endpoints, readable text plus links for web pages. Use this for anything on the internet — prices, docs, releases, articles — instead of running curl through shell_run: it reports real HTTP status codes, retries rate limits on its own, and is a plain read so a retry is always safe. For JSON APIs pass `select` with a dotted path (e.g. 'chart.result.0.meta') to return just that part instead of the whole payload.\n- **memory_search**(query, limit): Search agent memory for relevant past experiences, observations, and facts.\n- **memory_add**(content, kind) [capability_expand category: write]: Store a new observation or insight in memory for future reference.\n- **research_note**(kind, text): Record a compact, structured note about the current task's state so it survives context compression on long / multi-step tasks. Use for durable findings, open questions still to resolve, decisions / excluded paths, and the immediate next step. One concise sentence per note.\n- **capability_expand**(category): Fetch the full callable schema for every tool in a capability category (e.g. 'hub', 'gateway', 'desktop', 'delegate', 'file', 'memory', 'skill'). The compact tool index always lists every registered tool by name and a one-line summary, but only a static low-risk subset is directly callable each turn. If you need a tool from the index that is not yet callable, call capability_expand with its category first; the matching tools become callable in this turn. Never invent a tool name — expand the category instead.\n- **delegate_task**(goal, context) [capability_expand category: delegate]: Delegate a complex sub-task to an isolated subagent. The subagent gets a fresh context and restricted tool access. Use when a task is self-contained and can be solved independently.\n- **schedule_reentry**(kind, reason, delay_seconds, event_match, max_reentries, deadline_seconds): Register a re-entry so this task can resume later from its current orientation (findings / open questions / next step). Use when work must pause and continue after a delay (kind=time) or when a matching platform event arrives (kind=event), instead of finishing now. The research-ledger state is carried over automatically.\n- **hub_push**(skill_name, visibility, version) [capability_expand category: hub]: Push a local skill to the ModelScope Hub for sharing or backup.\n- **hub_pull**(repo_id, version) [capability_expand category: hub]: Pull a skill from the ModelScope Hub to install locally.\n- **hub_search**(query) [capability_expand category: hub]: Search for skills on the Hub by keyword or description.\n- **hub_sync**(mode, dry_run) [capability_expand category: hub]: Preview or execute sync between local skills and Hub.\n- **platform_action**(platform, action, payload, backend_kind) [capability_expand category: gateway]: Execute an exact registered business action on an external platform through LeapFlow's App Connector layer. Actions must be copied from the App Connector Capability Index and are addressed as domain.operation, e.g. im.send_message or docs.create_markdown. All business fields (chat_id, text, query, etc.) MUST be placed inside `payload`, never at the top level. Example: {\"platform\":\"feishu\",\"action\":\"im.send_message\",\"payload\":{\"chat_id\":\"oc_xxx\",\"text\":\"hello\"}}. Do not invent action names, do not use management actions such as list/guide/connect/status here.\n- **platform_connect**(action, platform, credentials, options, checkpoint) [capability_expand category: gateway]: List, guide, connect, disconnect, remove, or check status for external platforms using the App Connector management namespace. Supports REST and CLI backends. Use this for management actions such as list/guide/preflight/connect/status; use platform_action only for exact registered business actions.\n- **gateway_send**(platform, chat_id, text, thread_id) [capability_expand category: gateway]: Send a message to a connected external platform (Feishu group, Telegram chat, DingTalk conversation, etc.). Requires the platform to be connected via gateway_connect first. Use gateway_connect with action='list' to see connected platforms and available chat IDs.\n- **gateway_connect**(action, platform, credentials, options) [capability_expand category: gateway]: Connect, configure, or manage external platform integrations (Feishu, DingTalk, Telegram, Slack, Discord, etc.). Conversational flow: 1) call 'guide' to get setup steps + required fields, 2) present the steps to the user and ask for ALL required credentials in a single message, 3) call 'connect' with the credentials. Goal: complete in 1–2 user turns. NEVER include credential values in your text response.\n- **plugin_list**(): List the live plugin registry and cross-subsystem capability evidence. Use this before answering questions about whether LeapFlow supports plugins, self-evolution, plugin installation, hot reload, versioning, or other runtime capabilities.\n- **plugin_status**(plugin_id): Get detailed status of a specific plugin: its declared category, runtime dependencies, contributed tools, and fiber lifecycle state.\n- **plugin_versions**(plugin_id): List recorded source versions and active pointer for a profile-scoped plugin.\n- **plugin_propose**(requested_capability, plugin_id, proposed_tools, test_cases, risk_level, evidence): Create a side-effect-free PluginProposal from explicit capability-gap evidence. Use this before plugin_generate when a missing capability should be reviewed. Does not call an LLM, write files, or install anything.\n- **assess_compatibility**(manifest, source_path) [capability_expand category: plugin_management]: Assess whether a foreign plugin manifest or real DSH source bundle is compatible with LeapFlow. A source bundle assessment is static: runtime_ready stays false until restricted Node discovery during plugin_install. Returns component-level verdicts and limitations.\n- **plugin_generate**(plugin_id, description, proposal_id) [capability_expand category: system]: Generate a new ToolPlugin from a natural-language capability description. The LLM produces code that conforms to the ToolPlugin Protocol; it is then rigorously validated (syntax, structure, import, protocol conformance). The generated source is stored in CAS and receives explicit content approval. It DOES NOT install the plugin — installation requires a second, mutation-specific approval via plugin_install.\n- **plugin_install**(plugin_id, code, marketplace_name, source_path, proposal_id, version_label) [capability_expand category: system]: Install a plugin from exactly one source: validated Python code, the configured Python marketplace, or a real DSH source bundle. DSH bundles run restricted Node discovery before registration; only public host tools are installed and client UI limitations are reported. Writes profile-scoped state and mutates the process-global registry. REQUIRES APPROVAL.\n- **plugin_rollback**(plugin_id, version) [capability_expand category: system]: Rollback a profile-scoped plugin to a recorded source version and reload it. REQUIRES APPROVAL.\n- **plugin_reload**(plugin_id, version_label) [capability_expand category: system]: Hot-reload a plugin at runtime. Disposes the old plugin fiber, re-imports its module, and registers a fresh instance. Existing in-flight turns are unaffected (snapshot isolation). REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_disable**(plugin_id) [capability_expand category: system]: Disable a plugin by disposing its fiber, removing its tools from the runtime registry. Cannot disable self_management itself. REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_remove**(plugin_id, delete_source) [capability_expand category: system]: Terminally remove a plugin: dispose its fiber, unregister its tools, remove reload metadata, and optionally delete its profile-scoped source file. Cannot remove self_management itself. REQUIRES APPROVAL.\n- **plugin_unquarantine**(plugin_id) [capability_expand category: plugin_management]: Restore a quarantined plugin to probation status for re-evaluation. Unfreezes the trust ledger, transitions the proposal back to PROBATION, and reloads the plugin. REQUIRES APPROVAL.\n- **plugin_enable**(plugin_id) [capability_expand category: system]: Re-enable a previously disabled plugin by reloading its module and registering a fresh instance. REQUIRES APPROVAL.\n- **self_describe**(facet): Introspect LeapFlow's own identity, capabilities, runtime state, evolution metrics, or platform connections. Use facet='all' only when a comprehensive self-check is explicitly requested.\n- **runtime_snapshot**(): Lightweight (~150 token) flat snapshot of current runtime state: model, context budget, posture, disclosure level, turn count, cache hit rate, uptime, tool count, and pending approvals.\n- **schedule_create**(trigger_expression, instruction, execution_mode, max_retries, delivery_target) [capability_expand category: scheduler]: Create a new scheduled task. Specify a trigger expression (e.g. '30m', 'every 2h', '0 9 * * *') and the instruction to execute on each trigger. Optionally provide an execution mode and a delivery target to receive notifications.\n- **schedule_list**() [capability_expand category: scheduler]: List all scheduled tasks with their current state, trigger, and next due time.\n- **schedule_status**(task_id) [capability_expand category: scheduler]: Get detailed status and recent execution history for a scheduled task.\n- **schedule_pause**(task_id) [capability_expand category: scheduler]: Pause a scheduled task so it stops firing without being cancelled.\n- **schedule_resume**(task_id) [capability_expand category: scheduler]: Resume a paused scheduled task — re-arms it and recalculates next due time.\n- **schedule_cancel**(task_id) [capability_expand category: scheduler]: Cancel a scheduled task permanently.\n- **tool_search**(query, max_results): Search all registered tools by keyword relevance. Returns a ranked list of matching tools with name, category, and summary. Use this when you need a tool but are unsure of its exact name or category.\n- **tool_describe**(tool_name): Get the full callable schema of a single tool by exact name. Returns the tool's description, parameters, and metadata. Use after tool_search to inspect a specific tool before calling it.\n- **session_search**(query, limit) [capability_expand category: unclassified]: Search past conversation sessions in the current workspace for relevant context.\n- **session_list**(limit): List recent conversation sessions in the current workspace with stable ids, titles, dates, and summaries. Use for browsing past tasks or when user asks to see history without specific search terms. No keywords needed — returns chronological list.\n- **session_detail**(session_id, limit, offset, include_inactive) [capability_expand category: unclassified]: Read a paginated persisted transcript for one past conversation session in the current workspace. Use after session_list or session_search returns a session_id.\n- **fixture_echo**(text): Return the supplied text from a pre-built DSH package.\n- **click**(element_index) [capability_expand category: desktop]: Click a UI element by its element_index (from the latest observe_ui snapshot)\n- **get_clipboard**() [capability_expand category: desktop]: Read current clipboard text content\n- **list_apps**(filter, running_only) [capability_expand category: desktop]: List available applications on this system. Use to discover correct bundle_id before switch_app.\n- **list_windows**() [capability_expand category: desktop]: List all top-level windows with pid, window_id, title, and per-window state (minimized, on-screen). Call this first to pick the pid and window_id that observe_ui and other window tools require.\n- **observe_ui**(pid, window_id, query) [capability_expand category: desktop]: Snapshot one window's actionable UI elements, each tagged with an element_index for click/right_click/read_text. Re-observe after actions — indices belong to one snapshot. Requires the window's pid and window_id from list_windows.\n- **open_url**(url, app_id) [capability_expand category: desktop]: Open a URL in the default or specified browser\n- **read_text**(element_index) [capability_expand category: desktop]: Read the text content of a specific UI element from the latest snapshot\n- **right_click**(element_index) [capability_expand category: desktop]: Right-click a UI element to open its context menu. Returns visible menu items.\n- **screenshot**(pid, window_id) [capability_expand category: desktop]: Capture a screenshot for visual verification. With pid + window_id captures that window (works across all displays); defaults to the last observed window, or the full desktop when no window has been observed.\n- **scroll**(element_index, direction, amount, pid, window_id) [capability_expand category: desktop]: Scroll a scrollable area of a window. Omit element_index to scroll the window's focused/page scroller; pass one to scroll an exact element from the latest snapshot.\n- **select_text**(element_index) [capability_expand category: desktop]: Select all text in a UI element (focus + select-all, for subsequent copy)\n- **set_clipboard**(text) [capability_expand category: desktop]: Write text to the clipboard\n- **shortcut**(keys) [capability_expand category: desktop]: Execute a keyboard shortcut\n- **switch_app**(app_id) [capability_expand category: desktop]: Switch to an app (launch if needed, activate, verify)\n- **type_text**(text) [capability_expand category: desktop]: Type text into the currently focused element\n- **wait**(seconds) [capability_expand category: desktop]: Wait for a specified duration before continuing\n- **wait_until**(condition, pid, window_id, timeout, poll_interval) [capability_expand category: desktop]: Wait until a UI condition is met (polls UI tree). Returns elements when found or on timeout.\n- **wait_until_stable**(timeout, poll_interval, pid, window_id) [capability_expand category: desktop]: Wait until the UI stops changing (element set stabilizes across polls).\n\n## App Connector Capability Index\nLeapFlow can onboard and manage external apps through `platform_connect` and execute exact registered business actions through `platform_action`.\nFor requests about connecting, setting up, configuring, enabling, or managing a supported app, use `platform_connect` first instead of generating SDK/Webhook sample code.\n`platform_connect.action` is the management namespace: list, guide, preflight, connect, disconnect, remove, status, events_start, events_stop, events_status.\n`platform_action.action` is only for exact registered platform business actions listed below, such as `im.send_message`; never use management actions like `list` or `guide` there.\nAll business fields MUST go inside `payload`; top-level keys are only `platform`, `action`, and `payload`.\nDo not invent platform IDs or platform action names. If the needed action is not listed, ask for discovery/clarification instead of guessing.\nUse `platform_connect` with `action='guide'` and the matching `platform` to start onboarding; use `action='list'` when the app is unclear.\nWhen a pending onboarding state is present, continue from that state with `platform_connect` instead of asking the user to restate the app.\nSupported apps:\n- `api_server`: API Server (OpenAI Compatible) (category=api; backend=adapter; platform_action actions=none registered)\n- `dingtalk`: 钉钉 (DingTalk) (category=im; backend=adapter; platform_action actions=none registered)\n- `feishu`: 飞书 (Feishu/Lark) (category=im; backend=cli)\n - `calendar.create_event` payload={summary*, start_time*, end_time*, attendees} [write/high]\n - `docs.create_markdown` payload={title*, markdown*, folder_token} [write/medium]\n - `drive.search` payload={query*, limit} [read/medium]\n - `im.add_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.download_resource` payload={message_id*, file_key*, type*} [read/low]\n - `im.get_messages` payload={message_ids*} [read/low]\n - `im.list_chats` payload={page_size} [read/low]\n - `im.list_messages` payload={chat_id*} [read/low]\n - `im.list_thread_messages` payload={thread*} [read/low]\n - `im.remove_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.reply_message` payload={message_id*, text*} [send/high]\n - `im.search_chats` payload={query*, page_size} [read/low]\n - `im.search_messages` payload={query*} [read/medium]\n - `im.send_message` payload={chat_id*, text*, thread_id} [send/high]\n - `im.update_card` payload={token*, card*} [write/medium]\n - `im.update_message` payload={message_id*, text*} [write/medium]\n - `mail.search_unread` payload={query, limit} [read/high]\n - `sheets.append_row` payload={spreadsheet_token*, sheet_id*, values*} [write/medium]\n - `task.create` payload={title*, description, due_time} [write/medium]\n- `telegram`: Telegram (category=social; backend=adapter; platform_action actions=none registered)\n- `webhook`: Webhook (Generic) (category=webhook; backend=adapter; platform_action actions=none registered)\n\n## Tool Usage\nTools are normally invoked through the native function-calling mechanism, not by writing JSON in your reply\ntext. Only if the provider signals that native function calling is unavailable for this turn, fall back to a\nsingle JSON code block: `{\"name\": \"tool_name\", \"arguments\": {\"key\": \"value\"}}` — use this fallback format\nonly, never both. Only call a tool whose exact name appears in the tool index above and is currently callable\n(or reachable via `capability_expand`). Never invent, rename, alias, or guess a tool name, platform ID, or\nplatform action from argument shape or wording — if the index does not list it, it does not exist.\n`platform_connect.action` (list/guide/preflight/connect/disconnect/remove/status/events_start/events_stop/\nevents_status) is the App Connector management namespace; `platform_action.action` only accepts exact\nregistered business actions such as `im.send_message` shown in the App Connector Capability Index — never mix\nthe two namespaces. If a tool call returns an unknown/unavailable result, use the returned suggestions or\navailable names for a single retry instead of trying further variations of the same guess.\n\n**Side-effect action rule** (`platform_action` with effect=send/write/execute):\n- Call each unique action+payload **exactly once**. Never include duplicates in the same turn.\n- Once the result returns `\"completed\": true`, that action is DONE for this task. Do NOT call it again\n in any subsequent turn — immediately summarize the result for the user instead.\n- The system enforces idempotency: duplicate calls are blocked and will not execute.\n- If the user explicitly requests sending/writing multiple times, use distinct payloads per call.\n\n**Resource identifier provenance rule**:\n- NEVER fabricate, guess, or infer resource identifiers (chat_id, message_id, file_key, user_id, etc.).\n Every resource ID used in a side-effect action MUST come from a successful API response in this session.\n- If a read/list action fails (e.g. authorization error), you do NOT have valid resource IDs.\n Report the failure to the user — do NOT attempt the dependent write/send action with a guessed ID.\n- When a tool result contains `\"llm_instruction\"`, follow it exactly.\n\n## Guidelines\n1. **Direct answers first**: If you already know the answer, respond directly without tools.\n2. **Avoid redundant tool calls**: Do not call the same tool with the same arguments more than once in the same user turn. When an existing tool result already answers the user's request, stop calling tools and answer directly.\n3. **Use tools proactively**: When the user asks about files, time, system state, or needs actions performed, use the appropriate tool.\n4. **Chain tools when needed**: You can call multiple tools in sequence (e.g., list files → read file → summarize).\n5. **Handle failures gracefully**: If a tool fails, explain what went wrong and suggest alternatives. If it failed because of an unknown tool/platform/action name, retry once with an exact name from the returned suggestions, then explain rather than keep guessing.\n6. **Summarize results naturally**: After tool execution, synthesize the results into a helpful answer rather than dumping raw output.\n7. **Stay conversational**: Maintain a natural, helpful tone. Acknowledge context from earlier in the conversation.\n8. **Recall past work**:\n - Broad queries (\"之前做了什么\", \"列出任务\"): answer from the \"Recent Task History\" section already in your context. If insufficient, call session_list.\n - Specific lookups (\"上次那个配置怎么改的\"): call session_search with relevant phrases (NOT single characters).\n - Do NOT call search tools repeatedly with keyword fragments. One well-phrased call is better than ten fragmented ones.\n\n## Coding & Verification\nWhen working with code, prefer the precise built-in tools over ad-hoc shell: use `repo_map` to orient in an\nunfamiliar project, `code_search`/`file_find` to locate and `code_intel` for symbols, `edit_file` (anchored\nsearch-replace, or a unified `diff`) to change files — never rewrite a whole file to change a few lines — and\n`git_query` to inspect diffs/log. After edits, check `syntax_ok` in the result and run `test_run`/`lint_check`\nbefore declaring the work done. Batch independent read-only calls (search/read/query) into a single turn — they\nrun in parallel.\n\n## Presentation Style\n1. **Polished Markdown only**: Format user-facing answers with clean Markdown headings, short paragraphs, and concise bullets. Use tables only when they improve comparison or scanning.\n2. **Terminal-friendly layout**: Keep lines readable in a TUI; avoid dense walls of text, deeply nested lists, oversized ASCII art, or heavy visual blocks.\n3. **Elegant emphasis**: Use bold text sparingly for key terms and conclusions. Avoid excessive emojis, decorative symbols, repeated separators, or visual noise.\n4. **Theme-safe colors**: Do not emit ANSI escape codes, HTML color tags, Rich markup, or hardcoded color names. Rely on the TUI theme to render Markdown professionally.\n5. **No leaked tool protocol**: Never show tool-call JSON, internal schemas, raw observations, tool result payloads, or hidden reasoning in the final answer unless the user explicitly asks for raw/debug output. Treat any prior `{\"name\": ..., \"arguments\": ...}` blocks and `Tool result (...)` messages as internal execution context only.\n6. **Professional closure**: End with a concise conclusion or next step when helpful; avoid rambling after the useful answer is complete.\n\nWhen finished with all tool calls, respond normally without a JSON block, tool-call transcript, or process log.\n\n## Task Contract\n- Task ID: turn-1\n- Original user request: Invoke fixture_echo with the text after restart.\n- Workspace root: \n- Allowed roots: \n- Treat relative project paths as relative to the workspace root; never infer `.` as the project root when a workspace root is provided.\n- Workspace boundary is enforced by tools: do not read, search, edit, or run commands against paths outside the allowed roots unless the user explicitly requests an external path and the tool/approval policy permits it.\n- LeapFlow workspace config is optional at `/.leapflow/config.yaml`; runtime config is loaded from `~/.leapflow/config/user.yaml` and `~/.leapflow/profiles//config/*.yaml`.\n- Preserve this task contract across summarization, compression, tool loops, and memory retrieval.\n" - }, - { - "role": "system", - "content": "\n## Recent Session Summary\n- [user] Invoke fixture_echo with the text after restart.\n" - }, - { - "role": "user", - "content": "Invoke fixture_echo with the text after restart.\nInvoke fixture_echo with the text after restart." - }, - { - "role": "assistant", - "content": "", - "tool_calls": [ - "fixture_echo" - ] - }, - { - "role": "tool", - "content": "{\"ok\": true, \"echo\": \"after restart\", \"execution_id\": \"\", \"idempotency_key\": \"\", \"execution_policy\": \"read_only\", \"tool_call_id\": \"call_1\", \"execution_status\": \"completed\"}", - "tool_result": true - } - ], - "tools": [ - "capability_expand", - "code_intel", - "code_search", - "config_get", - "config_list", - "env_info", - "file_find", - "file_list", - "file_read", - "fixture_echo", - "git_query", - "lint_check", - "memory_search", - "plugin_list", - "plugin_propose", - "plugin_status", - "plugin_versions", - "repo_map", - "research_note", - "runtime_snapshot", - "schedule_reentry", - "self_describe", - "session_list", - "skill_view", - "skills_list", - "terminal_list", - "terminal_read", - "test_run", - "text_search", - "time_get", - "tool_describe", - "tool_search", - "web_fetch" - ] - }, - "responses": [ - { - "status": 200, - "content_type": "application/json", - "body": "{\"id\": \"chatcmpl-cassette\", \"object\": \"chat.completion\", \"model\": \"cassette-model\", \"choices\": [{\"index\": 0, \"message\": {\"role\": \"assistant\", \"content\": \"The DSH tool ran after restart.\"}, \"finish_reason\": \"stop\"}], \"usage\": {\"prompt_tokens\": 64, \"completion_tokens\": 16, \"total_tokens\": 80}}" - } - ] -} diff --git a/tests/_fixtures/cassettes/r6_lifecycle/cassette-model-1a13aff243894a1b.cassette.json b/tests/_fixtures/cassettes/r6_lifecycle/cassette-model-1a13aff243894a1b.cassette.json deleted file mode 100644 index 6547acdd..00000000 --- a/tests/_fixtures/cassettes/r6_lifecycle/cassette-model-1a13aff243894a1b.cassette.json +++ /dev/null @@ -1,68 +0,0 @@ -{ - "fingerprint": "1a13aff243894a1b61f599e3799239b0739ed92f1e71e2e2406bfb957d254939", - "note": "captured in seed mode", - "request": { - "model": "cassette-model", - "stream": false, - "messages": [ - { - "role": "system", - "content": "You are LeapFlow, an intelligent assistant that can both converse naturally and take real actions on the user's computer.\n\n## Capabilities\nThe tool index below lists **every** registered tool by name and a one-line summary — this is the complete\ncapability contract; nothing else exists. Only a subset is directly callable this turn (via native tool calling,\nnot a JSON block in your reply). If you need a tool from the index that is not yet callable, call\n`capability_expand` with its category name first — the matching tools become callable immediately after.\nWhen the user asks what LeapFlow itself supports, whether it supports plugins, or which runtime capabilities\nare available, use the live capability evidence exposed by `plugin_list` before making capability claims; report\nconfiguration-dependent or unavailable capabilities as limitations instead of inferring from documentation.\n- **text_search**(text, pattern): Search for a regex pattern in text.\n- **text_replace**(text, old, new, count) [capability_expand category: write]: Replace occurrences of a substring in text.\n- **time_get**(): Get current date and time.\n- **env_info**(): Get system environment information (OS, Python version, cwd).\n- **skills_list**(query, category, source): List available learned skills. Use when user asks about capabilities or you need a specific skill.\n- **skill_view**(name): View the full content of a specific skill document.\n- **code_intel**(path, operation): Precise document symbols (outline) for a source file: classes, functions, and methods with line ranges. Python uses an exact AST parse; other languages use a keyword-prefix scan. Prefer over file_read mode=symbols for accurate navigation before editing. Read-only.\n- **repo_map**(path): Compact project orientation for a repository root: languages, detected test/lint commands, top-level structure, entry points, manifest, and VCS branch. Call this first when entering an unfamiliar codebase. Read-only.\n- **scm_sync**(action, cwd, remote, pull_ref, push_ref, timeout) [capability_expand category: scm]: Run a typed git SCM action. Use this instead of shell_run for git pull/push/status. For 'pull origin main then push', set action='pull_then_push', remote='origin', pull_ref='main', and omit push_ref so LeapFlow pushes the current local branch.\n- **git_query**(action, cwd, ref, path, staged, max_count, stat): Read-only structured git inspection: action=diff|log|status|branch|show. Prefer over shell_run for reading repo state — output is clipped, redacted, and log/branch are parsed into structured fields. Use scm_sync for pull/push.\n- **git_write**(action, cwd, message, stage_all, name, ref, create) [capability_expand category: scm]: Mutating git actions: action=commit (message, stage_all), branch (create+switch), checkout (switch; create=true for -b). Approval-gated. Use scm_sync for pull/push and git_query for reads.\n- **test_run**(command, cwd, timeout): Run the project's test suite and return structured results (framework, passed/failed counts, failing tests). Auto-detects the runner (pytest/npm/go/cargo) or uses a configured/explicit command; executes via the governed shell. ok=true means the runner executed — see 'success' for pass/fail.\n- **lint_check**(command, cwd, timeout): Run the project's linter and return a structured clean/issue result. Auto-detects the linter (ruff/eslint/go vet/clippy) or uses a configured/explicit command; executes via the governed shell. ok=true means the linter ran — see 'clean'.\n- **file_list**(path, pattern, depth): List files and directories at a given path. Use depth=1 or depth=2 to get a recursive tree in one call instead of listing each sub-directory separately.\n- **file_read**(path, max_lines, start_line, max_chars, mode): Read text file content with adaptive context governance. For large or unfamiliar files, prefer mode='outline' or mode='symbols' first, then use mode='raw' with start_line/max_lines for the specific range you actually need. For LeapFlow's own settings, use config_list / config_get / config_set — its config files are outside the workspace and not readable here.\n- **file_write**(path, content, mode) [capability_expand category: write]: Write content to a file (overwrite or append).\n- **code_search**(pattern, patterns, path, glob, ignore_case, multiline, max_results, context_lines): Search file CONTENTS by regex pattern across a directory tree (ripgrep-backed). Requires a regex pattern. NOT for listing or browsing directory contents — use file_list for that. Prefer this over shell_run grep: faster, skips VCS/dependency/build dirs, and returns structured path:line:column matches. Batch related lookups into ONE call via `patterns` (OR-combined, single pass) instead of issuing several separate searches. Use file_read for the surrounding context of a hit.\n- **file_find**(glob, path, max_results): Find files by a recursive glob pattern under a base path (e.g. '**/test_*.py' or '*.md'). Prefer this over shell_run find; skips VCS/dependency/build dirs.\n- **edit_file**(path, edits, dry_run, diff) [capability_expand category: write]: Apply targeted, anchored search-replace edits to an EXISTING text file (use file_write to create/overwrite). Each edit is {original_text, new_text, replace_all?}; original_text must match exactly and uniquely (or set replace_all) — a non-unique or missing anchor is rejected so files are never corrupted. Set dry_run to preview. Alternatively pass a unified 'diff' to apply its hunks as anchored edits. Far cheaper and safer than rewriting a whole file.\n- **shell_run**(command, cwd, timeout) [capability_expand category: shell]: Execute a one-shot shell command with timeout protection. Runs in the active workspace; paths resolving outside it are refused. Reach for a structured tool first when one fits — web_fetch for anything over HTTP(S), git_query/scm_sync for git, code_search/file_find/file_read for the repo, config_get/config_set for LeapFlow's own settings — because those report typed results, while a failed shell command can only be diagnosed from its exit code and stderr. Every shell run counts as an external side effect, so a failure stops the rest of the batch and is not retried automatically.\n- **terminal_open**(command, cwd, shell) [capability_expand category: terminal]: Open a PERSISTENT shell session (REPL/dev server/watch), returning a session_id for terminal_send/read/close. Disabled unless tools.terminal_session_enabled is set. For one-shot commands use shell_run instead.\n- **terminal_send**(session_id, input, wait) [capability_expand category: terminal]: Send a line of input to a persistent terminal session and return output captured shortly after.\n- **terminal_read**(session_id, wait): Drain buffered output from a persistent terminal session (optionally waiting briefly first).\n- **terminal_close**(session_id) [capability_expand category: terminal]: Terminate a persistent terminal session and release its process group.\n- **terminal_list**(): List active persistent terminal sessions.\n- **config_list**(category, limit): List LeapFlow's own writable settings (LLM model and endpoint, daemon, memory, perception, gateway, …) with current values. Use this to discover the exact key before changing anything. Optionally narrow by `category`. This is the only correct way to inspect LeapFlow configuration — never read config files from disk.\n- **config_get**(key): Read one LeapFlow setting by key (e.g. 'llm.model', 'llm.base_url', 'daemon.log_level'), returning its current value, type, scopes, and whether a change needs a daemon restart. There is no 'llm.provider' key: provider behavior is inferred from the OpenAI-compatible 'llm.base_url'. Never read LeapFlow config files from disk — use this.\n- **config_set**(key, value, scope) [capability_expand category: config]: Change one LeapFlow setting by key, e.g. switch an OpenAI-compatible LLM with key='llm.model' and key='llm.base_url'. There is no 'llm.provider' key: provider behavior is inferred from the endpoint. Values are validated and coerced; credentials are stored in the vault automatically. Call config_list or config_get first if unsure of the key. The result states whether a `leap daemon restart` is required. Never edit LeapFlow config files directly.\n- **web_fetch**(url, select, timeout, max_bytes): Read a URL over HTTP(S) and get back extracted, context-sized content: parsed JSON for API endpoints, readable text plus links for web pages. Use this for anything on the internet — prices, docs, releases, articles — instead of running curl through shell_run: it reports real HTTP status codes, retries rate limits on its own, and is a plain read so a retry is always safe. For JSON APIs pass `select` with a dotted path (e.g. 'chart.result.0.meta') to return just that part instead of the whole payload.\n- **memory_search**(query, limit): Search agent memory for relevant past experiences, observations, and facts.\n- **memory_add**(content, kind) [capability_expand category: write]: Store a new observation or insight in memory for future reference.\n- **research_note**(kind, text): Record a compact, structured note about the current task's state so it survives context compression on long / multi-step tasks. Use for durable findings, open questions still to resolve, decisions / excluded paths, and the immediate next step. One concise sentence per note.\n- **capability_expand**(category): Fetch the full callable schema for every tool in a capability category (e.g. 'hub', 'gateway', 'desktop', 'delegate', 'file', 'memory', 'skill'). The compact tool index always lists every registered tool by name and a one-line summary, but only a static low-risk subset is directly callable each turn. If you need a tool from the index that is not yet callable, call capability_expand with its category first; the matching tools become callable in this turn. Never invent a tool name — expand the category instead.\n- **delegate_task**(goal, context) [capability_expand category: delegate]: Delegate a complex sub-task to an isolated subagent. The subagent gets a fresh context and restricted tool access. Use when a task is self-contained and can be solved independently.\n- **schedule_reentry**(kind, reason, delay_seconds, event_match, max_reentries, deadline_seconds): Register a re-entry so this task can resume later from its current orientation (findings / open questions / next step). Use when work must pause and continue after a delay (kind=time) or when a matching platform event arrives (kind=event), instead of finishing now. The research-ledger state is carried over automatically.\n- **hub_push**(skill_name, visibility, version) [capability_expand category: hub]: Push a local skill to the ModelScope Hub for sharing or backup.\n- **hub_pull**(repo_id, version) [capability_expand category: hub]: Pull a skill from the ModelScope Hub to install locally.\n- **hub_search**(query) [capability_expand category: hub]: Search for skills on the Hub by keyword or description.\n- **hub_sync**(mode, dry_run) [capability_expand category: hub]: Preview or execute sync between local skills and Hub.\n- **platform_action**(platform, action, payload, backend_kind) [capability_expand category: gateway]: Execute an exact registered business action on an external platform through LeapFlow's App Connector layer. Actions must be copied from the App Connector Capability Index and are addressed as domain.operation, e.g. im.send_message or docs.create_markdown. All business fields (chat_id, text, query, etc.) MUST be placed inside `payload`, never at the top level. Example: {\"platform\":\"feishu\",\"action\":\"im.send_message\",\"payload\":{\"chat_id\":\"oc_xxx\",\"text\":\"hello\"}}. Do not invent action names, do not use management actions such as list/guide/connect/status here.\n- **platform_connect**(action, platform, credentials, options, checkpoint) [capability_expand category: gateway]: List, guide, connect, disconnect, remove, or check status for external platforms using the App Connector management namespace. Supports REST and CLI backends. Use this for management actions such as list/guide/preflight/connect/status; use platform_action only for exact registered business actions.\n- **gateway_send**(platform, chat_id, text, thread_id) [capability_expand category: gateway]: Send a message to a connected external platform (Feishu group, Telegram chat, DingTalk conversation, etc.). Requires the platform to be connected via gateway_connect first. Use gateway_connect with action='list' to see connected platforms and available chat IDs.\n- **gateway_connect**(action, platform, credentials, options) [capability_expand category: gateway]: Connect, configure, or manage external platform integrations (Feishu, DingTalk, Telegram, Slack, Discord, etc.). Conversational flow: 1) call 'guide' to get setup steps + required fields, 2) present the steps to the user and ask for ALL required credentials in a single message, 3) call 'connect' with the credentials. Goal: complete in 1–2 user turns. NEVER include credential values in your text response.\n- **plugin_list**(): List the live plugin registry and cross-subsystem capability evidence. Use this before answering questions about whether LeapFlow supports plugins, self-evolution, plugin installation, hot reload, versioning, or other runtime capabilities.\n- **plugin_status**(plugin_id): Get detailed status of a specific plugin: its declared category, runtime dependencies, contributed tools, and fiber lifecycle state.\n- **plugin_versions**(plugin_id): List recorded source versions and active pointer for a profile-scoped plugin.\n- **plugin_propose**(requested_capability, plugin_id, proposed_tools, test_cases, risk_level, evidence): Create a side-effect-free PluginProposal from explicit capability-gap evidence. Use this before plugin_generate when a missing capability should be reviewed. Does not call an LLM, write files, or install anything.\n- **assess_compatibility**(manifest, source_path) [capability_expand category: plugin_management]: Assess whether a foreign plugin manifest or real DSH source bundle is compatible with LeapFlow. A source bundle assessment is static: runtime_ready stays false until restricted Node discovery during plugin_install. Returns component-level verdicts and limitations.\n- **plugin_generate**(plugin_id, description, proposal_id) [capability_expand category: system]: Generate a new ToolPlugin from a natural-language capability description. The LLM produces code that conforms to the ToolPlugin Protocol; it is then rigorously validated (syntax, structure, import, protocol conformance). The generated source is stored in CAS and receives explicit content approval. It DOES NOT install the plugin — installation requires a second, mutation-specific approval via plugin_install.\n- **plugin_install**(plugin_id, code, marketplace_name, source_path, proposal_id, version_label) [capability_expand category: system]: Install a plugin from exactly one source: validated Python code, the configured Python marketplace, or a real DSH source bundle. DSH bundles run restricted Node discovery before registration; only public host tools are installed and client UI limitations are reported. Writes profile-scoped state and mutates the process-global registry. REQUIRES APPROVAL.\n- **plugin_rollback**(plugin_id, version) [capability_expand category: system]: Rollback a profile-scoped plugin to a recorded source version and reload it. REQUIRES APPROVAL.\n- **plugin_reload**(plugin_id, version_label) [capability_expand category: system]: Hot-reload a plugin at runtime. Disposes the old plugin fiber, re-imports its module, and registers a fresh instance. Existing in-flight turns are unaffected (snapshot isolation). REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_disable**(plugin_id) [capability_expand category: system]: Disable a plugin by disposing its fiber, removing its tools from the runtime registry. Cannot disable self_management itself. REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_remove**(plugin_id, delete_source) [capability_expand category: system]: Terminally remove a plugin: dispose its fiber, unregister its tools, remove reload metadata, and optionally delete its profile-scoped source file. Cannot remove self_management itself. REQUIRES APPROVAL.\n- **plugin_unquarantine**(plugin_id) [capability_expand category: plugin_management]: Restore a quarantined plugin to probation status for re-evaluation. Unfreezes the trust ledger, transitions the proposal back to PROBATION, and reloads the plugin. REQUIRES APPROVAL.\n- **plugin_enable**(plugin_id) [capability_expand category: system]: Re-enable a previously disabled plugin by reloading its module and registering a fresh instance. REQUIRES APPROVAL.\n- **self_describe**(facet): Introspect LeapFlow's own identity, capabilities, runtime state, evolution metrics, or platform connections. Use facet='all' only when a comprehensive self-check is explicitly requested.\n- **runtime_snapshot**(): Lightweight (~150 token) flat snapshot of current runtime state: model, context budget, posture, disclosure level, turn count, cache hit rate, uptime, tool count, and pending approvals.\n- **schedule_create**(trigger_expression, instruction, execution_mode, max_retries, delivery_target) [capability_expand category: scheduler]: Create a new scheduled task. Specify a trigger expression (e.g. '30m', 'every 2h', '0 9 * * *') and the instruction to execute on each trigger. Optionally provide an execution mode and a delivery target to receive notifications.\n- **schedule_list**() [capability_expand category: scheduler]: List all scheduled tasks with their current state, trigger, and next due time.\n- **schedule_status**(task_id) [capability_expand category: scheduler]: Get detailed status and recent execution history for a scheduled task.\n- **schedule_pause**(task_id) [capability_expand category: scheduler]: Pause a scheduled task so it stops firing without being cancelled.\n- **schedule_resume**(task_id) [capability_expand category: scheduler]: Resume a paused scheduled task — re-arms it and recalculates next due time.\n- **schedule_cancel**(task_id) [capability_expand category: scheduler]: Cancel a scheduled task permanently.\n- **tool_search**(query, max_results): Search all registered tools by keyword relevance. Returns a ranked list of matching tools with name, category, and summary. Use this when you need a tool but are unsure of its exact name or category.\n- **tool_describe**(tool_name): Get the full callable schema of a single tool by exact name. Returns the tool's description, parameters, and metadata. Use after tool_search to inspect a specific tool before calling it.\n- **fixture_echo**(text): Return the supplied text from a pre-built DSH package.\n- **session_search**(query, limit) [capability_expand category: unclassified]: Search past conversation sessions in the current workspace for relevant context.\n- **session_list**(limit): List recent conversation sessions in the current workspace with stable ids, titles, dates, and summaries. Use for browsing past tasks or when user asks to see history without specific search terms. No keywords needed — returns chronological list.\n- **session_detail**(session_id, limit, offset, include_inactive) [capability_expand category: unclassified]: Read a paginated persisted transcript for one past conversation session in the current workspace. Use after session_list or session_search returns a session_id.\n- **click**(element_index) [capability_expand category: desktop]: Click a UI element by its element_index (from the latest observe_ui snapshot)\n- **get_clipboard**() [capability_expand category: desktop]: Read current clipboard text content\n- **list_apps**(filter, running_only) [capability_expand category: desktop]: List available applications on this system. Use to discover correct bundle_id before switch_app.\n- **list_windows**() [capability_expand category: desktop]: List all top-level windows with pid, window_id, title, and per-window state (minimized, on-screen). Call this first to pick the pid and window_id that observe_ui and other window tools require.\n- **observe_ui**(pid, window_id, query) [capability_expand category: desktop]: Snapshot one window's actionable UI elements, each tagged with an element_index for click/right_click/read_text. Re-observe after actions — indices belong to one snapshot. Requires the window's pid and window_id from list_windows.\n- **open_url**(url, app_id) [capability_expand category: desktop]: Open a URL in the default or specified browser\n- **read_text**(element_index) [capability_expand category: desktop]: Read the text content of a specific UI element from the latest snapshot\n- **right_click**(element_index) [capability_expand category: desktop]: Right-click a UI element to open its context menu. Returns visible menu items.\n- **screenshot**(pid, window_id) [capability_expand category: desktop]: Capture a screenshot for visual verification. With pid + window_id captures that window (works across all displays); defaults to the last observed window, or the full desktop when no window has been observed.\n- **scroll**(element_index, direction, amount, pid, window_id) [capability_expand category: desktop]: Scroll a scrollable area of a window. Omit element_index to scroll the window's focused/page scroller; pass one to scroll an exact element from the latest snapshot.\n- **select_text**(element_index) [capability_expand category: desktop]: Select all text in a UI element (focus + select-all, for subsequent copy)\n- **set_clipboard**(text) [capability_expand category: desktop]: Write text to the clipboard\n- **shortcut**(keys) [capability_expand category: desktop]: Execute a keyboard shortcut\n- **switch_app**(app_id) [capability_expand category: desktop]: Switch to an app (launch if needed, activate, verify)\n- **type_text**(text) [capability_expand category: desktop]: Type text into the currently focused element\n- **wait**(seconds) [capability_expand category: desktop]: Wait for a specified duration before continuing\n- **wait_until**(condition, pid, window_id, timeout, poll_interval) [capability_expand category: desktop]: Wait until a UI condition is met (polls UI tree). Returns elements when found or on timeout.\n- **wait_until_stable**(timeout, poll_interval, pid, window_id) [capability_expand category: desktop]: Wait until the UI stops changing (element set stabilizes across polls).\n\n## App Connector Capability Index\nLeapFlow can onboard and manage external apps through `platform_connect` and execute exact registered business actions through `platform_action`.\nFor requests about connecting, setting up, configuring, enabling, or managing a supported app, use `platform_connect` first instead of generating SDK/Webhook sample code.\n`platform_connect.action` is the management namespace: list, guide, preflight, connect, disconnect, remove, status, events_start, events_stop, events_status.\n`platform_action.action` is only for exact registered platform business actions listed below, such as `im.send_message`; never use management actions like `list` or `guide` there.\nAll business fields MUST go inside `payload`; top-level keys are only `platform`, `action`, and `payload`.\nDo not invent platform IDs or platform action names. If the needed action is not listed, ask for discovery/clarification instead of guessing.\nUse `platform_connect` with `action='guide'` and the matching `platform` to start onboarding; use `action='list'` when the app is unclear.\nWhen a pending onboarding state is present, continue from that state with `platform_connect` instead of asking the user to restate the app.\nSupported apps:\n- `api_server`: API Server (OpenAI Compatible) (category=api; backend=adapter; platform_action actions=none registered)\n- `dingtalk`: 钉钉 (DingTalk) (category=im; backend=adapter; platform_action actions=none registered)\n- `feishu`: 飞书 (Feishu/Lark) (category=im; backend=cli)\n - `calendar.create_event` payload={summary*, start_time*, end_time*, attendees} [write/high]\n - `docs.create_markdown` payload={title*, markdown*, folder_token} [write/medium]\n - `drive.search` payload={query*, limit} [read/medium]\n - `im.add_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.download_resource` payload={message_id*, file_key*, type*} [read/low]\n - `im.get_messages` payload={message_ids*} [read/low]\n - `im.list_chats` payload={page_size} [read/low]\n - `im.list_messages` payload={chat_id*} [read/low]\n - `im.list_thread_messages` payload={thread*} [read/low]\n - `im.remove_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.reply_message` payload={message_id*, text*} [send/high]\n - `im.search_chats` payload={query*, page_size} [read/low]\n - `im.search_messages` payload={query*} [read/medium]\n - `im.send_message` payload={chat_id*, text*, thread_id} [send/high]\n - `im.update_card` payload={token*, card*} [write/medium]\n - `im.update_message` payload={message_id*, text*} [write/medium]\n - `mail.search_unread` payload={query, limit} [read/high]\n - `sheets.append_row` payload={spreadsheet_token*, sheet_id*, values*} [write/medium]\n - `task.create` payload={title*, description, due_time} [write/medium]\n- `telegram`: Telegram (category=social; backend=adapter; platform_action actions=none registered)\n- `webhook`: Webhook (Generic) (category=webhook; backend=adapter; platform_action actions=none registered)\n\n## Tool Usage\nTools are normally invoked through the native function-calling mechanism, not by writing JSON in your reply\ntext. Only if the provider signals that native function calling is unavailable for this turn, fall back to a\nsingle JSON code block: `{\"name\": \"tool_name\", \"arguments\": {\"key\": \"value\"}}` — use this fallback format\nonly, never both. Only call a tool whose exact name appears in the tool index above and is currently callable\n(or reachable via `capability_expand`). Never invent, rename, alias, or guess a tool name, platform ID, or\nplatform action from argument shape or wording — if the index does not list it, it does not exist.\n`platform_connect.action` (list/guide/preflight/connect/disconnect/remove/status/events_start/events_stop/\nevents_status) is the App Connector management namespace; `platform_action.action` only accepts exact\nregistered business actions such as `im.send_message` shown in the App Connector Capability Index — never mix\nthe two namespaces. If a tool call returns an unknown/unavailable result, use the returned suggestions or\navailable names for a single retry instead of trying further variations of the same guess.\n\n**Side-effect action rule** (`platform_action` with effect=send/write/execute):\n- Call each unique action+payload **exactly once**. Never include duplicates in the same turn.\n- Once the result returns `\"completed\": true`, that action is DONE for this task. Do NOT call it again\n in any subsequent turn — immediately summarize the result for the user instead.\n- The system enforces idempotency: duplicate calls are blocked and will not execute.\n- If the user explicitly requests sending/writing multiple times, use distinct payloads per call.\n\n**Resource identifier provenance rule**:\n- NEVER fabricate, guess, or infer resource identifiers (chat_id, message_id, file_key, user_id, etc.).\n Every resource ID used in a side-effect action MUST come from a successful API response in this session.\n- If a read/list action fails (e.g. authorization error), you do NOT have valid resource IDs.\n Report the failure to the user — do NOT attempt the dependent write/send action with a guessed ID.\n- When a tool result contains `\"llm_instruction\"`, follow it exactly.\n\n## Guidelines\n1. **Direct answers first**: If you already know the answer, respond directly without tools.\n2. **Avoid redundant tool calls**: Do not call the same tool with the same arguments more than once in the same user turn. When an existing tool result already answers the user's request, stop calling tools and answer directly.\n3. **Use tools proactively**: When the user asks about files, time, system state, or needs actions performed, use the appropriate tool.\n4. **Chain tools when needed**: You can call multiple tools in sequence (e.g., list files → read file → summarize).\n5. **Handle failures gracefully**: If a tool fails, explain what went wrong and suggest alternatives. If it failed because of an unknown tool/platform/action name, retry once with an exact name from the returned suggestions, then explain rather than keep guessing.\n6. **Summarize results naturally**: After tool execution, synthesize the results into a helpful answer rather than dumping raw output.\n7. **Stay conversational**: Maintain a natural, helpful tone. Acknowledge context from earlier in the conversation.\n8. **Recall past work**:\n - Broad queries (\"之前做了什么\", \"列出任务\"): answer from the \"Recent Task History\" section already in your context. If insufficient, call session_list.\n - Specific lookups (\"上次那个配置怎么改的\"): call session_search with relevant phrases (NOT single characters).\n - Do NOT call search tools repeatedly with keyword fragments. One well-phrased call is better than ten fragmented ones.\n\n## Coding & Verification\nWhen working with code, prefer the precise built-in tools over ad-hoc shell: use `repo_map` to orient in an\nunfamiliar project, `code_search`/`file_find` to locate and `code_intel` for symbols, `edit_file` (anchored\nsearch-replace, or a unified `diff`) to change files — never rewrite a whole file to change a few lines — and\n`git_query` to inspect diffs/log. After edits, check `syntax_ok` in the result and run `test_run`/`lint_check`\nbefore declaring the work done. Batch independent read-only calls (search/read/query) into a single turn — they\nrun in parallel.\n\n## Presentation Style\n1. **Polished Markdown only**: Format user-facing answers with clean Markdown headings, short paragraphs, and concise bullets. Use tables only when they improve comparison or scanning.\n2. **Terminal-friendly layout**: Keep lines readable in a TUI; avoid dense walls of text, deeply nested lists, oversized ASCII art, or heavy visual blocks.\n3. **Elegant emphasis**: Use bold text sparingly for key terms and conclusions. Avoid excessive emojis, decorative symbols, repeated separators, or visual noise.\n4. **Theme-safe colors**: Do not emit ANSI escape codes, HTML color tags, Rich markup, or hardcoded color names. Rely on the TUI theme to render Markdown professionally.\n5. **No leaked tool protocol**: Never show tool-call JSON, internal schemas, raw observations, tool result payloads, or hidden reasoning in the final answer unless the user explicitly asks for raw/debug output. Treat any prior `{\"name\": ..., \"arguments\": ...}` blocks and `Tool result (...)` messages as internal execution context only.\n6. **Professional closure**: End with a concise conclusion or next step when helpful; avoid rambling after the useful answer is complete.\n\nWhen finished with all tool calls, respond normally without a JSON block, tool-call transcript, or process log.\n\n## Task Contract\n- Task ID: turn-2\n- Original user request: Remove the hermetic DSH echo plugin completely.\n- Workspace root: \n- Allowed roots: \n- Treat relative project paths as relative to the workspace root; never infer `.` as the project root when a workspace root is provided.\n- Workspace boundary is enforced by tools: do not read, search, edit, or run commands against paths outside the allowed roots unless the user explicitly requests an external path and the tool/approval policy permits it.\n- LeapFlow workspace config is optional at `/.leapflow/config.yaml`; runtime config is loaded from `~/.leapflow/config/user.yaml` and `~/.leapflow/profiles//config/*.yaml`.\n- Preserve this task contract across summarization, compression, tool loops, and memory retrieval.\n" - }, - { - "role": "system", - "content": "\n## Recent Session Summary\n- [assistant] The DSH tool ran after restart.\n- [user] Remove the hermetic DSH echo plugin completely.\n" - }, - { - "role": "assistant", - "content": "The DSH tool ran after restart." - }, - { - "role": "user", - "content": "Remove the hermetic DSH echo plugin completely.\nRemove the hermetic DSH echo plugin completely." - } - ], - "tools": [ - "capability_expand", - "code_intel", - "code_search", - "config_get", - "config_list", - "env_info", - "file_find", - "file_list", - "file_read", - "fixture_echo", - "git_query", - "lint_check", - "memory_search", - "plugin_list", - "plugin_propose", - "plugin_status", - "plugin_versions", - "repo_map", - "research_note", - "runtime_snapshot", - "schedule_reentry", - "self_describe", - "session_list", - "skill_view", - "skills_list", - "terminal_list", - "terminal_read", - "test_run", - "text_search", - "time_get", - "tool_describe", - "tool_search", - "web_fetch" - ] - }, - "responses": [ - { - "status": 200, - "content_type": "application/json", - "body": "{\"id\": \"chatcmpl-cassette\", \"object\": \"chat.completion\", \"model\": \"cassette-model\", \"choices\": [{\"index\": 0, \"message\": {\"role\": \"assistant\", \"content\": \"\", \"tool_calls\": [{\"id\": \"call_1\", \"type\": \"function\", \"function\": {\"name\": \"plugin_remove\", \"arguments\": \"{\\\"plugin_id\\\": \\\"r6_dsh_echo\\\", \\\"delete_source\\\": true}\"}}]}, \"finish_reason\": \"tool_calls\"}], \"usage\": {\"prompt_tokens\": 64, \"completion_tokens\": 16, \"total_tokens\": 80}}" - } - ] -} diff --git a/tests/_fixtures/cassettes/r6_lifecycle/cassette-model-41234a1a103ae958.cassette.json b/tests/_fixtures/cassettes/r6_lifecycle/cassette-model-41234a1a103ae958.cassette.json new file mode 100644 index 00000000..c8c1f0a0 --- /dev/null +++ b/tests/_fixtures/cassettes/r6_lifecycle/cassette-model-41234a1a103ae958.cassette.json @@ -0,0 +1,63 @@ +{ + "fingerprint": "41234a1a103ae958c8c413d27a89823f3ecac85253891d518846ac5088714538", + "note": "captured in seed mode", + "request": { + "model": "cassette-model", + "stream": false, + "messages": [ + { + "role": "system", + "content": "You are LeapFlow, an intelligent assistant that can both converse naturally and take real actions on the user's computer.\n\n## Capabilities\nThe tool index below lists **every** registered tool by name and a one-line summary — this is the complete\ncapability contract; nothing else exists. Only a subset is directly callable this turn (via native tool calling,\nnot a JSON block in your reply). If you need a tool from the index that is not yet callable, call\n`capability_expand` with its category name first — the matching tools become callable immediately after.\nWhen the user asks about LeapFlow **itself** — its identity/version/model, what tools, plugins, or capabilities\nit has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of\nself-awareness — answer from live runtime evidence via `self_describe` (facet=capabilities for \"what can you\ndo / do you support X\", identity for version/model, runtime for context/posture, or `all` for a full self-check),\nnever by reading LeapFlow's own source code, documentation, or memory. Report configuration-dependent or\nunavailable capabilities as limitations rather than inferring them.\n- **text_search**(text, pattern): Search for a regex pattern in text.\n- **text_replace**(text, old, new, count) [capability_expand category: write]: Replace occurrences of a substring in text.\n- **time_get**(): Get current date and time.\n- **env_info**(): Get system environment information (OS, Python version, cwd).\n- **skills_list**(query, category, source): List available learned skills. Use when user asks about capabilities or you need a specific skill.\n- **skill_view**(name): View the full content of a specific skill document.\n- **skill_list_files**(name) [capability_expand category: skills]: List all support files in a skill directory (configs, templates, examples). Use before skill_file_read to discover available files.\n- **skill_file_read**(name, file_path) [capability_expand category: skills]: Read an individual support file from a skill directory. Use skill_list_files first to discover available files.\n- **code_intel**(path, operation): Precise document symbols (outline) for a source file: classes, functions, and methods with line ranges. Python uses an exact AST parse; other languages use a keyword-prefix scan. Prefer over file_read mode=symbols for accurate navigation before editing. Read-only.\n- **repo_map**(path): Compact project orientation for a repository root: languages, detected test/lint commands, top-level structure, entry points, manifest, and VCS branch. Call this first when entering an unfamiliar codebase. Read-only.\n- **scm_sync**(action, cwd, remote, pull_ref, push_ref, timeout) [capability_expand category: scm]: Run a typed git SCM action. Use this instead of shell_run for git pull/push/status. For 'pull origin main then push', set action='pull_then_push', remote='origin', pull_ref='main', and omit push_ref so LeapFlow pushes the current local branch.\n- **git_query**(action, cwd, ref, path, staged, max_count, stat): Read-only structured git inspection: action=diff|log|status|branch|show. Prefer over shell_run for reading repo state — output is clipped, redacted, and log/branch are parsed into structured fields. Use scm_sync for pull/push.\n- **git_write**(action, cwd, message, stage_all, name, ref, create) [capability_expand category: scm]: Mutating git actions: action=commit (message, stage_all), branch (create+switch), checkout (switch; create=true for -b). Approval-gated. Use scm_sync for pull/push and git_query for reads.\n- **test_run**(command, cwd, timeout): Run the project's test suite and return structured results (framework, passed/failed counts, failing tests). Auto-detects the runner (pytest/npm/go/cargo) or uses a configured/explicit command; executes via the governed shell. ok=true means the runner executed — see 'success' for pass/fail.\n- **lint_check**(command, cwd, timeout): Run the project's linter and return a structured clean/issue result. Auto-detects the linter (ruff/eslint/go vet/clippy) or uses a configured/explicit command; executes via the governed shell. ok=true means the linter ran — see 'clean'.\n- **file_list**(path, pattern, depth): List files and directories at a given path. Use depth=1 or depth=2 to get a recursive tree in one call instead of listing each sub-directory separately.\n- **file_read**(path, max_lines, start_line, max_chars, mode): Read text file content with adaptive context governance. For large or unfamiliar files, prefer mode='outline' or mode='symbols' first, then use mode='raw' with start_line/max_lines for the specific range you actually need. For LeapFlow's own settings, use config_list / config_get / config_set — its config files are outside the workspace and not readable here.\n- **file_write**(path, content, mode) [capability_expand category: write]: Write content to a file (overwrite or append).\n- **code_search**(pattern, patterns, path, glob, ignore_case, multiline, max_results, context_lines): Search file CONTENTS by regex pattern across a directory tree (ripgrep-backed). Requires a regex pattern. NOT for listing or browsing directory contents — use file_list for that. Prefer this over shell_run grep: faster, skips VCS/dependency/build dirs, and returns structured path:line:column matches. Batch related lookups into ONE call via `patterns` (OR-combined, single pass) instead of issuing several separate searches. Use file_read for the surrounding context of a hit.\n- **file_find**(glob, path, max_results): Find files by a recursive glob pattern under a base path (e.g. '**/test_*.py' or '*.md'). Prefer this over shell_run find; skips VCS/dependency/build dirs.\n- **edit_file**(path, edits, dry_run, diff) [capability_expand category: write]: Apply targeted, anchored search-replace edits to an EXISTING text file (use file_write to create/overwrite). Each edit is {original_text, new_text, replace_all?}; original_text must match exactly and uniquely (or set replace_all) — a non-unique or missing anchor is rejected so files are never corrupted. Set dry_run to preview. Alternatively pass a unified 'diff' to apply its hunks as anchored edits. Far cheaper and safer than rewriting a whole file.\n- **shell_run**(command, cwd, timeout) [capability_expand category: shell]: Execute a one-shot shell command with timeout protection. Runs in the active workspace; paths resolving outside it are refused. Reach for a structured tool first when one fits — web_fetch for anything over HTTP(S), git_query/scm_sync for git, code_search/file_find/file_read for the repo, config_get/config_set for LeapFlow's own settings — because those report typed results, while a failed shell command can only be diagnosed from its exit code and stderr. Every shell run counts as an external side effect, so a failure stops the rest of the batch and is not retried automatically.\n- **terminal_open**(command, cwd, shell) [capability_expand category: terminal]: Open a PERSISTENT shell session (REPL/dev server/watch), returning a session_id for terminal_send/read/close. Disabled unless tools.terminal_session_enabled is set. For one-shot commands use shell_run instead.\n- **terminal_send**(session_id, input, wait) [capability_expand category: terminal]: Send a line of input to a persistent terminal session and return output captured shortly after.\n- **terminal_read**(session_id, wait): Drain buffered output from a persistent terminal session (optionally waiting briefly first).\n- **terminal_close**(session_id) [capability_expand category: terminal]: Terminate a persistent terminal session and release its process group.\n- **terminal_list**(): List active persistent terminal sessions.\n- **config_list**(category, limit): List LeapFlow's own writable settings (LLM model and endpoint, daemon, memory, perception, gateway, …) with current values. Use this to discover the exact key before changing anything. Optionally narrow by `category`. This is the only correct way to inspect LeapFlow configuration — never read config files from disk.\n- **config_get**(key): Read one LeapFlow setting by key (e.g. 'llm.model', 'llm.base_url', 'daemon.log_level'), returning its current value, type, scopes, and whether a change needs a daemon restart. There is no 'llm.provider' key: provider behavior is inferred from the OpenAI-compatible 'llm.base_url'. Never read LeapFlow config files from disk — use this.\n- **config_set**(key, value, scope) [capability_expand category: config]: Change one LeapFlow setting by key, e.g. switch an OpenAI-compatible LLM with key='llm.model' and key='llm.base_url'. There is no 'llm.provider' key: provider behavior is inferred from the endpoint. Values are validated and coerced; credentials are stored in the vault automatically. Call config_list or config_get first if unsure of the key. The result states whether a `leap daemon restart` is required. Never edit LeapFlow config files directly.\n- **web_fetch**(url, select, timeout, max_bytes): Read a URL over HTTP(S) and get back extracted, context-sized content: parsed JSON for API endpoints, readable text plus links for web pages. Use this for anything on the internet — prices, docs, releases, articles — instead of running curl through shell_run: it reports real HTTP status codes, retries rate limits on its own, and is a plain read so a retry is always safe. For JSON APIs pass `select` with a dotted path (e.g. 'chart.result.0.meta') to return just that part instead of the whole payload.\n- **memory_search**(query, limit): Search agent memory for relevant past experiences, observations, and facts.\n- **memory_add**(content, kind) [capability_expand category: write]: Store a new observation or insight in memory for future reference.\n- **research_note**(kind, text): Record a compact, structured note about the current task's state so it survives context compression on long / multi-step tasks. Use for durable findings, open questions still to resolve, decisions / excluded paths, and the immediate next step. One concise sentence per note.\n- **capability_expand**(category): Fetch the full callable schema for every tool in a capability category (e.g. 'hub', 'gateway', 'desktop', 'delegate', 'file', 'memory', 'skill'). The compact tool index always lists every registered tool by name and a one-line summary, but only a static low-risk subset is directly callable each turn. If you need a tool from the index that is not yet callable, call capability_expand with its category first; the matching tools become callable in this turn. Never invent a tool name — expand the category instead.\n- **delegate_task**(goal, context) [capability_expand category: delegate]: Delegate a complex sub-task to an isolated subagent. The subagent gets a fresh context and restricted tool access. Use when a task is self-contained and can be solved independently.\n- **schedule_reentry**(kind, reason, delay_seconds, event_match, max_reentries, deadline_seconds): Register a re-entry so this task can resume later from its current orientation (findings / open questions / next step). Use when work must pause and continue after a delay (kind=time) or when a matching platform event arrives (kind=event), instead of finishing now. The research-ledger state is carried over automatically.\n- **hub_push**(skill_name, visibility, version) [capability_expand category: hub]: Push a local skill to the ModelScope Hub for sharing or backup.\n- **hub_pull**(repo_id, version) [capability_expand category: hub]: Pull a skill from the ModelScope Hub to install locally.\n- **hub_search**(query) [capability_expand category: hub]: Search for skills on the Hub by keyword or description.\n- **hub_sync**(mode, dry_run) [capability_expand category: hub]: Preview or execute sync between local skills and Hub.\n- **platform_action**(platform, action, payload, backend_kind) [capability_expand category: gateway]: Execute an exact registered business action on an external platform through LeapFlow's App Connector layer. Actions must be copied from the App Connector Capability Index and are addressed as domain.operation, e.g. im.send_message or docs.create_markdown. All business fields (chat_id, text, query, etc.) MUST be placed inside `payload`, never at the top level. Example: {\"platform\":\"feishu\",\"action\":\"im.send_message\",\"payload\":{\"chat_id\":\"oc_xxx\",\"text\":\"hello\"}}. Do not invent action names, do not use management actions such as list/guide/connect/status here.\n- **platform_connect**(action, platform, credentials, options, checkpoint) [capability_expand category: gateway]: List, guide, connect, disconnect, remove, or check status for external platforms using the App Connector management namespace. Supports REST and CLI backends. Use this for management actions such as list/guide/preflight/connect/status; use platform_action only for exact registered business actions.\n- **gateway_send**(platform, chat_id, text, thread_id) [capability_expand category: gateway]: Send a message to a connected external platform (Feishu group, Telegram chat, DingTalk conversation, etc.). Requires the platform to be connected via gateway_connect first. Use gateway_connect with action='list' to see connected platforms and available chat IDs.\n- **gateway_connect**(action, platform, credentials, options) [capability_expand category: gateway]: Connect, configure, or manage external platform integrations (Feishu, DingTalk, Telegram, Slack, Discord, etc.). Conversational flow: 1) call 'guide' to get setup steps + required fields, 2) present the steps to the user and ask for ALL required credentials in a single message, 3) call 'connect' with the credentials. Goal: complete in 1–2 user turns. NEVER include credential values in your text response.\n- **plugin_list**(): List the live plugin registry across Tool/Gateway/LLM subsystems for plugin management: which plugins are loaded, their category and fiber lifecycle state. For questions about LeapFlow's own capabilities or whether it supports X, use self_describe instead.\n- **plugin_status**(plugin_id): Get detailed status of a specific plugin: its declared category, runtime dependencies, contributed tools, and fiber lifecycle state.\n- **plugin_versions**(plugin_id): List recorded source versions and active pointer for a profile-scoped plugin.\n- **plugin_propose**(requested_capability, plugin_id, proposed_tools, test_cases, risk_level, evidence): Create a side-effect-free PluginProposal from explicit capability-gap evidence. Use this before plugin_generate when a missing capability should be reviewed. Does not call an LLM, write files, or install anything.\n- **assess_compatibility**(manifest, source_path) [capability_expand category: plugin_management]: Assess whether a foreign plugin manifest or real DSH source bundle is compatible with LeapFlow. A source bundle assessment is static: runtime_ready stays false until restricted Node discovery during plugin_install. Returns component-level verdicts and limitations.\n- **plugin_generate**(plugin_id, description, proposal_id) [capability_expand category: system]: Generate a new ToolPlugin from a natural-language capability description. The LLM produces code that conforms to the ToolPlugin Protocol; it is then rigorously validated (syntax, structure, import, protocol conformance). The generated source is stored in CAS and receives explicit content approval. It DOES NOT install the plugin — installation requires a second, mutation-specific approval via plugin_install.\n- **plugin_install**(plugin_id, code, marketplace_name, source_path, proposal_id, version_label) [capability_expand category: system]: Install a plugin from exactly one source: validated Python code, the configured Python marketplace, or a real DSH source bundle. DSH bundles run restricted Node discovery before registration; only public host tools are installed and client UI limitations are reported. Writes profile-scoped state and mutates the process-global registry. REQUIRES APPROVAL.\n- **plugin_rollback**(plugin_id, version) [capability_expand category: system]: Rollback a profile-scoped plugin to a recorded source version and reload it. REQUIRES APPROVAL.\n- **plugin_reload**(plugin_id, version_label) [capability_expand category: system]: Hot-reload a plugin at runtime. Disposes the old plugin fiber, re-imports its module, and registers a fresh instance. Existing in-flight turns are unaffected (snapshot isolation). REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_disable**(plugin_id) [capability_expand category: system]: Disable a plugin by disposing its fiber, removing its tools from the runtime registry. Cannot disable self_management itself. REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_remove**(plugin_id, delete_source) [capability_expand category: system]: Terminally remove a plugin: dispose its fiber, unregister its tools, remove reload metadata, and optionally delete its profile-scoped source file. Cannot remove self_management itself. REQUIRES APPROVAL.\n- **plugin_unquarantine**(plugin_id) [capability_expand category: plugin_management]: Restore a quarantined plugin to probation status for re-evaluation. Unfreezes the trust ledger, transitions the proposal back to PROBATION, and reloads the plugin. REQUIRES APPROVAL.\n- **plugin_enable**(plugin_id) [capability_expand category: system]: Re-enable a previously disabled plugin by reloading its module and registering a fresh instance. REQUIRES APPROVAL.\n- **self_describe**(facet): Answer questions about LeapFlow ITSELF from live runtime evidence — its identity/version/model, what tools and plugins it has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of self-awareness. Prefer this over reading LeapFlow's own source code. Pick facet=capabilities for 'what can you do / do you support X', identity for version/model, runtime for context/posture, or all for a full self-check when explicitly requested.\n- **runtime_snapshot**(): Lightweight (~150 token) flat snapshot of current runtime state: model, context budget, posture, disclosure level, turn count, cache hit rate, uptime, tool count, and pending approvals.\n- **schedule_create**(trigger_expression, instruction, execution_mode, max_retries, delivery_target) [capability_expand category: scheduler]: Create a new scheduled task. Specify a trigger expression (e.g. '30m', 'every 2h', '0 9 * * *') and the instruction to execute on each trigger. Optionally provide an execution mode and a delivery target to receive notifications.\n- **schedule_list**() [capability_expand category: scheduler]: List all scheduled tasks with their current state, trigger, and next due time.\n- **schedule_status**(task_id) [capability_expand category: scheduler]: Get detailed status and recent execution history for a scheduled task.\n- **schedule_pause**(task_id) [capability_expand category: scheduler]: Pause a scheduled task so it stops firing without being cancelled.\n- **schedule_resume**(task_id) [capability_expand category: scheduler]: Resume a paused scheduled task — re-arms it and recalculates next due time.\n- **schedule_cancel**(task_id) [capability_expand category: scheduler]: Cancel a scheduled task permanently.\n- **tool_search**(query, max_results): Search all registered tools by keyword relevance. Returns a ranked list of matching tools with name, category, and summary. Use this when you need a tool but are unsure of its exact name or category.\n- **tool_describe**(tool_name): Get the full callable schema of a single tool by exact name. Returns the tool's description, parameters, and metadata. Use after tool_search to inspect a specific tool before calling it.\n- **session_search**(query, limit) [capability_expand category: unclassified]: Search past conversation sessions in the current workspace for relevant context.\n- **session_list**(limit): List recent conversation sessions in the current workspace with stable ids, titles, dates, and summaries. Use for browsing past tasks or when user asks to see history without specific search terms. No keywords needed — returns chronological list.\n- **session_detail**(session_id, limit, offset, include_inactive) [capability_expand category: unclassified]: Read a paginated persisted transcript for one past conversation session in the current workspace. Use after session_list or session_search returns a session_id.\n- **click**(element_index) [capability_expand category: desktop]: Click a UI element by its element_index (from the latest observe_ui snapshot)\n- **get_clipboard**() [capability_expand category: desktop]: Read current clipboard text content\n- **list_apps**(filter, running_only) [capability_expand category: desktop]: List available applications on this system. Use to discover correct bundle_id before switch_app.\n- **list_windows**() [capability_expand category: desktop]: List all top-level windows with pid, window_id, title, and per-window state (minimized, on-screen). Call this first to pick the pid and window_id that observe_ui and other window tools require.\n- **observe_ui**(pid, window_id, query) [capability_expand category: desktop]: Snapshot one window's actionable UI elements, each tagged with an element_index for click/right_click/read_text. Re-observe after actions — indices belong to one snapshot. Requires the window's pid and window_id from list_windows.\n- **open_url**(url, app_id) [capability_expand category: desktop]: Open a URL in the default or specified browser\n- **read_text**(element_index) [capability_expand category: desktop]: Read the text content of a specific UI element from the latest snapshot\n- **right_click**(element_index) [capability_expand category: desktop]: Right-click a UI element to open its context menu. Returns visible menu items.\n- **screenshot**(pid, window_id) [capability_expand category: desktop]: Capture a screenshot for visual verification. With pid + window_id captures that window (works across all displays); defaults to the last observed window, or the full desktop when no window has been observed.\n- **scroll**(element_index, direction, amount, pid, window_id) [capability_expand category: desktop]: Scroll a scrollable area of a window. Omit element_index to scroll the window's focused/page scroller; pass one to scroll an exact element from the latest snapshot.\n- **select_text**(element_index) [capability_expand category: desktop]: Select all text in a UI element (focus + select-all, for subsequent copy)\n- **set_clipboard**(text) [capability_expand category: desktop]: Write text to the clipboard\n- **shortcut**(keys) [capability_expand category: desktop]: Execute a keyboard shortcut\n- **switch_app**(app_id) [capability_expand category: desktop]: Switch to an app (launch if needed, activate, verify)\n- **type_text**(text) [capability_expand category: desktop]: Type text into the currently focused element\n- **wait**(seconds) [capability_expand category: desktop]: Wait for a specified duration before continuing\n- **wait_until**(condition, pid, window_id, timeout, poll_interval) [capability_expand category: desktop]: Wait until a UI condition is met (polls UI tree). Returns elements when found or on timeout.\n- **wait_until_stable**(timeout, poll_interval, pid, window_id) [capability_expand category: desktop]: Wait until the UI stops changing (element set stabilizes across polls).\n\n## App Connector Capability Index\nLeapFlow can onboard and manage external apps through `platform_connect` and execute exact registered business actions through `platform_action`.\nFor requests about connecting, setting up, configuring, enabling, or managing a supported app, use `platform_connect` first instead of generating SDK/Webhook sample code.\n`platform_connect.action` is the management namespace: list, guide, preflight, connect, disconnect, remove, status, events_start, events_stop, events_status.\n`platform_action.action` is only for exact registered platform business actions listed below, such as `im.send_message`; never use management actions like `list` or `guide` there.\nAll business fields MUST go inside `payload`; top-level keys are only `platform`, `action`, and `payload`.\nDo not invent platform IDs or platform action names. If the needed action is not listed, ask for discovery/clarification instead of guessing.\nUse `platform_connect` with `action='guide'` and the matching `platform` to start onboarding; use `action='list'` when the app is unclear.\nWhen a pending onboarding state is present, continue from that state with `platform_connect` instead of asking the user to restate the app.\nSupported apps:\n- `api_server`: API Server (OpenAI Compatible) (category=api; backend=adapter; platform_action actions=none registered)\n- `dingtalk`: 钉钉 (DingTalk) (category=im; backend=adapter; platform_action actions=none registered)\n- `feishu`: 飞书 (Feishu/Lark) (category=im; backend=cli)\n - `calendar.create_event` payload={summary*, start_time*, end_time*, attendees} [write/high]\n - `docs.create_markdown` payload={title*, markdown*, folder_token} [write/medium]\n - `drive.search` payload={query*, limit} [read/medium]\n - `im.add_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.download_resource` payload={message_id*, file_key*, type*} [read/low]\n - `im.get_messages` payload={message_ids*} [read/low]\n - `im.list_chats` payload={page_size} [read/low]\n - `im.list_messages` payload={chat_id*} [read/low]\n - `im.list_thread_messages` payload={thread*} [read/low]\n - `im.remove_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.reply_message` payload={message_id*, text*} [send/high]\n - `im.search_chats` payload={query*, page_size} [read/low]\n - `im.search_messages` payload={query*} [read/medium]\n - `im.send_message` payload={chat_id*, text*, thread_id} [send/high]\n - `im.update_card` payload={token*, card*} [write/medium]\n - `im.update_message` payload={message_id*, text*} [write/medium]\n - `mail.search_unread` payload={query, limit} [read/high]\n - `sheets.append_row` payload={spreadsheet_token*, sheet_id*, values*} [write/medium]\n - `task.create` payload={title*, description, due_time} [write/medium]\n- `telegram`: Telegram (category=social; backend=adapter; platform_action actions=none registered)\n- `webhook`: Webhook (Generic) (category=webhook; backend=adapter; platform_action actions=none registered)\n\n## Tool Usage\nTools are normally invoked through the native function-calling mechanism, not by writing JSON in your reply\ntext. Only if the provider signals that native function calling is unavailable for this turn, fall back to a\nsingle JSON code block: `{\"name\": \"tool_name\", \"arguments\": {\"key\": \"value\"}}` — use this fallback format\nonly, never both. Only call a tool whose exact name appears in the tool index above and is currently callable\n(or reachable via `capability_expand`). Never invent, rename, alias, or guess a tool name, platform ID, or\nplatform action from argument shape or wording — if the index does not list it, it does not exist.\n`platform_connect.action` (list/guide/preflight/connect/disconnect/remove/status/events_start/events_stop/\nevents_status) is the App Connector management namespace; `platform_action.action` only accepts exact\nregistered business actions such as `im.send_message` shown in the App Connector Capability Index — never mix\nthe two namespaces. If a tool call returns an unknown/unavailable result, use the returned suggestions or\navailable names for a single retry instead of trying further variations of the same guess.\n\n**Side-effect action rule** (`platform_action` with effect=send/write/execute):\n- Call each unique action+payload **exactly once**. Never include duplicates in the same turn.\n- Once the result returns `\"completed\": true`, that action is DONE for this task. Do NOT call it again\n in any subsequent turn — immediately summarize the result for the user instead.\n- The system enforces idempotency: duplicate calls are blocked and will not execute.\n- If the user explicitly requests sending/writing multiple times, use distinct payloads per call.\n\n**Resource identifier provenance rule**:\n- NEVER fabricate, guess, or infer resource identifiers (chat_id, message_id, file_key, user_id, etc.).\n Every resource ID used in a side-effect action MUST come from a successful API response in this session.\n- If a read/list action fails (e.g. authorization error), you do NOT have valid resource IDs.\n Report the failure to the user — do NOT attempt the dependent write/send action with a guessed ID.\n- When a tool result contains `\"llm_instruction\"`, follow it exactly.\n\n## Guidelines\n1. **Direct answers first**: If you already know the answer, respond directly without tools.\n2. **Avoid redundant tool calls**: Do not call the same tool with the same arguments more than once in the same user turn. When an existing tool result already answers the user's request, stop calling tools and answer directly.\n3. **Use tools proactively**: When the user asks about files, time, system state, or needs actions performed, use the appropriate tool.\n4. **Chain tools when needed**: You can call multiple tools in sequence (e.g., list files → read file → summarize).\n5. **Handle failures gracefully**: If a tool fails, explain what went wrong and suggest alternatives. If it failed because of an unknown tool/platform/action name, retry once with an exact name from the returned suggestions, then explain rather than keep guessing.\n6. **Summarize results naturally**: After tool execution, synthesize the results into a helpful answer rather than dumping raw output.\n7. **Stay conversational**: Maintain a natural, helpful tone. Acknowledge context from earlier in the conversation.\n8. **Recall past work**:\n - Broad queries (\"之前做了什么\", \"列出任务\"): answer from the \"Recent Task History\" section already in your context. If insufficient, call session_list.\n - Specific lookups (\"上次那个配置怎么改的\"): call session_search with relevant phrases (NOT single characters).\n - Do NOT call search tools repeatedly with keyword fragments. One well-phrased call is better than ten fragmented ones.\n\n## Coding & Verification\nWhen working with code, prefer the precise built-in tools over ad-hoc shell: use `repo_map` to orient in an\nunfamiliar project, `code_search`/`file_find` to locate and `code_intel` for symbols, `edit_file` (anchored\nsearch-replace, or a unified `diff`) to change files — never rewrite a whole file to change a few lines — and\n`git_query` to inspect diffs/log. After edits, check `syntax_ok` in the result and run `test_run`/`lint_check`\nbefore declaring the work done. Batch independent read-only calls (search/read/query) into a single turn — they\nrun in parallel.\n\n## Presentation Style\n1. **Polished Markdown only**: Format user-facing answers with clean Markdown headings, short paragraphs, and concise bullets. Use tables only when they improve comparison or scanning.\n2. **Terminal-friendly layout**: Keep lines readable in a TUI; avoid dense walls of text, deeply nested lists, oversized ASCII art, or heavy visual blocks.\n3. **Elegant emphasis**: Use bold text sparingly for key terms and conclusions. Avoid excessive emojis, decorative symbols, repeated separators, or visual noise.\n4. **Theme-safe colors**: Do not emit ANSI escape codes, HTML color tags, Rich markup, or hardcoded color names. Rely on the TUI theme to render Markdown professionally.\n5. **No leaked tool protocol**: Never show tool-call JSON, internal schemas, raw observations, tool result payloads, or hidden reasoning in the final answer unless the user explicitly asks for raw/debug output. Treat any prior `{\"name\": ..., \"arguments\": ...}` blocks and `Tool result (...)` messages as internal execution context only.\n6. **Professional closure**: End with a concise conclusion or next step when helpful; avoid rambling after the useful answer is complete.\n\nWhen finished with all tool calls, respond normally without a JSON block, tool-call transcript, or process log.\n\n## Task Contract\n- Task ID: turn-1\n- Original user request: Install the hermetic DSH echo plugin from this workspace.\n- Workspace root: \n- Allowed roots: \n- Treat relative project paths as relative to the workspace root; never infer `.` as the project root when a workspace root is provided.\n- Workspace boundary is enforced by tools: do not read, search, edit, or run commands against paths outside the allowed roots unless the user explicitly requests an external path and the tool/approval policy permits it.\n- LeapFlow workspace config is optional at `/.leapflow/config.yaml`; runtime config is loaded from `~/.leapflow/config/user.yaml` and `~/.leapflow/profiles//config/*.yaml`.\n- Preserve this task contract across summarization, compression, tool loops, and memory retrieval.\n" + }, + { + "role": "system", + "content": "\n## Recent Session Summary\n- [user] Install the hermetic DSH echo plugin from this workspace.\n" + }, + { + "role": "user", + "content": "Install the hermetic DSH echo plugin from this workspace.\nInstall the hermetic DSH echo plugin from this workspace." + } + ], + "tools": [ + "capability_expand", + "code_intel", + "code_search", + "config_get", + "config_list", + "env_info", + "file_find", + "file_list", + "file_read", + "git_query", + "lint_check", + "memory_search", + "plugin_list", + "plugin_propose", + "plugin_status", + "plugin_versions", + "repo_map", + "research_note", + "runtime_snapshot", + "schedule_reentry", + "self_describe", + "session_list", + "skill_view", + "skills_list", + "terminal_list", + "terminal_read", + "test_run", + "text_search", + "time_get", + "tool_describe", + "tool_search", + "web_fetch" + ] + }, + "responses": [ + { + "status": 200, + "content_type": "application/json", + "body": "{\"id\": \"chatcmpl-cassette\", \"object\": \"chat.completion\", \"model\": \"cassette-model\", \"choices\": [{\"index\": 0, \"message\": {\"role\": \"assistant\", \"content\": \"\", \"tool_calls\": [{\"id\": \"call_1\", \"type\": \"function\", \"function\": {\"name\": \"plugin_install\", \"arguments\": \"{\\\"plugin_id\\\": \\\"r6_dsh_echo\\\", \\\"source_path\\\": \\\"/tmp/lfj-r6_lifecycle/workspaces/life/dsh-echo\\\", \\\"version_label\\\": \\\"r6\\\"}\"}}]}, \"finish_reason\": \"tool_calls\"}], \"usage\": {\"prompt_tokens\": 64, \"completion_tokens\": 16, \"total_tokens\": 80}}" + } + ] +} diff --git a/tests/_fixtures/cassettes/r6_lifecycle/cassette-model-762a64c11fccb54a.cassette.json b/tests/_fixtures/cassettes/r6_lifecycle/cassette-model-762a64c11fccb54a.cassette.json new file mode 100644 index 00000000..2311ce18 --- /dev/null +++ b/tests/_fixtures/cassettes/r6_lifecycle/cassette-model-762a64c11fccb54a.cassette.json @@ -0,0 +1,64 @@ +{ + "fingerprint": "762a64c11fccb54abeaa3a40eddf5d3437b98dc5037ea85176a8416289b9fca4", + "note": "captured in seed mode", + "request": { + "model": "cassette-model", + "stream": false, + "messages": [ + { + "role": "system", + "content": "You are LeapFlow, an intelligent assistant that can both converse naturally and take real actions on the user's computer.\n\n## Capabilities\nThe tool index below lists **every** registered tool by name and a one-line summary — this is the complete\ncapability contract; nothing else exists. Only a subset is directly callable this turn (via native tool calling,\nnot a JSON block in your reply). If you need a tool from the index that is not yet callable, call\n`capability_expand` with its category name first — the matching tools become callable immediately after.\nWhen the user asks about LeapFlow **itself** — its identity/version/model, what tools, plugins, or capabilities\nit has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of\nself-awareness — answer from live runtime evidence via `self_describe` (facet=capabilities for \"what can you\ndo / do you support X\", identity for version/model, runtime for context/posture, or `all` for a full self-check),\nnever by reading LeapFlow's own source code, documentation, or memory. Report configuration-dependent or\nunavailable capabilities as limitations rather than inferring them.\n- **text_search**(text, pattern): Search for a regex pattern in text.\n- **text_replace**(text, old, new, count) [capability_expand category: write]: Replace occurrences of a substring in text.\n- **time_get**(): Get current date and time.\n- **env_info**(): Get system environment information (OS, Python version, cwd).\n- **skills_list**(query, category, source): List available learned skills. Use when user asks about capabilities or you need a specific skill.\n- **skill_view**(name): View the full content of a specific skill document.\n- **skill_list_files**(name) [capability_expand category: skills]: List all support files in a skill directory (configs, templates, examples). Use before skill_file_read to discover available files.\n- **skill_file_read**(name, file_path) [capability_expand category: skills]: Read an individual support file from a skill directory. Use skill_list_files first to discover available files.\n- **code_intel**(path, operation): Precise document symbols (outline) for a source file: classes, functions, and methods with line ranges. Python uses an exact AST parse; other languages use a keyword-prefix scan. Prefer over file_read mode=symbols for accurate navigation before editing. Read-only.\n- **repo_map**(path): Compact project orientation for a repository root: languages, detected test/lint commands, top-level structure, entry points, manifest, and VCS branch. Call this first when entering an unfamiliar codebase. Read-only.\n- **scm_sync**(action, cwd, remote, pull_ref, push_ref, timeout) [capability_expand category: scm]: Run a typed git SCM action. Use this instead of shell_run for git pull/push/status. For 'pull origin main then push', set action='pull_then_push', remote='origin', pull_ref='main', and omit push_ref so LeapFlow pushes the current local branch.\n- **git_query**(action, cwd, ref, path, staged, max_count, stat): Read-only structured git inspection: action=diff|log|status|branch|show. Prefer over shell_run for reading repo state — output is clipped, redacted, and log/branch are parsed into structured fields. Use scm_sync for pull/push.\n- **git_write**(action, cwd, message, stage_all, name, ref, create) [capability_expand category: scm]: Mutating git actions: action=commit (message, stage_all), branch (create+switch), checkout (switch; create=true for -b). Approval-gated. Use scm_sync for pull/push and git_query for reads.\n- **test_run**(command, cwd, timeout): Run the project's test suite and return structured results (framework, passed/failed counts, failing tests). Auto-detects the runner (pytest/npm/go/cargo) or uses a configured/explicit command; executes via the governed shell. ok=true means the runner executed — see 'success' for pass/fail.\n- **lint_check**(command, cwd, timeout): Run the project's linter and return a structured clean/issue result. Auto-detects the linter (ruff/eslint/go vet/clippy) or uses a configured/explicit command; executes via the governed shell. ok=true means the linter ran — see 'clean'.\n- **file_list**(path, pattern, depth): List files and directories at a given path. Use depth=1 or depth=2 to get a recursive tree in one call instead of listing each sub-directory separately.\n- **file_read**(path, max_lines, start_line, max_chars, mode): Read text file content with adaptive context governance. For large or unfamiliar files, prefer mode='outline' or mode='symbols' first, then use mode='raw' with start_line/max_lines for the specific range you actually need. For LeapFlow's own settings, use config_list / config_get / config_set — its config files are outside the workspace and not readable here.\n- **file_write**(path, content, mode) [capability_expand category: write]: Write content to a file (overwrite or append).\n- **code_search**(pattern, patterns, path, glob, ignore_case, multiline, max_results, context_lines): Search file CONTENTS by regex pattern across a directory tree (ripgrep-backed). Requires a regex pattern. NOT for listing or browsing directory contents — use file_list for that. Prefer this over shell_run grep: faster, skips VCS/dependency/build dirs, and returns structured path:line:column matches. Batch related lookups into ONE call via `patterns` (OR-combined, single pass) instead of issuing several separate searches. Use file_read for the surrounding context of a hit.\n- **file_find**(glob, path, max_results): Find files by a recursive glob pattern under a base path (e.g. '**/test_*.py' or '*.md'). Prefer this over shell_run find; skips VCS/dependency/build dirs.\n- **edit_file**(path, edits, dry_run, diff) [capability_expand category: write]: Apply targeted, anchored search-replace edits to an EXISTING text file (use file_write to create/overwrite). Each edit is {original_text, new_text, replace_all?}; original_text must match exactly and uniquely (or set replace_all) — a non-unique or missing anchor is rejected so files are never corrupted. Set dry_run to preview. Alternatively pass a unified 'diff' to apply its hunks as anchored edits. Far cheaper and safer than rewriting a whole file.\n- **shell_run**(command, cwd, timeout) [capability_expand category: shell]: Execute a one-shot shell command with timeout protection. Runs in the active workspace; paths resolving outside it are refused. Reach for a structured tool first when one fits — web_fetch for anything over HTTP(S), git_query/scm_sync for git, code_search/file_find/file_read for the repo, config_get/config_set for LeapFlow's own settings — because those report typed results, while a failed shell command can only be diagnosed from its exit code and stderr. Every shell run counts as an external side effect, so a failure stops the rest of the batch and is not retried automatically.\n- **terminal_open**(command, cwd, shell) [capability_expand category: terminal]: Open a PERSISTENT shell session (REPL/dev server/watch), returning a session_id for terminal_send/read/close. Disabled unless tools.terminal_session_enabled is set. For one-shot commands use shell_run instead.\n- **terminal_send**(session_id, input, wait) [capability_expand category: terminal]: Send a line of input to a persistent terminal session and return output captured shortly after.\n- **terminal_read**(session_id, wait): Drain buffered output from a persistent terminal session (optionally waiting briefly first).\n- **terminal_close**(session_id) [capability_expand category: terminal]: Terminate a persistent terminal session and release its process group.\n- **terminal_list**(): List active persistent terminal sessions.\n- **config_list**(category, limit): List LeapFlow's own writable settings (LLM model and endpoint, daemon, memory, perception, gateway, …) with current values. Use this to discover the exact key before changing anything. Optionally narrow by `category`. This is the only correct way to inspect LeapFlow configuration — never read config files from disk.\n- **config_get**(key): Read one LeapFlow setting by key (e.g. 'llm.model', 'llm.base_url', 'daemon.log_level'), returning its current value, type, scopes, and whether a change needs a daemon restart. There is no 'llm.provider' key: provider behavior is inferred from the OpenAI-compatible 'llm.base_url'. Never read LeapFlow config files from disk — use this.\n- **config_set**(key, value, scope) [capability_expand category: config]: Change one LeapFlow setting by key, e.g. switch an OpenAI-compatible LLM with key='llm.model' and key='llm.base_url'. There is no 'llm.provider' key: provider behavior is inferred from the endpoint. Values are validated and coerced; credentials are stored in the vault automatically. Call config_list or config_get first if unsure of the key. The result states whether a `leap daemon restart` is required. Never edit LeapFlow config files directly.\n- **web_fetch**(url, select, timeout, max_bytes): Read a URL over HTTP(S) and get back extracted, context-sized content: parsed JSON for API endpoints, readable text plus links for web pages. Use this for anything on the internet — prices, docs, releases, articles — instead of running curl through shell_run: it reports real HTTP status codes, retries rate limits on its own, and is a plain read so a retry is always safe. For JSON APIs pass `select` with a dotted path (e.g. 'chart.result.0.meta') to return just that part instead of the whole payload.\n- **memory_search**(query, limit): Search agent memory for relevant past experiences, observations, and facts.\n- **memory_add**(content, kind) [capability_expand category: write]: Store a new observation or insight in memory for future reference.\n- **research_note**(kind, text): Record a compact, structured note about the current task's state so it survives context compression on long / multi-step tasks. Use for durable findings, open questions still to resolve, decisions / excluded paths, and the immediate next step. One concise sentence per note.\n- **capability_expand**(category): Fetch the full callable schema for every tool in a capability category (e.g. 'hub', 'gateway', 'desktop', 'delegate', 'file', 'memory', 'skill'). The compact tool index always lists every registered tool by name and a one-line summary, but only a static low-risk subset is directly callable each turn. If you need a tool from the index that is not yet callable, call capability_expand with its category first; the matching tools become callable in this turn. Never invent a tool name — expand the category instead.\n- **delegate_task**(goal, context) [capability_expand category: delegate]: Delegate a complex sub-task to an isolated subagent. The subagent gets a fresh context and restricted tool access. Use when a task is self-contained and can be solved independently.\n- **schedule_reentry**(kind, reason, delay_seconds, event_match, max_reentries, deadline_seconds): Register a re-entry so this task can resume later from its current orientation (findings / open questions / next step). Use when work must pause and continue after a delay (kind=time) or when a matching platform event arrives (kind=event), instead of finishing now. The research-ledger state is carried over automatically.\n- **hub_push**(skill_name, visibility, version) [capability_expand category: hub]: Push a local skill to the ModelScope Hub for sharing or backup.\n- **hub_pull**(repo_id, version) [capability_expand category: hub]: Pull a skill from the ModelScope Hub to install locally.\n- **hub_search**(query) [capability_expand category: hub]: Search for skills on the Hub by keyword or description.\n- **hub_sync**(mode, dry_run) [capability_expand category: hub]: Preview or execute sync between local skills and Hub.\n- **platform_action**(platform, action, payload, backend_kind) [capability_expand category: gateway]: Execute an exact registered business action on an external platform through LeapFlow's App Connector layer. Actions must be copied from the App Connector Capability Index and are addressed as domain.operation, e.g. im.send_message or docs.create_markdown. All business fields (chat_id, text, query, etc.) MUST be placed inside `payload`, never at the top level. Example: {\"platform\":\"feishu\",\"action\":\"im.send_message\",\"payload\":{\"chat_id\":\"oc_xxx\",\"text\":\"hello\"}}. Do not invent action names, do not use management actions such as list/guide/connect/status here.\n- **platform_connect**(action, platform, credentials, options, checkpoint) [capability_expand category: gateway]: List, guide, connect, disconnect, remove, or check status for external platforms using the App Connector management namespace. Supports REST and CLI backends. Use this for management actions such as list/guide/preflight/connect/status; use platform_action only for exact registered business actions.\n- **gateway_send**(platform, chat_id, text, thread_id) [capability_expand category: gateway]: Send a message to a connected external platform (Feishu group, Telegram chat, DingTalk conversation, etc.). Requires the platform to be connected via gateway_connect first. Use gateway_connect with action='list' to see connected platforms and available chat IDs.\n- **gateway_connect**(action, platform, credentials, options) [capability_expand category: gateway]: Connect, configure, or manage external platform integrations (Feishu, DingTalk, Telegram, Slack, Discord, etc.). Conversational flow: 1) call 'guide' to get setup steps + required fields, 2) present the steps to the user and ask for ALL required credentials in a single message, 3) call 'connect' with the credentials. Goal: complete in 1–2 user turns. NEVER include credential values in your text response.\n- **plugin_list**(): List the live plugin registry across Tool/Gateway/LLM subsystems for plugin management: which plugins are loaded, their category and fiber lifecycle state. For questions about LeapFlow's own capabilities or whether it supports X, use self_describe instead.\n- **plugin_status**(plugin_id): Get detailed status of a specific plugin: its declared category, runtime dependencies, contributed tools, and fiber lifecycle state.\n- **plugin_versions**(plugin_id): List recorded source versions and active pointer for a profile-scoped plugin.\n- **plugin_propose**(requested_capability, plugin_id, proposed_tools, test_cases, risk_level, evidence): Create a side-effect-free PluginProposal from explicit capability-gap evidence. Use this before plugin_generate when a missing capability should be reviewed. Does not call an LLM, write files, or install anything.\n- **assess_compatibility**(manifest, source_path) [capability_expand category: plugin_management]: Assess whether a foreign plugin manifest or real DSH source bundle is compatible with LeapFlow. A source bundle assessment is static: runtime_ready stays false until restricted Node discovery during plugin_install. Returns component-level verdicts and limitations.\n- **plugin_generate**(plugin_id, description, proposal_id) [capability_expand category: system]: Generate a new ToolPlugin from a natural-language capability description. The LLM produces code that conforms to the ToolPlugin Protocol; it is then rigorously validated (syntax, structure, import, protocol conformance). The generated source is stored in CAS and receives explicit content approval. It DOES NOT install the plugin — installation requires a second, mutation-specific approval via plugin_install.\n- **plugin_install**(plugin_id, code, marketplace_name, source_path, proposal_id, version_label) [capability_expand category: system]: Install a plugin from exactly one source: validated Python code, the configured Python marketplace, or a real DSH source bundle. DSH bundles run restricted Node discovery before registration; only public host tools are installed and client UI limitations are reported. Writes profile-scoped state and mutates the process-global registry. REQUIRES APPROVAL.\n- **plugin_rollback**(plugin_id, version) [capability_expand category: system]: Rollback a profile-scoped plugin to a recorded source version and reload it. REQUIRES APPROVAL.\n- **plugin_reload**(plugin_id, version_label) [capability_expand category: system]: Hot-reload a plugin at runtime. Disposes the old plugin fiber, re-imports its module, and registers a fresh instance. Existing in-flight turns are unaffected (snapshot isolation). REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_disable**(plugin_id) [capability_expand category: system]: Disable a plugin by disposing its fiber, removing its tools from the runtime registry. Cannot disable self_management itself. REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_remove**(plugin_id, delete_source) [capability_expand category: system]: Terminally remove a plugin: dispose its fiber, unregister its tools, remove reload metadata, and optionally delete its profile-scoped source file. Cannot remove self_management itself. REQUIRES APPROVAL.\n- **plugin_unquarantine**(plugin_id) [capability_expand category: plugin_management]: Restore a quarantined plugin to probation status for re-evaluation. Unfreezes the trust ledger, transitions the proposal back to PROBATION, and reloads the plugin. REQUIRES APPROVAL.\n- **plugin_enable**(plugin_id) [capability_expand category: system]: Re-enable a previously disabled plugin by reloading its module and registering a fresh instance. REQUIRES APPROVAL.\n- **self_describe**(facet): Answer questions about LeapFlow ITSELF from live runtime evidence — its identity/version/model, what tools and plugins it has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of self-awareness. Prefer this over reading LeapFlow's own source code. Pick facet=capabilities for 'what can you do / do you support X', identity for version/model, runtime for context/posture, or all for a full self-check when explicitly requested.\n- **runtime_snapshot**(): Lightweight (~150 token) flat snapshot of current runtime state: model, context budget, posture, disclosure level, turn count, cache hit rate, uptime, tool count, and pending approvals.\n- **schedule_create**(trigger_expression, instruction, execution_mode, max_retries, delivery_target) [capability_expand category: scheduler]: Create a new scheduled task. Specify a trigger expression (e.g. '30m', 'every 2h', '0 9 * * *') and the instruction to execute on each trigger. Optionally provide an execution mode and a delivery target to receive notifications.\n- **schedule_list**() [capability_expand category: scheduler]: List all scheduled tasks with their current state, trigger, and next due time.\n- **schedule_status**(task_id) [capability_expand category: scheduler]: Get detailed status and recent execution history for a scheduled task.\n- **schedule_pause**(task_id) [capability_expand category: scheduler]: Pause a scheduled task so it stops firing without being cancelled.\n- **schedule_resume**(task_id) [capability_expand category: scheduler]: Resume a paused scheduled task — re-arms it and recalculates next due time.\n- **schedule_cancel**(task_id) [capability_expand category: scheduler]: Cancel a scheduled task permanently.\n- **tool_search**(query, max_results): Search all registered tools by keyword relevance. Returns a ranked list of matching tools with name, category, and summary. Use this when you need a tool but are unsure of its exact name or category.\n- **tool_describe**(tool_name): Get the full callable schema of a single tool by exact name. Returns the tool's description, parameters, and metadata. Use after tool_search to inspect a specific tool before calling it.\n- **session_search**(query, limit) [capability_expand category: unclassified]: Search past conversation sessions in the current workspace for relevant context.\n- **session_list**(limit): List recent conversation sessions in the current workspace with stable ids, titles, dates, and summaries. Use for browsing past tasks or when user asks to see history without specific search terms. No keywords needed — returns chronological list.\n- **session_detail**(session_id, limit, offset, include_inactive) [capability_expand category: unclassified]: Read a paginated persisted transcript for one past conversation session in the current workspace. Use after session_list or session_search returns a session_id.\n- **fixture_echo**(text): Return the supplied text from a pre-built DSH package.\n- **click**(element_index) [capability_expand category: desktop]: Click a UI element by its element_index (from the latest observe_ui snapshot)\n- **get_clipboard**() [capability_expand category: desktop]: Read current clipboard text content\n- **list_apps**(filter, running_only) [capability_expand category: desktop]: List available applications on this system. Use to discover correct bundle_id before switch_app.\n- **list_windows**() [capability_expand category: desktop]: List all top-level windows with pid, window_id, title, and per-window state (minimized, on-screen). Call this first to pick the pid and window_id that observe_ui and other window tools require.\n- **observe_ui**(pid, window_id, query) [capability_expand category: desktop]: Snapshot one window's actionable UI elements, each tagged with an element_index for click/right_click/read_text. Re-observe after actions — indices belong to one snapshot. Requires the window's pid and window_id from list_windows.\n- **open_url**(url, app_id) [capability_expand category: desktop]: Open a URL in the default or specified browser\n- **read_text**(element_index) [capability_expand category: desktop]: Read the text content of a specific UI element from the latest snapshot\n- **right_click**(element_index) [capability_expand category: desktop]: Right-click a UI element to open its context menu. Returns visible menu items.\n- **screenshot**(pid, window_id) [capability_expand category: desktop]: Capture a screenshot for visual verification. With pid + window_id captures that window (works across all displays); defaults to the last observed window, or the full desktop when no window has been observed.\n- **scroll**(element_index, direction, amount, pid, window_id) [capability_expand category: desktop]: Scroll a scrollable area of a window. Omit element_index to scroll the window's focused/page scroller; pass one to scroll an exact element from the latest snapshot.\n- **select_text**(element_index) [capability_expand category: desktop]: Select all text in a UI element (focus + select-all, for subsequent copy)\n- **set_clipboard**(text) [capability_expand category: desktop]: Write text to the clipboard\n- **shortcut**(keys) [capability_expand category: desktop]: Execute a keyboard shortcut\n- **switch_app**(app_id) [capability_expand category: desktop]: Switch to an app (launch if needed, activate, verify)\n- **type_text**(text) [capability_expand category: desktop]: Type text into the currently focused element\n- **wait**(seconds) [capability_expand category: desktop]: Wait for a specified duration before continuing\n- **wait_until**(condition, pid, window_id, timeout, poll_interval) [capability_expand category: desktop]: Wait until a UI condition is met (polls UI tree). Returns elements when found or on timeout.\n- **wait_until_stable**(timeout, poll_interval, pid, window_id) [capability_expand category: desktop]: Wait until the UI stops changing (element set stabilizes across polls).\n\n## App Connector Capability Index\nLeapFlow can onboard and manage external apps through `platform_connect` and execute exact registered business actions through `platform_action`.\nFor requests about connecting, setting up, configuring, enabling, or managing a supported app, use `platform_connect` first instead of generating SDK/Webhook sample code.\n`platform_connect.action` is the management namespace: list, guide, preflight, connect, disconnect, remove, status, events_start, events_stop, events_status.\n`platform_action.action` is only for exact registered platform business actions listed below, such as `im.send_message`; never use management actions like `list` or `guide` there.\nAll business fields MUST go inside `payload`; top-level keys are only `platform`, `action`, and `payload`.\nDo not invent platform IDs or platform action names. If the needed action is not listed, ask for discovery/clarification instead of guessing.\nUse `platform_connect` with `action='guide'` and the matching `platform` to start onboarding; use `action='list'` when the app is unclear.\nWhen a pending onboarding state is present, continue from that state with `platform_connect` instead of asking the user to restate the app.\nSupported apps:\n- `api_server`: API Server (OpenAI Compatible) (category=api; backend=adapter; platform_action actions=none registered)\n- `dingtalk`: 钉钉 (DingTalk) (category=im; backend=adapter; platform_action actions=none registered)\n- `feishu`: 飞书 (Feishu/Lark) (category=im; backend=cli)\n - `calendar.create_event` payload={summary*, start_time*, end_time*, attendees} [write/high]\n - `docs.create_markdown` payload={title*, markdown*, folder_token} [write/medium]\n - `drive.search` payload={query*, limit} [read/medium]\n - `im.add_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.download_resource` payload={message_id*, file_key*, type*} [read/low]\n - `im.get_messages` payload={message_ids*} [read/low]\n - `im.list_chats` payload={page_size} [read/low]\n - `im.list_messages` payload={chat_id*} [read/low]\n - `im.list_thread_messages` payload={thread*} [read/low]\n - `im.remove_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.reply_message` payload={message_id*, text*} [send/high]\n - `im.search_chats` payload={query*, page_size} [read/low]\n - `im.search_messages` payload={query*} [read/medium]\n - `im.send_message` payload={chat_id*, text*, thread_id} [send/high]\n - `im.update_card` payload={token*, card*} [write/medium]\n - `im.update_message` payload={message_id*, text*} [write/medium]\n - `mail.search_unread` payload={query, limit} [read/high]\n - `sheets.append_row` payload={spreadsheet_token*, sheet_id*, values*} [write/medium]\n - `task.create` payload={title*, description, due_time} [write/medium]\n- `telegram`: Telegram (category=social; backend=adapter; platform_action actions=none registered)\n- `webhook`: Webhook (Generic) (category=webhook; backend=adapter; platform_action actions=none registered)\n\n## Tool Usage\nTools are normally invoked through the native function-calling mechanism, not by writing JSON in your reply\ntext. Only if the provider signals that native function calling is unavailable for this turn, fall back to a\nsingle JSON code block: `{\"name\": \"tool_name\", \"arguments\": {\"key\": \"value\"}}` — use this fallback format\nonly, never both. Only call a tool whose exact name appears in the tool index above and is currently callable\n(or reachable via `capability_expand`). Never invent, rename, alias, or guess a tool name, platform ID, or\nplatform action from argument shape or wording — if the index does not list it, it does not exist.\n`platform_connect.action` (list/guide/preflight/connect/disconnect/remove/status/events_start/events_stop/\nevents_status) is the App Connector management namespace; `platform_action.action` only accepts exact\nregistered business actions such as `im.send_message` shown in the App Connector Capability Index — never mix\nthe two namespaces. If a tool call returns an unknown/unavailable result, use the returned suggestions or\navailable names for a single retry instead of trying further variations of the same guess.\n\n**Side-effect action rule** (`platform_action` with effect=send/write/execute):\n- Call each unique action+payload **exactly once**. Never include duplicates in the same turn.\n- Once the result returns `\"completed\": true`, that action is DONE for this task. Do NOT call it again\n in any subsequent turn — immediately summarize the result for the user instead.\n- The system enforces idempotency: duplicate calls are blocked and will not execute.\n- If the user explicitly requests sending/writing multiple times, use distinct payloads per call.\n\n**Resource identifier provenance rule**:\n- NEVER fabricate, guess, or infer resource identifiers (chat_id, message_id, file_key, user_id, etc.).\n Every resource ID used in a side-effect action MUST come from a successful API response in this session.\n- If a read/list action fails (e.g. authorization error), you do NOT have valid resource IDs.\n Report the failure to the user — do NOT attempt the dependent write/send action with a guessed ID.\n- When a tool result contains `\"llm_instruction\"`, follow it exactly.\n\n## Guidelines\n1. **Direct answers first**: If you already know the answer, respond directly without tools.\n2. **Avoid redundant tool calls**: Do not call the same tool with the same arguments more than once in the same user turn. When an existing tool result already answers the user's request, stop calling tools and answer directly.\n3. **Use tools proactively**: When the user asks about files, time, system state, or needs actions performed, use the appropriate tool.\n4. **Chain tools when needed**: You can call multiple tools in sequence (e.g., list files → read file → summarize).\n5. **Handle failures gracefully**: If a tool fails, explain what went wrong and suggest alternatives. If it failed because of an unknown tool/platform/action name, retry once with an exact name from the returned suggestions, then explain rather than keep guessing.\n6. **Summarize results naturally**: After tool execution, synthesize the results into a helpful answer rather than dumping raw output.\n7. **Stay conversational**: Maintain a natural, helpful tone. Acknowledge context from earlier in the conversation.\n8. **Recall past work**:\n - Broad queries (\"之前做了什么\", \"列出任务\"): answer from the \"Recent Task History\" section already in your context. If insufficient, call session_list.\n - Specific lookups (\"上次那个配置怎么改的\"): call session_search with relevant phrases (NOT single characters).\n - Do NOT call search tools repeatedly with keyword fragments. One well-phrased call is better than ten fragmented ones.\n\n## Coding & Verification\nWhen working with code, prefer the precise built-in tools over ad-hoc shell: use `repo_map` to orient in an\nunfamiliar project, `code_search`/`file_find` to locate and `code_intel` for symbols, `edit_file` (anchored\nsearch-replace, or a unified `diff`) to change files — never rewrite a whole file to change a few lines — and\n`git_query` to inspect diffs/log. After edits, check `syntax_ok` in the result and run `test_run`/`lint_check`\nbefore declaring the work done. Batch independent read-only calls (search/read/query) into a single turn — they\nrun in parallel.\n\n## Presentation Style\n1. **Polished Markdown only**: Format user-facing answers with clean Markdown headings, short paragraphs, and concise bullets. Use tables only when they improve comparison or scanning.\n2. **Terminal-friendly layout**: Keep lines readable in a TUI; avoid dense walls of text, deeply nested lists, oversized ASCII art, or heavy visual blocks.\n3. **Elegant emphasis**: Use bold text sparingly for key terms and conclusions. Avoid excessive emojis, decorative symbols, repeated separators, or visual noise.\n4. **Theme-safe colors**: Do not emit ANSI escape codes, HTML color tags, Rich markup, or hardcoded color names. Rely on the TUI theme to render Markdown professionally.\n5. **No leaked tool protocol**: Never show tool-call JSON, internal schemas, raw observations, tool result payloads, or hidden reasoning in the final answer unless the user explicitly asks for raw/debug output. Treat any prior `{\"name\": ..., \"arguments\": ...}` blocks and `Tool result (...)` messages as internal execution context only.\n6. **Professional closure**: End with a concise conclusion or next step when helpful; avoid rambling after the useful answer is complete.\n\nWhen finished with all tool calls, respond normally without a JSON block, tool-call transcript, or process log.\n\n## Task Contract\n- Task ID: turn-1\n- Original user request: Invoke fixture_echo with the text after restart.\n- Workspace root: \n- Allowed roots: \n- Treat relative project paths as relative to the workspace root; never infer `.` as the project root when a workspace root is provided.\n- Workspace boundary is enforced by tools: do not read, search, edit, or run commands against paths outside the allowed roots unless the user explicitly requests an external path and the tool/approval policy permits it.\n- LeapFlow workspace config is optional at `/.leapflow/config.yaml`; runtime config is loaded from `~/.leapflow/config/user.yaml` and `~/.leapflow/profiles//config/*.yaml`.\n- Preserve this task contract across summarization, compression, tool loops, and memory retrieval.\n" + }, + { + "role": "system", + "content": "\n## Recent Session Summary\n- [user] Invoke fixture_echo with the text after restart.\n" + }, + { + "role": "user", + "content": "Invoke fixture_echo with the text after restart.\nInvoke fixture_echo with the text after restart." + } + ], + "tools": [ + "capability_expand", + "code_intel", + "code_search", + "config_get", + "config_list", + "env_info", + "file_find", + "file_list", + "file_read", + "fixture_echo", + "git_query", + "lint_check", + "memory_search", + "plugin_list", + "plugin_propose", + "plugin_status", + "plugin_versions", + "repo_map", + "research_note", + "runtime_snapshot", + "schedule_reentry", + "self_describe", + "session_list", + "skill_view", + "skills_list", + "terminal_list", + "terminal_read", + "test_run", + "text_search", + "time_get", + "tool_describe", + "tool_search", + "web_fetch" + ] + }, + "responses": [ + { + "status": 200, + "content_type": "application/json", + "body": "{\"id\": \"chatcmpl-cassette\", \"object\": \"chat.completion\", \"model\": \"cassette-model\", \"choices\": [{\"index\": 0, \"message\": {\"role\": \"assistant\", \"content\": \"\", \"tool_calls\": [{\"id\": \"call_1\", \"type\": \"function\", \"function\": {\"name\": \"fixture_echo\", \"arguments\": \"{\\\"text\\\": \\\"after restart\\\"}\"}}]}, \"finish_reason\": \"tool_calls\"}], \"usage\": {\"prompt_tokens\": 64, \"completion_tokens\": 16, \"total_tokens\": 80}}" + } + ] +} diff --git a/tests/_fixtures/cassettes/r6_lifecycle/cassette-model-8cd0e76f98de22bb.cassette.json b/tests/_fixtures/cassettes/r6_lifecycle/cassette-model-8cd0e76f98de22bb.cassette.json new file mode 100644 index 00000000..1f33433a --- /dev/null +++ b/tests/_fixtures/cassettes/r6_lifecycle/cassette-model-8cd0e76f98de22bb.cassette.json @@ -0,0 +1,76 @@ +{ + "fingerprint": "8cd0e76f98de22bbd5be9519c36d71090ccc2147fc611758e5f8e4a916a3de60", + "note": "captured in seed mode", + "request": { + "model": "cassette-model", + "stream": false, + "messages": [ + { + "role": "system", + "content": "You are LeapFlow, an intelligent assistant that can both converse naturally and take real actions on the user's computer.\n\n## Capabilities\nThe tool index below lists **every** registered tool by name and a one-line summary — this is the complete\ncapability contract; nothing else exists. Only a subset is directly callable this turn (via native tool calling,\nnot a JSON block in your reply). If you need a tool from the index that is not yet callable, call\n`capability_expand` with its category name first — the matching tools become callable immediately after.\nWhen the user asks about LeapFlow **itself** — its identity/version/model, what tools, plugins, or capabilities\nit has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of\nself-awareness — answer from live runtime evidence via `self_describe` (facet=capabilities for \"what can you\ndo / do you support X\", identity for version/model, runtime for context/posture, or `all` for a full self-check),\nnever by reading LeapFlow's own source code, documentation, or memory. Report configuration-dependent or\nunavailable capabilities as limitations rather than inferring them.\n- **text_search**(text, pattern): Search for a regex pattern in text.\n- **text_replace**(text, old, new, count) [capability_expand category: write]: Replace occurrences of a substring in text.\n- **time_get**(): Get current date and time.\n- **env_info**(): Get system environment information (OS, Python version, cwd).\n- **skills_list**(query, category, source): List available learned skills. Use when user asks about capabilities or you need a specific skill.\n- **skill_view**(name): View the full content of a specific skill document.\n- **skill_list_files**(name) [capability_expand category: skills]: List all support files in a skill directory (configs, templates, examples). Use before skill_file_read to discover available files.\n- **skill_file_read**(name, file_path) [capability_expand category: skills]: Read an individual support file from a skill directory. Use skill_list_files first to discover available files.\n- **code_intel**(path, operation): Precise document symbols (outline) for a source file: classes, functions, and methods with line ranges. Python uses an exact AST parse; other languages use a keyword-prefix scan. Prefer over file_read mode=symbols for accurate navigation before editing. Read-only.\n- **repo_map**(path): Compact project orientation for a repository root: languages, detected test/lint commands, top-level structure, entry points, manifest, and VCS branch. Call this first when entering an unfamiliar codebase. Read-only.\n- **scm_sync**(action, cwd, remote, pull_ref, push_ref, timeout) [capability_expand category: scm]: Run a typed git SCM action. Use this instead of shell_run for git pull/push/status. For 'pull origin main then push', set action='pull_then_push', remote='origin', pull_ref='main', and omit push_ref so LeapFlow pushes the current local branch.\n- **git_query**(action, cwd, ref, path, staged, max_count, stat): Read-only structured git inspection: action=diff|log|status|branch|show. Prefer over shell_run for reading repo state — output is clipped, redacted, and log/branch are parsed into structured fields. Use scm_sync for pull/push.\n- **git_write**(action, cwd, message, stage_all, name, ref, create) [capability_expand category: scm]: Mutating git actions: action=commit (message, stage_all), branch (create+switch), checkout (switch; create=true for -b). Approval-gated. Use scm_sync for pull/push and git_query for reads.\n- **test_run**(command, cwd, timeout): Run the project's test suite and return structured results (framework, passed/failed counts, failing tests). Auto-detects the runner (pytest/npm/go/cargo) or uses a configured/explicit command; executes via the governed shell. ok=true means the runner executed — see 'success' for pass/fail.\n- **lint_check**(command, cwd, timeout): Run the project's linter and return a structured clean/issue result. Auto-detects the linter (ruff/eslint/go vet/clippy) or uses a configured/explicit command; executes via the governed shell. ok=true means the linter ran — see 'clean'.\n- **file_list**(path, pattern, depth): List files and directories at a given path. Use depth=1 or depth=2 to get a recursive tree in one call instead of listing each sub-directory separately.\n- **file_read**(path, max_lines, start_line, max_chars, mode): Read text file content with adaptive context governance. For large or unfamiliar files, prefer mode='outline' or mode='symbols' first, then use mode='raw' with start_line/max_lines for the specific range you actually need. For LeapFlow's own settings, use config_list / config_get / config_set — its config files are outside the workspace and not readable here.\n- **file_write**(path, content, mode) [capability_expand category: write]: Write content to a file (overwrite or append).\n- **code_search**(pattern, patterns, path, glob, ignore_case, multiline, max_results, context_lines): Search file CONTENTS by regex pattern across a directory tree (ripgrep-backed). Requires a regex pattern. NOT for listing or browsing directory contents — use file_list for that. Prefer this over shell_run grep: faster, skips VCS/dependency/build dirs, and returns structured path:line:column matches. Batch related lookups into ONE call via `patterns` (OR-combined, single pass) instead of issuing several separate searches. Use file_read for the surrounding context of a hit.\n- **file_find**(glob, path, max_results): Find files by a recursive glob pattern under a base path (e.g. '**/test_*.py' or '*.md'). Prefer this over shell_run find; skips VCS/dependency/build dirs.\n- **edit_file**(path, edits, dry_run, diff) [capability_expand category: write]: Apply targeted, anchored search-replace edits to an EXISTING text file (use file_write to create/overwrite). Each edit is {original_text, new_text, replace_all?}; original_text must match exactly and uniquely (or set replace_all) — a non-unique or missing anchor is rejected so files are never corrupted. Set dry_run to preview. Alternatively pass a unified 'diff' to apply its hunks as anchored edits. Far cheaper and safer than rewriting a whole file.\n- **shell_run**(command, cwd, timeout) [capability_expand category: shell]: Execute a one-shot shell command with timeout protection. Runs in the active workspace; paths resolving outside it are refused. Reach for a structured tool first when one fits — web_fetch for anything over HTTP(S), git_query/scm_sync for git, code_search/file_find/file_read for the repo, config_get/config_set for LeapFlow's own settings — because those report typed results, while a failed shell command can only be diagnosed from its exit code and stderr. Every shell run counts as an external side effect, so a failure stops the rest of the batch and is not retried automatically.\n- **terminal_open**(command, cwd, shell) [capability_expand category: terminal]: Open a PERSISTENT shell session (REPL/dev server/watch), returning a session_id for terminal_send/read/close. Disabled unless tools.terminal_session_enabled is set. For one-shot commands use shell_run instead.\n- **terminal_send**(session_id, input, wait) [capability_expand category: terminal]: Send a line of input to a persistent terminal session and return output captured shortly after.\n- **terminal_read**(session_id, wait): Drain buffered output from a persistent terminal session (optionally waiting briefly first).\n- **terminal_close**(session_id) [capability_expand category: terminal]: Terminate a persistent terminal session and release its process group.\n- **terminal_list**(): List active persistent terminal sessions.\n- **config_list**(category, limit): List LeapFlow's own writable settings (LLM model and endpoint, daemon, memory, perception, gateway, …) with current values. Use this to discover the exact key before changing anything. Optionally narrow by `category`. This is the only correct way to inspect LeapFlow configuration — never read config files from disk.\n- **config_get**(key): Read one LeapFlow setting by key (e.g. 'llm.model', 'llm.base_url', 'daemon.log_level'), returning its current value, type, scopes, and whether a change needs a daemon restart. There is no 'llm.provider' key: provider behavior is inferred from the OpenAI-compatible 'llm.base_url'. Never read LeapFlow config files from disk — use this.\n- **config_set**(key, value, scope) [capability_expand category: config]: Change one LeapFlow setting by key, e.g. switch an OpenAI-compatible LLM with key='llm.model' and key='llm.base_url'. There is no 'llm.provider' key: provider behavior is inferred from the endpoint. Values are validated and coerced; credentials are stored in the vault automatically. Call config_list or config_get first if unsure of the key. The result states whether a `leap daemon restart` is required. Never edit LeapFlow config files directly.\n- **web_fetch**(url, select, timeout, max_bytes): Read a URL over HTTP(S) and get back extracted, context-sized content: parsed JSON for API endpoints, readable text plus links for web pages. Use this for anything on the internet — prices, docs, releases, articles — instead of running curl through shell_run: it reports real HTTP status codes, retries rate limits on its own, and is a plain read so a retry is always safe. For JSON APIs pass `select` with a dotted path (e.g. 'chart.result.0.meta') to return just that part instead of the whole payload.\n- **memory_search**(query, limit): Search agent memory for relevant past experiences, observations, and facts.\n- **memory_add**(content, kind) [capability_expand category: write]: Store a new observation or insight in memory for future reference.\n- **research_note**(kind, text): Record a compact, structured note about the current task's state so it survives context compression on long / multi-step tasks. Use for durable findings, open questions still to resolve, decisions / excluded paths, and the immediate next step. One concise sentence per note.\n- **capability_expand**(category): Fetch the full callable schema for every tool in a capability category (e.g. 'hub', 'gateway', 'desktop', 'delegate', 'file', 'memory', 'skill'). The compact tool index always lists every registered tool by name and a one-line summary, but only a static low-risk subset is directly callable each turn. If you need a tool from the index that is not yet callable, call capability_expand with its category first; the matching tools become callable in this turn. Never invent a tool name — expand the category instead.\n- **delegate_task**(goal, context) [capability_expand category: delegate]: Delegate a complex sub-task to an isolated subagent. The subagent gets a fresh context and restricted tool access. Use when a task is self-contained and can be solved independently.\n- **schedule_reentry**(kind, reason, delay_seconds, event_match, max_reentries, deadline_seconds): Register a re-entry so this task can resume later from its current orientation (findings / open questions / next step). Use when work must pause and continue after a delay (kind=time) or when a matching platform event arrives (kind=event), instead of finishing now. The research-ledger state is carried over automatically.\n- **hub_push**(skill_name, visibility, version) [capability_expand category: hub]: Push a local skill to the ModelScope Hub for sharing or backup.\n- **hub_pull**(repo_id, version) [capability_expand category: hub]: Pull a skill from the ModelScope Hub to install locally.\n- **hub_search**(query) [capability_expand category: hub]: Search for skills on the Hub by keyword or description.\n- **hub_sync**(mode, dry_run) [capability_expand category: hub]: Preview or execute sync between local skills and Hub.\n- **platform_action**(platform, action, payload, backend_kind) [capability_expand category: gateway]: Execute an exact registered business action on an external platform through LeapFlow's App Connector layer. Actions must be copied from the App Connector Capability Index and are addressed as domain.operation, e.g. im.send_message or docs.create_markdown. All business fields (chat_id, text, query, etc.) MUST be placed inside `payload`, never at the top level. Example: {\"platform\":\"feishu\",\"action\":\"im.send_message\",\"payload\":{\"chat_id\":\"oc_xxx\",\"text\":\"hello\"}}. Do not invent action names, do not use management actions such as list/guide/connect/status here.\n- **platform_connect**(action, platform, credentials, options, checkpoint) [capability_expand category: gateway]: List, guide, connect, disconnect, remove, or check status for external platforms using the App Connector management namespace. Supports REST and CLI backends. Use this for management actions such as list/guide/preflight/connect/status; use platform_action only for exact registered business actions.\n- **gateway_send**(platform, chat_id, text, thread_id) [capability_expand category: gateway]: Send a message to a connected external platform (Feishu group, Telegram chat, DingTalk conversation, etc.). Requires the platform to be connected via gateway_connect first. Use gateway_connect with action='list' to see connected platforms and available chat IDs.\n- **gateway_connect**(action, platform, credentials, options) [capability_expand category: gateway]: Connect, configure, or manage external platform integrations (Feishu, DingTalk, Telegram, Slack, Discord, etc.). Conversational flow: 1) call 'guide' to get setup steps + required fields, 2) present the steps to the user and ask for ALL required credentials in a single message, 3) call 'connect' with the credentials. Goal: complete in 1–2 user turns. NEVER include credential values in your text response.\n- **plugin_list**(): List the live plugin registry across Tool/Gateway/LLM subsystems for plugin management: which plugins are loaded, their category and fiber lifecycle state. For questions about LeapFlow's own capabilities or whether it supports X, use self_describe instead.\n- **plugin_status**(plugin_id): Get detailed status of a specific plugin: its declared category, runtime dependencies, contributed tools, and fiber lifecycle state.\n- **plugin_versions**(plugin_id): List recorded source versions and active pointer for a profile-scoped plugin.\n- **plugin_propose**(requested_capability, plugin_id, proposed_tools, test_cases, risk_level, evidence): Create a side-effect-free PluginProposal from explicit capability-gap evidence. Use this before plugin_generate when a missing capability should be reviewed. Does not call an LLM, write files, or install anything.\n- **assess_compatibility**(manifest, source_path) [capability_expand category: plugin_management]: Assess whether a foreign plugin manifest or real DSH source bundle is compatible with LeapFlow. A source bundle assessment is static: runtime_ready stays false until restricted Node discovery during plugin_install. Returns component-level verdicts and limitations.\n- **plugin_generate**(plugin_id, description, proposal_id) [capability_expand category: system]: Generate a new ToolPlugin from a natural-language capability description. The LLM produces code that conforms to the ToolPlugin Protocol; it is then rigorously validated (syntax, structure, import, protocol conformance). The generated source is stored in CAS and receives explicit content approval. It DOES NOT install the plugin — installation requires a second, mutation-specific approval via plugin_install.\n- **plugin_install**(plugin_id, code, marketplace_name, source_path, proposal_id, version_label) [capability_expand category: system]: Install a plugin from exactly one source: validated Python code, the configured Python marketplace, or a real DSH source bundle. DSH bundles run restricted Node discovery before registration; only public host tools are installed and client UI limitations are reported. Writes profile-scoped state and mutates the process-global registry. REQUIRES APPROVAL.\n- **plugin_rollback**(plugin_id, version) [capability_expand category: system]: Rollback a profile-scoped plugin to a recorded source version and reload it. REQUIRES APPROVAL.\n- **plugin_reload**(plugin_id, version_label) [capability_expand category: system]: Hot-reload a plugin at runtime. Disposes the old plugin fiber, re-imports its module, and registers a fresh instance. Existing in-flight turns are unaffected (snapshot isolation). REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_disable**(plugin_id) [capability_expand category: system]: Disable a plugin by disposing its fiber, removing its tools from the runtime registry. Cannot disable self_management itself. REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_remove**(plugin_id, delete_source) [capability_expand category: system]: Terminally remove a plugin: dispose its fiber, unregister its tools, remove reload metadata, and optionally delete its profile-scoped source file. Cannot remove self_management itself. REQUIRES APPROVAL.\n- **plugin_unquarantine**(plugin_id) [capability_expand category: plugin_management]: Restore a quarantined plugin to probation status for re-evaluation. Unfreezes the trust ledger, transitions the proposal back to PROBATION, and reloads the plugin. REQUIRES APPROVAL.\n- **plugin_enable**(plugin_id) [capability_expand category: system]: Re-enable a previously disabled plugin by reloading its module and registering a fresh instance. REQUIRES APPROVAL.\n- **self_describe**(facet): Answer questions about LeapFlow ITSELF from live runtime evidence — its identity/version/model, what tools and plugins it has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of self-awareness. Prefer this over reading LeapFlow's own source code. Pick facet=capabilities for 'what can you do / do you support X', identity for version/model, runtime for context/posture, or all for a full self-check when explicitly requested.\n- **runtime_snapshot**(): Lightweight (~150 token) flat snapshot of current runtime state: model, context budget, posture, disclosure level, turn count, cache hit rate, uptime, tool count, and pending approvals.\n- **schedule_create**(trigger_expression, instruction, execution_mode, max_retries, delivery_target) [capability_expand category: scheduler]: Create a new scheduled task. Specify a trigger expression (e.g. '30m', 'every 2h', '0 9 * * *') and the instruction to execute on each trigger. Optionally provide an execution mode and a delivery target to receive notifications.\n- **schedule_list**() [capability_expand category: scheduler]: List all scheduled tasks with their current state, trigger, and next due time.\n- **schedule_status**(task_id) [capability_expand category: scheduler]: Get detailed status and recent execution history for a scheduled task.\n- **schedule_pause**(task_id) [capability_expand category: scheduler]: Pause a scheduled task so it stops firing without being cancelled.\n- **schedule_resume**(task_id) [capability_expand category: scheduler]: Resume a paused scheduled task — re-arms it and recalculates next due time.\n- **schedule_cancel**(task_id) [capability_expand category: scheduler]: Cancel a scheduled task permanently.\n- **tool_search**(query, max_results): Search all registered tools by keyword relevance. Returns a ranked list of matching tools with name, category, and summary. Use this when you need a tool but are unsure of its exact name or category.\n- **tool_describe**(tool_name): Get the full callable schema of a single tool by exact name. Returns the tool's description, parameters, and metadata. Use after tool_search to inspect a specific tool before calling it.\n- **session_search**(query, limit) [capability_expand category: unclassified]: Search past conversation sessions in the current workspace for relevant context.\n- **session_list**(limit): List recent conversation sessions in the current workspace with stable ids, titles, dates, and summaries. Use for browsing past tasks or when user asks to see history without specific search terms. No keywords needed — returns chronological list.\n- **session_detail**(session_id, limit, offset, include_inactive) [capability_expand category: unclassified]: Read a paginated persisted transcript for one past conversation session in the current workspace. Use after session_list or session_search returns a session_id.\n- **fixture_echo**(text): Return the supplied text from a pre-built DSH package.\n- **click**(element_index) [capability_expand category: desktop]: Click a UI element by its element_index (from the latest observe_ui snapshot)\n- **get_clipboard**() [capability_expand category: desktop]: Read current clipboard text content\n- **list_apps**(filter, running_only) [capability_expand category: desktop]: List available applications on this system. Use to discover correct bundle_id before switch_app.\n- **list_windows**() [capability_expand category: desktop]: List all top-level windows with pid, window_id, title, and per-window state (minimized, on-screen). Call this first to pick the pid and window_id that observe_ui and other window tools require.\n- **observe_ui**(pid, window_id, query) [capability_expand category: desktop]: Snapshot one window's actionable UI elements, each tagged with an element_index for click/right_click/read_text. Re-observe after actions — indices belong to one snapshot. Requires the window's pid and window_id from list_windows.\n- **open_url**(url, app_id) [capability_expand category: desktop]: Open a URL in the default or specified browser\n- **read_text**(element_index) [capability_expand category: desktop]: Read the text content of a specific UI element from the latest snapshot\n- **right_click**(element_index) [capability_expand category: desktop]: Right-click a UI element to open its context menu. Returns visible menu items.\n- **screenshot**(pid, window_id) [capability_expand category: desktop]: Capture a screenshot for visual verification. With pid + window_id captures that window (works across all displays); defaults to the last observed window, or the full desktop when no window has been observed.\n- **scroll**(element_index, direction, amount, pid, window_id) [capability_expand category: desktop]: Scroll a scrollable area of a window. Omit element_index to scroll the window's focused/page scroller; pass one to scroll an exact element from the latest snapshot.\n- **select_text**(element_index) [capability_expand category: desktop]: Select all text in a UI element (focus + select-all, for subsequent copy)\n- **set_clipboard**(text) [capability_expand category: desktop]: Write text to the clipboard\n- **shortcut**(keys) [capability_expand category: desktop]: Execute a keyboard shortcut\n- **switch_app**(app_id) [capability_expand category: desktop]: Switch to an app (launch if needed, activate, verify)\n- **type_text**(text) [capability_expand category: desktop]: Type text into the currently focused element\n- **wait**(seconds) [capability_expand category: desktop]: Wait for a specified duration before continuing\n- **wait_until**(condition, pid, window_id, timeout, poll_interval) [capability_expand category: desktop]: Wait until a UI condition is met (polls UI tree). Returns elements when found or on timeout.\n- **wait_until_stable**(timeout, poll_interval, pid, window_id) [capability_expand category: desktop]: Wait until the UI stops changing (element set stabilizes across polls).\n\n## App Connector Capability Index\nLeapFlow can onboard and manage external apps through `platform_connect` and execute exact registered business actions through `platform_action`.\nFor requests about connecting, setting up, configuring, enabling, or managing a supported app, use `platform_connect` first instead of generating SDK/Webhook sample code.\n`platform_connect.action` is the management namespace: list, guide, preflight, connect, disconnect, remove, status, events_start, events_stop, events_status.\n`platform_action.action` is only for exact registered platform business actions listed below, such as `im.send_message`; never use management actions like `list` or `guide` there.\nAll business fields MUST go inside `payload`; top-level keys are only `platform`, `action`, and `payload`.\nDo not invent platform IDs or platform action names. If the needed action is not listed, ask for discovery/clarification instead of guessing.\nUse `platform_connect` with `action='guide'` and the matching `platform` to start onboarding; use `action='list'` when the app is unclear.\nWhen a pending onboarding state is present, continue from that state with `platform_connect` instead of asking the user to restate the app.\nSupported apps:\n- `api_server`: API Server (OpenAI Compatible) (category=api; backend=adapter; platform_action actions=none registered)\n- `dingtalk`: 钉钉 (DingTalk) (category=im; backend=adapter; platform_action actions=none registered)\n- `feishu`: 飞书 (Feishu/Lark) (category=im; backend=cli)\n - `calendar.create_event` payload={summary*, start_time*, end_time*, attendees} [write/high]\n - `docs.create_markdown` payload={title*, markdown*, folder_token} [write/medium]\n - `drive.search` payload={query*, limit} [read/medium]\n - `im.add_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.download_resource` payload={message_id*, file_key*, type*} [read/low]\n - `im.get_messages` payload={message_ids*} [read/low]\n - `im.list_chats` payload={page_size} [read/low]\n - `im.list_messages` payload={chat_id*} [read/low]\n - `im.list_thread_messages` payload={thread*} [read/low]\n - `im.remove_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.reply_message` payload={message_id*, text*} [send/high]\n - `im.search_chats` payload={query*, page_size} [read/low]\n - `im.search_messages` payload={query*} [read/medium]\n - `im.send_message` payload={chat_id*, text*, thread_id} [send/high]\n - `im.update_card` payload={token*, card*} [write/medium]\n - `im.update_message` payload={message_id*, text*} [write/medium]\n - `mail.search_unread` payload={query, limit} [read/high]\n - `sheets.append_row` payload={spreadsheet_token*, sheet_id*, values*} [write/medium]\n - `task.create` payload={title*, description, due_time} [write/medium]\n- `telegram`: Telegram (category=social; backend=adapter; platform_action actions=none registered)\n- `webhook`: Webhook (Generic) (category=webhook; backend=adapter; platform_action actions=none registered)\n\n## Tool Usage\nTools are normally invoked through the native function-calling mechanism, not by writing JSON in your reply\ntext. Only if the provider signals that native function calling is unavailable for this turn, fall back to a\nsingle JSON code block: `{\"name\": \"tool_name\", \"arguments\": {\"key\": \"value\"}}` — use this fallback format\nonly, never both. Only call a tool whose exact name appears in the tool index above and is currently callable\n(or reachable via `capability_expand`). Never invent, rename, alias, or guess a tool name, platform ID, or\nplatform action from argument shape or wording — if the index does not list it, it does not exist.\n`platform_connect.action` (list/guide/preflight/connect/disconnect/remove/status/events_start/events_stop/\nevents_status) is the App Connector management namespace; `platform_action.action` only accepts exact\nregistered business actions such as `im.send_message` shown in the App Connector Capability Index — never mix\nthe two namespaces. If a tool call returns an unknown/unavailable result, use the returned suggestions or\navailable names for a single retry instead of trying further variations of the same guess.\n\n**Side-effect action rule** (`platform_action` with effect=send/write/execute):\n- Call each unique action+payload **exactly once**. Never include duplicates in the same turn.\n- Once the result returns `\"completed\": true`, that action is DONE for this task. Do NOT call it again\n in any subsequent turn — immediately summarize the result for the user instead.\n- The system enforces idempotency: duplicate calls are blocked and will not execute.\n- If the user explicitly requests sending/writing multiple times, use distinct payloads per call.\n\n**Resource identifier provenance rule**:\n- NEVER fabricate, guess, or infer resource identifiers (chat_id, message_id, file_key, user_id, etc.).\n Every resource ID used in a side-effect action MUST come from a successful API response in this session.\n- If a read/list action fails (e.g. authorization error), you do NOT have valid resource IDs.\n Report the failure to the user — do NOT attempt the dependent write/send action with a guessed ID.\n- When a tool result contains `\"llm_instruction\"`, follow it exactly.\n\n## Guidelines\n1. **Direct answers first**: If you already know the answer, respond directly without tools.\n2. **Avoid redundant tool calls**: Do not call the same tool with the same arguments more than once in the same user turn. When an existing tool result already answers the user's request, stop calling tools and answer directly.\n3. **Use tools proactively**: When the user asks about files, time, system state, or needs actions performed, use the appropriate tool.\n4. **Chain tools when needed**: You can call multiple tools in sequence (e.g., list files → read file → summarize).\n5. **Handle failures gracefully**: If a tool fails, explain what went wrong and suggest alternatives. If it failed because of an unknown tool/platform/action name, retry once with an exact name from the returned suggestions, then explain rather than keep guessing.\n6. **Summarize results naturally**: After tool execution, synthesize the results into a helpful answer rather than dumping raw output.\n7. **Stay conversational**: Maintain a natural, helpful tone. Acknowledge context from earlier in the conversation.\n8. **Recall past work**:\n - Broad queries (\"之前做了什么\", \"列出任务\"): answer from the \"Recent Task History\" section already in your context. If insufficient, call session_list.\n - Specific lookups (\"上次那个配置怎么改的\"): call session_search with relevant phrases (NOT single characters).\n - Do NOT call search tools repeatedly with keyword fragments. One well-phrased call is better than ten fragmented ones.\n\n## Coding & Verification\nWhen working with code, prefer the precise built-in tools over ad-hoc shell: use `repo_map` to orient in an\nunfamiliar project, `code_search`/`file_find` to locate and `code_intel` for symbols, `edit_file` (anchored\nsearch-replace, or a unified `diff`) to change files — never rewrite a whole file to change a few lines — and\n`git_query` to inspect diffs/log. After edits, check `syntax_ok` in the result and run `test_run`/`lint_check`\nbefore declaring the work done. Batch independent read-only calls (search/read/query) into a single turn — they\nrun in parallel.\n\n## Presentation Style\n1. **Polished Markdown only**: Format user-facing answers with clean Markdown headings, short paragraphs, and concise bullets. Use tables only when they improve comparison or scanning.\n2. **Terminal-friendly layout**: Keep lines readable in a TUI; avoid dense walls of text, deeply nested lists, oversized ASCII art, or heavy visual blocks.\n3. **Elegant emphasis**: Use bold text sparingly for key terms and conclusions. Avoid excessive emojis, decorative symbols, repeated separators, or visual noise.\n4. **Theme-safe colors**: Do not emit ANSI escape codes, HTML color tags, Rich markup, or hardcoded color names. Rely on the TUI theme to render Markdown professionally.\n5. **No leaked tool protocol**: Never show tool-call JSON, internal schemas, raw observations, tool result payloads, or hidden reasoning in the final answer unless the user explicitly asks for raw/debug output. Treat any prior `{\"name\": ..., \"arguments\": ...}` blocks and `Tool result (...)` messages as internal execution context only.\n6. **Professional closure**: End with a concise conclusion or next step when helpful; avoid rambling after the useful answer is complete.\n\nWhen finished with all tool calls, respond normally without a JSON block, tool-call transcript, or process log.\n\n## Task Contract\n- Task ID: turn-1\n- Original user request: Invoke fixture_echo with the text after restart.\n- Workspace root: \n- Allowed roots: \n- Treat relative project paths as relative to the workspace root; never infer `.` as the project root when a workspace root is provided.\n- Workspace boundary is enforced by tools: do not read, search, edit, or run commands against paths outside the allowed roots unless the user explicitly requests an external path and the tool/approval policy permits it.\n- LeapFlow workspace config is optional at `/.leapflow/config.yaml`; runtime config is loaded from `~/.leapflow/config/user.yaml` and `~/.leapflow/profiles//config/*.yaml`.\n- Preserve this task contract across summarization, compression, tool loops, and memory retrieval.\n" + }, + { + "role": "system", + "content": "\n## Recent Session Summary\n- [user] Invoke fixture_echo with the text after restart.\n" + }, + { + "role": "user", + "content": "Invoke fixture_echo with the text after restart.\nInvoke fixture_echo with the text after restart." + }, + { + "role": "assistant", + "content": "", + "tool_calls": [ + "fixture_echo" + ] + }, + { + "role": "tool", + "content": "{\"ok\": true, \"echo\": \"after restart\", \"execution_id\": \"\", \"idempotency_key\": \"\", \"execution_policy\": \"read_only\", \"tool_call_id\": \"call_1\", \"execution_status\": \"completed\"}", + "tool_result": true + } + ], + "tools": [ + "capability_expand", + "code_intel", + "code_search", + "config_get", + "config_list", + "env_info", + "file_find", + "file_list", + "file_read", + "fixture_echo", + "git_query", + "lint_check", + "memory_search", + "plugin_list", + "plugin_propose", + "plugin_status", + "plugin_versions", + "repo_map", + "research_note", + "runtime_snapshot", + "schedule_reentry", + "self_describe", + "session_list", + "skill_view", + "skills_list", + "terminal_list", + "terminal_read", + "test_run", + "text_search", + "time_get", + "tool_describe", + "tool_search", + "web_fetch" + ] + }, + "responses": [ + { + "status": 200, + "content_type": "application/json", + "body": "{\"id\": \"chatcmpl-cassette\", \"object\": \"chat.completion\", \"model\": \"cassette-model\", \"choices\": [{\"index\": 0, \"message\": {\"role\": \"assistant\", \"content\": \"The DSH tool ran after restart.\"}, \"finish_reason\": \"stop\"}], \"usage\": {\"prompt_tokens\": 64, \"completion_tokens\": 16, \"total_tokens\": 80}}" + } + ] +} diff --git a/tests/_fixtures/cassettes/r6_lifecycle/cassette-model-9636485fee9d86e6.cassette.json b/tests/_fixtures/cassettes/r6_lifecycle/cassette-model-9636485fee9d86e6.cassette.json deleted file mode 100644 index f8480855..00000000 --- a/tests/_fixtures/cassettes/r6_lifecycle/cassette-model-9636485fee9d86e6.cassette.json +++ /dev/null @@ -1,80 +0,0 @@ -{ - "fingerprint": "9636485fee9d86e64deb00f18752619bcf3cac4cada17c0f1a3049512f62601b", - "note": "captured in seed mode", - "request": { - "model": "cassette-model", - "stream": false, - "messages": [ - { - "role": "system", - "content": "You are LeapFlow, an intelligent assistant that can both converse naturally and take real actions on the user's computer.\n\n## Capabilities\nThe tool index below lists **every** registered tool by name and a one-line summary — this is the complete\ncapability contract; nothing else exists. Only a subset is directly callable this turn (via native tool calling,\nnot a JSON block in your reply). If you need a tool from the index that is not yet callable, call\n`capability_expand` with its category name first — the matching tools become callable immediately after.\nWhen the user asks what LeapFlow itself supports, whether it supports plugins, or which runtime capabilities\nare available, use the live capability evidence exposed by `plugin_list` before making capability claims; report\nconfiguration-dependent or unavailable capabilities as limitations instead of inferring from documentation.\n- **text_search**(text, pattern): Search for a regex pattern in text.\n- **text_replace**(text, old, new, count) [capability_expand category: write]: Replace occurrences of a substring in text.\n- **time_get**(): Get current date and time.\n- **env_info**(): Get system environment information (OS, Python version, cwd).\n- **skills_list**(query, category, source): List available learned skills. Use when user asks about capabilities or you need a specific skill.\n- **skill_view**(name): View the full content of a specific skill document.\n- **code_intel**(path, operation): Precise document symbols (outline) for a source file: classes, functions, and methods with line ranges. Python uses an exact AST parse; other languages use a keyword-prefix scan. Prefer over file_read mode=symbols for accurate navigation before editing. Read-only.\n- **repo_map**(path): Compact project orientation for a repository root: languages, detected test/lint commands, top-level structure, entry points, manifest, and VCS branch. Call this first when entering an unfamiliar codebase. Read-only.\n- **scm_sync**(action, cwd, remote, pull_ref, push_ref, timeout) [capability_expand category: scm]: Run a typed git SCM action. Use this instead of shell_run for git pull/push/status. For 'pull origin main then push', set action='pull_then_push', remote='origin', pull_ref='main', and omit push_ref so LeapFlow pushes the current local branch.\n- **git_query**(action, cwd, ref, path, staged, max_count, stat): Read-only structured git inspection: action=diff|log|status|branch|show. Prefer over shell_run for reading repo state — output is clipped, redacted, and log/branch are parsed into structured fields. Use scm_sync for pull/push.\n- **git_write**(action, cwd, message, stage_all, name, ref, create) [capability_expand category: scm]: Mutating git actions: action=commit (message, stage_all), branch (create+switch), checkout (switch; create=true for -b). Approval-gated. Use scm_sync for pull/push and git_query for reads.\n- **test_run**(command, cwd, timeout): Run the project's test suite and return structured results (framework, passed/failed counts, failing tests). Auto-detects the runner (pytest/npm/go/cargo) or uses a configured/explicit command; executes via the governed shell. ok=true means the runner executed — see 'success' for pass/fail.\n- **lint_check**(command, cwd, timeout): Run the project's linter and return a structured clean/issue result. Auto-detects the linter (ruff/eslint/go vet/clippy) or uses a configured/explicit command; executes via the governed shell. ok=true means the linter ran — see 'clean'.\n- **file_list**(path, pattern, depth): List files and directories at a given path. Use depth=1 or depth=2 to get a recursive tree in one call instead of listing each sub-directory separately.\n- **file_read**(path, max_lines, start_line, max_chars, mode): Read text file content with adaptive context governance. For large or unfamiliar files, prefer mode='outline' or mode='symbols' first, then use mode='raw' with start_line/max_lines for the specific range you actually need. For LeapFlow's own settings, use config_list / config_get / config_set — its config files are outside the workspace and not readable here.\n- **file_write**(path, content, mode) [capability_expand category: write]: Write content to a file (overwrite or append).\n- **code_search**(pattern, patterns, path, glob, ignore_case, multiline, max_results, context_lines): Search file CONTENTS by regex pattern across a directory tree (ripgrep-backed). Requires a regex pattern. NOT for listing or browsing directory contents — use file_list for that. Prefer this over shell_run grep: faster, skips VCS/dependency/build dirs, and returns structured path:line:column matches. Batch related lookups into ONE call via `patterns` (OR-combined, single pass) instead of issuing several separate searches. Use file_read for the surrounding context of a hit.\n- **file_find**(glob, path, max_results): Find files by a recursive glob pattern under a base path (e.g. '**/test_*.py' or '*.md'). Prefer this over shell_run find; skips VCS/dependency/build dirs.\n- **edit_file**(path, edits, dry_run, diff) [capability_expand category: write]: Apply targeted, anchored search-replace edits to an EXISTING text file (use file_write to create/overwrite). Each edit is {original_text, new_text, replace_all?}; original_text must match exactly and uniquely (or set replace_all) — a non-unique or missing anchor is rejected so files are never corrupted. Set dry_run to preview. Alternatively pass a unified 'diff' to apply its hunks as anchored edits. Far cheaper and safer than rewriting a whole file.\n- **shell_run**(command, cwd, timeout) [capability_expand category: shell]: Execute a one-shot shell command with timeout protection. Runs in the active workspace; paths resolving outside it are refused. Reach for a structured tool first when one fits — web_fetch for anything over HTTP(S), git_query/scm_sync for git, code_search/file_find/file_read for the repo, config_get/config_set for LeapFlow's own settings — because those report typed results, while a failed shell command can only be diagnosed from its exit code and stderr. Every shell run counts as an external side effect, so a failure stops the rest of the batch and is not retried automatically.\n- **terminal_open**(command, cwd, shell) [capability_expand category: terminal]: Open a PERSISTENT shell session (REPL/dev server/watch), returning a session_id for terminal_send/read/close. Disabled unless tools.terminal_session_enabled is set. For one-shot commands use shell_run instead.\n- **terminal_send**(session_id, input, wait) [capability_expand category: terminal]: Send a line of input to a persistent terminal session and return output captured shortly after.\n- **terminal_read**(session_id, wait): Drain buffered output from a persistent terminal session (optionally waiting briefly first).\n- **terminal_close**(session_id) [capability_expand category: terminal]: Terminate a persistent terminal session and release its process group.\n- **terminal_list**(): List active persistent terminal sessions.\n- **config_list**(category, limit): List LeapFlow's own writable settings (LLM model and endpoint, daemon, memory, perception, gateway, …) with current values. Use this to discover the exact key before changing anything. Optionally narrow by `category`. This is the only correct way to inspect LeapFlow configuration — never read config files from disk.\n- **config_get**(key): Read one LeapFlow setting by key (e.g. 'llm.model', 'llm.base_url', 'daemon.log_level'), returning its current value, type, scopes, and whether a change needs a daemon restart. There is no 'llm.provider' key: provider behavior is inferred from the OpenAI-compatible 'llm.base_url'. Never read LeapFlow config files from disk — use this.\n- **config_set**(key, value, scope) [capability_expand category: config]: Change one LeapFlow setting by key, e.g. switch an OpenAI-compatible LLM with key='llm.model' and key='llm.base_url'. There is no 'llm.provider' key: provider behavior is inferred from the endpoint. Values are validated and coerced; credentials are stored in the vault automatically. Call config_list or config_get first if unsure of the key. The result states whether a `leap daemon restart` is required. Never edit LeapFlow config files directly.\n- **web_fetch**(url, select, timeout, max_bytes): Read a URL over HTTP(S) and get back extracted, context-sized content: parsed JSON for API endpoints, readable text plus links for web pages. Use this for anything on the internet — prices, docs, releases, articles — instead of running curl through shell_run: it reports real HTTP status codes, retries rate limits on its own, and is a plain read so a retry is always safe. For JSON APIs pass `select` with a dotted path (e.g. 'chart.result.0.meta') to return just that part instead of the whole payload.\n- **memory_search**(query, limit): Search agent memory for relevant past experiences, observations, and facts.\n- **memory_add**(content, kind) [capability_expand category: write]: Store a new observation or insight in memory for future reference.\n- **research_note**(kind, text): Record a compact, structured note about the current task's state so it survives context compression on long / multi-step tasks. Use for durable findings, open questions still to resolve, decisions / excluded paths, and the immediate next step. One concise sentence per note.\n- **capability_expand**(category): Fetch the full callable schema for every tool in a capability category (e.g. 'hub', 'gateway', 'desktop', 'delegate', 'file', 'memory', 'skill'). The compact tool index always lists every registered tool by name and a one-line summary, but only a static low-risk subset is directly callable each turn. If you need a tool from the index that is not yet callable, call capability_expand with its category first; the matching tools become callable in this turn. Never invent a tool name — expand the category instead.\n- **delegate_task**(goal, context) [capability_expand category: delegate]: Delegate a complex sub-task to an isolated subagent. The subagent gets a fresh context and restricted tool access. Use when a task is self-contained and can be solved independently.\n- **schedule_reentry**(kind, reason, delay_seconds, event_match, max_reentries, deadline_seconds): Register a re-entry so this task can resume later from its current orientation (findings / open questions / next step). Use when work must pause and continue after a delay (kind=time) or when a matching platform event arrives (kind=event), instead of finishing now. The research-ledger state is carried over automatically.\n- **hub_push**(skill_name, visibility, version) [capability_expand category: hub]: Push a local skill to the ModelScope Hub for sharing or backup.\n- **hub_pull**(repo_id, version) [capability_expand category: hub]: Pull a skill from the ModelScope Hub to install locally.\n- **hub_search**(query) [capability_expand category: hub]: Search for skills on the Hub by keyword or description.\n- **hub_sync**(mode, dry_run) [capability_expand category: hub]: Preview or execute sync between local skills and Hub.\n- **platform_action**(platform, action, payload, backend_kind) [capability_expand category: gateway]: Execute an exact registered business action on an external platform through LeapFlow's App Connector layer. Actions must be copied from the App Connector Capability Index and are addressed as domain.operation, e.g. im.send_message or docs.create_markdown. All business fields (chat_id, text, query, etc.) MUST be placed inside `payload`, never at the top level. Example: {\"platform\":\"feishu\",\"action\":\"im.send_message\",\"payload\":{\"chat_id\":\"oc_xxx\",\"text\":\"hello\"}}. Do not invent action names, do not use management actions such as list/guide/connect/status here.\n- **platform_connect**(action, platform, credentials, options, checkpoint) [capability_expand category: gateway]: List, guide, connect, disconnect, remove, or check status for external platforms using the App Connector management namespace. Supports REST and CLI backends. Use this for management actions such as list/guide/preflight/connect/status; use platform_action only for exact registered business actions.\n- **gateway_send**(platform, chat_id, text, thread_id) [capability_expand category: gateway]: Send a message to a connected external platform (Feishu group, Telegram chat, DingTalk conversation, etc.). Requires the platform to be connected via gateway_connect first. Use gateway_connect with action='list' to see connected platforms and available chat IDs.\n- **gateway_connect**(action, platform, credentials, options) [capability_expand category: gateway]: Connect, configure, or manage external platform integrations (Feishu, DingTalk, Telegram, Slack, Discord, etc.). Conversational flow: 1) call 'guide' to get setup steps + required fields, 2) present the steps to the user and ask for ALL required credentials in a single message, 3) call 'connect' with the credentials. Goal: complete in 1–2 user turns. NEVER include credential values in your text response.\n- **plugin_list**(): List the live plugin registry and cross-subsystem capability evidence. Use this before answering questions about whether LeapFlow supports plugins, self-evolution, plugin installation, hot reload, versioning, or other runtime capabilities.\n- **plugin_status**(plugin_id): Get detailed status of a specific plugin: its declared category, runtime dependencies, contributed tools, and fiber lifecycle state.\n- **plugin_versions**(plugin_id): List recorded source versions and active pointer for a profile-scoped plugin.\n- **plugin_propose**(requested_capability, plugin_id, proposed_tools, test_cases, risk_level, evidence): Create a side-effect-free PluginProposal from explicit capability-gap evidence. Use this before plugin_generate when a missing capability should be reviewed. Does not call an LLM, write files, or install anything.\n- **assess_compatibility**(manifest, source_path) [capability_expand category: plugin_management]: Assess whether a foreign plugin manifest or real DSH source bundle is compatible with LeapFlow. A source bundle assessment is static: runtime_ready stays false until restricted Node discovery during plugin_install. Returns component-level verdicts and limitations.\n- **plugin_generate**(plugin_id, description, proposal_id) [capability_expand category: system]: Generate a new ToolPlugin from a natural-language capability description. The LLM produces code that conforms to the ToolPlugin Protocol; it is then rigorously validated (syntax, structure, import, protocol conformance). The generated source is stored in CAS and receives explicit content approval. It DOES NOT install the plugin — installation requires a second, mutation-specific approval via plugin_install.\n- **plugin_install**(plugin_id, code, marketplace_name, source_path, proposal_id, version_label) [capability_expand category: system]: Install a plugin from exactly one source: validated Python code, the configured Python marketplace, or a real DSH source bundle. DSH bundles run restricted Node discovery before registration; only public host tools are installed and client UI limitations are reported. Writes profile-scoped state and mutates the process-global registry. REQUIRES APPROVAL.\n- **plugin_rollback**(plugin_id, version) [capability_expand category: system]: Rollback a profile-scoped plugin to a recorded source version and reload it. REQUIRES APPROVAL.\n- **plugin_reload**(plugin_id, version_label) [capability_expand category: system]: Hot-reload a plugin at runtime. Disposes the old plugin fiber, re-imports its module, and registers a fresh instance. Existing in-flight turns are unaffected (snapshot isolation). REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_disable**(plugin_id) [capability_expand category: system]: Disable a plugin by disposing its fiber, removing its tools from the runtime registry. Cannot disable self_management itself. REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_remove**(plugin_id, delete_source) [capability_expand category: system]: Terminally remove a plugin: dispose its fiber, unregister its tools, remove reload metadata, and optionally delete its profile-scoped source file. Cannot remove self_management itself. REQUIRES APPROVAL.\n- **plugin_unquarantine**(plugin_id) [capability_expand category: plugin_management]: Restore a quarantined plugin to probation status for re-evaluation. Unfreezes the trust ledger, transitions the proposal back to PROBATION, and reloads the plugin. REQUIRES APPROVAL.\n- **plugin_enable**(plugin_id) [capability_expand category: system]: Re-enable a previously disabled plugin by reloading its module and registering a fresh instance. REQUIRES APPROVAL.\n- **self_describe**(facet): Introspect LeapFlow's own identity, capabilities, runtime state, evolution metrics, or platform connections. Use facet='all' only when a comprehensive self-check is explicitly requested.\n- **runtime_snapshot**(): Lightweight (~150 token) flat snapshot of current runtime state: model, context budget, posture, disclosure level, turn count, cache hit rate, uptime, tool count, and pending approvals.\n- **schedule_create**(trigger_expression, instruction, execution_mode, max_retries, delivery_target) [capability_expand category: scheduler]: Create a new scheduled task. Specify a trigger expression (e.g. '30m', 'every 2h', '0 9 * * *') and the instruction to execute on each trigger. Optionally provide an execution mode and a delivery target to receive notifications.\n- **schedule_list**() [capability_expand category: scheduler]: List all scheduled tasks with their current state, trigger, and next due time.\n- **schedule_status**(task_id) [capability_expand category: scheduler]: Get detailed status and recent execution history for a scheduled task.\n- **schedule_pause**(task_id) [capability_expand category: scheduler]: Pause a scheduled task so it stops firing without being cancelled.\n- **schedule_resume**(task_id) [capability_expand category: scheduler]: Resume a paused scheduled task — re-arms it and recalculates next due time.\n- **schedule_cancel**(task_id) [capability_expand category: scheduler]: Cancel a scheduled task permanently.\n- **tool_search**(query, max_results): Search all registered tools by keyword relevance. Returns a ranked list of matching tools with name, category, and summary. Use this when you need a tool but are unsure of its exact name or category.\n- **tool_describe**(tool_name): Get the full callable schema of a single tool by exact name. Returns the tool's description, parameters, and metadata. Use after tool_search to inspect a specific tool before calling it.\n- **fixture_echo**(text): Return the supplied text from a pre-built DSH package.\n- **session_search**(query, limit) [capability_expand category: unclassified]: Search past conversation sessions in the current workspace for relevant context.\n- **session_list**(limit): List recent conversation sessions in the current workspace with stable ids, titles, dates, and summaries. Use for browsing past tasks or when user asks to see history without specific search terms. No keywords needed — returns chronological list.\n- **session_detail**(session_id, limit, offset, include_inactive) [capability_expand category: unclassified]: Read a paginated persisted transcript for one past conversation session in the current workspace. Use after session_list or session_search returns a session_id.\n- **click**(element_index) [capability_expand category: desktop]: Click a UI element by its element_index (from the latest observe_ui snapshot)\n- **get_clipboard**() [capability_expand category: desktop]: Read current clipboard text content\n- **list_apps**(filter, running_only) [capability_expand category: desktop]: List available applications on this system. Use to discover correct bundle_id before switch_app.\n- **list_windows**() [capability_expand category: desktop]: List all top-level windows with pid, window_id, title, and per-window state (minimized, on-screen). Call this first to pick the pid and window_id that observe_ui and other window tools require.\n- **observe_ui**(pid, window_id, query) [capability_expand category: desktop]: Snapshot one window's actionable UI elements, each tagged with an element_index for click/right_click/read_text. Re-observe after actions — indices belong to one snapshot. Requires the window's pid and window_id from list_windows.\n- **open_url**(url, app_id) [capability_expand category: desktop]: Open a URL in the default or specified browser\n- **read_text**(element_index) [capability_expand category: desktop]: Read the text content of a specific UI element from the latest snapshot\n- **right_click**(element_index) [capability_expand category: desktop]: Right-click a UI element to open its context menu. Returns visible menu items.\n- **screenshot**(pid, window_id) [capability_expand category: desktop]: Capture a screenshot for visual verification. With pid + window_id captures that window (works across all displays); defaults to the last observed window, or the full desktop when no window has been observed.\n- **scroll**(element_index, direction, amount, pid, window_id) [capability_expand category: desktop]: Scroll a scrollable area of a window. Omit element_index to scroll the window's focused/page scroller; pass one to scroll an exact element from the latest snapshot.\n- **select_text**(element_index) [capability_expand category: desktop]: Select all text in a UI element (focus + select-all, for subsequent copy)\n- **set_clipboard**(text) [capability_expand category: desktop]: Write text to the clipboard\n- **shortcut**(keys) [capability_expand category: desktop]: Execute a keyboard shortcut\n- **switch_app**(app_id) [capability_expand category: desktop]: Switch to an app (launch if needed, activate, verify)\n- **type_text**(text) [capability_expand category: desktop]: Type text into the currently focused element\n- **wait**(seconds) [capability_expand category: desktop]: Wait for a specified duration before continuing\n- **wait_until**(condition, pid, window_id, timeout, poll_interval) [capability_expand category: desktop]: Wait until a UI condition is met (polls UI tree). Returns elements when found or on timeout.\n- **wait_until_stable**(timeout, poll_interval, pid, window_id) [capability_expand category: desktop]: Wait until the UI stops changing (element set stabilizes across polls).\n\n## App Connector Capability Index\nLeapFlow can onboard and manage external apps through `platform_connect` and execute exact registered business actions through `platform_action`.\nFor requests about connecting, setting up, configuring, enabling, or managing a supported app, use `platform_connect` first instead of generating SDK/Webhook sample code.\n`platform_connect.action` is the management namespace: list, guide, preflight, connect, disconnect, remove, status, events_start, events_stop, events_status.\n`platform_action.action` is only for exact registered platform business actions listed below, such as `im.send_message`; never use management actions like `list` or `guide` there.\nAll business fields MUST go inside `payload`; top-level keys are only `platform`, `action`, and `payload`.\nDo not invent platform IDs or platform action names. If the needed action is not listed, ask for discovery/clarification instead of guessing.\nUse `platform_connect` with `action='guide'` and the matching `platform` to start onboarding; use `action='list'` when the app is unclear.\nWhen a pending onboarding state is present, continue from that state with `platform_connect` instead of asking the user to restate the app.\nSupported apps:\n- `api_server`: API Server (OpenAI Compatible) (category=api; backend=adapter; platform_action actions=none registered)\n- `dingtalk`: 钉钉 (DingTalk) (category=im; backend=adapter; platform_action actions=none registered)\n- `feishu`: 飞书 (Feishu/Lark) (category=im; backend=cli)\n - `calendar.create_event` payload={summary*, start_time*, end_time*, attendees} [write/high]\n - `docs.create_markdown` payload={title*, markdown*, folder_token} [write/medium]\n - `drive.search` payload={query*, limit} [read/medium]\n - `im.add_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.download_resource` payload={message_id*, file_key*, type*} [read/low]\n - `im.get_messages` payload={message_ids*} [read/low]\n - `im.list_chats` payload={page_size} [read/low]\n - `im.list_messages` payload={chat_id*} [read/low]\n - `im.list_thread_messages` payload={thread*} [read/low]\n - `im.remove_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.reply_message` payload={message_id*, text*} [send/high]\n - `im.search_chats` payload={query*, page_size} [read/low]\n - `im.search_messages` payload={query*} [read/medium]\n - `im.send_message` payload={chat_id*, text*, thread_id} [send/high]\n - `im.update_card` payload={token*, card*} [write/medium]\n - `im.update_message` payload={message_id*, text*} [write/medium]\n - `mail.search_unread` payload={query, limit} [read/high]\n - `sheets.append_row` payload={spreadsheet_token*, sheet_id*, values*} [write/medium]\n - `task.create` payload={title*, description, due_time} [write/medium]\n- `telegram`: Telegram (category=social; backend=adapter; platform_action actions=none registered)\n- `webhook`: Webhook (Generic) (category=webhook; backend=adapter; platform_action actions=none registered)\n\n## Tool Usage\nTools are normally invoked through the native function-calling mechanism, not by writing JSON in your reply\ntext. Only if the provider signals that native function calling is unavailable for this turn, fall back to a\nsingle JSON code block: `{\"name\": \"tool_name\", \"arguments\": {\"key\": \"value\"}}` — use this fallback format\nonly, never both. Only call a tool whose exact name appears in the tool index above and is currently callable\n(or reachable via `capability_expand`). Never invent, rename, alias, or guess a tool name, platform ID, or\nplatform action from argument shape or wording — if the index does not list it, it does not exist.\n`platform_connect.action` (list/guide/preflight/connect/disconnect/remove/status/events_start/events_stop/\nevents_status) is the App Connector management namespace; `platform_action.action` only accepts exact\nregistered business actions such as `im.send_message` shown in the App Connector Capability Index — never mix\nthe two namespaces. If a tool call returns an unknown/unavailable result, use the returned suggestions or\navailable names for a single retry instead of trying further variations of the same guess.\n\n**Side-effect action rule** (`platform_action` with effect=send/write/execute):\n- Call each unique action+payload **exactly once**. Never include duplicates in the same turn.\n- Once the result returns `\"completed\": true`, that action is DONE for this task. Do NOT call it again\n in any subsequent turn — immediately summarize the result for the user instead.\n- The system enforces idempotency: duplicate calls are blocked and will not execute.\n- If the user explicitly requests sending/writing multiple times, use distinct payloads per call.\n\n**Resource identifier provenance rule**:\n- NEVER fabricate, guess, or infer resource identifiers (chat_id, message_id, file_key, user_id, etc.).\n Every resource ID used in a side-effect action MUST come from a successful API response in this session.\n- If a read/list action fails (e.g. authorization error), you do NOT have valid resource IDs.\n Report the failure to the user — do NOT attempt the dependent write/send action with a guessed ID.\n- When a tool result contains `\"llm_instruction\"`, follow it exactly.\n\n## Guidelines\n1. **Direct answers first**: If you already know the answer, respond directly without tools.\n2. **Avoid redundant tool calls**: Do not call the same tool with the same arguments more than once in the same user turn. When an existing tool result already answers the user's request, stop calling tools and answer directly.\n3. **Use tools proactively**: When the user asks about files, time, system state, or needs actions performed, use the appropriate tool.\n4. **Chain tools when needed**: You can call multiple tools in sequence (e.g., list files → read file → summarize).\n5. **Handle failures gracefully**: If a tool fails, explain what went wrong and suggest alternatives. If it failed because of an unknown tool/platform/action name, retry once with an exact name from the returned suggestions, then explain rather than keep guessing.\n6. **Summarize results naturally**: After tool execution, synthesize the results into a helpful answer rather than dumping raw output.\n7. **Stay conversational**: Maintain a natural, helpful tone. Acknowledge context from earlier in the conversation.\n8. **Recall past work**:\n - Broad queries (\"之前做了什么\", \"列出任务\"): answer from the \"Recent Task History\" section already in your context. If insufficient, call session_list.\n - Specific lookups (\"上次那个配置怎么改的\"): call session_search with relevant phrases (NOT single characters).\n - Do NOT call search tools repeatedly with keyword fragments. One well-phrased call is better than ten fragmented ones.\n\n## Coding & Verification\nWhen working with code, prefer the precise built-in tools over ad-hoc shell: use `repo_map` to orient in an\nunfamiliar project, `code_search`/`file_find` to locate and `code_intel` for symbols, `edit_file` (anchored\nsearch-replace, or a unified `diff`) to change files — never rewrite a whole file to change a few lines — and\n`git_query` to inspect diffs/log. After edits, check `syntax_ok` in the result and run `test_run`/`lint_check`\nbefore declaring the work done. Batch independent read-only calls (search/read/query) into a single turn — they\nrun in parallel.\n\n## Presentation Style\n1. **Polished Markdown only**: Format user-facing answers with clean Markdown headings, short paragraphs, and concise bullets. Use tables only when they improve comparison or scanning.\n2. **Terminal-friendly layout**: Keep lines readable in a TUI; avoid dense walls of text, deeply nested lists, oversized ASCII art, or heavy visual blocks.\n3. **Elegant emphasis**: Use bold text sparingly for key terms and conclusions. Avoid excessive emojis, decorative symbols, repeated separators, or visual noise.\n4. **Theme-safe colors**: Do not emit ANSI escape codes, HTML color tags, Rich markup, or hardcoded color names. Rely on the TUI theme to render Markdown professionally.\n5. **No leaked tool protocol**: Never show tool-call JSON, internal schemas, raw observations, tool result payloads, or hidden reasoning in the final answer unless the user explicitly asks for raw/debug output. Treat any prior `{\"name\": ..., \"arguments\": ...}` blocks and `Tool result (...)` messages as internal execution context only.\n6. **Professional closure**: End with a concise conclusion or next step when helpful; avoid rambling after the useful answer is complete.\n\nWhen finished with all tool calls, respond normally without a JSON block, tool-call transcript, or process log.\n\n## Task Contract\n- Task ID: turn-2\n- Original user request: Remove the hermetic DSH echo plugin completely.\n- Workspace root: \n- Allowed roots: \n- Treat relative project paths as relative to the workspace root; never infer `.` as the project root when a workspace root is provided.\n- Workspace boundary is enforced by tools: do not read, search, edit, or run commands against paths outside the allowed roots unless the user explicitly requests an external path and the tool/approval policy permits it.\n- LeapFlow workspace config is optional at `/.leapflow/config.yaml`; runtime config is loaded from `~/.leapflow/config/user.yaml` and `~/.leapflow/profiles//config/*.yaml`.\n- Preserve this task contract across summarization, compression, tool loops, and memory retrieval.\n" - }, - { - "role": "system", - "content": "\n## Recent Session Summary\n- [assistant] The DSH tool ran after restart.\n- [user] Remove the hermetic DSH echo plugin completely.\n" - }, - { - "role": "assistant", - "content": "The DSH tool ran after restart." - }, - { - "role": "user", - "content": "Remove the hermetic DSH echo plugin completely.\nRemove the hermetic DSH echo plugin completely." - }, - { - "role": "assistant", - "content": "", - "tool_calls": [ - "plugin_remove" - ] - }, - { - "role": "tool", - "content": "{\"ok\": true, \"action\": \"remove\", \"plugin_id\": \"r6_dsh_echo\", \"state\": \"disposed\", \"source_path\": \"\", \"source_deleted\": true, \"execution_id\": \"\", \"idempotency_key\": \"\", \"execution_policy\": \"mutating_once\", \"tool_call_id\": \"call_1\", \"execution_status\": \"completed\"}", - "tool_result": true - } - ], - "tools": [ - "capability_expand", - "code_intel", - "code_search", - "config_get", - "config_list", - "env_info", - "file_find", - "file_list", - "file_read", - "fixture_echo", - "git_query", - "lint_check", - "memory_search", - "plugin_list", - "plugin_propose", - "plugin_status", - "plugin_versions", - "repo_map", - "research_note", - "runtime_snapshot", - "schedule_reentry", - "self_describe", - "session_list", - "skill_view", - "skills_list", - "terminal_list", - "terminal_read", - "test_run", - "text_search", - "time_get", - "tool_describe", - "tool_search", - "web_fetch" - ] - }, - "responses": [ - { - "status": 200, - "content_type": "application/json", - "body": "{\"id\": \"chatcmpl-cassette\", \"object\": \"chat.completion\", \"model\": \"cassette-model\", \"choices\": [{\"index\": 0, \"message\": {\"role\": \"assistant\", \"content\": \"Removed the hermetic DSH plugin.\"}, \"finish_reason\": \"stop\"}], \"usage\": {\"prompt_tokens\": 64, \"completion_tokens\": 16, \"total_tokens\": 80}}" - } - ] -} diff --git a/tests/_fixtures/cassettes/r6_lifecycle/cassette-model-9d4d96f5b3e803b7.cassette.json b/tests/_fixtures/cassettes/r6_lifecycle/cassette-model-9d4d96f5b3e803b7.cassette.json new file mode 100644 index 00000000..d1657a99 --- /dev/null +++ b/tests/_fixtures/cassettes/r6_lifecycle/cassette-model-9d4d96f5b3e803b7.cassette.json @@ -0,0 +1,68 @@ +{ + "fingerprint": "9d4d96f5b3e803b7c0b99cb833604b2a7d8cd768ca060d27f526030698296808", + "note": "captured in seed mode", + "request": { + "model": "cassette-model", + "stream": false, + "messages": [ + { + "role": "system", + "content": "You are LeapFlow, an intelligent assistant that can both converse naturally and take real actions on the user's computer.\n\n## Capabilities\nThe tool index below lists **every** registered tool by name and a one-line summary — this is the complete\ncapability contract; nothing else exists. Only a subset is directly callable this turn (via native tool calling,\nnot a JSON block in your reply). If you need a tool from the index that is not yet callable, call\n`capability_expand` with its category name first — the matching tools become callable immediately after.\nWhen the user asks about LeapFlow **itself** — its identity/version/model, what tools, plugins, or capabilities\nit has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of\nself-awareness — answer from live runtime evidence via `self_describe` (facet=capabilities for \"what can you\ndo / do you support X\", identity for version/model, runtime for context/posture, or `all` for a full self-check),\nnever by reading LeapFlow's own source code, documentation, or memory. Report configuration-dependent or\nunavailable capabilities as limitations rather than inferring them.\n- **text_search**(text, pattern): Search for a regex pattern in text.\n- **text_replace**(text, old, new, count) [capability_expand category: write]: Replace occurrences of a substring in text.\n- **time_get**(): Get current date and time.\n- **env_info**(): Get system environment information (OS, Python version, cwd).\n- **skills_list**(query, category, source): List available learned skills. Use when user asks about capabilities or you need a specific skill.\n- **skill_view**(name): View the full content of a specific skill document.\n- **skill_list_files**(name) [capability_expand category: skills]: List all support files in a skill directory (configs, templates, examples). Use before skill_file_read to discover available files.\n- **skill_file_read**(name, file_path) [capability_expand category: skills]: Read an individual support file from a skill directory. Use skill_list_files first to discover available files.\n- **code_intel**(path, operation): Precise document symbols (outline) for a source file: classes, functions, and methods with line ranges. Python uses an exact AST parse; other languages use a keyword-prefix scan. Prefer over file_read mode=symbols for accurate navigation before editing. Read-only.\n- **repo_map**(path): Compact project orientation for a repository root: languages, detected test/lint commands, top-level structure, entry points, manifest, and VCS branch. Call this first when entering an unfamiliar codebase. Read-only.\n- **scm_sync**(action, cwd, remote, pull_ref, push_ref, timeout) [capability_expand category: scm]: Run a typed git SCM action. Use this instead of shell_run for git pull/push/status. For 'pull origin main then push', set action='pull_then_push', remote='origin', pull_ref='main', and omit push_ref so LeapFlow pushes the current local branch.\n- **git_query**(action, cwd, ref, path, staged, max_count, stat): Read-only structured git inspection: action=diff|log|status|branch|show. Prefer over shell_run for reading repo state — output is clipped, redacted, and log/branch are parsed into structured fields. Use scm_sync for pull/push.\n- **git_write**(action, cwd, message, stage_all, name, ref, create) [capability_expand category: scm]: Mutating git actions: action=commit (message, stage_all), branch (create+switch), checkout (switch; create=true for -b). Approval-gated. Use scm_sync for pull/push and git_query for reads.\n- **test_run**(command, cwd, timeout): Run the project's test suite and return structured results (framework, passed/failed counts, failing tests). Auto-detects the runner (pytest/npm/go/cargo) or uses a configured/explicit command; executes via the governed shell. ok=true means the runner executed — see 'success' for pass/fail.\n- **lint_check**(command, cwd, timeout): Run the project's linter and return a structured clean/issue result. Auto-detects the linter (ruff/eslint/go vet/clippy) or uses a configured/explicit command; executes via the governed shell. ok=true means the linter ran — see 'clean'.\n- **file_list**(path, pattern, depth): List files and directories at a given path. Use depth=1 or depth=2 to get a recursive tree in one call instead of listing each sub-directory separately.\n- **file_read**(path, max_lines, start_line, max_chars, mode): Read text file content with adaptive context governance. For large or unfamiliar files, prefer mode='outline' or mode='symbols' first, then use mode='raw' with start_line/max_lines for the specific range you actually need. For LeapFlow's own settings, use config_list / config_get / config_set — its config files are outside the workspace and not readable here.\n- **file_write**(path, content, mode) [capability_expand category: write]: Write content to a file (overwrite or append).\n- **code_search**(pattern, patterns, path, glob, ignore_case, multiline, max_results, context_lines): Search file CONTENTS by regex pattern across a directory tree (ripgrep-backed). Requires a regex pattern. NOT for listing or browsing directory contents — use file_list for that. Prefer this over shell_run grep: faster, skips VCS/dependency/build dirs, and returns structured path:line:column matches. Batch related lookups into ONE call via `patterns` (OR-combined, single pass) instead of issuing several separate searches. Use file_read for the surrounding context of a hit.\n- **file_find**(glob, path, max_results): Find files by a recursive glob pattern under a base path (e.g. '**/test_*.py' or '*.md'). Prefer this over shell_run find; skips VCS/dependency/build dirs.\n- **edit_file**(path, edits, dry_run, diff) [capability_expand category: write]: Apply targeted, anchored search-replace edits to an EXISTING text file (use file_write to create/overwrite). Each edit is {original_text, new_text, replace_all?}; original_text must match exactly and uniquely (or set replace_all) — a non-unique or missing anchor is rejected so files are never corrupted. Set dry_run to preview. Alternatively pass a unified 'diff' to apply its hunks as anchored edits. Far cheaper and safer than rewriting a whole file.\n- **shell_run**(command, cwd, timeout) [capability_expand category: shell]: Execute a one-shot shell command with timeout protection. Runs in the active workspace; paths resolving outside it are refused. Reach for a structured tool first when one fits — web_fetch for anything over HTTP(S), git_query/scm_sync for git, code_search/file_find/file_read for the repo, config_get/config_set for LeapFlow's own settings — because those report typed results, while a failed shell command can only be diagnosed from its exit code and stderr. Every shell run counts as an external side effect, so a failure stops the rest of the batch and is not retried automatically.\n- **terminal_open**(command, cwd, shell) [capability_expand category: terminal]: Open a PERSISTENT shell session (REPL/dev server/watch), returning a session_id for terminal_send/read/close. Disabled unless tools.terminal_session_enabled is set. For one-shot commands use shell_run instead.\n- **terminal_send**(session_id, input, wait) [capability_expand category: terminal]: Send a line of input to a persistent terminal session and return output captured shortly after.\n- **terminal_read**(session_id, wait): Drain buffered output from a persistent terminal session (optionally waiting briefly first).\n- **terminal_close**(session_id) [capability_expand category: terminal]: Terminate a persistent terminal session and release its process group.\n- **terminal_list**(): List active persistent terminal sessions.\n- **config_list**(category, limit): List LeapFlow's own writable settings (LLM model and endpoint, daemon, memory, perception, gateway, …) with current values. Use this to discover the exact key before changing anything. Optionally narrow by `category`. This is the only correct way to inspect LeapFlow configuration — never read config files from disk.\n- **config_get**(key): Read one LeapFlow setting by key (e.g. 'llm.model', 'llm.base_url', 'daemon.log_level'), returning its current value, type, scopes, and whether a change needs a daemon restart. There is no 'llm.provider' key: provider behavior is inferred from the OpenAI-compatible 'llm.base_url'. Never read LeapFlow config files from disk — use this.\n- **config_set**(key, value, scope) [capability_expand category: config]: Change one LeapFlow setting by key, e.g. switch an OpenAI-compatible LLM with key='llm.model' and key='llm.base_url'. There is no 'llm.provider' key: provider behavior is inferred from the endpoint. Values are validated and coerced; credentials are stored in the vault automatically. Call config_list or config_get first if unsure of the key. The result states whether a `leap daemon restart` is required. Never edit LeapFlow config files directly.\n- **web_fetch**(url, select, timeout, max_bytes): Read a URL over HTTP(S) and get back extracted, context-sized content: parsed JSON for API endpoints, readable text plus links for web pages. Use this for anything on the internet — prices, docs, releases, articles — instead of running curl through shell_run: it reports real HTTP status codes, retries rate limits on its own, and is a plain read so a retry is always safe. For JSON APIs pass `select` with a dotted path (e.g. 'chart.result.0.meta') to return just that part instead of the whole payload.\n- **memory_search**(query, limit): Search agent memory for relevant past experiences, observations, and facts.\n- **memory_add**(content, kind) [capability_expand category: write]: Store a new observation or insight in memory for future reference.\n- **research_note**(kind, text): Record a compact, structured note about the current task's state so it survives context compression on long / multi-step tasks. Use for durable findings, open questions still to resolve, decisions / excluded paths, and the immediate next step. One concise sentence per note.\n- **capability_expand**(category): Fetch the full callable schema for every tool in a capability category (e.g. 'hub', 'gateway', 'desktop', 'delegate', 'file', 'memory', 'skill'). The compact tool index always lists every registered tool by name and a one-line summary, but only a static low-risk subset is directly callable each turn. If you need a tool from the index that is not yet callable, call capability_expand with its category first; the matching tools become callable in this turn. Never invent a tool name — expand the category instead.\n- **delegate_task**(goal, context) [capability_expand category: delegate]: Delegate a complex sub-task to an isolated subagent. The subagent gets a fresh context and restricted tool access. Use when a task is self-contained and can be solved independently.\n- **schedule_reentry**(kind, reason, delay_seconds, event_match, max_reentries, deadline_seconds): Register a re-entry so this task can resume later from its current orientation (findings / open questions / next step). Use when work must pause and continue after a delay (kind=time) or when a matching platform event arrives (kind=event), instead of finishing now. The research-ledger state is carried over automatically.\n- **hub_push**(skill_name, visibility, version) [capability_expand category: hub]: Push a local skill to the ModelScope Hub for sharing or backup.\n- **hub_pull**(repo_id, version) [capability_expand category: hub]: Pull a skill from the ModelScope Hub to install locally.\n- **hub_search**(query) [capability_expand category: hub]: Search for skills on the Hub by keyword or description.\n- **hub_sync**(mode, dry_run) [capability_expand category: hub]: Preview or execute sync between local skills and Hub.\n- **platform_action**(platform, action, payload, backend_kind) [capability_expand category: gateway]: Execute an exact registered business action on an external platform through LeapFlow's App Connector layer. Actions must be copied from the App Connector Capability Index and are addressed as domain.operation, e.g. im.send_message or docs.create_markdown. All business fields (chat_id, text, query, etc.) MUST be placed inside `payload`, never at the top level. Example: {\"platform\":\"feishu\",\"action\":\"im.send_message\",\"payload\":{\"chat_id\":\"oc_xxx\",\"text\":\"hello\"}}. Do not invent action names, do not use management actions such as list/guide/connect/status here.\n- **platform_connect**(action, platform, credentials, options, checkpoint) [capability_expand category: gateway]: List, guide, connect, disconnect, remove, or check status for external platforms using the App Connector management namespace. Supports REST and CLI backends. Use this for management actions such as list/guide/preflight/connect/status; use platform_action only for exact registered business actions.\n- **gateway_send**(platform, chat_id, text, thread_id) [capability_expand category: gateway]: Send a message to a connected external platform (Feishu group, Telegram chat, DingTalk conversation, etc.). Requires the platform to be connected via gateway_connect first. Use gateway_connect with action='list' to see connected platforms and available chat IDs.\n- **gateway_connect**(action, platform, credentials, options) [capability_expand category: gateway]: Connect, configure, or manage external platform integrations (Feishu, DingTalk, Telegram, Slack, Discord, etc.). Conversational flow: 1) call 'guide' to get setup steps + required fields, 2) present the steps to the user and ask for ALL required credentials in a single message, 3) call 'connect' with the credentials. Goal: complete in 1–2 user turns. NEVER include credential values in your text response.\n- **plugin_list**(): List the live plugin registry across Tool/Gateway/LLM subsystems for plugin management: which plugins are loaded, their category and fiber lifecycle state. For questions about LeapFlow's own capabilities or whether it supports X, use self_describe instead.\n- **plugin_status**(plugin_id): Get detailed status of a specific plugin: its declared category, runtime dependencies, contributed tools, and fiber lifecycle state.\n- **plugin_versions**(plugin_id): List recorded source versions and active pointer for a profile-scoped plugin.\n- **plugin_propose**(requested_capability, plugin_id, proposed_tools, test_cases, risk_level, evidence): Create a side-effect-free PluginProposal from explicit capability-gap evidence. Use this before plugin_generate when a missing capability should be reviewed. Does not call an LLM, write files, or install anything.\n- **assess_compatibility**(manifest, source_path) [capability_expand category: plugin_management]: Assess whether a foreign plugin manifest or real DSH source bundle is compatible with LeapFlow. A source bundle assessment is static: runtime_ready stays false until restricted Node discovery during plugin_install. Returns component-level verdicts and limitations.\n- **plugin_generate**(plugin_id, description, proposal_id) [capability_expand category: system]: Generate a new ToolPlugin from a natural-language capability description. The LLM produces code that conforms to the ToolPlugin Protocol; it is then rigorously validated (syntax, structure, import, protocol conformance). The generated source is stored in CAS and receives explicit content approval. It DOES NOT install the plugin — installation requires a second, mutation-specific approval via plugin_install.\n- **plugin_install**(plugin_id, code, marketplace_name, source_path, proposal_id, version_label) [capability_expand category: system]: Install a plugin from exactly one source: validated Python code, the configured Python marketplace, or a real DSH source bundle. DSH bundles run restricted Node discovery before registration; only public host tools are installed and client UI limitations are reported. Writes profile-scoped state and mutates the process-global registry. REQUIRES APPROVAL.\n- **plugin_rollback**(plugin_id, version) [capability_expand category: system]: Rollback a profile-scoped plugin to a recorded source version and reload it. REQUIRES APPROVAL.\n- **plugin_reload**(plugin_id, version_label) [capability_expand category: system]: Hot-reload a plugin at runtime. Disposes the old plugin fiber, re-imports its module, and registers a fresh instance. Existing in-flight turns are unaffected (snapshot isolation). REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_disable**(plugin_id) [capability_expand category: system]: Disable a plugin by disposing its fiber, removing its tools from the runtime registry. Cannot disable self_management itself. REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_remove**(plugin_id, delete_source) [capability_expand category: system]: Terminally remove a plugin: dispose its fiber, unregister its tools, remove reload metadata, and optionally delete its profile-scoped source file. Cannot remove self_management itself. REQUIRES APPROVAL.\n- **plugin_unquarantine**(plugin_id) [capability_expand category: plugin_management]: Restore a quarantined plugin to probation status for re-evaluation. Unfreezes the trust ledger, transitions the proposal back to PROBATION, and reloads the plugin. REQUIRES APPROVAL.\n- **plugin_enable**(plugin_id) [capability_expand category: system]: Re-enable a previously disabled plugin by reloading its module and registering a fresh instance. REQUIRES APPROVAL.\n- **self_describe**(facet): Answer questions about LeapFlow ITSELF from live runtime evidence — its identity/version/model, what tools and plugins it has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of self-awareness. Prefer this over reading LeapFlow's own source code. Pick facet=capabilities for 'what can you do / do you support X', identity for version/model, runtime for context/posture, or all for a full self-check when explicitly requested.\n- **runtime_snapshot**(): Lightweight (~150 token) flat snapshot of current runtime state: model, context budget, posture, disclosure level, turn count, cache hit rate, uptime, tool count, and pending approvals.\n- **schedule_create**(trigger_expression, instruction, execution_mode, max_retries, delivery_target) [capability_expand category: scheduler]: Create a new scheduled task. Specify a trigger expression (e.g. '30m', 'every 2h', '0 9 * * *') and the instruction to execute on each trigger. Optionally provide an execution mode and a delivery target to receive notifications.\n- **schedule_list**() [capability_expand category: scheduler]: List all scheduled tasks with their current state, trigger, and next due time.\n- **schedule_status**(task_id) [capability_expand category: scheduler]: Get detailed status and recent execution history for a scheduled task.\n- **schedule_pause**(task_id) [capability_expand category: scheduler]: Pause a scheduled task so it stops firing without being cancelled.\n- **schedule_resume**(task_id) [capability_expand category: scheduler]: Resume a paused scheduled task — re-arms it and recalculates next due time.\n- **schedule_cancel**(task_id) [capability_expand category: scheduler]: Cancel a scheduled task permanently.\n- **tool_search**(query, max_results): Search all registered tools by keyword relevance. Returns a ranked list of matching tools with name, category, and summary. Use this when you need a tool but are unsure of its exact name or category.\n- **tool_describe**(tool_name): Get the full callable schema of a single tool by exact name. Returns the tool's description, parameters, and metadata. Use after tool_search to inspect a specific tool before calling it.\n- **session_search**(query, limit) [capability_expand category: unclassified]: Search past conversation sessions in the current workspace for relevant context.\n- **session_list**(limit): List recent conversation sessions in the current workspace with stable ids, titles, dates, and summaries. Use for browsing past tasks or when user asks to see history without specific search terms. No keywords needed — returns chronological list.\n- **session_detail**(session_id, limit, offset, include_inactive) [capability_expand category: unclassified]: Read a paginated persisted transcript for one past conversation session in the current workspace. Use after session_list or session_search returns a session_id.\n- **fixture_echo**(text): Return the supplied text from a pre-built DSH package.\n- **click**(element_index) [capability_expand category: desktop]: Click a UI element by its element_index (from the latest observe_ui snapshot)\n- **get_clipboard**() [capability_expand category: desktop]: Read current clipboard text content\n- **list_apps**(filter, running_only) [capability_expand category: desktop]: List available applications on this system. Use to discover correct bundle_id before switch_app.\n- **list_windows**() [capability_expand category: desktop]: List all top-level windows with pid, window_id, title, and per-window state (minimized, on-screen). Call this first to pick the pid and window_id that observe_ui and other window tools require.\n- **observe_ui**(pid, window_id, query) [capability_expand category: desktop]: Snapshot one window's actionable UI elements, each tagged with an element_index for click/right_click/read_text. Re-observe after actions — indices belong to one snapshot. Requires the window's pid and window_id from list_windows.\n- **open_url**(url, app_id) [capability_expand category: desktop]: Open a URL in the default or specified browser\n- **read_text**(element_index) [capability_expand category: desktop]: Read the text content of a specific UI element from the latest snapshot\n- **right_click**(element_index) [capability_expand category: desktop]: Right-click a UI element to open its context menu. Returns visible menu items.\n- **screenshot**(pid, window_id) [capability_expand category: desktop]: Capture a screenshot for visual verification. With pid + window_id captures that window (works across all displays); defaults to the last observed window, or the full desktop when no window has been observed.\n- **scroll**(element_index, direction, amount, pid, window_id) [capability_expand category: desktop]: Scroll a scrollable area of a window. Omit element_index to scroll the window's focused/page scroller; pass one to scroll an exact element from the latest snapshot.\n- **select_text**(element_index) [capability_expand category: desktop]: Select all text in a UI element (focus + select-all, for subsequent copy)\n- **set_clipboard**(text) [capability_expand category: desktop]: Write text to the clipboard\n- **shortcut**(keys) [capability_expand category: desktop]: Execute a keyboard shortcut\n- **switch_app**(app_id) [capability_expand category: desktop]: Switch to an app (launch if needed, activate, verify)\n- **type_text**(text) [capability_expand category: desktop]: Type text into the currently focused element\n- **wait**(seconds) [capability_expand category: desktop]: Wait for a specified duration before continuing\n- **wait_until**(condition, pid, window_id, timeout, poll_interval) [capability_expand category: desktop]: Wait until a UI condition is met (polls UI tree). Returns elements when found or on timeout.\n- **wait_until_stable**(timeout, poll_interval, pid, window_id) [capability_expand category: desktop]: Wait until the UI stops changing (element set stabilizes across polls).\n\n## App Connector Capability Index\nLeapFlow can onboard and manage external apps through `platform_connect` and execute exact registered business actions through `platform_action`.\nFor requests about connecting, setting up, configuring, enabling, or managing a supported app, use `platform_connect` first instead of generating SDK/Webhook sample code.\n`platform_connect.action` is the management namespace: list, guide, preflight, connect, disconnect, remove, status, events_start, events_stop, events_status.\n`platform_action.action` is only for exact registered platform business actions listed below, such as `im.send_message`; never use management actions like `list` or `guide` there.\nAll business fields MUST go inside `payload`; top-level keys are only `platform`, `action`, and `payload`.\nDo not invent platform IDs or platform action names. If the needed action is not listed, ask for discovery/clarification instead of guessing.\nUse `platform_connect` with `action='guide'` and the matching `platform` to start onboarding; use `action='list'` when the app is unclear.\nWhen a pending onboarding state is present, continue from that state with `platform_connect` instead of asking the user to restate the app.\nSupported apps:\n- `api_server`: API Server (OpenAI Compatible) (category=api; backend=adapter; platform_action actions=none registered)\n- `dingtalk`: 钉钉 (DingTalk) (category=im; backend=adapter; platform_action actions=none registered)\n- `feishu`: 飞书 (Feishu/Lark) (category=im; backend=cli)\n - `calendar.create_event` payload={summary*, start_time*, end_time*, attendees} [write/high]\n - `docs.create_markdown` payload={title*, markdown*, folder_token} [write/medium]\n - `drive.search` payload={query*, limit} [read/medium]\n - `im.add_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.download_resource` payload={message_id*, file_key*, type*} [read/low]\n - `im.get_messages` payload={message_ids*} [read/low]\n - `im.list_chats` payload={page_size} [read/low]\n - `im.list_messages` payload={chat_id*} [read/low]\n - `im.list_thread_messages` payload={thread*} [read/low]\n - `im.remove_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.reply_message` payload={message_id*, text*} [send/high]\n - `im.search_chats` payload={query*, page_size} [read/low]\n - `im.search_messages` payload={query*} [read/medium]\n - `im.send_message` payload={chat_id*, text*, thread_id} [send/high]\n - `im.update_card` payload={token*, card*} [write/medium]\n - `im.update_message` payload={message_id*, text*} [write/medium]\n - `mail.search_unread` payload={query, limit} [read/high]\n - `sheets.append_row` payload={spreadsheet_token*, sheet_id*, values*} [write/medium]\n - `task.create` payload={title*, description, due_time} [write/medium]\n- `telegram`: Telegram (category=social; backend=adapter; platform_action actions=none registered)\n- `webhook`: Webhook (Generic) (category=webhook; backend=adapter; platform_action actions=none registered)\n\n## Tool Usage\nTools are normally invoked through the native function-calling mechanism, not by writing JSON in your reply\ntext. Only if the provider signals that native function calling is unavailable for this turn, fall back to a\nsingle JSON code block: `{\"name\": \"tool_name\", \"arguments\": {\"key\": \"value\"}}` — use this fallback format\nonly, never both. Only call a tool whose exact name appears in the tool index above and is currently callable\n(or reachable via `capability_expand`). Never invent, rename, alias, or guess a tool name, platform ID, or\nplatform action from argument shape or wording — if the index does not list it, it does not exist.\n`platform_connect.action` (list/guide/preflight/connect/disconnect/remove/status/events_start/events_stop/\nevents_status) is the App Connector management namespace; `platform_action.action` only accepts exact\nregistered business actions such as `im.send_message` shown in the App Connector Capability Index — never mix\nthe two namespaces. If a tool call returns an unknown/unavailable result, use the returned suggestions or\navailable names for a single retry instead of trying further variations of the same guess.\n\n**Side-effect action rule** (`platform_action` with effect=send/write/execute):\n- Call each unique action+payload **exactly once**. Never include duplicates in the same turn.\n- Once the result returns `\"completed\": true`, that action is DONE for this task. Do NOT call it again\n in any subsequent turn — immediately summarize the result for the user instead.\n- The system enforces idempotency: duplicate calls are blocked and will not execute.\n- If the user explicitly requests sending/writing multiple times, use distinct payloads per call.\n\n**Resource identifier provenance rule**:\n- NEVER fabricate, guess, or infer resource identifiers (chat_id, message_id, file_key, user_id, etc.).\n Every resource ID used in a side-effect action MUST come from a successful API response in this session.\n- If a read/list action fails (e.g. authorization error), you do NOT have valid resource IDs.\n Report the failure to the user — do NOT attempt the dependent write/send action with a guessed ID.\n- When a tool result contains `\"llm_instruction\"`, follow it exactly.\n\n## Guidelines\n1. **Direct answers first**: If you already know the answer, respond directly without tools.\n2. **Avoid redundant tool calls**: Do not call the same tool with the same arguments more than once in the same user turn. When an existing tool result already answers the user's request, stop calling tools and answer directly.\n3. **Use tools proactively**: When the user asks about files, time, system state, or needs actions performed, use the appropriate tool.\n4. **Chain tools when needed**: You can call multiple tools in sequence (e.g., list files → read file → summarize).\n5. **Handle failures gracefully**: If a tool fails, explain what went wrong and suggest alternatives. If it failed because of an unknown tool/platform/action name, retry once with an exact name from the returned suggestions, then explain rather than keep guessing.\n6. **Summarize results naturally**: After tool execution, synthesize the results into a helpful answer rather than dumping raw output.\n7. **Stay conversational**: Maintain a natural, helpful tone. Acknowledge context from earlier in the conversation.\n8. **Recall past work**:\n - Broad queries (\"之前做了什么\", \"列出任务\"): answer from the \"Recent Task History\" section already in your context. If insufficient, call session_list.\n - Specific lookups (\"上次那个配置怎么改的\"): call session_search with relevant phrases (NOT single characters).\n - Do NOT call search tools repeatedly with keyword fragments. One well-phrased call is better than ten fragmented ones.\n\n## Coding & Verification\nWhen working with code, prefer the precise built-in tools over ad-hoc shell: use `repo_map` to orient in an\nunfamiliar project, `code_search`/`file_find` to locate and `code_intel` for symbols, `edit_file` (anchored\nsearch-replace, or a unified `diff`) to change files — never rewrite a whole file to change a few lines — and\n`git_query` to inspect diffs/log. After edits, check `syntax_ok` in the result and run `test_run`/`lint_check`\nbefore declaring the work done. Batch independent read-only calls (search/read/query) into a single turn — they\nrun in parallel.\n\n## Presentation Style\n1. **Polished Markdown only**: Format user-facing answers with clean Markdown headings, short paragraphs, and concise bullets. Use tables only when they improve comparison or scanning.\n2. **Terminal-friendly layout**: Keep lines readable in a TUI; avoid dense walls of text, deeply nested lists, oversized ASCII art, or heavy visual blocks.\n3. **Elegant emphasis**: Use bold text sparingly for key terms and conclusions. Avoid excessive emojis, decorative symbols, repeated separators, or visual noise.\n4. **Theme-safe colors**: Do not emit ANSI escape codes, HTML color tags, Rich markup, or hardcoded color names. Rely on the TUI theme to render Markdown professionally.\n5. **No leaked tool protocol**: Never show tool-call JSON, internal schemas, raw observations, tool result payloads, or hidden reasoning in the final answer unless the user explicitly asks for raw/debug output. Treat any prior `{\"name\": ..., \"arguments\": ...}` blocks and `Tool result (...)` messages as internal execution context only.\n6. **Professional closure**: End with a concise conclusion or next step when helpful; avoid rambling after the useful answer is complete.\n\nWhen finished with all tool calls, respond normally without a JSON block, tool-call transcript, or process log.\n\n## Task Contract\n- Task ID: turn-2\n- Original user request: Invoke fixture_echo with the text before restart.\n- Workspace root: \n- Allowed roots: \n- Treat relative project paths as relative to the workspace root; never infer `.` as the project root when a workspace root is provided.\n- Workspace boundary is enforced by tools: do not read, search, edit, or run commands against paths outside the allowed roots unless the user explicitly requests an external path and the tool/approval policy permits it.\n- LeapFlow workspace config is optional at `/.leapflow/config.yaml`; runtime config is loaded from `~/.leapflow/config/user.yaml` and `~/.leapflow/profiles//config/*.yaml`.\n- Preserve this task contract across summarization, compression, tool loops, and memory retrieval.\n" + }, + { + "role": "system", + "content": "\n## Recent Session Summary\n- [assistant] Installed the hermetic DSH plugin.\n- [user] Invoke fixture_echo with the text before restart.\n" + }, + { + "role": "assistant", + "content": "Installed the hermetic DSH plugin." + }, + { + "role": "user", + "content": "Invoke fixture_echo with the text before restart.\nInvoke fixture_echo with the text before restart." + } + ], + "tools": [ + "capability_expand", + "code_intel", + "code_search", + "config_get", + "config_list", + "env_info", + "file_find", + "file_list", + "file_read", + "fixture_echo", + "git_query", + "lint_check", + "memory_search", + "plugin_list", + "plugin_propose", + "plugin_status", + "plugin_versions", + "repo_map", + "research_note", + "runtime_snapshot", + "schedule_reentry", + "self_describe", + "session_list", + "skill_view", + "skills_list", + "terminal_list", + "terminal_read", + "test_run", + "text_search", + "time_get", + "tool_describe", + "tool_search", + "web_fetch" + ] + }, + "responses": [ + { + "status": 200, + "content_type": "application/json", + "body": "{\"id\": \"chatcmpl-cassette\", \"object\": \"chat.completion\", \"model\": \"cassette-model\", \"choices\": [{\"index\": 0, \"message\": {\"role\": \"assistant\", \"content\": \"\", \"tool_calls\": [{\"id\": \"call_1\", \"type\": \"function\", \"function\": {\"name\": \"fixture_echo\", \"arguments\": \"{\\\"text\\\": \\\"before restart\\\"}\"}}]}, \"finish_reason\": \"tool_calls\"}], \"usage\": {\"prompt_tokens\": 64, \"completion_tokens\": 16, \"total_tokens\": 80}}" + } + ] +} diff --git a/tests/_fixtures/cassettes/r6_lifecycle/cassette-model-b3aa805eb9999a33.cassette.json b/tests/_fixtures/cassettes/r6_lifecycle/cassette-model-b3aa805eb9999a33.cassette.json new file mode 100644 index 00000000..80e61ec2 --- /dev/null +++ b/tests/_fixtures/cassettes/r6_lifecycle/cassette-model-b3aa805eb9999a33.cassette.json @@ -0,0 +1,75 @@ +{ + "fingerprint": "b3aa805eb9999a333a3641426bda019241e5d29188d1a6dadc22da891b58671a", + "note": "captured in seed mode", + "request": { + "model": "cassette-model", + "stream": false, + "messages": [ + { + "role": "system", + "content": "You are LeapFlow, an intelligent assistant that can both converse naturally and take real actions on the user's computer.\n\n## Capabilities\nThe tool index below lists **every** registered tool by name and a one-line summary — this is the complete\ncapability contract; nothing else exists. Only a subset is directly callable this turn (via native tool calling,\nnot a JSON block in your reply). If you need a tool from the index that is not yet callable, call\n`capability_expand` with its category name first — the matching tools become callable immediately after.\nWhen the user asks about LeapFlow **itself** — its identity/version/model, what tools, plugins, or capabilities\nit has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of\nself-awareness — answer from live runtime evidence via `self_describe` (facet=capabilities for \"what can you\ndo / do you support X\", identity for version/model, runtime for context/posture, or `all` for a full self-check),\nnever by reading LeapFlow's own source code, documentation, or memory. Report configuration-dependent or\nunavailable capabilities as limitations rather than inferring them.\n- **text_search**(text, pattern): Search for a regex pattern in text.\n- **text_replace**(text, old, new, count) [capability_expand category: write]: Replace occurrences of a substring in text.\n- **time_get**(): Get current date and time.\n- **env_info**(): Get system environment information (OS, Python version, cwd).\n- **skills_list**(query, category, source): List available learned skills. Use when user asks about capabilities or you need a specific skill.\n- **skill_view**(name): View the full content of a specific skill document.\n- **skill_list_files**(name) [capability_expand category: skills]: List all support files in a skill directory (configs, templates, examples). Use before skill_file_read to discover available files.\n- **skill_file_read**(name, file_path) [capability_expand category: skills]: Read an individual support file from a skill directory. Use skill_list_files first to discover available files.\n- **code_intel**(path, operation): Precise document symbols (outline) for a source file: classes, functions, and methods with line ranges. Python uses an exact AST parse; other languages use a keyword-prefix scan. Prefer over file_read mode=symbols for accurate navigation before editing. Read-only.\n- **repo_map**(path): Compact project orientation for a repository root: languages, detected test/lint commands, top-level structure, entry points, manifest, and VCS branch. Call this first when entering an unfamiliar codebase. Read-only.\n- **scm_sync**(action, cwd, remote, pull_ref, push_ref, timeout) [capability_expand category: scm]: Run a typed git SCM action. Use this instead of shell_run for git pull/push/status. For 'pull origin main then push', set action='pull_then_push', remote='origin', pull_ref='main', and omit push_ref so LeapFlow pushes the current local branch.\n- **git_query**(action, cwd, ref, path, staged, max_count, stat): Read-only structured git inspection: action=diff|log|status|branch|show. Prefer over shell_run for reading repo state — output is clipped, redacted, and log/branch are parsed into structured fields. Use scm_sync for pull/push.\n- **git_write**(action, cwd, message, stage_all, name, ref, create) [capability_expand category: scm]: Mutating git actions: action=commit (message, stage_all), branch (create+switch), checkout (switch; create=true for -b). Approval-gated. Use scm_sync for pull/push and git_query for reads.\n- **test_run**(command, cwd, timeout): Run the project's test suite and return structured results (framework, passed/failed counts, failing tests). Auto-detects the runner (pytest/npm/go/cargo) or uses a configured/explicit command; executes via the governed shell. ok=true means the runner executed — see 'success' for pass/fail.\n- **lint_check**(command, cwd, timeout): Run the project's linter and return a structured clean/issue result. Auto-detects the linter (ruff/eslint/go vet/clippy) or uses a configured/explicit command; executes via the governed shell. ok=true means the linter ran — see 'clean'.\n- **file_list**(path, pattern, depth): List files and directories at a given path. Use depth=1 or depth=2 to get a recursive tree in one call instead of listing each sub-directory separately.\n- **file_read**(path, max_lines, start_line, max_chars, mode): Read text file content with adaptive context governance. For large or unfamiliar files, prefer mode='outline' or mode='symbols' first, then use mode='raw' with start_line/max_lines for the specific range you actually need. For LeapFlow's own settings, use config_list / config_get / config_set — its config files are outside the workspace and not readable here.\n- **file_write**(path, content, mode) [capability_expand category: write]: Write content to a file (overwrite or append).\n- **code_search**(pattern, patterns, path, glob, ignore_case, multiline, max_results, context_lines): Search file CONTENTS by regex pattern across a directory tree (ripgrep-backed). Requires a regex pattern. NOT for listing or browsing directory contents — use file_list for that. Prefer this over shell_run grep: faster, skips VCS/dependency/build dirs, and returns structured path:line:column matches. Batch related lookups into ONE call via `patterns` (OR-combined, single pass) instead of issuing several separate searches. Use file_read for the surrounding context of a hit.\n- **file_find**(glob, path, max_results): Find files by a recursive glob pattern under a base path (e.g. '**/test_*.py' or '*.md'). Prefer this over shell_run find; skips VCS/dependency/build dirs.\n- **edit_file**(path, edits, dry_run, diff) [capability_expand category: write]: Apply targeted, anchored search-replace edits to an EXISTING text file (use file_write to create/overwrite). Each edit is {original_text, new_text, replace_all?}; original_text must match exactly and uniquely (or set replace_all) — a non-unique or missing anchor is rejected so files are never corrupted. Set dry_run to preview. Alternatively pass a unified 'diff' to apply its hunks as anchored edits. Far cheaper and safer than rewriting a whole file.\n- **shell_run**(command, cwd, timeout) [capability_expand category: shell]: Execute a one-shot shell command with timeout protection. Runs in the active workspace; paths resolving outside it are refused. Reach for a structured tool first when one fits — web_fetch for anything over HTTP(S), git_query/scm_sync for git, code_search/file_find/file_read for the repo, config_get/config_set for LeapFlow's own settings — because those report typed results, while a failed shell command can only be diagnosed from its exit code and stderr. Every shell run counts as an external side effect, so a failure stops the rest of the batch and is not retried automatically.\n- **terminal_open**(command, cwd, shell) [capability_expand category: terminal]: Open a PERSISTENT shell session (REPL/dev server/watch), returning a session_id for terminal_send/read/close. Disabled unless tools.terminal_session_enabled is set. For one-shot commands use shell_run instead.\n- **terminal_send**(session_id, input, wait) [capability_expand category: terminal]: Send a line of input to a persistent terminal session and return output captured shortly after.\n- **terminal_read**(session_id, wait): Drain buffered output from a persistent terminal session (optionally waiting briefly first).\n- **terminal_close**(session_id) [capability_expand category: terminal]: Terminate a persistent terminal session and release its process group.\n- **terminal_list**(): List active persistent terminal sessions.\n- **config_list**(category, limit): List LeapFlow's own writable settings (LLM model and endpoint, daemon, memory, perception, gateway, …) with current values. Use this to discover the exact key before changing anything. Optionally narrow by `category`. This is the only correct way to inspect LeapFlow configuration — never read config files from disk.\n- **config_get**(key): Read one LeapFlow setting by key (e.g. 'llm.model', 'llm.base_url', 'daemon.log_level'), returning its current value, type, scopes, and whether a change needs a daemon restart. There is no 'llm.provider' key: provider behavior is inferred from the OpenAI-compatible 'llm.base_url'. Never read LeapFlow config files from disk — use this.\n- **config_set**(key, value, scope) [capability_expand category: config]: Change one LeapFlow setting by key, e.g. switch an OpenAI-compatible LLM with key='llm.model' and key='llm.base_url'. There is no 'llm.provider' key: provider behavior is inferred from the endpoint. Values are validated and coerced; credentials are stored in the vault automatically. Call config_list or config_get first if unsure of the key. The result states whether a `leap daemon restart` is required. Never edit LeapFlow config files directly.\n- **web_fetch**(url, select, timeout, max_bytes): Read a URL over HTTP(S) and get back extracted, context-sized content: parsed JSON for API endpoints, readable text plus links for web pages. Use this for anything on the internet — prices, docs, releases, articles — instead of running curl through shell_run: it reports real HTTP status codes, retries rate limits on its own, and is a plain read so a retry is always safe. For JSON APIs pass `select` with a dotted path (e.g. 'chart.result.0.meta') to return just that part instead of the whole payload.\n- **memory_search**(query, limit): Search agent memory for relevant past experiences, observations, and facts.\n- **memory_add**(content, kind) [capability_expand category: write]: Store a new observation or insight in memory for future reference.\n- **research_note**(kind, text): Record a compact, structured note about the current task's state so it survives context compression on long / multi-step tasks. Use for durable findings, open questions still to resolve, decisions / excluded paths, and the immediate next step. One concise sentence per note.\n- **capability_expand**(category): Fetch the full callable schema for every tool in a capability category (e.g. 'hub', 'gateway', 'desktop', 'delegate', 'file', 'memory', 'skill'). The compact tool index always lists every registered tool by name and a one-line summary, but only a static low-risk subset is directly callable each turn. If you need a tool from the index that is not yet callable, call capability_expand with its category first; the matching tools become callable in this turn. Never invent a tool name — expand the category instead.\n- **delegate_task**(goal, context) [capability_expand category: delegate]: Delegate a complex sub-task to an isolated subagent. The subagent gets a fresh context and restricted tool access. Use when a task is self-contained and can be solved independently.\n- **schedule_reentry**(kind, reason, delay_seconds, event_match, max_reentries, deadline_seconds): Register a re-entry so this task can resume later from its current orientation (findings / open questions / next step). Use when work must pause and continue after a delay (kind=time) or when a matching platform event arrives (kind=event), instead of finishing now. The research-ledger state is carried over automatically.\n- **hub_push**(skill_name, visibility, version) [capability_expand category: hub]: Push a local skill to the ModelScope Hub for sharing or backup.\n- **hub_pull**(repo_id, version) [capability_expand category: hub]: Pull a skill from the ModelScope Hub to install locally.\n- **hub_search**(query) [capability_expand category: hub]: Search for skills on the Hub by keyword or description.\n- **hub_sync**(mode, dry_run) [capability_expand category: hub]: Preview or execute sync between local skills and Hub.\n- **platform_action**(platform, action, payload, backend_kind) [capability_expand category: gateway]: Execute an exact registered business action on an external platform through LeapFlow's App Connector layer. Actions must be copied from the App Connector Capability Index and are addressed as domain.operation, e.g. im.send_message or docs.create_markdown. All business fields (chat_id, text, query, etc.) MUST be placed inside `payload`, never at the top level. Example: {\"platform\":\"feishu\",\"action\":\"im.send_message\",\"payload\":{\"chat_id\":\"oc_xxx\",\"text\":\"hello\"}}. Do not invent action names, do not use management actions such as list/guide/connect/status here.\n- **platform_connect**(action, platform, credentials, options, checkpoint) [capability_expand category: gateway]: List, guide, connect, disconnect, remove, or check status for external platforms using the App Connector management namespace. Supports REST and CLI backends. Use this for management actions such as list/guide/preflight/connect/status; use platform_action only for exact registered business actions.\n- **gateway_send**(platform, chat_id, text, thread_id) [capability_expand category: gateway]: Send a message to a connected external platform (Feishu group, Telegram chat, DingTalk conversation, etc.). Requires the platform to be connected via gateway_connect first. Use gateway_connect with action='list' to see connected platforms and available chat IDs.\n- **gateway_connect**(action, platform, credentials, options) [capability_expand category: gateway]: Connect, configure, or manage external platform integrations (Feishu, DingTalk, Telegram, Slack, Discord, etc.). Conversational flow: 1) call 'guide' to get setup steps + required fields, 2) present the steps to the user and ask for ALL required credentials in a single message, 3) call 'connect' with the credentials. Goal: complete in 1–2 user turns. NEVER include credential values in your text response.\n- **plugin_list**(): List the live plugin registry across Tool/Gateway/LLM subsystems for plugin management: which plugins are loaded, their category and fiber lifecycle state. For questions about LeapFlow's own capabilities or whether it supports X, use self_describe instead.\n- **plugin_status**(plugin_id): Get detailed status of a specific plugin: its declared category, runtime dependencies, contributed tools, and fiber lifecycle state.\n- **plugin_versions**(plugin_id): List recorded source versions and active pointer for a profile-scoped plugin.\n- **plugin_propose**(requested_capability, plugin_id, proposed_tools, test_cases, risk_level, evidence): Create a side-effect-free PluginProposal from explicit capability-gap evidence. Use this before plugin_generate when a missing capability should be reviewed. Does not call an LLM, write files, or install anything.\n- **assess_compatibility**(manifest, source_path) [capability_expand category: plugin_management]: Assess whether a foreign plugin manifest or real DSH source bundle is compatible with LeapFlow. A source bundle assessment is static: runtime_ready stays false until restricted Node discovery during plugin_install. Returns component-level verdicts and limitations.\n- **plugin_generate**(plugin_id, description, proposal_id) [capability_expand category: system]: Generate a new ToolPlugin from a natural-language capability description. The LLM produces code that conforms to the ToolPlugin Protocol; it is then rigorously validated (syntax, structure, import, protocol conformance). The generated source is stored in CAS and receives explicit content approval. It DOES NOT install the plugin — installation requires a second, mutation-specific approval via plugin_install.\n- **plugin_install**(plugin_id, code, marketplace_name, source_path, proposal_id, version_label) [capability_expand category: system]: Install a plugin from exactly one source: validated Python code, the configured Python marketplace, or a real DSH source bundle. DSH bundles run restricted Node discovery before registration; only public host tools are installed and client UI limitations are reported. Writes profile-scoped state and mutates the process-global registry. REQUIRES APPROVAL.\n- **plugin_rollback**(plugin_id, version) [capability_expand category: system]: Rollback a profile-scoped plugin to a recorded source version and reload it. REQUIRES APPROVAL.\n- **plugin_reload**(plugin_id, version_label) [capability_expand category: system]: Hot-reload a plugin at runtime. Disposes the old plugin fiber, re-imports its module, and registers a fresh instance. Existing in-flight turns are unaffected (snapshot isolation). REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_disable**(plugin_id) [capability_expand category: system]: Disable a plugin by disposing its fiber, removing its tools from the runtime registry. Cannot disable self_management itself. REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_remove**(plugin_id, delete_source) [capability_expand category: system]: Terminally remove a plugin: dispose its fiber, unregister its tools, remove reload metadata, and optionally delete its profile-scoped source file. Cannot remove self_management itself. REQUIRES APPROVAL.\n- **plugin_unquarantine**(plugin_id) [capability_expand category: plugin_management]: Restore a quarantined plugin to probation status for re-evaluation. Unfreezes the trust ledger, transitions the proposal back to PROBATION, and reloads the plugin. REQUIRES APPROVAL.\n- **plugin_enable**(plugin_id) [capability_expand category: system]: Re-enable a previously disabled plugin by reloading its module and registering a fresh instance. REQUIRES APPROVAL.\n- **self_describe**(facet): Answer questions about LeapFlow ITSELF from live runtime evidence — its identity/version/model, what tools and plugins it has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of self-awareness. Prefer this over reading LeapFlow's own source code. Pick facet=capabilities for 'what can you do / do you support X', identity for version/model, runtime for context/posture, or all for a full self-check when explicitly requested.\n- **runtime_snapshot**(): Lightweight (~150 token) flat snapshot of current runtime state: model, context budget, posture, disclosure level, turn count, cache hit rate, uptime, tool count, and pending approvals.\n- **schedule_create**(trigger_expression, instruction, execution_mode, max_retries, delivery_target) [capability_expand category: scheduler]: Create a new scheduled task. Specify a trigger expression (e.g. '30m', 'every 2h', '0 9 * * *') and the instruction to execute on each trigger. Optionally provide an execution mode and a delivery target to receive notifications.\n- **schedule_list**() [capability_expand category: scheduler]: List all scheduled tasks with their current state, trigger, and next due time.\n- **schedule_status**(task_id) [capability_expand category: scheduler]: Get detailed status and recent execution history for a scheduled task.\n- **schedule_pause**(task_id) [capability_expand category: scheduler]: Pause a scheduled task so it stops firing without being cancelled.\n- **schedule_resume**(task_id) [capability_expand category: scheduler]: Resume a paused scheduled task — re-arms it and recalculates next due time.\n- **schedule_cancel**(task_id) [capability_expand category: scheduler]: Cancel a scheduled task permanently.\n- **tool_search**(query, max_results): Search all registered tools by keyword relevance. Returns a ranked list of matching tools with name, category, and summary. Use this when you need a tool but are unsure of its exact name or category.\n- **tool_describe**(tool_name): Get the full callable schema of a single tool by exact name. Returns the tool's description, parameters, and metadata. Use after tool_search to inspect a specific tool before calling it.\n- **session_search**(query, limit) [capability_expand category: unclassified]: Search past conversation sessions in the current workspace for relevant context.\n- **session_list**(limit): List recent conversation sessions in the current workspace with stable ids, titles, dates, and summaries. Use for browsing past tasks or when user asks to see history without specific search terms. No keywords needed — returns chronological list.\n- **session_detail**(session_id, limit, offset, include_inactive) [capability_expand category: unclassified]: Read a paginated persisted transcript for one past conversation session in the current workspace. Use after session_list or session_search returns a session_id.\n- **click**(element_index) [capability_expand category: desktop]: Click a UI element by its element_index (from the latest observe_ui snapshot)\n- **get_clipboard**() [capability_expand category: desktop]: Read current clipboard text content\n- **list_apps**(filter, running_only) [capability_expand category: desktop]: List available applications on this system. Use to discover correct bundle_id before switch_app.\n- **list_windows**() [capability_expand category: desktop]: List all top-level windows with pid, window_id, title, and per-window state (minimized, on-screen). Call this first to pick the pid and window_id that observe_ui and other window tools require.\n- **observe_ui**(pid, window_id, query) [capability_expand category: desktop]: Snapshot one window's actionable UI elements, each tagged with an element_index for click/right_click/read_text. Re-observe after actions — indices belong to one snapshot. Requires the window's pid and window_id from list_windows.\n- **open_url**(url, app_id) [capability_expand category: desktop]: Open a URL in the default or specified browser\n- **read_text**(element_index) [capability_expand category: desktop]: Read the text content of a specific UI element from the latest snapshot\n- **right_click**(element_index) [capability_expand category: desktop]: Right-click a UI element to open its context menu. Returns visible menu items.\n- **screenshot**(pid, window_id) [capability_expand category: desktop]: Capture a screenshot for visual verification. With pid + window_id captures that window (works across all displays); defaults to the last observed window, or the full desktop when no window has been observed.\n- **scroll**(element_index, direction, amount, pid, window_id) [capability_expand category: desktop]: Scroll a scrollable area of a window. Omit element_index to scroll the window's focused/page scroller; pass one to scroll an exact element from the latest snapshot.\n- **select_text**(element_index) [capability_expand category: desktop]: Select all text in a UI element (focus + select-all, for subsequent copy)\n- **set_clipboard**(text) [capability_expand category: desktop]: Write text to the clipboard\n- **shortcut**(keys) [capability_expand category: desktop]: Execute a keyboard shortcut\n- **switch_app**(app_id) [capability_expand category: desktop]: Switch to an app (launch if needed, activate, verify)\n- **type_text**(text) [capability_expand category: desktop]: Type text into the currently focused element\n- **wait**(seconds) [capability_expand category: desktop]: Wait for a specified duration before continuing\n- **wait_until**(condition, pid, window_id, timeout, poll_interval) [capability_expand category: desktop]: Wait until a UI condition is met (polls UI tree). Returns elements when found or on timeout.\n- **wait_until_stable**(timeout, poll_interval, pid, window_id) [capability_expand category: desktop]: Wait until the UI stops changing (element set stabilizes across polls).\n\n## App Connector Capability Index\nLeapFlow can onboard and manage external apps through `platform_connect` and execute exact registered business actions through `platform_action`.\nFor requests about connecting, setting up, configuring, enabling, or managing a supported app, use `platform_connect` first instead of generating SDK/Webhook sample code.\n`platform_connect.action` is the management namespace: list, guide, preflight, connect, disconnect, remove, status, events_start, events_stop, events_status.\n`platform_action.action` is only for exact registered platform business actions listed below, such as `im.send_message`; never use management actions like `list` or `guide` there.\nAll business fields MUST go inside `payload`; top-level keys are only `platform`, `action`, and `payload`.\nDo not invent platform IDs or platform action names. If the needed action is not listed, ask for discovery/clarification instead of guessing.\nUse `platform_connect` with `action='guide'` and the matching `platform` to start onboarding; use `action='list'` when the app is unclear.\nWhen a pending onboarding state is present, continue from that state with `platform_connect` instead of asking the user to restate the app.\nSupported apps:\n- `api_server`: API Server (OpenAI Compatible) (category=api; backend=adapter; platform_action actions=none registered)\n- `dingtalk`: 钉钉 (DingTalk) (category=im; backend=adapter; platform_action actions=none registered)\n- `feishu`: 飞书 (Feishu/Lark) (category=im; backend=cli)\n - `calendar.create_event` payload={summary*, start_time*, end_time*, attendees} [write/high]\n - `docs.create_markdown` payload={title*, markdown*, folder_token} [write/medium]\n - `drive.search` payload={query*, limit} [read/medium]\n - `im.add_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.download_resource` payload={message_id*, file_key*, type*} [read/low]\n - `im.get_messages` payload={message_ids*} [read/low]\n - `im.list_chats` payload={page_size} [read/low]\n - `im.list_messages` payload={chat_id*} [read/low]\n - `im.list_thread_messages` payload={thread*} [read/low]\n - `im.remove_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.reply_message` payload={message_id*, text*} [send/high]\n - `im.search_chats` payload={query*, page_size} [read/low]\n - `im.search_messages` payload={query*} [read/medium]\n - `im.send_message` payload={chat_id*, text*, thread_id} [send/high]\n - `im.update_card` payload={token*, card*} [write/medium]\n - `im.update_message` payload={message_id*, text*} [write/medium]\n - `mail.search_unread` payload={query, limit} [read/high]\n - `sheets.append_row` payload={spreadsheet_token*, sheet_id*, values*} [write/medium]\n - `task.create` payload={title*, description, due_time} [write/medium]\n- `telegram`: Telegram (category=social; backend=adapter; platform_action actions=none registered)\n- `webhook`: Webhook (Generic) (category=webhook; backend=adapter; platform_action actions=none registered)\n\n## Tool Usage\nTools are normally invoked through the native function-calling mechanism, not by writing JSON in your reply\ntext. Only if the provider signals that native function calling is unavailable for this turn, fall back to a\nsingle JSON code block: `{\"name\": \"tool_name\", \"arguments\": {\"key\": \"value\"}}` — use this fallback format\nonly, never both. Only call a tool whose exact name appears in the tool index above and is currently callable\n(or reachable via `capability_expand`). Never invent, rename, alias, or guess a tool name, platform ID, or\nplatform action from argument shape or wording — if the index does not list it, it does not exist.\n`platform_connect.action` (list/guide/preflight/connect/disconnect/remove/status/events_start/events_stop/\nevents_status) is the App Connector management namespace; `platform_action.action` only accepts exact\nregistered business actions such as `im.send_message` shown in the App Connector Capability Index — never mix\nthe two namespaces. If a tool call returns an unknown/unavailable result, use the returned suggestions or\navailable names for a single retry instead of trying further variations of the same guess.\n\n**Side-effect action rule** (`platform_action` with effect=send/write/execute):\n- Call each unique action+payload **exactly once**. Never include duplicates in the same turn.\n- Once the result returns `\"completed\": true`, that action is DONE for this task. Do NOT call it again\n in any subsequent turn — immediately summarize the result for the user instead.\n- The system enforces idempotency: duplicate calls are blocked and will not execute.\n- If the user explicitly requests sending/writing multiple times, use distinct payloads per call.\n\n**Resource identifier provenance rule**:\n- NEVER fabricate, guess, or infer resource identifiers (chat_id, message_id, file_key, user_id, etc.).\n Every resource ID used in a side-effect action MUST come from a successful API response in this session.\n- If a read/list action fails (e.g. authorization error), you do NOT have valid resource IDs.\n Report the failure to the user — do NOT attempt the dependent write/send action with a guessed ID.\n- When a tool result contains `\"llm_instruction\"`, follow it exactly.\n\n## Guidelines\n1. **Direct answers first**: If you already know the answer, respond directly without tools.\n2. **Avoid redundant tool calls**: Do not call the same tool with the same arguments more than once in the same user turn. When an existing tool result already answers the user's request, stop calling tools and answer directly.\n3. **Use tools proactively**: When the user asks about files, time, system state, or needs actions performed, use the appropriate tool.\n4. **Chain tools when needed**: You can call multiple tools in sequence (e.g., list files → read file → summarize).\n5. **Handle failures gracefully**: If a tool fails, explain what went wrong and suggest alternatives. If it failed because of an unknown tool/platform/action name, retry once with an exact name from the returned suggestions, then explain rather than keep guessing.\n6. **Summarize results naturally**: After tool execution, synthesize the results into a helpful answer rather than dumping raw output.\n7. **Stay conversational**: Maintain a natural, helpful tone. Acknowledge context from earlier in the conversation.\n8. **Recall past work**:\n - Broad queries (\"之前做了什么\", \"列出任务\"): answer from the \"Recent Task History\" section already in your context. If insufficient, call session_list.\n - Specific lookups (\"上次那个配置怎么改的\"): call session_search with relevant phrases (NOT single characters).\n - Do NOT call search tools repeatedly with keyword fragments. One well-phrased call is better than ten fragmented ones.\n\n## Coding & Verification\nWhen working with code, prefer the precise built-in tools over ad-hoc shell: use `repo_map` to orient in an\nunfamiliar project, `code_search`/`file_find` to locate and `code_intel` for symbols, `edit_file` (anchored\nsearch-replace, or a unified `diff`) to change files — never rewrite a whole file to change a few lines — and\n`git_query` to inspect diffs/log. After edits, check `syntax_ok` in the result and run `test_run`/`lint_check`\nbefore declaring the work done. Batch independent read-only calls (search/read/query) into a single turn — they\nrun in parallel.\n\n## Presentation Style\n1. **Polished Markdown only**: Format user-facing answers with clean Markdown headings, short paragraphs, and concise bullets. Use tables only when they improve comparison or scanning.\n2. **Terminal-friendly layout**: Keep lines readable in a TUI; avoid dense walls of text, deeply nested lists, oversized ASCII art, or heavy visual blocks.\n3. **Elegant emphasis**: Use bold text sparingly for key terms and conclusions. Avoid excessive emojis, decorative symbols, repeated separators, or visual noise.\n4. **Theme-safe colors**: Do not emit ANSI escape codes, HTML color tags, Rich markup, or hardcoded color names. Rely on the TUI theme to render Markdown professionally.\n5. **No leaked tool protocol**: Never show tool-call JSON, internal schemas, raw observations, tool result payloads, or hidden reasoning in the final answer unless the user explicitly asks for raw/debug output. Treat any prior `{\"name\": ..., \"arguments\": ...}` blocks and `Tool result (...)` messages as internal execution context only.\n6. **Professional closure**: End with a concise conclusion or next step when helpful; avoid rambling after the useful answer is complete.\n\nWhen finished with all tool calls, respond normally without a JSON block, tool-call transcript, or process log.\n\n## Task Contract\n- Task ID: turn-1\n- Original user request: Install the hermetic DSH echo plugin from this workspace.\n- Workspace root: \n- Allowed roots: \n- Treat relative project paths as relative to the workspace root; never infer `.` as the project root when a workspace root is provided.\n- Workspace boundary is enforced by tools: do not read, search, edit, or run commands against paths outside the allowed roots unless the user explicitly requests an external path and the tool/approval policy permits it.\n- LeapFlow workspace config is optional at `/.leapflow/config.yaml`; runtime config is loaded from `~/.leapflow/config/user.yaml` and `~/.leapflow/profiles//config/*.yaml`.\n- Preserve this task contract across summarization, compression, tool loops, and memory retrieval.\n" + }, + { + "role": "system", + "content": "\n## Recent Session Summary\n- [user] Install the hermetic DSH echo plugin from this workspace.\n" + }, + { + "role": "user", + "content": "Install the hermetic DSH echo plugin from this workspace.\nInstall the hermetic DSH echo plugin from this workspace." + }, + { + "role": "assistant", + "content": "", + "tool_calls": [ + "plugin_install" + ] + }, + { + "role": "tool", + "content": "{\"ok\": true, \"action\": \"install\", \"plugin_id\": \"r6_dsh_echo\", \"installed_tools\": [\"fixture_echo\"], \"state\": \"active\", \"version\": \"r6\", \"source_kind\": \"dsh_package\", \"bundle_sha256\": \"\", \"descriptor_path\": \"\", \"verdict\": \"adaptable\", \"limitations\": [], \"client_components\": [], \"execution_id\": \"\", \"idempotency_key\": \"\", \"execution_policy\": \"mutating_once\", \"tool_call_id\": \"call_1\", \"execution_status\": \"completed\"}", + "tool_result": true + } + ], + "tools": [ + "capability_expand", + "code_intel", + "code_search", + "config_get", + "config_list", + "env_info", + "file_find", + "file_list", + "file_read", + "git_query", + "lint_check", + "memory_search", + "plugin_list", + "plugin_propose", + "plugin_status", + "plugin_versions", + "repo_map", + "research_note", + "runtime_snapshot", + "schedule_reentry", + "self_describe", + "session_list", + "skill_view", + "skills_list", + "terminal_list", + "terminal_read", + "test_run", + "text_search", + "time_get", + "tool_describe", + "tool_search", + "web_fetch" + ] + }, + "responses": [ + { + "status": 200, + "content_type": "application/json", + "body": "{\"id\": \"chatcmpl-cassette\", \"object\": \"chat.completion\", \"model\": \"cassette-model\", \"choices\": [{\"index\": 0, \"message\": {\"role\": \"assistant\", \"content\": \"Installed the hermetic DSH plugin.\"}, \"finish_reason\": \"stop\"}], \"usage\": {\"prompt_tokens\": 64, \"completion_tokens\": 16, \"total_tokens\": 80}}" + } + ] +} diff --git a/tests/_fixtures/cassettes/r6_lifecycle/cassette-model-b71caaf94fa09b73.cassette.json b/tests/_fixtures/cassettes/r6_lifecycle/cassette-model-b71caaf94fa09b73.cassette.json deleted file mode 100644 index 79ca0470..00000000 --- a/tests/_fixtures/cassettes/r6_lifecycle/cassette-model-b71caaf94fa09b73.cassette.json +++ /dev/null @@ -1,64 +0,0 @@ -{ - "fingerprint": "b71caaf94fa09b73cba1f9097006389bd1bead342654e72d41f5c7e2e904866c", - "note": "captured in seed mode", - "request": { - "model": "cassette-model", - "stream": false, - "messages": [ - { - "role": "system", - "content": "You are LeapFlow, an intelligent assistant that can both converse naturally and take real actions on the user's computer.\n\n## Capabilities\nThe tool index below lists **every** registered tool by name and a one-line summary — this is the complete\ncapability contract; nothing else exists. Only a subset is directly callable this turn (via native tool calling,\nnot a JSON block in your reply). If you need a tool from the index that is not yet callable, call\n`capability_expand` with its category name first — the matching tools become callable immediately after.\nWhen the user asks what LeapFlow itself supports, whether it supports plugins, or which runtime capabilities\nare available, use the live capability evidence exposed by `plugin_list` before making capability claims; report\nconfiguration-dependent or unavailable capabilities as limitations instead of inferring from documentation.\n- **text_search**(text, pattern): Search for a regex pattern in text.\n- **text_replace**(text, old, new, count) [capability_expand category: write]: Replace occurrences of a substring in text.\n- **time_get**(): Get current date and time.\n- **env_info**(): Get system environment information (OS, Python version, cwd).\n- **skills_list**(query, category, source): List available learned skills. Use when user asks about capabilities or you need a specific skill.\n- **skill_view**(name): View the full content of a specific skill document.\n- **code_intel**(path, operation): Precise document symbols (outline) for a source file: classes, functions, and methods with line ranges. Python uses an exact AST parse; other languages use a keyword-prefix scan. Prefer over file_read mode=symbols for accurate navigation before editing. Read-only.\n- **repo_map**(path): Compact project orientation for a repository root: languages, detected test/lint commands, top-level structure, entry points, manifest, and VCS branch. Call this first when entering an unfamiliar codebase. Read-only.\n- **scm_sync**(action, cwd, remote, pull_ref, push_ref, timeout) [capability_expand category: scm]: Run a typed git SCM action. Use this instead of shell_run for git pull/push/status. For 'pull origin main then push', set action='pull_then_push', remote='origin', pull_ref='main', and omit push_ref so LeapFlow pushes the current local branch.\n- **git_query**(action, cwd, ref, path, staged, max_count, stat): Read-only structured git inspection: action=diff|log|status|branch|show. Prefer over shell_run for reading repo state — output is clipped, redacted, and log/branch are parsed into structured fields. Use scm_sync for pull/push.\n- **git_write**(action, cwd, message, stage_all, name, ref, create) [capability_expand category: scm]: Mutating git actions: action=commit (message, stage_all), branch (create+switch), checkout (switch; create=true for -b). Approval-gated. Use scm_sync for pull/push and git_query for reads.\n- **test_run**(command, cwd, timeout): Run the project's test suite and return structured results (framework, passed/failed counts, failing tests). Auto-detects the runner (pytest/npm/go/cargo) or uses a configured/explicit command; executes via the governed shell. ok=true means the runner executed — see 'success' for pass/fail.\n- **lint_check**(command, cwd, timeout): Run the project's linter and return a structured clean/issue result. Auto-detects the linter (ruff/eslint/go vet/clippy) or uses a configured/explicit command; executes via the governed shell. ok=true means the linter ran — see 'clean'.\n- **file_list**(path, pattern, depth): List files and directories at a given path. Use depth=1 or depth=2 to get a recursive tree in one call instead of listing each sub-directory separately.\n- **file_read**(path, max_lines, start_line, max_chars, mode): Read text file content with adaptive context governance. For large or unfamiliar files, prefer mode='outline' or mode='symbols' first, then use mode='raw' with start_line/max_lines for the specific range you actually need. For LeapFlow's own settings, use config_list / config_get / config_set — its config files are outside the workspace and not readable here.\n- **file_write**(path, content, mode) [capability_expand category: write]: Write content to a file (overwrite or append).\n- **code_search**(pattern, patterns, path, glob, ignore_case, multiline, max_results, context_lines): Search file CONTENTS by regex pattern across a directory tree (ripgrep-backed). Requires a regex pattern. NOT for listing or browsing directory contents — use file_list for that. Prefer this over shell_run grep: faster, skips VCS/dependency/build dirs, and returns structured path:line:column matches. Batch related lookups into ONE call via `patterns` (OR-combined, single pass) instead of issuing several separate searches. Use file_read for the surrounding context of a hit.\n- **file_find**(glob, path, max_results): Find files by a recursive glob pattern under a base path (e.g. '**/test_*.py' or '*.md'). Prefer this over shell_run find; skips VCS/dependency/build dirs.\n- **edit_file**(path, edits, dry_run, diff) [capability_expand category: write]: Apply targeted, anchored search-replace edits to an EXISTING text file (use file_write to create/overwrite). Each edit is {original_text, new_text, replace_all?}; original_text must match exactly and uniquely (or set replace_all) — a non-unique or missing anchor is rejected so files are never corrupted. Set dry_run to preview. Alternatively pass a unified 'diff' to apply its hunks as anchored edits. Far cheaper and safer than rewriting a whole file.\n- **shell_run**(command, cwd, timeout) [capability_expand category: shell]: Execute a one-shot shell command with timeout protection. Runs in the active workspace; paths resolving outside it are refused. Reach for a structured tool first when one fits — web_fetch for anything over HTTP(S), git_query/scm_sync for git, code_search/file_find/file_read for the repo, config_get/config_set for LeapFlow's own settings — because those report typed results, while a failed shell command can only be diagnosed from its exit code and stderr. Every shell run counts as an external side effect, so a failure stops the rest of the batch and is not retried automatically.\n- **terminal_open**(command, cwd, shell) [capability_expand category: terminal]: Open a PERSISTENT shell session (REPL/dev server/watch), returning a session_id for terminal_send/read/close. Disabled unless tools.terminal_session_enabled is set. For one-shot commands use shell_run instead.\n- **terminal_send**(session_id, input, wait) [capability_expand category: terminal]: Send a line of input to a persistent terminal session and return output captured shortly after.\n- **terminal_read**(session_id, wait): Drain buffered output from a persistent terminal session (optionally waiting briefly first).\n- **terminal_close**(session_id) [capability_expand category: terminal]: Terminate a persistent terminal session and release its process group.\n- **terminal_list**(): List active persistent terminal sessions.\n- **config_list**(category, limit): List LeapFlow's own writable settings (LLM model and endpoint, daemon, memory, perception, gateway, …) with current values. Use this to discover the exact key before changing anything. Optionally narrow by `category`. This is the only correct way to inspect LeapFlow configuration — never read config files from disk.\n- **config_get**(key): Read one LeapFlow setting by key (e.g. 'llm.model', 'llm.base_url', 'daemon.log_level'), returning its current value, type, scopes, and whether a change needs a daemon restart. There is no 'llm.provider' key: provider behavior is inferred from the OpenAI-compatible 'llm.base_url'. Never read LeapFlow config files from disk — use this.\n- **config_set**(key, value, scope) [capability_expand category: config]: Change one LeapFlow setting by key, e.g. switch an OpenAI-compatible LLM with key='llm.model' and key='llm.base_url'. There is no 'llm.provider' key: provider behavior is inferred from the endpoint. Values are validated and coerced; credentials are stored in the vault automatically. Call config_list or config_get first if unsure of the key. The result states whether a `leap daemon restart` is required. Never edit LeapFlow config files directly.\n- **web_fetch**(url, select, timeout, max_bytes): Read a URL over HTTP(S) and get back extracted, context-sized content: parsed JSON for API endpoints, readable text plus links for web pages. Use this for anything on the internet — prices, docs, releases, articles — instead of running curl through shell_run: it reports real HTTP status codes, retries rate limits on its own, and is a plain read so a retry is always safe. For JSON APIs pass `select` with a dotted path (e.g. 'chart.result.0.meta') to return just that part instead of the whole payload.\n- **memory_search**(query, limit): Search agent memory for relevant past experiences, observations, and facts.\n- **memory_add**(content, kind) [capability_expand category: write]: Store a new observation or insight in memory for future reference.\n- **research_note**(kind, text): Record a compact, structured note about the current task's state so it survives context compression on long / multi-step tasks. Use for durable findings, open questions still to resolve, decisions / excluded paths, and the immediate next step. One concise sentence per note.\n- **capability_expand**(category): Fetch the full callable schema for every tool in a capability category (e.g. 'hub', 'gateway', 'desktop', 'delegate', 'file', 'memory', 'skill'). The compact tool index always lists every registered tool by name and a one-line summary, but only a static low-risk subset is directly callable each turn. If you need a tool from the index that is not yet callable, call capability_expand with its category first; the matching tools become callable in this turn. Never invent a tool name — expand the category instead.\n- **delegate_task**(goal, context) [capability_expand category: delegate]: Delegate a complex sub-task to an isolated subagent. The subagent gets a fresh context and restricted tool access. Use when a task is self-contained and can be solved independently.\n- **schedule_reentry**(kind, reason, delay_seconds, event_match, max_reentries, deadline_seconds): Register a re-entry so this task can resume later from its current orientation (findings / open questions / next step). Use when work must pause and continue after a delay (kind=time) or when a matching platform event arrives (kind=event), instead of finishing now. The research-ledger state is carried over automatically.\n- **hub_push**(skill_name, visibility, version) [capability_expand category: hub]: Push a local skill to the ModelScope Hub for sharing or backup.\n- **hub_pull**(repo_id, version) [capability_expand category: hub]: Pull a skill from the ModelScope Hub to install locally.\n- **hub_search**(query) [capability_expand category: hub]: Search for skills on the Hub by keyword or description.\n- **hub_sync**(mode, dry_run) [capability_expand category: hub]: Preview or execute sync between local skills and Hub.\n- **platform_action**(platform, action, payload, backend_kind) [capability_expand category: gateway]: Execute an exact registered business action on an external platform through LeapFlow's App Connector layer. Actions must be copied from the App Connector Capability Index and are addressed as domain.operation, e.g. im.send_message or docs.create_markdown. All business fields (chat_id, text, query, etc.) MUST be placed inside `payload`, never at the top level. Example: {\"platform\":\"feishu\",\"action\":\"im.send_message\",\"payload\":{\"chat_id\":\"oc_xxx\",\"text\":\"hello\"}}. Do not invent action names, do not use management actions such as list/guide/connect/status here.\n- **platform_connect**(action, platform, credentials, options, checkpoint) [capability_expand category: gateway]: List, guide, connect, disconnect, remove, or check status for external platforms using the App Connector management namespace. Supports REST and CLI backends. Use this for management actions such as list/guide/preflight/connect/status; use platform_action only for exact registered business actions.\n- **gateway_send**(platform, chat_id, text, thread_id) [capability_expand category: gateway]: Send a message to a connected external platform (Feishu group, Telegram chat, DingTalk conversation, etc.). Requires the platform to be connected via gateway_connect first. Use gateway_connect with action='list' to see connected platforms and available chat IDs.\n- **gateway_connect**(action, platform, credentials, options) [capability_expand category: gateway]: Connect, configure, or manage external platform integrations (Feishu, DingTalk, Telegram, Slack, Discord, etc.). Conversational flow: 1) call 'guide' to get setup steps + required fields, 2) present the steps to the user and ask for ALL required credentials in a single message, 3) call 'connect' with the credentials. Goal: complete in 1–2 user turns. NEVER include credential values in your text response.\n- **plugin_list**(): List the live plugin registry and cross-subsystem capability evidence. Use this before answering questions about whether LeapFlow supports plugins, self-evolution, plugin installation, hot reload, versioning, or other runtime capabilities.\n- **plugin_status**(plugin_id): Get detailed status of a specific plugin: its declared category, runtime dependencies, contributed tools, and fiber lifecycle state.\n- **plugin_versions**(plugin_id): List recorded source versions and active pointer for a profile-scoped plugin.\n- **plugin_propose**(requested_capability, plugin_id, proposed_tools, test_cases, risk_level, evidence): Create a side-effect-free PluginProposal from explicit capability-gap evidence. Use this before plugin_generate when a missing capability should be reviewed. Does not call an LLM, write files, or install anything.\n- **assess_compatibility**(manifest, source_path) [capability_expand category: plugin_management]: Assess whether a foreign plugin manifest or real DSH source bundle is compatible with LeapFlow. A source bundle assessment is static: runtime_ready stays false until restricted Node discovery during plugin_install. Returns component-level verdicts and limitations.\n- **plugin_generate**(plugin_id, description, proposal_id) [capability_expand category: system]: Generate a new ToolPlugin from a natural-language capability description. The LLM produces code that conforms to the ToolPlugin Protocol; it is then rigorously validated (syntax, structure, import, protocol conformance). The generated source is stored in CAS and receives explicit content approval. It DOES NOT install the plugin — installation requires a second, mutation-specific approval via plugin_install.\n- **plugin_install**(plugin_id, code, marketplace_name, source_path, proposal_id, version_label) [capability_expand category: system]: Install a plugin from exactly one source: validated Python code, the configured Python marketplace, or a real DSH source bundle. DSH bundles run restricted Node discovery before registration; only public host tools are installed and client UI limitations are reported. Writes profile-scoped state and mutates the process-global registry. REQUIRES APPROVAL.\n- **plugin_rollback**(plugin_id, version) [capability_expand category: system]: Rollback a profile-scoped plugin to a recorded source version and reload it. REQUIRES APPROVAL.\n- **plugin_reload**(plugin_id, version_label) [capability_expand category: system]: Hot-reload a plugin at runtime. Disposes the old plugin fiber, re-imports its module, and registers a fresh instance. Existing in-flight turns are unaffected (snapshot isolation). REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_disable**(plugin_id) [capability_expand category: system]: Disable a plugin by disposing its fiber, removing its tools from the runtime registry. Cannot disable self_management itself. REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_remove**(plugin_id, delete_source) [capability_expand category: system]: Terminally remove a plugin: dispose its fiber, unregister its tools, remove reload metadata, and optionally delete its profile-scoped source file. Cannot remove self_management itself. REQUIRES APPROVAL.\n- **plugin_unquarantine**(plugin_id) [capability_expand category: plugin_management]: Restore a quarantined plugin to probation status for re-evaluation. Unfreezes the trust ledger, transitions the proposal back to PROBATION, and reloads the plugin. REQUIRES APPROVAL.\n- **plugin_enable**(plugin_id) [capability_expand category: system]: Re-enable a previously disabled plugin by reloading its module and registering a fresh instance. REQUIRES APPROVAL.\n- **self_describe**(facet): Introspect LeapFlow's own identity, capabilities, runtime state, evolution metrics, or platform connections. Use facet='all' only when a comprehensive self-check is explicitly requested.\n- **runtime_snapshot**(): Lightweight (~150 token) flat snapshot of current runtime state: model, context budget, posture, disclosure level, turn count, cache hit rate, uptime, tool count, and pending approvals.\n- **schedule_create**(trigger_expression, instruction, execution_mode, max_retries, delivery_target) [capability_expand category: scheduler]: Create a new scheduled task. Specify a trigger expression (e.g. '30m', 'every 2h', '0 9 * * *') and the instruction to execute on each trigger. Optionally provide an execution mode and a delivery target to receive notifications.\n- **schedule_list**() [capability_expand category: scheduler]: List all scheduled tasks with their current state, trigger, and next due time.\n- **schedule_status**(task_id) [capability_expand category: scheduler]: Get detailed status and recent execution history for a scheduled task.\n- **schedule_pause**(task_id) [capability_expand category: scheduler]: Pause a scheduled task so it stops firing without being cancelled.\n- **schedule_resume**(task_id) [capability_expand category: scheduler]: Resume a paused scheduled task — re-arms it and recalculates next due time.\n- **schedule_cancel**(task_id) [capability_expand category: scheduler]: Cancel a scheduled task permanently.\n- **tool_search**(query, max_results): Search all registered tools by keyword relevance. Returns a ranked list of matching tools with name, category, and summary. Use this when you need a tool but are unsure of its exact name or category.\n- **tool_describe**(tool_name): Get the full callable schema of a single tool by exact name. Returns the tool's description, parameters, and metadata. Use after tool_search to inspect a specific tool before calling it.\n- **session_search**(query, limit) [capability_expand category: unclassified]: Search past conversation sessions in the current workspace for relevant context.\n- **session_list**(limit): List recent conversation sessions in the current workspace with stable ids, titles, dates, and summaries. Use for browsing past tasks or when user asks to see history without specific search terms. No keywords needed — returns chronological list.\n- **session_detail**(session_id, limit, offset, include_inactive) [capability_expand category: unclassified]: Read a paginated persisted transcript for one past conversation session in the current workspace. Use after session_list or session_search returns a session_id.\n- **fixture_echo**(text): Return the supplied text from a pre-built DSH package.\n- **click**(element_index) [capability_expand category: desktop]: Click a UI element by its element_index (from the latest observe_ui snapshot)\n- **get_clipboard**() [capability_expand category: desktop]: Read current clipboard text content\n- **list_apps**(filter, running_only) [capability_expand category: desktop]: List available applications on this system. Use to discover correct bundle_id before switch_app.\n- **list_windows**() [capability_expand category: desktop]: List all top-level windows with pid, window_id, title, and per-window state (minimized, on-screen). Call this first to pick the pid and window_id that observe_ui and other window tools require.\n- **observe_ui**(pid, window_id, query) [capability_expand category: desktop]: Snapshot one window's actionable UI elements, each tagged with an element_index for click/right_click/read_text. Re-observe after actions — indices belong to one snapshot. Requires the window's pid and window_id from list_windows.\n- **open_url**(url, app_id) [capability_expand category: desktop]: Open a URL in the default or specified browser\n- **read_text**(element_index) [capability_expand category: desktop]: Read the text content of a specific UI element from the latest snapshot\n- **right_click**(element_index) [capability_expand category: desktop]: Right-click a UI element to open its context menu. Returns visible menu items.\n- **screenshot**(pid, window_id) [capability_expand category: desktop]: Capture a screenshot for visual verification. With pid + window_id captures that window (works across all displays); defaults to the last observed window, or the full desktop when no window has been observed.\n- **scroll**(element_index, direction, amount, pid, window_id) [capability_expand category: desktop]: Scroll a scrollable area of a window. Omit element_index to scroll the window's focused/page scroller; pass one to scroll an exact element from the latest snapshot.\n- **select_text**(element_index) [capability_expand category: desktop]: Select all text in a UI element (focus + select-all, for subsequent copy)\n- **set_clipboard**(text) [capability_expand category: desktop]: Write text to the clipboard\n- **shortcut**(keys) [capability_expand category: desktop]: Execute a keyboard shortcut\n- **switch_app**(app_id) [capability_expand category: desktop]: Switch to an app (launch if needed, activate, verify)\n- **type_text**(text) [capability_expand category: desktop]: Type text into the currently focused element\n- **wait**(seconds) [capability_expand category: desktop]: Wait for a specified duration before continuing\n- **wait_until**(condition, pid, window_id, timeout, poll_interval) [capability_expand category: desktop]: Wait until a UI condition is met (polls UI tree). Returns elements when found or on timeout.\n- **wait_until_stable**(timeout, poll_interval, pid, window_id) [capability_expand category: desktop]: Wait until the UI stops changing (element set stabilizes across polls).\n\n## App Connector Capability Index\nLeapFlow can onboard and manage external apps through `platform_connect` and execute exact registered business actions through `platform_action`.\nFor requests about connecting, setting up, configuring, enabling, or managing a supported app, use `platform_connect` first instead of generating SDK/Webhook sample code.\n`platform_connect.action` is the management namespace: list, guide, preflight, connect, disconnect, remove, status, events_start, events_stop, events_status.\n`platform_action.action` is only for exact registered platform business actions listed below, such as `im.send_message`; never use management actions like `list` or `guide` there.\nAll business fields MUST go inside `payload`; top-level keys are only `platform`, `action`, and `payload`.\nDo not invent platform IDs or platform action names. If the needed action is not listed, ask for discovery/clarification instead of guessing.\nUse `platform_connect` with `action='guide'` and the matching `platform` to start onboarding; use `action='list'` when the app is unclear.\nWhen a pending onboarding state is present, continue from that state with `platform_connect` instead of asking the user to restate the app.\nSupported apps:\n- `api_server`: API Server (OpenAI Compatible) (category=api; backend=adapter; platform_action actions=none registered)\n- `dingtalk`: 钉钉 (DingTalk) (category=im; backend=adapter; platform_action actions=none registered)\n- `feishu`: 飞书 (Feishu/Lark) (category=im; backend=cli)\n - `calendar.create_event` payload={summary*, start_time*, end_time*, attendees} [write/high]\n - `docs.create_markdown` payload={title*, markdown*, folder_token} [write/medium]\n - `drive.search` payload={query*, limit} [read/medium]\n - `im.add_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.download_resource` payload={message_id*, file_key*, type*} [read/low]\n - `im.get_messages` payload={message_ids*} [read/low]\n - `im.list_chats` payload={page_size} [read/low]\n - `im.list_messages` payload={chat_id*} [read/low]\n - `im.list_thread_messages` payload={thread*} [read/low]\n - `im.remove_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.reply_message` payload={message_id*, text*} [send/high]\n - `im.search_chats` payload={query*, page_size} [read/low]\n - `im.search_messages` payload={query*} [read/medium]\n - `im.send_message` payload={chat_id*, text*, thread_id} [send/high]\n - `im.update_card` payload={token*, card*} [write/medium]\n - `im.update_message` payload={message_id*, text*} [write/medium]\n - `mail.search_unread` payload={query, limit} [read/high]\n - `sheets.append_row` payload={spreadsheet_token*, sheet_id*, values*} [write/medium]\n - `task.create` payload={title*, description, due_time} [write/medium]\n- `telegram`: Telegram (category=social; backend=adapter; platform_action actions=none registered)\n- `webhook`: Webhook (Generic) (category=webhook; backend=adapter; platform_action actions=none registered)\n\n## Tool Usage\nTools are normally invoked through the native function-calling mechanism, not by writing JSON in your reply\ntext. Only if the provider signals that native function calling is unavailable for this turn, fall back to a\nsingle JSON code block: `{\"name\": \"tool_name\", \"arguments\": {\"key\": \"value\"}}` — use this fallback format\nonly, never both. Only call a tool whose exact name appears in the tool index above and is currently callable\n(or reachable via `capability_expand`). Never invent, rename, alias, or guess a tool name, platform ID, or\nplatform action from argument shape or wording — if the index does not list it, it does not exist.\n`platform_connect.action` (list/guide/preflight/connect/disconnect/remove/status/events_start/events_stop/\nevents_status) is the App Connector management namespace; `platform_action.action` only accepts exact\nregistered business actions such as `im.send_message` shown in the App Connector Capability Index — never mix\nthe two namespaces. If a tool call returns an unknown/unavailable result, use the returned suggestions or\navailable names for a single retry instead of trying further variations of the same guess.\n\n**Side-effect action rule** (`platform_action` with effect=send/write/execute):\n- Call each unique action+payload **exactly once**. Never include duplicates in the same turn.\n- Once the result returns `\"completed\": true`, that action is DONE for this task. Do NOT call it again\n in any subsequent turn — immediately summarize the result for the user instead.\n- The system enforces idempotency: duplicate calls are blocked and will not execute.\n- If the user explicitly requests sending/writing multiple times, use distinct payloads per call.\n\n**Resource identifier provenance rule**:\n- NEVER fabricate, guess, or infer resource identifiers (chat_id, message_id, file_key, user_id, etc.).\n Every resource ID used in a side-effect action MUST come from a successful API response in this session.\n- If a read/list action fails (e.g. authorization error), you do NOT have valid resource IDs.\n Report the failure to the user — do NOT attempt the dependent write/send action with a guessed ID.\n- When a tool result contains `\"llm_instruction\"`, follow it exactly.\n\n## Guidelines\n1. **Direct answers first**: If you already know the answer, respond directly without tools.\n2. **Avoid redundant tool calls**: Do not call the same tool with the same arguments more than once in the same user turn. When an existing tool result already answers the user's request, stop calling tools and answer directly.\n3. **Use tools proactively**: When the user asks about files, time, system state, or needs actions performed, use the appropriate tool.\n4. **Chain tools when needed**: You can call multiple tools in sequence (e.g., list files → read file → summarize).\n5. **Handle failures gracefully**: If a tool fails, explain what went wrong and suggest alternatives. If it failed because of an unknown tool/platform/action name, retry once with an exact name from the returned suggestions, then explain rather than keep guessing.\n6. **Summarize results naturally**: After tool execution, synthesize the results into a helpful answer rather than dumping raw output.\n7. **Stay conversational**: Maintain a natural, helpful tone. Acknowledge context from earlier in the conversation.\n8. **Recall past work**:\n - Broad queries (\"之前做了什么\", \"列出任务\"): answer from the \"Recent Task History\" section already in your context. If insufficient, call session_list.\n - Specific lookups (\"上次那个配置怎么改的\"): call session_search with relevant phrases (NOT single characters).\n - Do NOT call search tools repeatedly with keyword fragments. One well-phrased call is better than ten fragmented ones.\n\n## Coding & Verification\nWhen working with code, prefer the precise built-in tools over ad-hoc shell: use `repo_map` to orient in an\nunfamiliar project, `code_search`/`file_find` to locate and `code_intel` for symbols, `edit_file` (anchored\nsearch-replace, or a unified `diff`) to change files — never rewrite a whole file to change a few lines — and\n`git_query` to inspect diffs/log. After edits, check `syntax_ok` in the result and run `test_run`/`lint_check`\nbefore declaring the work done. Batch independent read-only calls (search/read/query) into a single turn — they\nrun in parallel.\n\n## Presentation Style\n1. **Polished Markdown only**: Format user-facing answers with clean Markdown headings, short paragraphs, and concise bullets. Use tables only when they improve comparison or scanning.\n2. **Terminal-friendly layout**: Keep lines readable in a TUI; avoid dense walls of text, deeply nested lists, oversized ASCII art, or heavy visual blocks.\n3. **Elegant emphasis**: Use bold text sparingly for key terms and conclusions. Avoid excessive emojis, decorative symbols, repeated separators, or visual noise.\n4. **Theme-safe colors**: Do not emit ANSI escape codes, HTML color tags, Rich markup, or hardcoded color names. Rely on the TUI theme to render Markdown professionally.\n5. **No leaked tool protocol**: Never show tool-call JSON, internal schemas, raw observations, tool result payloads, or hidden reasoning in the final answer unless the user explicitly asks for raw/debug output. Treat any prior `{\"name\": ..., \"arguments\": ...}` blocks and `Tool result (...)` messages as internal execution context only.\n6. **Professional closure**: End with a concise conclusion or next step when helpful; avoid rambling after the useful answer is complete.\n\nWhen finished with all tool calls, respond normally without a JSON block, tool-call transcript, or process log.\n\n## Task Contract\n- Task ID: turn-1\n- Original user request: Invoke fixture_echo with the text after restart.\n- Workspace root: \n- Allowed roots: \n- Treat relative project paths as relative to the workspace root; never infer `.` as the project root when a workspace root is provided.\n- Workspace boundary is enforced by tools: do not read, search, edit, or run commands against paths outside the allowed roots unless the user explicitly requests an external path and the tool/approval policy permits it.\n- LeapFlow workspace config is optional at `/.leapflow/config.yaml`; runtime config is loaded from `~/.leapflow/config/user.yaml` and `~/.leapflow/profiles//config/*.yaml`.\n- Preserve this task contract across summarization, compression, tool loops, and memory retrieval.\n" - }, - { - "role": "system", - "content": "\n## Recent Session Summary\n- [user] Invoke fixture_echo with the text after restart.\n" - }, - { - "role": "user", - "content": "Invoke fixture_echo with the text after restart.\nInvoke fixture_echo with the text after restart." - } - ], - "tools": [ - "capability_expand", - "code_intel", - "code_search", - "config_get", - "config_list", - "env_info", - "file_find", - "file_list", - "file_read", - "fixture_echo", - "git_query", - "lint_check", - "memory_search", - "plugin_list", - "plugin_propose", - "plugin_status", - "plugin_versions", - "repo_map", - "research_note", - "runtime_snapshot", - "schedule_reentry", - "self_describe", - "session_list", - "skill_view", - "skills_list", - "terminal_list", - "terminal_read", - "test_run", - "text_search", - "time_get", - "tool_describe", - "tool_search", - "web_fetch" - ] - }, - "responses": [ - { - "status": 200, - "content_type": "application/json", - "body": "{\"id\": \"chatcmpl-cassette\", \"object\": \"chat.completion\", \"model\": \"cassette-model\", \"choices\": [{\"index\": 0, \"message\": {\"role\": \"assistant\", \"content\": \"\", \"tool_calls\": [{\"id\": \"call_1\", \"type\": \"function\", \"function\": {\"name\": \"fixture_echo\", \"arguments\": \"{\\\"text\\\": \\\"after restart\\\"}\"}}]}, \"finish_reason\": \"tool_calls\"}], \"usage\": {\"prompt_tokens\": 64, \"completion_tokens\": 16, \"total_tokens\": 80}}" - } - ] -} diff --git a/tests/_fixtures/cassettes/r6_lifecycle/cassette-model-c6f993b9ef27fc12.cassette.json b/tests/_fixtures/cassettes/r6_lifecycle/cassette-model-c6f993b9ef27fc12.cassette.json new file mode 100644 index 00000000..d43e9a0c --- /dev/null +++ b/tests/_fixtures/cassettes/r6_lifecycle/cassette-model-c6f993b9ef27fc12.cassette.json @@ -0,0 +1,68 @@ +{ + "fingerprint": "c6f993b9ef27fc1245bd64f89303564b80f3cc780f9ca4ef2a5db6a3664f16b4", + "note": "captured in seed mode", + "request": { + "model": "cassette-model", + "stream": false, + "messages": [ + { + "role": "system", + "content": "You are LeapFlow, an intelligent assistant that can both converse naturally and take real actions on the user's computer.\n\n## Capabilities\nThe tool index below lists **every** registered tool by name and a one-line summary — this is the complete\ncapability contract; nothing else exists. Only a subset is directly callable this turn (via native tool calling,\nnot a JSON block in your reply). If you need a tool from the index that is not yet callable, call\n`capability_expand` with its category name first — the matching tools become callable immediately after.\nWhen the user asks about LeapFlow **itself** — its identity/version/model, what tools, plugins, or capabilities\nit has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of\nself-awareness — answer from live runtime evidence via `self_describe` (facet=capabilities for \"what can you\ndo / do you support X\", identity for version/model, runtime for context/posture, or `all` for a full self-check),\nnever by reading LeapFlow's own source code, documentation, or memory. Report configuration-dependent or\nunavailable capabilities as limitations rather than inferring them.\n- **text_search**(text, pattern): Search for a regex pattern in text.\n- **text_replace**(text, old, new, count) [capability_expand category: write]: Replace occurrences of a substring in text.\n- **time_get**(): Get current date and time.\n- **env_info**(): Get system environment information (OS, Python version, cwd).\n- **skills_list**(query, category, source): List available learned skills. Use when user asks about capabilities or you need a specific skill.\n- **skill_view**(name): View the full content of a specific skill document.\n- **skill_list_files**(name) [capability_expand category: skills]: List all support files in a skill directory (configs, templates, examples). Use before skill_file_read to discover available files.\n- **skill_file_read**(name, file_path) [capability_expand category: skills]: Read an individual support file from a skill directory. Use skill_list_files first to discover available files.\n- **code_intel**(path, operation): Precise document symbols (outline) for a source file: classes, functions, and methods with line ranges. Python uses an exact AST parse; other languages use a keyword-prefix scan. Prefer over file_read mode=symbols for accurate navigation before editing. Read-only.\n- **repo_map**(path): Compact project orientation for a repository root: languages, detected test/lint commands, top-level structure, entry points, manifest, and VCS branch. Call this first when entering an unfamiliar codebase. Read-only.\n- **scm_sync**(action, cwd, remote, pull_ref, push_ref, timeout) [capability_expand category: scm]: Run a typed git SCM action. Use this instead of shell_run for git pull/push/status. For 'pull origin main then push', set action='pull_then_push', remote='origin', pull_ref='main', and omit push_ref so LeapFlow pushes the current local branch.\n- **git_query**(action, cwd, ref, path, staged, max_count, stat): Read-only structured git inspection: action=diff|log|status|branch|show. Prefer over shell_run for reading repo state — output is clipped, redacted, and log/branch are parsed into structured fields. Use scm_sync for pull/push.\n- **git_write**(action, cwd, message, stage_all, name, ref, create) [capability_expand category: scm]: Mutating git actions: action=commit (message, stage_all), branch (create+switch), checkout (switch; create=true for -b). Approval-gated. Use scm_sync for pull/push and git_query for reads.\n- **test_run**(command, cwd, timeout): Run the project's test suite and return structured results (framework, passed/failed counts, failing tests). Auto-detects the runner (pytest/npm/go/cargo) or uses a configured/explicit command; executes via the governed shell. ok=true means the runner executed — see 'success' for pass/fail.\n- **lint_check**(command, cwd, timeout): Run the project's linter and return a structured clean/issue result. Auto-detects the linter (ruff/eslint/go vet/clippy) or uses a configured/explicit command; executes via the governed shell. ok=true means the linter ran — see 'clean'.\n- **file_list**(path, pattern, depth): List files and directories at a given path. Use depth=1 or depth=2 to get a recursive tree in one call instead of listing each sub-directory separately.\n- **file_read**(path, max_lines, start_line, max_chars, mode): Read text file content with adaptive context governance. For large or unfamiliar files, prefer mode='outline' or mode='symbols' first, then use mode='raw' with start_line/max_lines for the specific range you actually need. For LeapFlow's own settings, use config_list / config_get / config_set — its config files are outside the workspace and not readable here.\n- **file_write**(path, content, mode) [capability_expand category: write]: Write content to a file (overwrite or append).\n- **code_search**(pattern, patterns, path, glob, ignore_case, multiline, max_results, context_lines): Search file CONTENTS by regex pattern across a directory tree (ripgrep-backed). Requires a regex pattern. NOT for listing or browsing directory contents — use file_list for that. Prefer this over shell_run grep: faster, skips VCS/dependency/build dirs, and returns structured path:line:column matches. Batch related lookups into ONE call via `patterns` (OR-combined, single pass) instead of issuing several separate searches. Use file_read for the surrounding context of a hit.\n- **file_find**(glob, path, max_results): Find files by a recursive glob pattern under a base path (e.g. '**/test_*.py' or '*.md'). Prefer this over shell_run find; skips VCS/dependency/build dirs.\n- **edit_file**(path, edits, dry_run, diff) [capability_expand category: write]: Apply targeted, anchored search-replace edits to an EXISTING text file (use file_write to create/overwrite). Each edit is {original_text, new_text, replace_all?}; original_text must match exactly and uniquely (or set replace_all) — a non-unique or missing anchor is rejected so files are never corrupted. Set dry_run to preview. Alternatively pass a unified 'diff' to apply its hunks as anchored edits. Far cheaper and safer than rewriting a whole file.\n- **shell_run**(command, cwd, timeout) [capability_expand category: shell]: Execute a one-shot shell command with timeout protection. Runs in the active workspace; paths resolving outside it are refused. Reach for a structured tool first when one fits — web_fetch for anything over HTTP(S), git_query/scm_sync for git, code_search/file_find/file_read for the repo, config_get/config_set for LeapFlow's own settings — because those report typed results, while a failed shell command can only be diagnosed from its exit code and stderr. Every shell run counts as an external side effect, so a failure stops the rest of the batch and is not retried automatically.\n- **terminal_open**(command, cwd, shell) [capability_expand category: terminal]: Open a PERSISTENT shell session (REPL/dev server/watch), returning a session_id for terminal_send/read/close. Disabled unless tools.terminal_session_enabled is set. For one-shot commands use shell_run instead.\n- **terminal_send**(session_id, input, wait) [capability_expand category: terminal]: Send a line of input to a persistent terminal session and return output captured shortly after.\n- **terminal_read**(session_id, wait): Drain buffered output from a persistent terminal session (optionally waiting briefly first).\n- **terminal_close**(session_id) [capability_expand category: terminal]: Terminate a persistent terminal session and release its process group.\n- **terminal_list**(): List active persistent terminal sessions.\n- **config_list**(category, limit): List LeapFlow's own writable settings (LLM model and endpoint, daemon, memory, perception, gateway, …) with current values. Use this to discover the exact key before changing anything. Optionally narrow by `category`. This is the only correct way to inspect LeapFlow configuration — never read config files from disk.\n- **config_get**(key): Read one LeapFlow setting by key (e.g. 'llm.model', 'llm.base_url', 'daemon.log_level'), returning its current value, type, scopes, and whether a change needs a daemon restart. There is no 'llm.provider' key: provider behavior is inferred from the OpenAI-compatible 'llm.base_url'. Never read LeapFlow config files from disk — use this.\n- **config_set**(key, value, scope) [capability_expand category: config]: Change one LeapFlow setting by key, e.g. switch an OpenAI-compatible LLM with key='llm.model' and key='llm.base_url'. There is no 'llm.provider' key: provider behavior is inferred from the endpoint. Values are validated and coerced; credentials are stored in the vault automatically. Call config_list or config_get first if unsure of the key. The result states whether a `leap daemon restart` is required. Never edit LeapFlow config files directly.\n- **web_fetch**(url, select, timeout, max_bytes): Read a URL over HTTP(S) and get back extracted, context-sized content: parsed JSON for API endpoints, readable text plus links for web pages. Use this for anything on the internet — prices, docs, releases, articles — instead of running curl through shell_run: it reports real HTTP status codes, retries rate limits on its own, and is a plain read so a retry is always safe. For JSON APIs pass `select` with a dotted path (e.g. 'chart.result.0.meta') to return just that part instead of the whole payload.\n- **memory_search**(query, limit): Search agent memory for relevant past experiences, observations, and facts.\n- **memory_add**(content, kind) [capability_expand category: write]: Store a new observation or insight in memory for future reference.\n- **research_note**(kind, text): Record a compact, structured note about the current task's state so it survives context compression on long / multi-step tasks. Use for durable findings, open questions still to resolve, decisions / excluded paths, and the immediate next step. One concise sentence per note.\n- **capability_expand**(category): Fetch the full callable schema for every tool in a capability category (e.g. 'hub', 'gateway', 'desktop', 'delegate', 'file', 'memory', 'skill'). The compact tool index always lists every registered tool by name and a one-line summary, but only a static low-risk subset is directly callable each turn. If you need a tool from the index that is not yet callable, call capability_expand with its category first; the matching tools become callable in this turn. Never invent a tool name — expand the category instead.\n- **delegate_task**(goal, context) [capability_expand category: delegate]: Delegate a complex sub-task to an isolated subagent. The subagent gets a fresh context and restricted tool access. Use when a task is self-contained and can be solved independently.\n- **schedule_reentry**(kind, reason, delay_seconds, event_match, max_reentries, deadline_seconds): Register a re-entry so this task can resume later from its current orientation (findings / open questions / next step). Use when work must pause and continue after a delay (kind=time) or when a matching platform event arrives (kind=event), instead of finishing now. The research-ledger state is carried over automatically.\n- **hub_push**(skill_name, visibility, version) [capability_expand category: hub]: Push a local skill to the ModelScope Hub for sharing or backup.\n- **hub_pull**(repo_id, version) [capability_expand category: hub]: Pull a skill from the ModelScope Hub to install locally.\n- **hub_search**(query) [capability_expand category: hub]: Search for skills on the Hub by keyword or description.\n- **hub_sync**(mode, dry_run) [capability_expand category: hub]: Preview or execute sync between local skills and Hub.\n- **platform_action**(platform, action, payload, backend_kind) [capability_expand category: gateway]: Execute an exact registered business action on an external platform through LeapFlow's App Connector layer. Actions must be copied from the App Connector Capability Index and are addressed as domain.operation, e.g. im.send_message or docs.create_markdown. All business fields (chat_id, text, query, etc.) MUST be placed inside `payload`, never at the top level. Example: {\"platform\":\"feishu\",\"action\":\"im.send_message\",\"payload\":{\"chat_id\":\"oc_xxx\",\"text\":\"hello\"}}. Do not invent action names, do not use management actions such as list/guide/connect/status here.\n- **platform_connect**(action, platform, credentials, options, checkpoint) [capability_expand category: gateway]: List, guide, connect, disconnect, remove, or check status for external platforms using the App Connector management namespace. Supports REST and CLI backends. Use this for management actions such as list/guide/preflight/connect/status; use platform_action only for exact registered business actions.\n- **gateway_send**(platform, chat_id, text, thread_id) [capability_expand category: gateway]: Send a message to a connected external platform (Feishu group, Telegram chat, DingTalk conversation, etc.). Requires the platform to be connected via gateway_connect first. Use gateway_connect with action='list' to see connected platforms and available chat IDs.\n- **gateway_connect**(action, platform, credentials, options) [capability_expand category: gateway]: Connect, configure, or manage external platform integrations (Feishu, DingTalk, Telegram, Slack, Discord, etc.). Conversational flow: 1) call 'guide' to get setup steps + required fields, 2) present the steps to the user and ask for ALL required credentials in a single message, 3) call 'connect' with the credentials. Goal: complete in 1–2 user turns. NEVER include credential values in your text response.\n- **plugin_list**(): List the live plugin registry across Tool/Gateway/LLM subsystems for plugin management: which plugins are loaded, their category and fiber lifecycle state. For questions about LeapFlow's own capabilities or whether it supports X, use self_describe instead.\n- **plugin_status**(plugin_id): Get detailed status of a specific plugin: its declared category, runtime dependencies, contributed tools, and fiber lifecycle state.\n- **plugin_versions**(plugin_id): List recorded source versions and active pointer for a profile-scoped plugin.\n- **plugin_propose**(requested_capability, plugin_id, proposed_tools, test_cases, risk_level, evidence): Create a side-effect-free PluginProposal from explicit capability-gap evidence. Use this before plugin_generate when a missing capability should be reviewed. Does not call an LLM, write files, or install anything.\n- **assess_compatibility**(manifest, source_path) [capability_expand category: plugin_management]: Assess whether a foreign plugin manifest or real DSH source bundle is compatible with LeapFlow. A source bundle assessment is static: runtime_ready stays false until restricted Node discovery during plugin_install. Returns component-level verdicts and limitations.\n- **plugin_generate**(plugin_id, description, proposal_id) [capability_expand category: system]: Generate a new ToolPlugin from a natural-language capability description. The LLM produces code that conforms to the ToolPlugin Protocol; it is then rigorously validated (syntax, structure, import, protocol conformance). The generated source is stored in CAS and receives explicit content approval. It DOES NOT install the plugin — installation requires a second, mutation-specific approval via plugin_install.\n- **plugin_install**(plugin_id, code, marketplace_name, source_path, proposal_id, version_label) [capability_expand category: system]: Install a plugin from exactly one source: validated Python code, the configured Python marketplace, or a real DSH source bundle. DSH bundles run restricted Node discovery before registration; only public host tools are installed and client UI limitations are reported. Writes profile-scoped state and mutates the process-global registry. REQUIRES APPROVAL.\n- **plugin_rollback**(plugin_id, version) [capability_expand category: system]: Rollback a profile-scoped plugin to a recorded source version and reload it. REQUIRES APPROVAL.\n- **plugin_reload**(plugin_id, version_label) [capability_expand category: system]: Hot-reload a plugin at runtime. Disposes the old plugin fiber, re-imports its module, and registers a fresh instance. Existing in-flight turns are unaffected (snapshot isolation). REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_disable**(plugin_id) [capability_expand category: system]: Disable a plugin by disposing its fiber, removing its tools from the runtime registry. Cannot disable self_management itself. REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_remove**(plugin_id, delete_source) [capability_expand category: system]: Terminally remove a plugin: dispose its fiber, unregister its tools, remove reload metadata, and optionally delete its profile-scoped source file. Cannot remove self_management itself. REQUIRES APPROVAL.\n- **plugin_unquarantine**(plugin_id) [capability_expand category: plugin_management]: Restore a quarantined plugin to probation status for re-evaluation. Unfreezes the trust ledger, transitions the proposal back to PROBATION, and reloads the plugin. REQUIRES APPROVAL.\n- **plugin_enable**(plugin_id) [capability_expand category: system]: Re-enable a previously disabled plugin by reloading its module and registering a fresh instance. REQUIRES APPROVAL.\n- **self_describe**(facet): Answer questions about LeapFlow ITSELF from live runtime evidence — its identity/version/model, what tools and plugins it has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of self-awareness. Prefer this over reading LeapFlow's own source code. Pick facet=capabilities for 'what can you do / do you support X', identity for version/model, runtime for context/posture, or all for a full self-check when explicitly requested.\n- **runtime_snapshot**(): Lightweight (~150 token) flat snapshot of current runtime state: model, context budget, posture, disclosure level, turn count, cache hit rate, uptime, tool count, and pending approvals.\n- **schedule_create**(trigger_expression, instruction, execution_mode, max_retries, delivery_target) [capability_expand category: scheduler]: Create a new scheduled task. Specify a trigger expression (e.g. '30m', 'every 2h', '0 9 * * *') and the instruction to execute on each trigger. Optionally provide an execution mode and a delivery target to receive notifications.\n- **schedule_list**() [capability_expand category: scheduler]: List all scheduled tasks with their current state, trigger, and next due time.\n- **schedule_status**(task_id) [capability_expand category: scheduler]: Get detailed status and recent execution history for a scheduled task.\n- **schedule_pause**(task_id) [capability_expand category: scheduler]: Pause a scheduled task so it stops firing without being cancelled.\n- **schedule_resume**(task_id) [capability_expand category: scheduler]: Resume a paused scheduled task — re-arms it and recalculates next due time.\n- **schedule_cancel**(task_id) [capability_expand category: scheduler]: Cancel a scheduled task permanently.\n- **tool_search**(query, max_results): Search all registered tools by keyword relevance. Returns a ranked list of matching tools with name, category, and summary. Use this when you need a tool but are unsure of its exact name or category.\n- **tool_describe**(tool_name): Get the full callable schema of a single tool by exact name. Returns the tool's description, parameters, and metadata. Use after tool_search to inspect a specific tool before calling it.\n- **fixture_echo**(text): Return the supplied text from a pre-built DSH package.\n- **session_search**(query, limit) [capability_expand category: unclassified]: Search past conversation sessions in the current workspace for relevant context.\n- **session_list**(limit): List recent conversation sessions in the current workspace with stable ids, titles, dates, and summaries. Use for browsing past tasks or when user asks to see history without specific search terms. No keywords needed — returns chronological list.\n- **session_detail**(session_id, limit, offset, include_inactive) [capability_expand category: unclassified]: Read a paginated persisted transcript for one past conversation session in the current workspace. Use after session_list or session_search returns a session_id.\n- **click**(element_index) [capability_expand category: desktop]: Click a UI element by its element_index (from the latest observe_ui snapshot)\n- **get_clipboard**() [capability_expand category: desktop]: Read current clipboard text content\n- **list_apps**(filter, running_only) [capability_expand category: desktop]: List available applications on this system. Use to discover correct bundle_id before switch_app.\n- **list_windows**() [capability_expand category: desktop]: List all top-level windows with pid, window_id, title, and per-window state (minimized, on-screen). Call this first to pick the pid and window_id that observe_ui and other window tools require.\n- **observe_ui**(pid, window_id, query) [capability_expand category: desktop]: Snapshot one window's actionable UI elements, each tagged with an element_index for click/right_click/read_text. Re-observe after actions — indices belong to one snapshot. Requires the window's pid and window_id from list_windows.\n- **open_url**(url, app_id) [capability_expand category: desktop]: Open a URL in the default or specified browser\n- **read_text**(element_index) [capability_expand category: desktop]: Read the text content of a specific UI element from the latest snapshot\n- **right_click**(element_index) [capability_expand category: desktop]: Right-click a UI element to open its context menu. Returns visible menu items.\n- **screenshot**(pid, window_id) [capability_expand category: desktop]: Capture a screenshot for visual verification. With pid + window_id captures that window (works across all displays); defaults to the last observed window, or the full desktop when no window has been observed.\n- **scroll**(element_index, direction, amount, pid, window_id) [capability_expand category: desktop]: Scroll a scrollable area of a window. Omit element_index to scroll the window's focused/page scroller; pass one to scroll an exact element from the latest snapshot.\n- **select_text**(element_index) [capability_expand category: desktop]: Select all text in a UI element (focus + select-all, for subsequent copy)\n- **set_clipboard**(text) [capability_expand category: desktop]: Write text to the clipboard\n- **shortcut**(keys) [capability_expand category: desktop]: Execute a keyboard shortcut\n- **switch_app**(app_id) [capability_expand category: desktop]: Switch to an app (launch if needed, activate, verify)\n- **type_text**(text) [capability_expand category: desktop]: Type text into the currently focused element\n- **wait**(seconds) [capability_expand category: desktop]: Wait for a specified duration before continuing\n- **wait_until**(condition, pid, window_id, timeout, poll_interval) [capability_expand category: desktop]: Wait until a UI condition is met (polls UI tree). Returns elements when found or on timeout.\n- **wait_until_stable**(timeout, poll_interval, pid, window_id) [capability_expand category: desktop]: Wait until the UI stops changing (element set stabilizes across polls).\n\n## App Connector Capability Index\nLeapFlow can onboard and manage external apps through `platform_connect` and execute exact registered business actions through `platform_action`.\nFor requests about connecting, setting up, configuring, enabling, or managing a supported app, use `platform_connect` first instead of generating SDK/Webhook sample code.\n`platform_connect.action` is the management namespace: list, guide, preflight, connect, disconnect, remove, status, events_start, events_stop, events_status.\n`platform_action.action` is only for exact registered platform business actions listed below, such as `im.send_message`; never use management actions like `list` or `guide` there.\nAll business fields MUST go inside `payload`; top-level keys are only `platform`, `action`, and `payload`.\nDo not invent platform IDs or platform action names. If the needed action is not listed, ask for discovery/clarification instead of guessing.\nUse `platform_connect` with `action='guide'` and the matching `platform` to start onboarding; use `action='list'` when the app is unclear.\nWhen a pending onboarding state is present, continue from that state with `platform_connect` instead of asking the user to restate the app.\nSupported apps:\n- `api_server`: API Server (OpenAI Compatible) (category=api; backend=adapter; platform_action actions=none registered)\n- `dingtalk`: 钉钉 (DingTalk) (category=im; backend=adapter; platform_action actions=none registered)\n- `feishu`: 飞书 (Feishu/Lark) (category=im; backend=cli)\n - `calendar.create_event` payload={summary*, start_time*, end_time*, attendees} [write/high]\n - `docs.create_markdown` payload={title*, markdown*, folder_token} [write/medium]\n - `drive.search` payload={query*, limit} [read/medium]\n - `im.add_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.download_resource` payload={message_id*, file_key*, type*} [read/low]\n - `im.get_messages` payload={message_ids*} [read/low]\n - `im.list_chats` payload={page_size} [read/low]\n - `im.list_messages` payload={chat_id*} [read/low]\n - `im.list_thread_messages` payload={thread*} [read/low]\n - `im.remove_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.reply_message` payload={message_id*, text*} [send/high]\n - `im.search_chats` payload={query*, page_size} [read/low]\n - `im.search_messages` payload={query*} [read/medium]\n - `im.send_message` payload={chat_id*, text*, thread_id} [send/high]\n - `im.update_card` payload={token*, card*} [write/medium]\n - `im.update_message` payload={message_id*, text*} [write/medium]\n - `mail.search_unread` payload={query, limit} [read/high]\n - `sheets.append_row` payload={spreadsheet_token*, sheet_id*, values*} [write/medium]\n - `task.create` payload={title*, description, due_time} [write/medium]\n- `telegram`: Telegram (category=social; backend=adapter; platform_action actions=none registered)\n- `webhook`: Webhook (Generic) (category=webhook; backend=adapter; platform_action actions=none registered)\n\n## Tool Usage\nTools are normally invoked through the native function-calling mechanism, not by writing JSON in your reply\ntext. Only if the provider signals that native function calling is unavailable for this turn, fall back to a\nsingle JSON code block: `{\"name\": \"tool_name\", \"arguments\": {\"key\": \"value\"}}` — use this fallback format\nonly, never both. Only call a tool whose exact name appears in the tool index above and is currently callable\n(or reachable via `capability_expand`). Never invent, rename, alias, or guess a tool name, platform ID, or\nplatform action from argument shape or wording — if the index does not list it, it does not exist.\n`platform_connect.action` (list/guide/preflight/connect/disconnect/remove/status/events_start/events_stop/\nevents_status) is the App Connector management namespace; `platform_action.action` only accepts exact\nregistered business actions such as `im.send_message` shown in the App Connector Capability Index — never mix\nthe two namespaces. If a tool call returns an unknown/unavailable result, use the returned suggestions or\navailable names for a single retry instead of trying further variations of the same guess.\n\n**Side-effect action rule** (`platform_action` with effect=send/write/execute):\n- Call each unique action+payload **exactly once**. Never include duplicates in the same turn.\n- Once the result returns `\"completed\": true`, that action is DONE for this task. Do NOT call it again\n in any subsequent turn — immediately summarize the result for the user instead.\n- The system enforces idempotency: duplicate calls are blocked and will not execute.\n- If the user explicitly requests sending/writing multiple times, use distinct payloads per call.\n\n**Resource identifier provenance rule**:\n- NEVER fabricate, guess, or infer resource identifiers (chat_id, message_id, file_key, user_id, etc.).\n Every resource ID used in a side-effect action MUST come from a successful API response in this session.\n- If a read/list action fails (e.g. authorization error), you do NOT have valid resource IDs.\n Report the failure to the user — do NOT attempt the dependent write/send action with a guessed ID.\n- When a tool result contains `\"llm_instruction\"`, follow it exactly.\n\n## Guidelines\n1. **Direct answers first**: If you already know the answer, respond directly without tools.\n2. **Avoid redundant tool calls**: Do not call the same tool with the same arguments more than once in the same user turn. When an existing tool result already answers the user's request, stop calling tools and answer directly.\n3. **Use tools proactively**: When the user asks about files, time, system state, or needs actions performed, use the appropriate tool.\n4. **Chain tools when needed**: You can call multiple tools in sequence (e.g., list files → read file → summarize).\n5. **Handle failures gracefully**: If a tool fails, explain what went wrong and suggest alternatives. If it failed because of an unknown tool/platform/action name, retry once with an exact name from the returned suggestions, then explain rather than keep guessing.\n6. **Summarize results naturally**: After tool execution, synthesize the results into a helpful answer rather than dumping raw output.\n7. **Stay conversational**: Maintain a natural, helpful tone. Acknowledge context from earlier in the conversation.\n8. **Recall past work**:\n - Broad queries (\"之前做了什么\", \"列出任务\"): answer from the \"Recent Task History\" section already in your context. If insufficient, call session_list.\n - Specific lookups (\"上次那个配置怎么改的\"): call session_search with relevant phrases (NOT single characters).\n - Do NOT call search tools repeatedly with keyword fragments. One well-phrased call is better than ten fragmented ones.\n\n## Coding & Verification\nWhen working with code, prefer the precise built-in tools over ad-hoc shell: use `repo_map` to orient in an\nunfamiliar project, `code_search`/`file_find` to locate and `code_intel` for symbols, `edit_file` (anchored\nsearch-replace, or a unified `diff`) to change files — never rewrite a whole file to change a few lines — and\n`git_query` to inspect diffs/log. After edits, check `syntax_ok` in the result and run `test_run`/`lint_check`\nbefore declaring the work done. Batch independent read-only calls (search/read/query) into a single turn — they\nrun in parallel.\n\n## Presentation Style\n1. **Polished Markdown only**: Format user-facing answers with clean Markdown headings, short paragraphs, and concise bullets. Use tables only when they improve comparison or scanning.\n2. **Terminal-friendly layout**: Keep lines readable in a TUI; avoid dense walls of text, deeply nested lists, oversized ASCII art, or heavy visual blocks.\n3. **Elegant emphasis**: Use bold text sparingly for key terms and conclusions. Avoid excessive emojis, decorative symbols, repeated separators, or visual noise.\n4. **Theme-safe colors**: Do not emit ANSI escape codes, HTML color tags, Rich markup, or hardcoded color names. Rely on the TUI theme to render Markdown professionally.\n5. **No leaked tool protocol**: Never show tool-call JSON, internal schemas, raw observations, tool result payloads, or hidden reasoning in the final answer unless the user explicitly asks for raw/debug output. Treat any prior `{\"name\": ..., \"arguments\": ...}` blocks and `Tool result (...)` messages as internal execution context only.\n6. **Professional closure**: End with a concise conclusion or next step when helpful; avoid rambling after the useful answer is complete.\n\nWhen finished with all tool calls, respond normally without a JSON block, tool-call transcript, or process log.\n\n## Task Contract\n- Task ID: turn-2\n- Original user request: Remove the hermetic DSH echo plugin completely.\n- Workspace root: \n- Allowed roots: \n- Treat relative project paths as relative to the workspace root; never infer `.` as the project root when a workspace root is provided.\n- Workspace boundary is enforced by tools: do not read, search, edit, or run commands against paths outside the allowed roots unless the user explicitly requests an external path and the tool/approval policy permits it.\n- LeapFlow workspace config is optional at `/.leapflow/config.yaml`; runtime config is loaded from `~/.leapflow/config/user.yaml` and `~/.leapflow/profiles//config/*.yaml`.\n- Preserve this task contract across summarization, compression, tool loops, and memory retrieval.\n" + }, + { + "role": "system", + "content": "\n## Recent Session Summary\n- [assistant] The DSH tool ran after restart.\n- [user] Remove the hermetic DSH echo plugin completely.\n" + }, + { + "role": "assistant", + "content": "The DSH tool ran after restart." + }, + { + "role": "user", + "content": "Remove the hermetic DSH echo plugin completely.\nRemove the hermetic DSH echo plugin completely." + } + ], + "tools": [ + "capability_expand", + "code_intel", + "code_search", + "config_get", + "config_list", + "env_info", + "file_find", + "file_list", + "file_read", + "fixture_echo", + "git_query", + "lint_check", + "memory_search", + "plugin_list", + "plugin_propose", + "plugin_status", + "plugin_versions", + "repo_map", + "research_note", + "runtime_snapshot", + "schedule_reentry", + "self_describe", + "session_list", + "skill_view", + "skills_list", + "terminal_list", + "terminal_read", + "test_run", + "text_search", + "time_get", + "tool_describe", + "tool_search", + "web_fetch" + ] + }, + "responses": [ + { + "status": 200, + "content_type": "application/json", + "body": "{\"id\": \"chatcmpl-cassette\", \"object\": \"chat.completion\", \"model\": \"cassette-model\", \"choices\": [{\"index\": 0, \"message\": {\"role\": \"assistant\", \"content\": \"\", \"tool_calls\": [{\"id\": \"call_1\", \"type\": \"function\", \"function\": {\"name\": \"plugin_remove\", \"arguments\": \"{\\\"plugin_id\\\": \\\"r6_dsh_echo\\\", \\\"delete_source\\\": true}\"}}]}, \"finish_reason\": \"tool_calls\"}], \"usage\": {\"prompt_tokens\": 64, \"completion_tokens\": 16, \"total_tokens\": 80}}" + } + ] +} diff --git a/tests/_fixtures/cassettes/r6_lifecycle/cassette-model-ca3f68468ce7ee23.cassette.json b/tests/_fixtures/cassettes/r6_lifecycle/cassette-model-ca3f68468ce7ee23.cassette.json deleted file mode 100644 index ffd78111..00000000 --- a/tests/_fixtures/cassettes/r6_lifecycle/cassette-model-ca3f68468ce7ee23.cassette.json +++ /dev/null @@ -1,75 +0,0 @@ -{ - "fingerprint": "ca3f68468ce7ee23613cdbdb9344a1fb0089b3a59a935d26d9de7e7d36da6457", - "note": "captured in seed mode", - "request": { - "model": "cassette-model", - "stream": false, - "messages": [ - { - "role": "system", - "content": "You are LeapFlow, an intelligent assistant that can both converse naturally and take real actions on the user's computer.\n\n## Capabilities\nThe tool index below lists **every** registered tool by name and a one-line summary — this is the complete\ncapability contract; nothing else exists. Only a subset is directly callable this turn (via native tool calling,\nnot a JSON block in your reply). If you need a tool from the index that is not yet callable, call\n`capability_expand` with its category name first — the matching tools become callable immediately after.\nWhen the user asks what LeapFlow itself supports, whether it supports plugins, or which runtime capabilities\nare available, use the live capability evidence exposed by `plugin_list` before making capability claims; report\nconfiguration-dependent or unavailable capabilities as limitations instead of inferring from documentation.\n- **text_search**(text, pattern): Search for a regex pattern in text.\n- **text_replace**(text, old, new, count) [capability_expand category: write]: Replace occurrences of a substring in text.\n- **time_get**(): Get current date and time.\n- **env_info**(): Get system environment information (OS, Python version, cwd).\n- **skills_list**(query, category, source): List available learned skills. Use when user asks about capabilities or you need a specific skill.\n- **skill_view**(name): View the full content of a specific skill document.\n- **code_intel**(path, operation): Precise document symbols (outline) for a source file: classes, functions, and methods with line ranges. Python uses an exact AST parse; other languages use a keyword-prefix scan. Prefer over file_read mode=symbols for accurate navigation before editing. Read-only.\n- **repo_map**(path): Compact project orientation for a repository root: languages, detected test/lint commands, top-level structure, entry points, manifest, and VCS branch. Call this first when entering an unfamiliar codebase. Read-only.\n- **scm_sync**(action, cwd, remote, pull_ref, push_ref, timeout) [capability_expand category: scm]: Run a typed git SCM action. Use this instead of shell_run for git pull/push/status. For 'pull origin main then push', set action='pull_then_push', remote='origin', pull_ref='main', and omit push_ref so LeapFlow pushes the current local branch.\n- **git_query**(action, cwd, ref, path, staged, max_count, stat): Read-only structured git inspection: action=diff|log|status|branch|show. Prefer over shell_run for reading repo state — output is clipped, redacted, and log/branch are parsed into structured fields. Use scm_sync for pull/push.\n- **git_write**(action, cwd, message, stage_all, name, ref, create) [capability_expand category: scm]: Mutating git actions: action=commit (message, stage_all), branch (create+switch), checkout (switch; create=true for -b). Approval-gated. Use scm_sync for pull/push and git_query for reads.\n- **test_run**(command, cwd, timeout): Run the project's test suite and return structured results (framework, passed/failed counts, failing tests). Auto-detects the runner (pytest/npm/go/cargo) or uses a configured/explicit command; executes via the governed shell. ok=true means the runner executed — see 'success' for pass/fail.\n- **lint_check**(command, cwd, timeout): Run the project's linter and return a structured clean/issue result. Auto-detects the linter (ruff/eslint/go vet/clippy) or uses a configured/explicit command; executes via the governed shell. ok=true means the linter ran — see 'clean'.\n- **file_list**(path, pattern, depth): List files and directories at a given path. Use depth=1 or depth=2 to get a recursive tree in one call instead of listing each sub-directory separately.\n- **file_read**(path, max_lines, start_line, max_chars, mode): Read text file content with adaptive context governance. For large or unfamiliar files, prefer mode='outline' or mode='symbols' first, then use mode='raw' with start_line/max_lines for the specific range you actually need. For LeapFlow's own settings, use config_list / config_get / config_set — its config files are outside the workspace and not readable here.\n- **file_write**(path, content, mode) [capability_expand category: write]: Write content to a file (overwrite or append).\n- **code_search**(pattern, patterns, path, glob, ignore_case, multiline, max_results, context_lines): Search file CONTENTS by regex pattern across a directory tree (ripgrep-backed). Requires a regex pattern. NOT for listing or browsing directory contents — use file_list for that. Prefer this over shell_run grep: faster, skips VCS/dependency/build dirs, and returns structured path:line:column matches. Batch related lookups into ONE call via `patterns` (OR-combined, single pass) instead of issuing several separate searches. Use file_read for the surrounding context of a hit.\n- **file_find**(glob, path, max_results): Find files by a recursive glob pattern under a base path (e.g. '**/test_*.py' or '*.md'). Prefer this over shell_run find; skips VCS/dependency/build dirs.\n- **edit_file**(path, edits, dry_run, diff) [capability_expand category: write]: Apply targeted, anchored search-replace edits to an EXISTING text file (use file_write to create/overwrite). Each edit is {original_text, new_text, replace_all?}; original_text must match exactly and uniquely (or set replace_all) — a non-unique or missing anchor is rejected so files are never corrupted. Set dry_run to preview. Alternatively pass a unified 'diff' to apply its hunks as anchored edits. Far cheaper and safer than rewriting a whole file.\n- **shell_run**(command, cwd, timeout) [capability_expand category: shell]: Execute a one-shot shell command with timeout protection. Runs in the active workspace; paths resolving outside it are refused. Reach for a structured tool first when one fits — web_fetch for anything over HTTP(S), git_query/scm_sync for git, code_search/file_find/file_read for the repo, config_get/config_set for LeapFlow's own settings — because those report typed results, while a failed shell command can only be diagnosed from its exit code and stderr. Every shell run counts as an external side effect, so a failure stops the rest of the batch and is not retried automatically.\n- **terminal_open**(command, cwd, shell) [capability_expand category: terminal]: Open a PERSISTENT shell session (REPL/dev server/watch), returning a session_id for terminal_send/read/close. Disabled unless tools.terminal_session_enabled is set. For one-shot commands use shell_run instead.\n- **terminal_send**(session_id, input, wait) [capability_expand category: terminal]: Send a line of input to a persistent terminal session and return output captured shortly after.\n- **terminal_read**(session_id, wait): Drain buffered output from a persistent terminal session (optionally waiting briefly first).\n- **terminal_close**(session_id) [capability_expand category: terminal]: Terminate a persistent terminal session and release its process group.\n- **terminal_list**(): List active persistent terminal sessions.\n- **config_list**(category, limit): List LeapFlow's own writable settings (LLM model and endpoint, daemon, memory, perception, gateway, …) with current values. Use this to discover the exact key before changing anything. Optionally narrow by `category`. This is the only correct way to inspect LeapFlow configuration — never read config files from disk.\n- **config_get**(key): Read one LeapFlow setting by key (e.g. 'llm.model', 'llm.base_url', 'daemon.log_level'), returning its current value, type, scopes, and whether a change needs a daemon restart. There is no 'llm.provider' key: provider behavior is inferred from the OpenAI-compatible 'llm.base_url'. Never read LeapFlow config files from disk — use this.\n- **config_set**(key, value, scope) [capability_expand category: config]: Change one LeapFlow setting by key, e.g. switch an OpenAI-compatible LLM with key='llm.model' and key='llm.base_url'. There is no 'llm.provider' key: provider behavior is inferred from the endpoint. Values are validated and coerced; credentials are stored in the vault automatically. Call config_list or config_get first if unsure of the key. The result states whether a `leap daemon restart` is required. Never edit LeapFlow config files directly.\n- **web_fetch**(url, select, timeout, max_bytes): Read a URL over HTTP(S) and get back extracted, context-sized content: parsed JSON for API endpoints, readable text plus links for web pages. Use this for anything on the internet — prices, docs, releases, articles — instead of running curl through shell_run: it reports real HTTP status codes, retries rate limits on its own, and is a plain read so a retry is always safe. For JSON APIs pass `select` with a dotted path (e.g. 'chart.result.0.meta') to return just that part instead of the whole payload.\n- **memory_search**(query, limit): Search agent memory for relevant past experiences, observations, and facts.\n- **memory_add**(content, kind) [capability_expand category: write]: Store a new observation or insight in memory for future reference.\n- **research_note**(kind, text): Record a compact, structured note about the current task's state so it survives context compression on long / multi-step tasks. Use for durable findings, open questions still to resolve, decisions / excluded paths, and the immediate next step. One concise sentence per note.\n- **capability_expand**(category): Fetch the full callable schema for every tool in a capability category (e.g. 'hub', 'gateway', 'desktop', 'delegate', 'file', 'memory', 'skill'). The compact tool index always lists every registered tool by name and a one-line summary, but only a static low-risk subset is directly callable each turn. If you need a tool from the index that is not yet callable, call capability_expand with its category first; the matching tools become callable in this turn. Never invent a tool name — expand the category instead.\n- **delegate_task**(goal, context) [capability_expand category: delegate]: Delegate a complex sub-task to an isolated subagent. The subagent gets a fresh context and restricted tool access. Use when a task is self-contained and can be solved independently.\n- **schedule_reentry**(kind, reason, delay_seconds, event_match, max_reentries, deadline_seconds): Register a re-entry so this task can resume later from its current orientation (findings / open questions / next step). Use when work must pause and continue after a delay (kind=time) or when a matching platform event arrives (kind=event), instead of finishing now. The research-ledger state is carried over automatically.\n- **hub_push**(skill_name, visibility, version) [capability_expand category: hub]: Push a local skill to the ModelScope Hub for sharing or backup.\n- **hub_pull**(repo_id, version) [capability_expand category: hub]: Pull a skill from the ModelScope Hub to install locally.\n- **hub_search**(query) [capability_expand category: hub]: Search for skills on the Hub by keyword or description.\n- **hub_sync**(mode, dry_run) [capability_expand category: hub]: Preview or execute sync between local skills and Hub.\n- **platform_action**(platform, action, payload, backend_kind) [capability_expand category: gateway]: Execute an exact registered business action on an external platform through LeapFlow's App Connector layer. Actions must be copied from the App Connector Capability Index and are addressed as domain.operation, e.g. im.send_message or docs.create_markdown. All business fields (chat_id, text, query, etc.) MUST be placed inside `payload`, never at the top level. Example: {\"platform\":\"feishu\",\"action\":\"im.send_message\",\"payload\":{\"chat_id\":\"oc_xxx\",\"text\":\"hello\"}}. Do not invent action names, do not use management actions such as list/guide/connect/status here.\n- **platform_connect**(action, platform, credentials, options, checkpoint) [capability_expand category: gateway]: List, guide, connect, disconnect, remove, or check status for external platforms using the App Connector management namespace. Supports REST and CLI backends. Use this for management actions such as list/guide/preflight/connect/status; use platform_action only for exact registered business actions.\n- **gateway_send**(platform, chat_id, text, thread_id) [capability_expand category: gateway]: Send a message to a connected external platform (Feishu group, Telegram chat, DingTalk conversation, etc.). Requires the platform to be connected via gateway_connect first. Use gateway_connect with action='list' to see connected platforms and available chat IDs.\n- **gateway_connect**(action, platform, credentials, options) [capability_expand category: gateway]: Connect, configure, or manage external platform integrations (Feishu, DingTalk, Telegram, Slack, Discord, etc.). Conversational flow: 1) call 'guide' to get setup steps + required fields, 2) present the steps to the user and ask for ALL required credentials in a single message, 3) call 'connect' with the credentials. Goal: complete in 1–2 user turns. NEVER include credential values in your text response.\n- **plugin_list**(): List the live plugin registry and cross-subsystem capability evidence. Use this before answering questions about whether LeapFlow supports plugins, self-evolution, plugin installation, hot reload, versioning, or other runtime capabilities.\n- **plugin_status**(plugin_id): Get detailed status of a specific plugin: its declared category, runtime dependencies, contributed tools, and fiber lifecycle state.\n- **plugin_versions**(plugin_id): List recorded source versions and active pointer for a profile-scoped plugin.\n- **plugin_propose**(requested_capability, plugin_id, proposed_tools, test_cases, risk_level, evidence): Create a side-effect-free PluginProposal from explicit capability-gap evidence. Use this before plugin_generate when a missing capability should be reviewed. Does not call an LLM, write files, or install anything.\n- **assess_compatibility**(manifest, source_path) [capability_expand category: plugin_management]: Assess whether a foreign plugin manifest or real DSH source bundle is compatible with LeapFlow. A source bundle assessment is static: runtime_ready stays false until restricted Node discovery during plugin_install. Returns component-level verdicts and limitations.\n- **plugin_generate**(plugin_id, description, proposal_id) [capability_expand category: system]: Generate a new ToolPlugin from a natural-language capability description. The LLM produces code that conforms to the ToolPlugin Protocol; it is then rigorously validated (syntax, structure, import, protocol conformance). The generated source is stored in CAS and receives explicit content approval. It DOES NOT install the plugin — installation requires a second, mutation-specific approval via plugin_install.\n- **plugin_install**(plugin_id, code, marketplace_name, source_path, proposal_id, version_label) [capability_expand category: system]: Install a plugin from exactly one source: validated Python code, the configured Python marketplace, or a real DSH source bundle. DSH bundles run restricted Node discovery before registration; only public host tools are installed and client UI limitations are reported. Writes profile-scoped state and mutates the process-global registry. REQUIRES APPROVAL.\n- **plugin_rollback**(plugin_id, version) [capability_expand category: system]: Rollback a profile-scoped plugin to a recorded source version and reload it. REQUIRES APPROVAL.\n- **plugin_reload**(plugin_id, version_label) [capability_expand category: system]: Hot-reload a plugin at runtime. Disposes the old plugin fiber, re-imports its module, and registers a fresh instance. Existing in-flight turns are unaffected (snapshot isolation). REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_disable**(plugin_id) [capability_expand category: system]: Disable a plugin by disposing its fiber, removing its tools from the runtime registry. Cannot disable self_management itself. REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_remove**(plugin_id, delete_source) [capability_expand category: system]: Terminally remove a plugin: dispose its fiber, unregister its tools, remove reload metadata, and optionally delete its profile-scoped source file. Cannot remove self_management itself. REQUIRES APPROVAL.\n- **plugin_unquarantine**(plugin_id) [capability_expand category: plugin_management]: Restore a quarantined plugin to probation status for re-evaluation. Unfreezes the trust ledger, transitions the proposal back to PROBATION, and reloads the plugin. REQUIRES APPROVAL.\n- **plugin_enable**(plugin_id) [capability_expand category: system]: Re-enable a previously disabled plugin by reloading its module and registering a fresh instance. REQUIRES APPROVAL.\n- **self_describe**(facet): Introspect LeapFlow's own identity, capabilities, runtime state, evolution metrics, or platform connections. Use facet='all' only when a comprehensive self-check is explicitly requested.\n- **runtime_snapshot**(): Lightweight (~150 token) flat snapshot of current runtime state: model, context budget, posture, disclosure level, turn count, cache hit rate, uptime, tool count, and pending approvals.\n- **schedule_create**(trigger_expression, instruction, execution_mode, max_retries, delivery_target) [capability_expand category: scheduler]: Create a new scheduled task. Specify a trigger expression (e.g. '30m', 'every 2h', '0 9 * * *') and the instruction to execute on each trigger. Optionally provide an execution mode and a delivery target to receive notifications.\n- **schedule_list**() [capability_expand category: scheduler]: List all scheduled tasks with their current state, trigger, and next due time.\n- **schedule_status**(task_id) [capability_expand category: scheduler]: Get detailed status and recent execution history for a scheduled task.\n- **schedule_pause**(task_id) [capability_expand category: scheduler]: Pause a scheduled task so it stops firing without being cancelled.\n- **schedule_resume**(task_id) [capability_expand category: scheduler]: Resume a paused scheduled task — re-arms it and recalculates next due time.\n- **schedule_cancel**(task_id) [capability_expand category: scheduler]: Cancel a scheduled task permanently.\n- **tool_search**(query, max_results): Search all registered tools by keyword relevance. Returns a ranked list of matching tools with name, category, and summary. Use this when you need a tool but are unsure of its exact name or category.\n- **tool_describe**(tool_name): Get the full callable schema of a single tool by exact name. Returns the tool's description, parameters, and metadata. Use after tool_search to inspect a specific tool before calling it.\n- **session_search**(query, limit) [capability_expand category: unclassified]: Search past conversation sessions in the current workspace for relevant context.\n- **session_list**(limit): List recent conversation sessions in the current workspace with stable ids, titles, dates, and summaries. Use for browsing past tasks or when user asks to see history without specific search terms. No keywords needed — returns chronological list.\n- **session_detail**(session_id, limit, offset, include_inactive) [capability_expand category: unclassified]: Read a paginated persisted transcript for one past conversation session in the current workspace. Use after session_list or session_search returns a session_id.\n- **click**(element_index) [capability_expand category: desktop]: Click a UI element by its element_index (from the latest observe_ui snapshot)\n- **get_clipboard**() [capability_expand category: desktop]: Read current clipboard text content\n- **list_apps**(filter, running_only) [capability_expand category: desktop]: List available applications on this system. Use to discover correct bundle_id before switch_app.\n- **list_windows**() [capability_expand category: desktop]: List all top-level windows with pid, window_id, title, and per-window state (minimized, on-screen). Call this first to pick the pid and window_id that observe_ui and other window tools require.\n- **observe_ui**(pid, window_id, query) [capability_expand category: desktop]: Snapshot one window's actionable UI elements, each tagged with an element_index for click/right_click/read_text. Re-observe after actions — indices belong to one snapshot. Requires the window's pid and window_id from list_windows.\n- **open_url**(url, app_id) [capability_expand category: desktop]: Open a URL in the default or specified browser\n- **read_text**(element_index) [capability_expand category: desktop]: Read the text content of a specific UI element from the latest snapshot\n- **right_click**(element_index) [capability_expand category: desktop]: Right-click a UI element to open its context menu. Returns visible menu items.\n- **screenshot**(pid, window_id) [capability_expand category: desktop]: Capture a screenshot for visual verification. With pid + window_id captures that window (works across all displays); defaults to the last observed window, or the full desktop when no window has been observed.\n- **scroll**(element_index, direction, amount, pid, window_id) [capability_expand category: desktop]: Scroll a scrollable area of a window. Omit element_index to scroll the window's focused/page scroller; pass one to scroll an exact element from the latest snapshot.\n- **select_text**(element_index) [capability_expand category: desktop]: Select all text in a UI element (focus + select-all, for subsequent copy)\n- **set_clipboard**(text) [capability_expand category: desktop]: Write text to the clipboard\n- **shortcut**(keys) [capability_expand category: desktop]: Execute a keyboard shortcut\n- **switch_app**(app_id) [capability_expand category: desktop]: Switch to an app (launch if needed, activate, verify)\n- **type_text**(text) [capability_expand category: desktop]: Type text into the currently focused element\n- **wait**(seconds) [capability_expand category: desktop]: Wait for a specified duration before continuing\n- **wait_until**(condition, pid, window_id, timeout, poll_interval) [capability_expand category: desktop]: Wait until a UI condition is met (polls UI tree). Returns elements when found or on timeout.\n- **wait_until_stable**(timeout, poll_interval, pid, window_id) [capability_expand category: desktop]: Wait until the UI stops changing (element set stabilizes across polls).\n\n## App Connector Capability Index\nLeapFlow can onboard and manage external apps through `platform_connect` and execute exact registered business actions through `platform_action`.\nFor requests about connecting, setting up, configuring, enabling, or managing a supported app, use `platform_connect` first instead of generating SDK/Webhook sample code.\n`platform_connect.action` is the management namespace: list, guide, preflight, connect, disconnect, remove, status, events_start, events_stop, events_status.\n`platform_action.action` is only for exact registered platform business actions listed below, such as `im.send_message`; never use management actions like `list` or `guide` there.\nAll business fields MUST go inside `payload`; top-level keys are only `platform`, `action`, and `payload`.\nDo not invent platform IDs or platform action names. If the needed action is not listed, ask for discovery/clarification instead of guessing.\nUse `platform_connect` with `action='guide'` and the matching `platform` to start onboarding; use `action='list'` when the app is unclear.\nWhen a pending onboarding state is present, continue from that state with `platform_connect` instead of asking the user to restate the app.\nSupported apps:\n- `api_server`: API Server (OpenAI Compatible) (category=api; backend=adapter; platform_action actions=none registered)\n- `dingtalk`: 钉钉 (DingTalk) (category=im; backend=adapter; platform_action actions=none registered)\n- `feishu`: 飞书 (Feishu/Lark) (category=im; backend=cli)\n - `calendar.create_event` payload={summary*, start_time*, end_time*, attendees} [write/high]\n - `docs.create_markdown` payload={title*, markdown*, folder_token} [write/medium]\n - `drive.search` payload={query*, limit} [read/medium]\n - `im.add_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.download_resource` payload={message_id*, file_key*, type*} [read/low]\n - `im.get_messages` payload={message_ids*} [read/low]\n - `im.list_chats` payload={page_size} [read/low]\n - `im.list_messages` payload={chat_id*} [read/low]\n - `im.list_thread_messages` payload={thread*} [read/low]\n - `im.remove_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.reply_message` payload={message_id*, text*} [send/high]\n - `im.search_chats` payload={query*, page_size} [read/low]\n - `im.search_messages` payload={query*} [read/medium]\n - `im.send_message` payload={chat_id*, text*, thread_id} [send/high]\n - `im.update_card` payload={token*, card*} [write/medium]\n - `im.update_message` payload={message_id*, text*} [write/medium]\n - `mail.search_unread` payload={query, limit} [read/high]\n - `sheets.append_row` payload={spreadsheet_token*, sheet_id*, values*} [write/medium]\n - `task.create` payload={title*, description, due_time} [write/medium]\n- `telegram`: Telegram (category=social; backend=adapter; platform_action actions=none registered)\n- `webhook`: Webhook (Generic) (category=webhook; backend=adapter; platform_action actions=none registered)\n\n## Tool Usage\nTools are normally invoked through the native function-calling mechanism, not by writing JSON in your reply\ntext. Only if the provider signals that native function calling is unavailable for this turn, fall back to a\nsingle JSON code block: `{\"name\": \"tool_name\", \"arguments\": {\"key\": \"value\"}}` — use this fallback format\nonly, never both. Only call a tool whose exact name appears in the tool index above and is currently callable\n(or reachable via `capability_expand`). Never invent, rename, alias, or guess a tool name, platform ID, or\nplatform action from argument shape or wording — if the index does not list it, it does not exist.\n`platform_connect.action` (list/guide/preflight/connect/disconnect/remove/status/events_start/events_stop/\nevents_status) is the App Connector management namespace; `platform_action.action` only accepts exact\nregistered business actions such as `im.send_message` shown in the App Connector Capability Index — never mix\nthe two namespaces. If a tool call returns an unknown/unavailable result, use the returned suggestions or\navailable names for a single retry instead of trying further variations of the same guess.\n\n**Side-effect action rule** (`platform_action` with effect=send/write/execute):\n- Call each unique action+payload **exactly once**. Never include duplicates in the same turn.\n- Once the result returns `\"completed\": true`, that action is DONE for this task. Do NOT call it again\n in any subsequent turn — immediately summarize the result for the user instead.\n- The system enforces idempotency: duplicate calls are blocked and will not execute.\n- If the user explicitly requests sending/writing multiple times, use distinct payloads per call.\n\n**Resource identifier provenance rule**:\n- NEVER fabricate, guess, or infer resource identifiers (chat_id, message_id, file_key, user_id, etc.).\n Every resource ID used in a side-effect action MUST come from a successful API response in this session.\n- If a read/list action fails (e.g. authorization error), you do NOT have valid resource IDs.\n Report the failure to the user — do NOT attempt the dependent write/send action with a guessed ID.\n- When a tool result contains `\"llm_instruction\"`, follow it exactly.\n\n## Guidelines\n1. **Direct answers first**: If you already know the answer, respond directly without tools.\n2. **Avoid redundant tool calls**: Do not call the same tool with the same arguments more than once in the same user turn. When an existing tool result already answers the user's request, stop calling tools and answer directly.\n3. **Use tools proactively**: When the user asks about files, time, system state, or needs actions performed, use the appropriate tool.\n4. **Chain tools when needed**: You can call multiple tools in sequence (e.g., list files → read file → summarize).\n5. **Handle failures gracefully**: If a tool fails, explain what went wrong and suggest alternatives. If it failed because of an unknown tool/platform/action name, retry once with an exact name from the returned suggestions, then explain rather than keep guessing.\n6. **Summarize results naturally**: After tool execution, synthesize the results into a helpful answer rather than dumping raw output.\n7. **Stay conversational**: Maintain a natural, helpful tone. Acknowledge context from earlier in the conversation.\n8. **Recall past work**:\n - Broad queries (\"之前做了什么\", \"列出任务\"): answer from the \"Recent Task History\" section already in your context. If insufficient, call session_list.\n - Specific lookups (\"上次那个配置怎么改的\"): call session_search with relevant phrases (NOT single characters).\n - Do NOT call search tools repeatedly with keyword fragments. One well-phrased call is better than ten fragmented ones.\n\n## Coding & Verification\nWhen working with code, prefer the precise built-in tools over ad-hoc shell: use `repo_map` to orient in an\nunfamiliar project, `code_search`/`file_find` to locate and `code_intel` for symbols, `edit_file` (anchored\nsearch-replace, or a unified `diff`) to change files — never rewrite a whole file to change a few lines — and\n`git_query` to inspect diffs/log. After edits, check `syntax_ok` in the result and run `test_run`/`lint_check`\nbefore declaring the work done. Batch independent read-only calls (search/read/query) into a single turn — they\nrun in parallel.\n\n## Presentation Style\n1. **Polished Markdown only**: Format user-facing answers with clean Markdown headings, short paragraphs, and concise bullets. Use tables only when they improve comparison or scanning.\n2. **Terminal-friendly layout**: Keep lines readable in a TUI; avoid dense walls of text, deeply nested lists, oversized ASCII art, or heavy visual blocks.\n3. **Elegant emphasis**: Use bold text sparingly for key terms and conclusions. Avoid excessive emojis, decorative symbols, repeated separators, or visual noise.\n4. **Theme-safe colors**: Do not emit ANSI escape codes, HTML color tags, Rich markup, or hardcoded color names. Rely on the TUI theme to render Markdown professionally.\n5. **No leaked tool protocol**: Never show tool-call JSON, internal schemas, raw observations, tool result payloads, or hidden reasoning in the final answer unless the user explicitly asks for raw/debug output. Treat any prior `{\"name\": ..., \"arguments\": ...}` blocks and `Tool result (...)` messages as internal execution context only.\n6. **Professional closure**: End with a concise conclusion or next step when helpful; avoid rambling after the useful answer is complete.\n\nWhen finished with all tool calls, respond normally without a JSON block, tool-call transcript, or process log.\n\n## Task Contract\n- Task ID: turn-1\n- Original user request: Install the hermetic DSH echo plugin from this workspace.\n- Workspace root: \n- Allowed roots: \n- Treat relative project paths as relative to the workspace root; never infer `.` as the project root when a workspace root is provided.\n- Workspace boundary is enforced by tools: do not read, search, edit, or run commands against paths outside the allowed roots unless the user explicitly requests an external path and the tool/approval policy permits it.\n- LeapFlow workspace config is optional at `/.leapflow/config.yaml`; runtime config is loaded from `~/.leapflow/config/user.yaml` and `~/.leapflow/profiles//config/*.yaml`.\n- Preserve this task contract across summarization, compression, tool loops, and memory retrieval.\n" - }, - { - "role": "system", - "content": "\n## Recent Session Summary\n- [user] Install the hermetic DSH echo plugin from this workspace.\n" - }, - { - "role": "user", - "content": "Install the hermetic DSH echo plugin from this workspace.\nInstall the hermetic DSH echo plugin from this workspace." - }, - { - "role": "assistant", - "content": "", - "tool_calls": [ - "plugin_install" - ] - }, - { - "role": "tool", - "content": "{\"ok\": true, \"action\": \"install\", \"plugin_id\": \"r6_dsh_echo\", \"installed_tools\": [\"fixture_echo\"], \"state\": \"active\", \"version\": \"r6\", \"source_kind\": \"dsh_package\", \"bundle_sha256\": \"\", \"descriptor_path\": \"\", \"verdict\": \"adaptable\", \"limitations\": [], \"client_components\": [], \"execution_id\": \"\", \"idempotency_key\": \"\", \"execution_policy\": \"mutating_once\", \"tool_call_id\": \"call_1\", \"execution_status\": \"completed\"}", - "tool_result": true - } - ], - "tools": [ - "capability_expand", - "code_intel", - "code_search", - "config_get", - "config_list", - "env_info", - "file_find", - "file_list", - "file_read", - "git_query", - "lint_check", - "memory_search", - "plugin_list", - "plugin_propose", - "plugin_status", - "plugin_versions", - "repo_map", - "research_note", - "runtime_snapshot", - "schedule_reentry", - "self_describe", - "session_list", - "skill_view", - "skills_list", - "terminal_list", - "terminal_read", - "test_run", - "text_search", - "time_get", - "tool_describe", - "tool_search", - "web_fetch" - ] - }, - "responses": [ - { - "status": 200, - "content_type": "application/json", - "body": "{\"id\": \"chatcmpl-cassette\", \"object\": \"chat.completion\", \"model\": \"cassette-model\", \"choices\": [{\"index\": 0, \"message\": {\"role\": \"assistant\", \"content\": \"Installed the hermetic DSH plugin.\"}, \"finish_reason\": \"stop\"}], \"usage\": {\"prompt_tokens\": 64, \"completion_tokens\": 16, \"total_tokens\": 80}}" - } - ] -} diff --git a/tests/_fixtures/cassettes/r6_lifecycle/cassette-model-cd919c33f2f9d638.cassette.json b/tests/_fixtures/cassettes/r6_lifecycle/cassette-model-cd919c33f2f9d638.cassette.json deleted file mode 100644 index 63fb28ef..00000000 --- a/tests/_fixtures/cassettes/r6_lifecycle/cassette-model-cd919c33f2f9d638.cassette.json +++ /dev/null @@ -1,68 +0,0 @@ -{ - "fingerprint": "cd919c33f2f9d6382aa50bedc2be87fa020c5e72a694e023cfd900b711fe175a", - "note": "captured in seed mode", - "request": { - "model": "cassette-model", - "stream": false, - "messages": [ - { - "role": "system", - "content": "You are LeapFlow, an intelligent assistant that can both converse naturally and take real actions on the user's computer.\n\n## Capabilities\nThe tool index below lists **every** registered tool by name and a one-line summary — this is the complete\ncapability contract; nothing else exists. Only a subset is directly callable this turn (via native tool calling,\nnot a JSON block in your reply). If you need a tool from the index that is not yet callable, call\n`capability_expand` with its category name first — the matching tools become callable immediately after.\nWhen the user asks what LeapFlow itself supports, whether it supports plugins, or which runtime capabilities\nare available, use the live capability evidence exposed by `plugin_list` before making capability claims; report\nconfiguration-dependent or unavailable capabilities as limitations instead of inferring from documentation.\n- **text_search**(text, pattern): Search for a regex pattern in text.\n- **text_replace**(text, old, new, count) [capability_expand category: write]: Replace occurrences of a substring in text.\n- **time_get**(): Get current date and time.\n- **env_info**(): Get system environment information (OS, Python version, cwd).\n- **skills_list**(query, category, source): List available learned skills. Use when user asks about capabilities or you need a specific skill.\n- **skill_view**(name): View the full content of a specific skill document.\n- **code_intel**(path, operation): Precise document symbols (outline) for a source file: classes, functions, and methods with line ranges. Python uses an exact AST parse; other languages use a keyword-prefix scan. Prefer over file_read mode=symbols for accurate navigation before editing. Read-only.\n- **repo_map**(path): Compact project orientation for a repository root: languages, detected test/lint commands, top-level structure, entry points, manifest, and VCS branch. Call this first when entering an unfamiliar codebase. Read-only.\n- **scm_sync**(action, cwd, remote, pull_ref, push_ref, timeout) [capability_expand category: scm]: Run a typed git SCM action. Use this instead of shell_run for git pull/push/status. For 'pull origin main then push', set action='pull_then_push', remote='origin', pull_ref='main', and omit push_ref so LeapFlow pushes the current local branch.\n- **git_query**(action, cwd, ref, path, staged, max_count, stat): Read-only structured git inspection: action=diff|log|status|branch|show. Prefer over shell_run for reading repo state — output is clipped, redacted, and log/branch are parsed into structured fields. Use scm_sync for pull/push.\n- **git_write**(action, cwd, message, stage_all, name, ref, create) [capability_expand category: scm]: Mutating git actions: action=commit (message, stage_all), branch (create+switch), checkout (switch; create=true for -b). Approval-gated. Use scm_sync for pull/push and git_query for reads.\n- **test_run**(command, cwd, timeout): Run the project's test suite and return structured results (framework, passed/failed counts, failing tests). Auto-detects the runner (pytest/npm/go/cargo) or uses a configured/explicit command; executes via the governed shell. ok=true means the runner executed — see 'success' for pass/fail.\n- **lint_check**(command, cwd, timeout): Run the project's linter and return a structured clean/issue result. Auto-detects the linter (ruff/eslint/go vet/clippy) or uses a configured/explicit command; executes via the governed shell. ok=true means the linter ran — see 'clean'.\n- **file_list**(path, pattern, depth): List files and directories at a given path. Use depth=1 or depth=2 to get a recursive tree in one call instead of listing each sub-directory separately.\n- **file_read**(path, max_lines, start_line, max_chars, mode): Read text file content with adaptive context governance. For large or unfamiliar files, prefer mode='outline' or mode='symbols' first, then use mode='raw' with start_line/max_lines for the specific range you actually need. For LeapFlow's own settings, use config_list / config_get / config_set — its config files are outside the workspace and not readable here.\n- **file_write**(path, content, mode) [capability_expand category: write]: Write content to a file (overwrite or append).\n- **code_search**(pattern, patterns, path, glob, ignore_case, multiline, max_results, context_lines): Search file CONTENTS by regex pattern across a directory tree (ripgrep-backed). Requires a regex pattern. NOT for listing or browsing directory contents — use file_list for that. Prefer this over shell_run grep: faster, skips VCS/dependency/build dirs, and returns structured path:line:column matches. Batch related lookups into ONE call via `patterns` (OR-combined, single pass) instead of issuing several separate searches. Use file_read for the surrounding context of a hit.\n- **file_find**(glob, path, max_results): Find files by a recursive glob pattern under a base path (e.g. '**/test_*.py' or '*.md'). Prefer this over shell_run find; skips VCS/dependency/build dirs.\n- **edit_file**(path, edits, dry_run, diff) [capability_expand category: write]: Apply targeted, anchored search-replace edits to an EXISTING text file (use file_write to create/overwrite). Each edit is {original_text, new_text, replace_all?}; original_text must match exactly and uniquely (or set replace_all) — a non-unique or missing anchor is rejected so files are never corrupted. Set dry_run to preview. Alternatively pass a unified 'diff' to apply its hunks as anchored edits. Far cheaper and safer than rewriting a whole file.\n- **shell_run**(command, cwd, timeout) [capability_expand category: shell]: Execute a one-shot shell command with timeout protection. Runs in the active workspace; paths resolving outside it are refused. Reach for a structured tool first when one fits — web_fetch for anything over HTTP(S), git_query/scm_sync for git, code_search/file_find/file_read for the repo, config_get/config_set for LeapFlow's own settings — because those report typed results, while a failed shell command can only be diagnosed from its exit code and stderr. Every shell run counts as an external side effect, so a failure stops the rest of the batch and is not retried automatically.\n- **terminal_open**(command, cwd, shell) [capability_expand category: terminal]: Open a PERSISTENT shell session (REPL/dev server/watch), returning a session_id for terminal_send/read/close. Disabled unless tools.terminal_session_enabled is set. For one-shot commands use shell_run instead.\n- **terminal_send**(session_id, input, wait) [capability_expand category: terminal]: Send a line of input to a persistent terminal session and return output captured shortly after.\n- **terminal_read**(session_id, wait): Drain buffered output from a persistent terminal session (optionally waiting briefly first).\n- **terminal_close**(session_id) [capability_expand category: terminal]: Terminate a persistent terminal session and release its process group.\n- **terminal_list**(): List active persistent terminal sessions.\n- **config_list**(category, limit): List LeapFlow's own writable settings (LLM model and endpoint, daemon, memory, perception, gateway, …) with current values. Use this to discover the exact key before changing anything. Optionally narrow by `category`. This is the only correct way to inspect LeapFlow configuration — never read config files from disk.\n- **config_get**(key): Read one LeapFlow setting by key (e.g. 'llm.model', 'llm.base_url', 'daemon.log_level'), returning its current value, type, scopes, and whether a change needs a daemon restart. There is no 'llm.provider' key: provider behavior is inferred from the OpenAI-compatible 'llm.base_url'. Never read LeapFlow config files from disk — use this.\n- **config_set**(key, value, scope) [capability_expand category: config]: Change one LeapFlow setting by key, e.g. switch an OpenAI-compatible LLM with key='llm.model' and key='llm.base_url'. There is no 'llm.provider' key: provider behavior is inferred from the endpoint. Values are validated and coerced; credentials are stored in the vault automatically. Call config_list or config_get first if unsure of the key. The result states whether a `leap daemon restart` is required. Never edit LeapFlow config files directly.\n- **web_fetch**(url, select, timeout, max_bytes): Read a URL over HTTP(S) and get back extracted, context-sized content: parsed JSON for API endpoints, readable text plus links for web pages. Use this for anything on the internet — prices, docs, releases, articles — instead of running curl through shell_run: it reports real HTTP status codes, retries rate limits on its own, and is a plain read so a retry is always safe. For JSON APIs pass `select` with a dotted path (e.g. 'chart.result.0.meta') to return just that part instead of the whole payload.\n- **memory_search**(query, limit): Search agent memory for relevant past experiences, observations, and facts.\n- **memory_add**(content, kind) [capability_expand category: write]: Store a new observation or insight in memory for future reference.\n- **research_note**(kind, text): Record a compact, structured note about the current task's state so it survives context compression on long / multi-step tasks. Use for durable findings, open questions still to resolve, decisions / excluded paths, and the immediate next step. One concise sentence per note.\n- **capability_expand**(category): Fetch the full callable schema for every tool in a capability category (e.g. 'hub', 'gateway', 'desktop', 'delegate', 'file', 'memory', 'skill'). The compact tool index always lists every registered tool by name and a one-line summary, but only a static low-risk subset is directly callable each turn. If you need a tool from the index that is not yet callable, call capability_expand with its category first; the matching tools become callable in this turn. Never invent a tool name — expand the category instead.\n- **delegate_task**(goal, context) [capability_expand category: delegate]: Delegate a complex sub-task to an isolated subagent. The subagent gets a fresh context and restricted tool access. Use when a task is self-contained and can be solved independently.\n- **schedule_reentry**(kind, reason, delay_seconds, event_match, max_reentries, deadline_seconds): Register a re-entry so this task can resume later from its current orientation (findings / open questions / next step). Use when work must pause and continue after a delay (kind=time) or when a matching platform event arrives (kind=event), instead of finishing now. The research-ledger state is carried over automatically.\n- **hub_push**(skill_name, visibility, version) [capability_expand category: hub]: Push a local skill to the ModelScope Hub for sharing or backup.\n- **hub_pull**(repo_id, version) [capability_expand category: hub]: Pull a skill from the ModelScope Hub to install locally.\n- **hub_search**(query) [capability_expand category: hub]: Search for skills on the Hub by keyword or description.\n- **hub_sync**(mode, dry_run) [capability_expand category: hub]: Preview or execute sync between local skills and Hub.\n- **platform_action**(platform, action, payload, backend_kind) [capability_expand category: gateway]: Execute an exact registered business action on an external platform through LeapFlow's App Connector layer. Actions must be copied from the App Connector Capability Index and are addressed as domain.operation, e.g. im.send_message or docs.create_markdown. All business fields (chat_id, text, query, etc.) MUST be placed inside `payload`, never at the top level. Example: {\"platform\":\"feishu\",\"action\":\"im.send_message\",\"payload\":{\"chat_id\":\"oc_xxx\",\"text\":\"hello\"}}. Do not invent action names, do not use management actions such as list/guide/connect/status here.\n- **platform_connect**(action, platform, credentials, options, checkpoint) [capability_expand category: gateway]: List, guide, connect, disconnect, remove, or check status for external platforms using the App Connector management namespace. Supports REST and CLI backends. Use this for management actions such as list/guide/preflight/connect/status; use platform_action only for exact registered business actions.\n- **gateway_send**(platform, chat_id, text, thread_id) [capability_expand category: gateway]: Send a message to a connected external platform (Feishu group, Telegram chat, DingTalk conversation, etc.). Requires the platform to be connected via gateway_connect first. Use gateway_connect with action='list' to see connected platforms and available chat IDs.\n- **gateway_connect**(action, platform, credentials, options) [capability_expand category: gateway]: Connect, configure, or manage external platform integrations (Feishu, DingTalk, Telegram, Slack, Discord, etc.). Conversational flow: 1) call 'guide' to get setup steps + required fields, 2) present the steps to the user and ask for ALL required credentials in a single message, 3) call 'connect' with the credentials. Goal: complete in 1–2 user turns. NEVER include credential values in your text response.\n- **plugin_list**(): List the live plugin registry and cross-subsystem capability evidence. Use this before answering questions about whether LeapFlow supports plugins, self-evolution, plugin installation, hot reload, versioning, or other runtime capabilities.\n- **plugin_status**(plugin_id): Get detailed status of a specific plugin: its declared category, runtime dependencies, contributed tools, and fiber lifecycle state.\n- **plugin_versions**(plugin_id): List recorded source versions and active pointer for a profile-scoped plugin.\n- **plugin_propose**(requested_capability, plugin_id, proposed_tools, test_cases, risk_level, evidence): Create a side-effect-free PluginProposal from explicit capability-gap evidence. Use this before plugin_generate when a missing capability should be reviewed. Does not call an LLM, write files, or install anything.\n- **assess_compatibility**(manifest, source_path) [capability_expand category: plugin_management]: Assess whether a foreign plugin manifest or real DSH source bundle is compatible with LeapFlow. A source bundle assessment is static: runtime_ready stays false until restricted Node discovery during plugin_install. Returns component-level verdicts and limitations.\n- **plugin_generate**(plugin_id, description, proposal_id) [capability_expand category: system]: Generate a new ToolPlugin from a natural-language capability description. The LLM produces code that conforms to the ToolPlugin Protocol; it is then rigorously validated (syntax, structure, import, protocol conformance). The generated source is stored in CAS and receives explicit content approval. It DOES NOT install the plugin — installation requires a second, mutation-specific approval via plugin_install.\n- **plugin_install**(plugin_id, code, marketplace_name, source_path, proposal_id, version_label) [capability_expand category: system]: Install a plugin from exactly one source: validated Python code, the configured Python marketplace, or a real DSH source bundle. DSH bundles run restricted Node discovery before registration; only public host tools are installed and client UI limitations are reported. Writes profile-scoped state and mutates the process-global registry. REQUIRES APPROVAL.\n- **plugin_rollback**(plugin_id, version) [capability_expand category: system]: Rollback a profile-scoped plugin to a recorded source version and reload it. REQUIRES APPROVAL.\n- **plugin_reload**(plugin_id, version_label) [capability_expand category: system]: Hot-reload a plugin at runtime. Disposes the old plugin fiber, re-imports its module, and registers a fresh instance. Existing in-flight turns are unaffected (snapshot isolation). REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_disable**(plugin_id) [capability_expand category: system]: Disable a plugin by disposing its fiber, removing its tools from the runtime registry. Cannot disable self_management itself. REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_remove**(plugin_id, delete_source) [capability_expand category: system]: Terminally remove a plugin: dispose its fiber, unregister its tools, remove reload metadata, and optionally delete its profile-scoped source file. Cannot remove self_management itself. REQUIRES APPROVAL.\n- **plugin_unquarantine**(plugin_id) [capability_expand category: plugin_management]: Restore a quarantined plugin to probation status for re-evaluation. Unfreezes the trust ledger, transitions the proposal back to PROBATION, and reloads the plugin. REQUIRES APPROVAL.\n- **plugin_enable**(plugin_id) [capability_expand category: system]: Re-enable a previously disabled plugin by reloading its module and registering a fresh instance. REQUIRES APPROVAL.\n- **self_describe**(facet): Introspect LeapFlow's own identity, capabilities, runtime state, evolution metrics, or platform connections. Use facet='all' only when a comprehensive self-check is explicitly requested.\n- **runtime_snapshot**(): Lightweight (~150 token) flat snapshot of current runtime state: model, context budget, posture, disclosure level, turn count, cache hit rate, uptime, tool count, and pending approvals.\n- **schedule_create**(trigger_expression, instruction, execution_mode, max_retries, delivery_target) [capability_expand category: scheduler]: Create a new scheduled task. Specify a trigger expression (e.g. '30m', 'every 2h', '0 9 * * *') and the instruction to execute on each trigger. Optionally provide an execution mode and a delivery target to receive notifications.\n- **schedule_list**() [capability_expand category: scheduler]: List all scheduled tasks with their current state, trigger, and next due time.\n- **schedule_status**(task_id) [capability_expand category: scheduler]: Get detailed status and recent execution history for a scheduled task.\n- **schedule_pause**(task_id) [capability_expand category: scheduler]: Pause a scheduled task so it stops firing without being cancelled.\n- **schedule_resume**(task_id) [capability_expand category: scheduler]: Resume a paused scheduled task — re-arms it and recalculates next due time.\n- **schedule_cancel**(task_id) [capability_expand category: scheduler]: Cancel a scheduled task permanently.\n- **tool_search**(query, max_results): Search all registered tools by keyword relevance. Returns a ranked list of matching tools with name, category, and summary. Use this when you need a tool but are unsure of its exact name or category.\n- **tool_describe**(tool_name): Get the full callable schema of a single tool by exact name. Returns the tool's description, parameters, and metadata. Use after tool_search to inspect a specific tool before calling it.\n- **session_search**(query, limit) [capability_expand category: unclassified]: Search past conversation sessions in the current workspace for relevant context.\n- **session_list**(limit): List recent conversation sessions in the current workspace with stable ids, titles, dates, and summaries. Use for browsing past tasks or when user asks to see history without specific search terms. No keywords needed — returns chronological list.\n- **session_detail**(session_id, limit, offset, include_inactive) [capability_expand category: unclassified]: Read a paginated persisted transcript for one past conversation session in the current workspace. Use after session_list or session_search returns a session_id.\n- **fixture_echo**(text): Return the supplied text from a pre-built DSH package.\n- **click**(element_index) [capability_expand category: desktop]: Click a UI element by its element_index (from the latest observe_ui snapshot)\n- **get_clipboard**() [capability_expand category: desktop]: Read current clipboard text content\n- **list_apps**(filter, running_only) [capability_expand category: desktop]: List available applications on this system. Use to discover correct bundle_id before switch_app.\n- **list_windows**() [capability_expand category: desktop]: List all top-level windows with pid, window_id, title, and per-window state (minimized, on-screen). Call this first to pick the pid and window_id that observe_ui and other window tools require.\n- **observe_ui**(pid, window_id, query) [capability_expand category: desktop]: Snapshot one window's actionable UI elements, each tagged with an element_index for click/right_click/read_text. Re-observe after actions — indices belong to one snapshot. Requires the window's pid and window_id from list_windows.\n- **open_url**(url, app_id) [capability_expand category: desktop]: Open a URL in the default or specified browser\n- **read_text**(element_index) [capability_expand category: desktop]: Read the text content of a specific UI element from the latest snapshot\n- **right_click**(element_index) [capability_expand category: desktop]: Right-click a UI element to open its context menu. Returns visible menu items.\n- **screenshot**(pid, window_id) [capability_expand category: desktop]: Capture a screenshot for visual verification. With pid + window_id captures that window (works across all displays); defaults to the last observed window, or the full desktop when no window has been observed.\n- **scroll**(element_index, direction, amount, pid, window_id) [capability_expand category: desktop]: Scroll a scrollable area of a window. Omit element_index to scroll the window's focused/page scroller; pass one to scroll an exact element from the latest snapshot.\n- **select_text**(element_index) [capability_expand category: desktop]: Select all text in a UI element (focus + select-all, for subsequent copy)\n- **set_clipboard**(text) [capability_expand category: desktop]: Write text to the clipboard\n- **shortcut**(keys) [capability_expand category: desktop]: Execute a keyboard shortcut\n- **switch_app**(app_id) [capability_expand category: desktop]: Switch to an app (launch if needed, activate, verify)\n- **type_text**(text) [capability_expand category: desktop]: Type text into the currently focused element\n- **wait**(seconds) [capability_expand category: desktop]: Wait for a specified duration before continuing\n- **wait_until**(condition, pid, window_id, timeout, poll_interval) [capability_expand category: desktop]: Wait until a UI condition is met (polls UI tree). Returns elements when found or on timeout.\n- **wait_until_stable**(timeout, poll_interval, pid, window_id) [capability_expand category: desktop]: Wait until the UI stops changing (element set stabilizes across polls).\n\n## App Connector Capability Index\nLeapFlow can onboard and manage external apps through `platform_connect` and execute exact registered business actions through `platform_action`.\nFor requests about connecting, setting up, configuring, enabling, or managing a supported app, use `platform_connect` first instead of generating SDK/Webhook sample code.\n`platform_connect.action` is the management namespace: list, guide, preflight, connect, disconnect, remove, status, events_start, events_stop, events_status.\n`platform_action.action` is only for exact registered platform business actions listed below, such as `im.send_message`; never use management actions like `list` or `guide` there.\nAll business fields MUST go inside `payload`; top-level keys are only `platform`, `action`, and `payload`.\nDo not invent platform IDs or platform action names. If the needed action is not listed, ask for discovery/clarification instead of guessing.\nUse `platform_connect` with `action='guide'` and the matching `platform` to start onboarding; use `action='list'` when the app is unclear.\nWhen a pending onboarding state is present, continue from that state with `platform_connect` instead of asking the user to restate the app.\nSupported apps:\n- `api_server`: API Server (OpenAI Compatible) (category=api; backend=adapter; platform_action actions=none registered)\n- `dingtalk`: 钉钉 (DingTalk) (category=im; backend=adapter; platform_action actions=none registered)\n- `feishu`: 飞书 (Feishu/Lark) (category=im; backend=cli)\n - `calendar.create_event` payload={summary*, start_time*, end_time*, attendees} [write/high]\n - `docs.create_markdown` payload={title*, markdown*, folder_token} [write/medium]\n - `drive.search` payload={query*, limit} [read/medium]\n - `im.add_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.download_resource` payload={message_id*, file_key*, type*} [read/low]\n - `im.get_messages` payload={message_ids*} [read/low]\n - `im.list_chats` payload={page_size} [read/low]\n - `im.list_messages` payload={chat_id*} [read/low]\n - `im.list_thread_messages` payload={thread*} [read/low]\n - `im.remove_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.reply_message` payload={message_id*, text*} [send/high]\n - `im.search_chats` payload={query*, page_size} [read/low]\n - `im.search_messages` payload={query*} [read/medium]\n - `im.send_message` payload={chat_id*, text*, thread_id} [send/high]\n - `im.update_card` payload={token*, card*} [write/medium]\n - `im.update_message` payload={message_id*, text*} [write/medium]\n - `mail.search_unread` payload={query, limit} [read/high]\n - `sheets.append_row` payload={spreadsheet_token*, sheet_id*, values*} [write/medium]\n - `task.create` payload={title*, description, due_time} [write/medium]\n- `telegram`: Telegram (category=social; backend=adapter; platform_action actions=none registered)\n- `webhook`: Webhook (Generic) (category=webhook; backend=adapter; platform_action actions=none registered)\n\n## Tool Usage\nTools are normally invoked through the native function-calling mechanism, not by writing JSON in your reply\ntext. Only if the provider signals that native function calling is unavailable for this turn, fall back to a\nsingle JSON code block: `{\"name\": \"tool_name\", \"arguments\": {\"key\": \"value\"}}` — use this fallback format\nonly, never both. Only call a tool whose exact name appears in the tool index above and is currently callable\n(or reachable via `capability_expand`). Never invent, rename, alias, or guess a tool name, platform ID, or\nplatform action from argument shape or wording — if the index does not list it, it does not exist.\n`platform_connect.action` (list/guide/preflight/connect/disconnect/remove/status/events_start/events_stop/\nevents_status) is the App Connector management namespace; `platform_action.action` only accepts exact\nregistered business actions such as `im.send_message` shown in the App Connector Capability Index — never mix\nthe two namespaces. If a tool call returns an unknown/unavailable result, use the returned suggestions or\navailable names for a single retry instead of trying further variations of the same guess.\n\n**Side-effect action rule** (`platform_action` with effect=send/write/execute):\n- Call each unique action+payload **exactly once**. Never include duplicates in the same turn.\n- Once the result returns `\"completed\": true`, that action is DONE for this task. Do NOT call it again\n in any subsequent turn — immediately summarize the result for the user instead.\n- The system enforces idempotency: duplicate calls are blocked and will not execute.\n- If the user explicitly requests sending/writing multiple times, use distinct payloads per call.\n\n**Resource identifier provenance rule**:\n- NEVER fabricate, guess, or infer resource identifiers (chat_id, message_id, file_key, user_id, etc.).\n Every resource ID used in a side-effect action MUST come from a successful API response in this session.\n- If a read/list action fails (e.g. authorization error), you do NOT have valid resource IDs.\n Report the failure to the user — do NOT attempt the dependent write/send action with a guessed ID.\n- When a tool result contains `\"llm_instruction\"`, follow it exactly.\n\n## Guidelines\n1. **Direct answers first**: If you already know the answer, respond directly without tools.\n2. **Avoid redundant tool calls**: Do not call the same tool with the same arguments more than once in the same user turn. When an existing tool result already answers the user's request, stop calling tools and answer directly.\n3. **Use tools proactively**: When the user asks about files, time, system state, or needs actions performed, use the appropriate tool.\n4. **Chain tools when needed**: You can call multiple tools in sequence (e.g., list files → read file → summarize).\n5. **Handle failures gracefully**: If a tool fails, explain what went wrong and suggest alternatives. If it failed because of an unknown tool/platform/action name, retry once with an exact name from the returned suggestions, then explain rather than keep guessing.\n6. **Summarize results naturally**: After tool execution, synthesize the results into a helpful answer rather than dumping raw output.\n7. **Stay conversational**: Maintain a natural, helpful tone. Acknowledge context from earlier in the conversation.\n8. **Recall past work**:\n - Broad queries (\"之前做了什么\", \"列出任务\"): answer from the \"Recent Task History\" section already in your context. If insufficient, call session_list.\n - Specific lookups (\"上次那个配置怎么改的\"): call session_search with relevant phrases (NOT single characters).\n - Do NOT call search tools repeatedly with keyword fragments. One well-phrased call is better than ten fragmented ones.\n\n## Coding & Verification\nWhen working with code, prefer the precise built-in tools over ad-hoc shell: use `repo_map` to orient in an\nunfamiliar project, `code_search`/`file_find` to locate and `code_intel` for symbols, `edit_file` (anchored\nsearch-replace, or a unified `diff`) to change files — never rewrite a whole file to change a few lines — and\n`git_query` to inspect diffs/log. After edits, check `syntax_ok` in the result and run `test_run`/`lint_check`\nbefore declaring the work done. Batch independent read-only calls (search/read/query) into a single turn — they\nrun in parallel.\n\n## Presentation Style\n1. **Polished Markdown only**: Format user-facing answers with clean Markdown headings, short paragraphs, and concise bullets. Use tables only when they improve comparison or scanning.\n2. **Terminal-friendly layout**: Keep lines readable in a TUI; avoid dense walls of text, deeply nested lists, oversized ASCII art, or heavy visual blocks.\n3. **Elegant emphasis**: Use bold text sparingly for key terms and conclusions. Avoid excessive emojis, decorative symbols, repeated separators, or visual noise.\n4. **Theme-safe colors**: Do not emit ANSI escape codes, HTML color tags, Rich markup, or hardcoded color names. Rely on the TUI theme to render Markdown professionally.\n5. **No leaked tool protocol**: Never show tool-call JSON, internal schemas, raw observations, tool result payloads, or hidden reasoning in the final answer unless the user explicitly asks for raw/debug output. Treat any prior `{\"name\": ..., \"arguments\": ...}` blocks and `Tool result (...)` messages as internal execution context only.\n6. **Professional closure**: End with a concise conclusion or next step when helpful; avoid rambling after the useful answer is complete.\n\nWhen finished with all tool calls, respond normally without a JSON block, tool-call transcript, or process log.\n\n## Task Contract\n- Task ID: turn-2\n- Original user request: Invoke fixture_echo with the text before restart.\n- Workspace root: \n- Allowed roots: \n- Treat relative project paths as relative to the workspace root; never infer `.` as the project root when a workspace root is provided.\n- Workspace boundary is enforced by tools: do not read, search, edit, or run commands against paths outside the allowed roots unless the user explicitly requests an external path and the tool/approval policy permits it.\n- LeapFlow workspace config is optional at `/.leapflow/config.yaml`; runtime config is loaded from `~/.leapflow/config/user.yaml` and `~/.leapflow/profiles//config/*.yaml`.\n- Preserve this task contract across summarization, compression, tool loops, and memory retrieval.\n" - }, - { - "role": "system", - "content": "\n## Recent Session Summary\n- [assistant] Installed the hermetic DSH plugin.\n- [user] Invoke fixture_echo with the text before restart.\n" - }, - { - "role": "assistant", - "content": "Installed the hermetic DSH plugin." - }, - { - "role": "user", - "content": "Invoke fixture_echo with the text before restart.\nInvoke fixture_echo with the text before restart." - } - ], - "tools": [ - "capability_expand", - "code_intel", - "code_search", - "config_get", - "config_list", - "env_info", - "file_find", - "file_list", - "file_read", - "fixture_echo", - "git_query", - "lint_check", - "memory_search", - "plugin_list", - "plugin_propose", - "plugin_status", - "plugin_versions", - "repo_map", - "research_note", - "runtime_snapshot", - "schedule_reentry", - "self_describe", - "session_list", - "skill_view", - "skills_list", - "terminal_list", - "terminal_read", - "test_run", - "text_search", - "time_get", - "tool_describe", - "tool_search", - "web_fetch" - ] - }, - "responses": [ - { - "status": 200, - "content_type": "application/json", - "body": "{\"id\": \"chatcmpl-cassette\", \"object\": \"chat.completion\", \"model\": \"cassette-model\", \"choices\": [{\"index\": 0, \"message\": {\"role\": \"assistant\", \"content\": \"\", \"tool_calls\": [{\"id\": \"call_1\", \"type\": \"function\", \"function\": {\"name\": \"fixture_echo\", \"arguments\": \"{\\\"text\\\": \\\"before restart\\\"}\"}}]}, \"finish_reason\": \"tool_calls\"}], \"usage\": {\"prompt_tokens\": 64, \"completion_tokens\": 16, \"total_tokens\": 80}}" - } - ] -} diff --git a/tests/_fixtures/cassettes/r6_lifecycle/cassette-model-e69606f5ea8a656b.cassette.json b/tests/_fixtures/cassettes/r6_lifecycle/cassette-model-e69606f5ea8a656b.cassette.json deleted file mode 100644 index 9ebefc69..00000000 --- a/tests/_fixtures/cassettes/r6_lifecycle/cassette-model-e69606f5ea8a656b.cassette.json +++ /dev/null @@ -1,63 +0,0 @@ -{ - "fingerprint": "e69606f5ea8a656b9ce15c6276b898629f4c166a95dded5021aabd7678ffdffa", - "note": "captured in seed mode", - "request": { - "model": "cassette-model", - "stream": false, - "messages": [ - { - "role": "system", - "content": "You are LeapFlow, an intelligent assistant that can both converse naturally and take real actions on the user's computer.\n\n## Capabilities\nThe tool index below lists **every** registered tool by name and a one-line summary — this is the complete\ncapability contract; nothing else exists. Only a subset is directly callable this turn (via native tool calling,\nnot a JSON block in your reply). If you need a tool from the index that is not yet callable, call\n`capability_expand` with its category name first — the matching tools become callable immediately after.\nWhen the user asks what LeapFlow itself supports, whether it supports plugins, or which runtime capabilities\nare available, use the live capability evidence exposed by `plugin_list` before making capability claims; report\nconfiguration-dependent or unavailable capabilities as limitations instead of inferring from documentation.\n- **text_search**(text, pattern): Search for a regex pattern in text.\n- **text_replace**(text, old, new, count) [capability_expand category: write]: Replace occurrences of a substring in text.\n- **time_get**(): Get current date and time.\n- **env_info**(): Get system environment information (OS, Python version, cwd).\n- **skills_list**(query, category, source): List available learned skills. Use when user asks about capabilities or you need a specific skill.\n- **skill_view**(name): View the full content of a specific skill document.\n- **code_intel**(path, operation): Precise document symbols (outline) for a source file: classes, functions, and methods with line ranges. Python uses an exact AST parse; other languages use a keyword-prefix scan. Prefer over file_read mode=symbols for accurate navigation before editing. Read-only.\n- **repo_map**(path): Compact project orientation for a repository root: languages, detected test/lint commands, top-level structure, entry points, manifest, and VCS branch. Call this first when entering an unfamiliar codebase. Read-only.\n- **scm_sync**(action, cwd, remote, pull_ref, push_ref, timeout) [capability_expand category: scm]: Run a typed git SCM action. Use this instead of shell_run for git pull/push/status. For 'pull origin main then push', set action='pull_then_push', remote='origin', pull_ref='main', and omit push_ref so LeapFlow pushes the current local branch.\n- **git_query**(action, cwd, ref, path, staged, max_count, stat): Read-only structured git inspection: action=diff|log|status|branch|show. Prefer over shell_run for reading repo state — output is clipped, redacted, and log/branch are parsed into structured fields. Use scm_sync for pull/push.\n- **git_write**(action, cwd, message, stage_all, name, ref, create) [capability_expand category: scm]: Mutating git actions: action=commit (message, stage_all), branch (create+switch), checkout (switch; create=true for -b). Approval-gated. Use scm_sync for pull/push and git_query for reads.\n- **test_run**(command, cwd, timeout): Run the project's test suite and return structured results (framework, passed/failed counts, failing tests). Auto-detects the runner (pytest/npm/go/cargo) or uses a configured/explicit command; executes via the governed shell. ok=true means the runner executed — see 'success' for pass/fail.\n- **lint_check**(command, cwd, timeout): Run the project's linter and return a structured clean/issue result. Auto-detects the linter (ruff/eslint/go vet/clippy) or uses a configured/explicit command; executes via the governed shell. ok=true means the linter ran — see 'clean'.\n- **file_list**(path, pattern, depth): List files and directories at a given path. Use depth=1 or depth=2 to get a recursive tree in one call instead of listing each sub-directory separately.\n- **file_read**(path, max_lines, start_line, max_chars, mode): Read text file content with adaptive context governance. For large or unfamiliar files, prefer mode='outline' or mode='symbols' first, then use mode='raw' with start_line/max_lines for the specific range you actually need. For LeapFlow's own settings, use config_list / config_get / config_set — its config files are outside the workspace and not readable here.\n- **file_write**(path, content, mode) [capability_expand category: write]: Write content to a file (overwrite or append).\n- **code_search**(pattern, patterns, path, glob, ignore_case, multiline, max_results, context_lines): Search file CONTENTS by regex pattern across a directory tree (ripgrep-backed). Requires a regex pattern. NOT for listing or browsing directory contents — use file_list for that. Prefer this over shell_run grep: faster, skips VCS/dependency/build dirs, and returns structured path:line:column matches. Batch related lookups into ONE call via `patterns` (OR-combined, single pass) instead of issuing several separate searches. Use file_read for the surrounding context of a hit.\n- **file_find**(glob, path, max_results): Find files by a recursive glob pattern under a base path (e.g. '**/test_*.py' or '*.md'). Prefer this over shell_run find; skips VCS/dependency/build dirs.\n- **edit_file**(path, edits, dry_run, diff) [capability_expand category: write]: Apply targeted, anchored search-replace edits to an EXISTING text file (use file_write to create/overwrite). Each edit is {original_text, new_text, replace_all?}; original_text must match exactly and uniquely (or set replace_all) — a non-unique or missing anchor is rejected so files are never corrupted. Set dry_run to preview. Alternatively pass a unified 'diff' to apply its hunks as anchored edits. Far cheaper and safer than rewriting a whole file.\n- **shell_run**(command, cwd, timeout) [capability_expand category: shell]: Execute a one-shot shell command with timeout protection. Runs in the active workspace; paths resolving outside it are refused. Reach for a structured tool first when one fits — web_fetch for anything over HTTP(S), git_query/scm_sync for git, code_search/file_find/file_read for the repo, config_get/config_set for LeapFlow's own settings — because those report typed results, while a failed shell command can only be diagnosed from its exit code and stderr. Every shell run counts as an external side effect, so a failure stops the rest of the batch and is not retried automatically.\n- **terminal_open**(command, cwd, shell) [capability_expand category: terminal]: Open a PERSISTENT shell session (REPL/dev server/watch), returning a session_id for terminal_send/read/close. Disabled unless tools.terminal_session_enabled is set. For one-shot commands use shell_run instead.\n- **terminal_send**(session_id, input, wait) [capability_expand category: terminal]: Send a line of input to a persistent terminal session and return output captured shortly after.\n- **terminal_read**(session_id, wait): Drain buffered output from a persistent terminal session (optionally waiting briefly first).\n- **terminal_close**(session_id) [capability_expand category: terminal]: Terminate a persistent terminal session and release its process group.\n- **terminal_list**(): List active persistent terminal sessions.\n- **config_list**(category, limit): List LeapFlow's own writable settings (LLM model and endpoint, daemon, memory, perception, gateway, …) with current values. Use this to discover the exact key before changing anything. Optionally narrow by `category`. This is the only correct way to inspect LeapFlow configuration — never read config files from disk.\n- **config_get**(key): Read one LeapFlow setting by key (e.g. 'llm.model', 'llm.base_url', 'daemon.log_level'), returning its current value, type, scopes, and whether a change needs a daemon restart. There is no 'llm.provider' key: provider behavior is inferred from the OpenAI-compatible 'llm.base_url'. Never read LeapFlow config files from disk — use this.\n- **config_set**(key, value, scope) [capability_expand category: config]: Change one LeapFlow setting by key, e.g. switch an OpenAI-compatible LLM with key='llm.model' and key='llm.base_url'. There is no 'llm.provider' key: provider behavior is inferred from the endpoint. Values are validated and coerced; credentials are stored in the vault automatically. Call config_list or config_get first if unsure of the key. The result states whether a `leap daemon restart` is required. Never edit LeapFlow config files directly.\n- **web_fetch**(url, select, timeout, max_bytes): Read a URL over HTTP(S) and get back extracted, context-sized content: parsed JSON for API endpoints, readable text plus links for web pages. Use this for anything on the internet — prices, docs, releases, articles — instead of running curl through shell_run: it reports real HTTP status codes, retries rate limits on its own, and is a plain read so a retry is always safe. For JSON APIs pass `select` with a dotted path (e.g. 'chart.result.0.meta') to return just that part instead of the whole payload.\n- **memory_search**(query, limit): Search agent memory for relevant past experiences, observations, and facts.\n- **memory_add**(content, kind) [capability_expand category: write]: Store a new observation or insight in memory for future reference.\n- **research_note**(kind, text): Record a compact, structured note about the current task's state so it survives context compression on long / multi-step tasks. Use for durable findings, open questions still to resolve, decisions / excluded paths, and the immediate next step. One concise sentence per note.\n- **capability_expand**(category): Fetch the full callable schema for every tool in a capability category (e.g. 'hub', 'gateway', 'desktop', 'delegate', 'file', 'memory', 'skill'). The compact tool index always lists every registered tool by name and a one-line summary, but only a static low-risk subset is directly callable each turn. If you need a tool from the index that is not yet callable, call capability_expand with its category first; the matching tools become callable in this turn. Never invent a tool name — expand the category instead.\n- **delegate_task**(goal, context) [capability_expand category: delegate]: Delegate a complex sub-task to an isolated subagent. The subagent gets a fresh context and restricted tool access. Use when a task is self-contained and can be solved independently.\n- **schedule_reentry**(kind, reason, delay_seconds, event_match, max_reentries, deadline_seconds): Register a re-entry so this task can resume later from its current orientation (findings / open questions / next step). Use when work must pause and continue after a delay (kind=time) or when a matching platform event arrives (kind=event), instead of finishing now. The research-ledger state is carried over automatically.\n- **hub_push**(skill_name, visibility, version) [capability_expand category: hub]: Push a local skill to the ModelScope Hub for sharing or backup.\n- **hub_pull**(repo_id, version) [capability_expand category: hub]: Pull a skill from the ModelScope Hub to install locally.\n- **hub_search**(query) [capability_expand category: hub]: Search for skills on the Hub by keyword or description.\n- **hub_sync**(mode, dry_run) [capability_expand category: hub]: Preview or execute sync between local skills and Hub.\n- **platform_action**(platform, action, payload, backend_kind) [capability_expand category: gateway]: Execute an exact registered business action on an external platform through LeapFlow's App Connector layer. Actions must be copied from the App Connector Capability Index and are addressed as domain.operation, e.g. im.send_message or docs.create_markdown. All business fields (chat_id, text, query, etc.) MUST be placed inside `payload`, never at the top level. Example: {\"platform\":\"feishu\",\"action\":\"im.send_message\",\"payload\":{\"chat_id\":\"oc_xxx\",\"text\":\"hello\"}}. Do not invent action names, do not use management actions such as list/guide/connect/status here.\n- **platform_connect**(action, platform, credentials, options, checkpoint) [capability_expand category: gateway]: List, guide, connect, disconnect, remove, or check status for external platforms using the App Connector management namespace. Supports REST and CLI backends. Use this for management actions such as list/guide/preflight/connect/status; use platform_action only for exact registered business actions.\n- **gateway_send**(platform, chat_id, text, thread_id) [capability_expand category: gateway]: Send a message to a connected external platform (Feishu group, Telegram chat, DingTalk conversation, etc.). Requires the platform to be connected via gateway_connect first. Use gateway_connect with action='list' to see connected platforms and available chat IDs.\n- **gateway_connect**(action, platform, credentials, options) [capability_expand category: gateway]: Connect, configure, or manage external platform integrations (Feishu, DingTalk, Telegram, Slack, Discord, etc.). Conversational flow: 1) call 'guide' to get setup steps + required fields, 2) present the steps to the user and ask for ALL required credentials in a single message, 3) call 'connect' with the credentials. Goal: complete in 1–2 user turns. NEVER include credential values in your text response.\n- **plugin_list**(): List the live plugin registry and cross-subsystem capability evidence. Use this before answering questions about whether LeapFlow supports plugins, self-evolution, plugin installation, hot reload, versioning, or other runtime capabilities.\n- **plugin_status**(plugin_id): Get detailed status of a specific plugin: its declared category, runtime dependencies, contributed tools, and fiber lifecycle state.\n- **plugin_versions**(plugin_id): List recorded source versions and active pointer for a profile-scoped plugin.\n- **plugin_propose**(requested_capability, plugin_id, proposed_tools, test_cases, risk_level, evidence): Create a side-effect-free PluginProposal from explicit capability-gap evidence. Use this before plugin_generate when a missing capability should be reviewed. Does not call an LLM, write files, or install anything.\n- **assess_compatibility**(manifest, source_path) [capability_expand category: plugin_management]: Assess whether a foreign plugin manifest or real DSH source bundle is compatible with LeapFlow. A source bundle assessment is static: runtime_ready stays false until restricted Node discovery during plugin_install. Returns component-level verdicts and limitations.\n- **plugin_generate**(plugin_id, description, proposal_id) [capability_expand category: system]: Generate a new ToolPlugin from a natural-language capability description. The LLM produces code that conforms to the ToolPlugin Protocol; it is then rigorously validated (syntax, structure, import, protocol conformance). The generated source is stored in CAS and receives explicit content approval. It DOES NOT install the plugin — installation requires a second, mutation-specific approval via plugin_install.\n- **plugin_install**(plugin_id, code, marketplace_name, source_path, proposal_id, version_label) [capability_expand category: system]: Install a plugin from exactly one source: validated Python code, the configured Python marketplace, or a real DSH source bundle. DSH bundles run restricted Node discovery before registration; only public host tools are installed and client UI limitations are reported. Writes profile-scoped state and mutates the process-global registry. REQUIRES APPROVAL.\n- **plugin_rollback**(plugin_id, version) [capability_expand category: system]: Rollback a profile-scoped plugin to a recorded source version and reload it. REQUIRES APPROVAL.\n- **plugin_reload**(plugin_id, version_label) [capability_expand category: system]: Hot-reload a plugin at runtime. Disposes the old plugin fiber, re-imports its module, and registers a fresh instance. Existing in-flight turns are unaffected (snapshot isolation). REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_disable**(plugin_id) [capability_expand category: system]: Disable a plugin by disposing its fiber, removing its tools from the runtime registry. Cannot disable self_management itself. REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_remove**(plugin_id, delete_source) [capability_expand category: system]: Terminally remove a plugin: dispose its fiber, unregister its tools, remove reload metadata, and optionally delete its profile-scoped source file. Cannot remove self_management itself. REQUIRES APPROVAL.\n- **plugin_unquarantine**(plugin_id) [capability_expand category: plugin_management]: Restore a quarantined plugin to probation status for re-evaluation. Unfreezes the trust ledger, transitions the proposal back to PROBATION, and reloads the plugin. REQUIRES APPROVAL.\n- **plugin_enable**(plugin_id) [capability_expand category: system]: Re-enable a previously disabled plugin by reloading its module and registering a fresh instance. REQUIRES APPROVAL.\n- **self_describe**(facet): Introspect LeapFlow's own identity, capabilities, runtime state, evolution metrics, or platform connections. Use facet='all' only when a comprehensive self-check is explicitly requested.\n- **runtime_snapshot**(): Lightweight (~150 token) flat snapshot of current runtime state: model, context budget, posture, disclosure level, turn count, cache hit rate, uptime, tool count, and pending approvals.\n- **schedule_create**(trigger_expression, instruction, execution_mode, max_retries, delivery_target) [capability_expand category: scheduler]: Create a new scheduled task. Specify a trigger expression (e.g. '30m', 'every 2h', '0 9 * * *') and the instruction to execute on each trigger. Optionally provide an execution mode and a delivery target to receive notifications.\n- **schedule_list**() [capability_expand category: scheduler]: List all scheduled tasks with their current state, trigger, and next due time.\n- **schedule_status**(task_id) [capability_expand category: scheduler]: Get detailed status and recent execution history for a scheduled task.\n- **schedule_pause**(task_id) [capability_expand category: scheduler]: Pause a scheduled task so it stops firing without being cancelled.\n- **schedule_resume**(task_id) [capability_expand category: scheduler]: Resume a paused scheduled task — re-arms it and recalculates next due time.\n- **schedule_cancel**(task_id) [capability_expand category: scheduler]: Cancel a scheduled task permanently.\n- **tool_search**(query, max_results): Search all registered tools by keyword relevance. Returns a ranked list of matching tools with name, category, and summary. Use this when you need a tool but are unsure of its exact name or category.\n- **tool_describe**(tool_name): Get the full callable schema of a single tool by exact name. Returns the tool's description, parameters, and metadata. Use after tool_search to inspect a specific tool before calling it.\n- **session_search**(query, limit) [capability_expand category: unclassified]: Search past conversation sessions in the current workspace for relevant context.\n- **session_list**(limit): List recent conversation sessions in the current workspace with stable ids, titles, dates, and summaries. Use for browsing past tasks or when user asks to see history without specific search terms. No keywords needed — returns chronological list.\n- **session_detail**(session_id, limit, offset, include_inactive) [capability_expand category: unclassified]: Read a paginated persisted transcript for one past conversation session in the current workspace. Use after session_list or session_search returns a session_id.\n- **click**(element_index) [capability_expand category: desktop]: Click a UI element by its element_index (from the latest observe_ui snapshot)\n- **get_clipboard**() [capability_expand category: desktop]: Read current clipboard text content\n- **list_apps**(filter, running_only) [capability_expand category: desktop]: List available applications on this system. Use to discover correct bundle_id before switch_app.\n- **list_windows**() [capability_expand category: desktop]: List all top-level windows with pid, window_id, title, and per-window state (minimized, on-screen). Call this first to pick the pid and window_id that observe_ui and other window tools require.\n- **observe_ui**(pid, window_id, query) [capability_expand category: desktop]: Snapshot one window's actionable UI elements, each tagged with an element_index for click/right_click/read_text. Re-observe after actions — indices belong to one snapshot. Requires the window's pid and window_id from list_windows.\n- **open_url**(url, app_id) [capability_expand category: desktop]: Open a URL in the default or specified browser\n- **read_text**(element_index) [capability_expand category: desktop]: Read the text content of a specific UI element from the latest snapshot\n- **right_click**(element_index) [capability_expand category: desktop]: Right-click a UI element to open its context menu. Returns visible menu items.\n- **screenshot**(pid, window_id) [capability_expand category: desktop]: Capture a screenshot for visual verification. With pid + window_id captures that window (works across all displays); defaults to the last observed window, or the full desktop when no window has been observed.\n- **scroll**(element_index, direction, amount, pid, window_id) [capability_expand category: desktop]: Scroll a scrollable area of a window. Omit element_index to scroll the window's focused/page scroller; pass one to scroll an exact element from the latest snapshot.\n- **select_text**(element_index) [capability_expand category: desktop]: Select all text in a UI element (focus + select-all, for subsequent copy)\n- **set_clipboard**(text) [capability_expand category: desktop]: Write text to the clipboard\n- **shortcut**(keys) [capability_expand category: desktop]: Execute a keyboard shortcut\n- **switch_app**(app_id) [capability_expand category: desktop]: Switch to an app (launch if needed, activate, verify)\n- **type_text**(text) [capability_expand category: desktop]: Type text into the currently focused element\n- **wait**(seconds) [capability_expand category: desktop]: Wait for a specified duration before continuing\n- **wait_until**(condition, pid, window_id, timeout, poll_interval) [capability_expand category: desktop]: Wait until a UI condition is met (polls UI tree). Returns elements when found or on timeout.\n- **wait_until_stable**(timeout, poll_interval, pid, window_id) [capability_expand category: desktop]: Wait until the UI stops changing (element set stabilizes across polls).\n\n## App Connector Capability Index\nLeapFlow can onboard and manage external apps through `platform_connect` and execute exact registered business actions through `platform_action`.\nFor requests about connecting, setting up, configuring, enabling, or managing a supported app, use `platform_connect` first instead of generating SDK/Webhook sample code.\n`platform_connect.action` is the management namespace: list, guide, preflight, connect, disconnect, remove, status, events_start, events_stop, events_status.\n`platform_action.action` is only for exact registered platform business actions listed below, such as `im.send_message`; never use management actions like `list` or `guide` there.\nAll business fields MUST go inside `payload`; top-level keys are only `platform`, `action`, and `payload`.\nDo not invent platform IDs or platform action names. If the needed action is not listed, ask for discovery/clarification instead of guessing.\nUse `platform_connect` with `action='guide'` and the matching `platform` to start onboarding; use `action='list'` when the app is unclear.\nWhen a pending onboarding state is present, continue from that state with `platform_connect` instead of asking the user to restate the app.\nSupported apps:\n- `api_server`: API Server (OpenAI Compatible) (category=api; backend=adapter; platform_action actions=none registered)\n- `dingtalk`: 钉钉 (DingTalk) (category=im; backend=adapter; platform_action actions=none registered)\n- `feishu`: 飞书 (Feishu/Lark) (category=im; backend=cli)\n - `calendar.create_event` payload={summary*, start_time*, end_time*, attendees} [write/high]\n - `docs.create_markdown` payload={title*, markdown*, folder_token} [write/medium]\n - `drive.search` payload={query*, limit} [read/medium]\n - `im.add_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.download_resource` payload={message_id*, file_key*, type*} [read/low]\n - `im.get_messages` payload={message_ids*} [read/low]\n - `im.list_chats` payload={page_size} [read/low]\n - `im.list_messages` payload={chat_id*} [read/low]\n - `im.list_thread_messages` payload={thread*} [read/low]\n - `im.remove_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.reply_message` payload={message_id*, text*} [send/high]\n - `im.search_chats` payload={query*, page_size} [read/low]\n - `im.search_messages` payload={query*} [read/medium]\n - `im.send_message` payload={chat_id*, text*, thread_id} [send/high]\n - `im.update_card` payload={token*, card*} [write/medium]\n - `im.update_message` payload={message_id*, text*} [write/medium]\n - `mail.search_unread` payload={query, limit} [read/high]\n - `sheets.append_row` payload={spreadsheet_token*, sheet_id*, values*} [write/medium]\n - `task.create` payload={title*, description, due_time} [write/medium]\n- `telegram`: Telegram (category=social; backend=adapter; platform_action actions=none registered)\n- `webhook`: Webhook (Generic) (category=webhook; backend=adapter; platform_action actions=none registered)\n\n## Tool Usage\nTools are normally invoked through the native function-calling mechanism, not by writing JSON in your reply\ntext. Only if the provider signals that native function calling is unavailable for this turn, fall back to a\nsingle JSON code block: `{\"name\": \"tool_name\", \"arguments\": {\"key\": \"value\"}}` — use this fallback format\nonly, never both. Only call a tool whose exact name appears in the tool index above and is currently callable\n(or reachable via `capability_expand`). Never invent, rename, alias, or guess a tool name, platform ID, or\nplatform action from argument shape or wording — if the index does not list it, it does not exist.\n`platform_connect.action` (list/guide/preflight/connect/disconnect/remove/status/events_start/events_stop/\nevents_status) is the App Connector management namespace; `platform_action.action` only accepts exact\nregistered business actions such as `im.send_message` shown in the App Connector Capability Index — never mix\nthe two namespaces. If a tool call returns an unknown/unavailable result, use the returned suggestions or\navailable names for a single retry instead of trying further variations of the same guess.\n\n**Side-effect action rule** (`platform_action` with effect=send/write/execute):\n- Call each unique action+payload **exactly once**. Never include duplicates in the same turn.\n- Once the result returns `\"completed\": true`, that action is DONE for this task. Do NOT call it again\n in any subsequent turn — immediately summarize the result for the user instead.\n- The system enforces idempotency: duplicate calls are blocked and will not execute.\n- If the user explicitly requests sending/writing multiple times, use distinct payloads per call.\n\n**Resource identifier provenance rule**:\n- NEVER fabricate, guess, or infer resource identifiers (chat_id, message_id, file_key, user_id, etc.).\n Every resource ID used in a side-effect action MUST come from a successful API response in this session.\n- If a read/list action fails (e.g. authorization error), you do NOT have valid resource IDs.\n Report the failure to the user — do NOT attempt the dependent write/send action with a guessed ID.\n- When a tool result contains `\"llm_instruction\"`, follow it exactly.\n\n## Guidelines\n1. **Direct answers first**: If you already know the answer, respond directly without tools.\n2. **Avoid redundant tool calls**: Do not call the same tool with the same arguments more than once in the same user turn. When an existing tool result already answers the user's request, stop calling tools and answer directly.\n3. **Use tools proactively**: When the user asks about files, time, system state, or needs actions performed, use the appropriate tool.\n4. **Chain tools when needed**: You can call multiple tools in sequence (e.g., list files → read file → summarize).\n5. **Handle failures gracefully**: If a tool fails, explain what went wrong and suggest alternatives. If it failed because of an unknown tool/platform/action name, retry once with an exact name from the returned suggestions, then explain rather than keep guessing.\n6. **Summarize results naturally**: After tool execution, synthesize the results into a helpful answer rather than dumping raw output.\n7. **Stay conversational**: Maintain a natural, helpful tone. Acknowledge context from earlier in the conversation.\n8. **Recall past work**:\n - Broad queries (\"之前做了什么\", \"列出任务\"): answer from the \"Recent Task History\" section already in your context. If insufficient, call session_list.\n - Specific lookups (\"上次那个配置怎么改的\"): call session_search with relevant phrases (NOT single characters).\n - Do NOT call search tools repeatedly with keyword fragments. One well-phrased call is better than ten fragmented ones.\n\n## Coding & Verification\nWhen working with code, prefer the precise built-in tools over ad-hoc shell: use `repo_map` to orient in an\nunfamiliar project, `code_search`/`file_find` to locate and `code_intel` for symbols, `edit_file` (anchored\nsearch-replace, or a unified `diff`) to change files — never rewrite a whole file to change a few lines — and\n`git_query` to inspect diffs/log. After edits, check `syntax_ok` in the result and run `test_run`/`lint_check`\nbefore declaring the work done. Batch independent read-only calls (search/read/query) into a single turn — they\nrun in parallel.\n\n## Presentation Style\n1. **Polished Markdown only**: Format user-facing answers with clean Markdown headings, short paragraphs, and concise bullets. Use tables only when they improve comparison or scanning.\n2. **Terminal-friendly layout**: Keep lines readable in a TUI; avoid dense walls of text, deeply nested lists, oversized ASCII art, or heavy visual blocks.\n3. **Elegant emphasis**: Use bold text sparingly for key terms and conclusions. Avoid excessive emojis, decorative symbols, repeated separators, or visual noise.\n4. **Theme-safe colors**: Do not emit ANSI escape codes, HTML color tags, Rich markup, or hardcoded color names. Rely on the TUI theme to render Markdown professionally.\n5. **No leaked tool protocol**: Never show tool-call JSON, internal schemas, raw observations, tool result payloads, or hidden reasoning in the final answer unless the user explicitly asks for raw/debug output. Treat any prior `{\"name\": ..., \"arguments\": ...}` blocks and `Tool result (...)` messages as internal execution context only.\n6. **Professional closure**: End with a concise conclusion or next step when helpful; avoid rambling after the useful answer is complete.\n\nWhen finished with all tool calls, respond normally without a JSON block, tool-call transcript, or process log.\n\n## Task Contract\n- Task ID: turn-1\n- Original user request: Install the hermetic DSH echo plugin from this workspace.\n- Workspace root: \n- Allowed roots: \n- Treat relative project paths as relative to the workspace root; never infer `.` as the project root when a workspace root is provided.\n- Workspace boundary is enforced by tools: do not read, search, edit, or run commands against paths outside the allowed roots unless the user explicitly requests an external path and the tool/approval policy permits it.\n- LeapFlow workspace config is optional at `/.leapflow/config.yaml`; runtime config is loaded from `~/.leapflow/config/user.yaml` and `~/.leapflow/profiles//config/*.yaml`.\n- Preserve this task contract across summarization, compression, tool loops, and memory retrieval.\n" - }, - { - "role": "system", - "content": "\n## Recent Session Summary\n- [user] Install the hermetic DSH echo plugin from this workspace.\n" - }, - { - "role": "user", - "content": "Install the hermetic DSH echo plugin from this workspace.\nInstall the hermetic DSH echo plugin from this workspace." - } - ], - "tools": [ - "capability_expand", - "code_intel", - "code_search", - "config_get", - "config_list", - "env_info", - "file_find", - "file_list", - "file_read", - "git_query", - "lint_check", - "memory_search", - "plugin_list", - "plugin_propose", - "plugin_status", - "plugin_versions", - "repo_map", - "research_note", - "runtime_snapshot", - "schedule_reentry", - "self_describe", - "session_list", - "skill_view", - "skills_list", - "terminal_list", - "terminal_read", - "test_run", - "text_search", - "time_get", - "tool_describe", - "tool_search", - "web_fetch" - ] - }, - "responses": [ - { - "status": 200, - "content_type": "application/json", - "body": "{\"id\": \"chatcmpl-cassette\", \"object\": \"chat.completion\", \"model\": \"cassette-model\", \"choices\": [{\"index\": 0, \"message\": {\"role\": \"assistant\", \"content\": \"\", \"tool_calls\": [{\"id\": \"call_1\", \"type\": \"function\", \"function\": {\"name\": \"plugin_install\", \"arguments\": \"{\\\"plugin_id\\\": \\\"r6_dsh_echo\\\", \\\"source_path\\\": \\\"/tmp/lfj-r6_lifecycle/workspaces/life/dsh-echo\\\", \\\"version_label\\\": \\\"r6\\\"}\"}}]}, \"finish_reason\": \"tool_calls\"}], \"usage\": {\"prompt_tokens\": 64, \"completion_tokens\": 16, \"total_tokens\": 80}}" - } - ] -} diff --git a/tests/_fixtures/cassettes/r6_lifecycle/cassette-model-fe57e4e3e4d734cb.cassette.json b/tests/_fixtures/cassettes/r6_lifecycle/cassette-model-fe57e4e3e4d734cb.cassette.json new file mode 100644 index 00000000..627ebdda --- /dev/null +++ b/tests/_fixtures/cassettes/r6_lifecycle/cassette-model-fe57e4e3e4d734cb.cassette.json @@ -0,0 +1,80 @@ +{ + "fingerprint": "fe57e4e3e4d734cb0e5eda64eccaed3fae2166881b2998e9c806165933bfff9f", + "note": "captured in seed mode", + "request": { + "model": "cassette-model", + "stream": false, + "messages": [ + { + "role": "system", + "content": "You are LeapFlow, an intelligent assistant that can both converse naturally and take real actions on the user's computer.\n\n## Capabilities\nThe tool index below lists **every** registered tool by name and a one-line summary — this is the complete\ncapability contract; nothing else exists. Only a subset is directly callable this turn (via native tool calling,\nnot a JSON block in your reply). If you need a tool from the index that is not yet callable, call\n`capability_expand` with its category name first — the matching tools become callable immediately after.\nWhen the user asks about LeapFlow **itself** — its identity/version/model, what tools, plugins, or capabilities\nit has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of\nself-awareness — answer from live runtime evidence via `self_describe` (facet=capabilities for \"what can you\ndo / do you support X\", identity for version/model, runtime for context/posture, or `all` for a full self-check),\nnever by reading LeapFlow's own source code, documentation, or memory. Report configuration-dependent or\nunavailable capabilities as limitations rather than inferring them.\n- **text_search**(text, pattern): Search for a regex pattern in text.\n- **text_replace**(text, old, new, count) [capability_expand category: write]: Replace occurrences of a substring in text.\n- **time_get**(): Get current date and time.\n- **env_info**(): Get system environment information (OS, Python version, cwd).\n- **skills_list**(query, category, source): List available learned skills. Use when user asks about capabilities or you need a specific skill.\n- **skill_view**(name): View the full content of a specific skill document.\n- **skill_list_files**(name) [capability_expand category: skills]: List all support files in a skill directory (configs, templates, examples). Use before skill_file_read to discover available files.\n- **skill_file_read**(name, file_path) [capability_expand category: skills]: Read an individual support file from a skill directory. Use skill_list_files first to discover available files.\n- **code_intel**(path, operation): Precise document symbols (outline) for a source file: classes, functions, and methods with line ranges. Python uses an exact AST parse; other languages use a keyword-prefix scan. Prefer over file_read mode=symbols for accurate navigation before editing. Read-only.\n- **repo_map**(path): Compact project orientation for a repository root: languages, detected test/lint commands, top-level structure, entry points, manifest, and VCS branch. Call this first when entering an unfamiliar codebase. Read-only.\n- **scm_sync**(action, cwd, remote, pull_ref, push_ref, timeout) [capability_expand category: scm]: Run a typed git SCM action. Use this instead of shell_run for git pull/push/status. For 'pull origin main then push', set action='pull_then_push', remote='origin', pull_ref='main', and omit push_ref so LeapFlow pushes the current local branch.\n- **git_query**(action, cwd, ref, path, staged, max_count, stat): Read-only structured git inspection: action=diff|log|status|branch|show. Prefer over shell_run for reading repo state — output is clipped, redacted, and log/branch are parsed into structured fields. Use scm_sync for pull/push.\n- **git_write**(action, cwd, message, stage_all, name, ref, create) [capability_expand category: scm]: Mutating git actions: action=commit (message, stage_all), branch (create+switch), checkout (switch; create=true for -b). Approval-gated. Use scm_sync for pull/push and git_query for reads.\n- **test_run**(command, cwd, timeout): Run the project's test suite and return structured results (framework, passed/failed counts, failing tests). Auto-detects the runner (pytest/npm/go/cargo) or uses a configured/explicit command; executes via the governed shell. ok=true means the runner executed — see 'success' for pass/fail.\n- **lint_check**(command, cwd, timeout): Run the project's linter and return a structured clean/issue result. Auto-detects the linter (ruff/eslint/go vet/clippy) or uses a configured/explicit command; executes via the governed shell. ok=true means the linter ran — see 'clean'.\n- **file_list**(path, pattern, depth): List files and directories at a given path. Use depth=1 or depth=2 to get a recursive tree in one call instead of listing each sub-directory separately.\n- **file_read**(path, max_lines, start_line, max_chars, mode): Read text file content with adaptive context governance. For large or unfamiliar files, prefer mode='outline' or mode='symbols' first, then use mode='raw' with start_line/max_lines for the specific range you actually need. For LeapFlow's own settings, use config_list / config_get / config_set — its config files are outside the workspace and not readable here.\n- **file_write**(path, content, mode) [capability_expand category: write]: Write content to a file (overwrite or append).\n- **code_search**(pattern, patterns, path, glob, ignore_case, multiline, max_results, context_lines): Search file CONTENTS by regex pattern across a directory tree (ripgrep-backed). Requires a regex pattern. NOT for listing or browsing directory contents — use file_list for that. Prefer this over shell_run grep: faster, skips VCS/dependency/build dirs, and returns structured path:line:column matches. Batch related lookups into ONE call via `patterns` (OR-combined, single pass) instead of issuing several separate searches. Use file_read for the surrounding context of a hit.\n- **file_find**(glob, path, max_results): Find files by a recursive glob pattern under a base path (e.g. '**/test_*.py' or '*.md'). Prefer this over shell_run find; skips VCS/dependency/build dirs.\n- **edit_file**(path, edits, dry_run, diff) [capability_expand category: write]: Apply targeted, anchored search-replace edits to an EXISTING text file (use file_write to create/overwrite). Each edit is {original_text, new_text, replace_all?}; original_text must match exactly and uniquely (or set replace_all) — a non-unique or missing anchor is rejected so files are never corrupted. Set dry_run to preview. Alternatively pass a unified 'diff' to apply its hunks as anchored edits. Far cheaper and safer than rewriting a whole file.\n- **shell_run**(command, cwd, timeout) [capability_expand category: shell]: Execute a one-shot shell command with timeout protection. Runs in the active workspace; paths resolving outside it are refused. Reach for a structured tool first when one fits — web_fetch for anything over HTTP(S), git_query/scm_sync for git, code_search/file_find/file_read for the repo, config_get/config_set for LeapFlow's own settings — because those report typed results, while a failed shell command can only be diagnosed from its exit code and stderr. Every shell run counts as an external side effect, so a failure stops the rest of the batch and is not retried automatically.\n- **terminal_open**(command, cwd, shell) [capability_expand category: terminal]: Open a PERSISTENT shell session (REPL/dev server/watch), returning a session_id for terminal_send/read/close. Disabled unless tools.terminal_session_enabled is set. For one-shot commands use shell_run instead.\n- **terminal_send**(session_id, input, wait) [capability_expand category: terminal]: Send a line of input to a persistent terminal session and return output captured shortly after.\n- **terminal_read**(session_id, wait): Drain buffered output from a persistent terminal session (optionally waiting briefly first).\n- **terminal_close**(session_id) [capability_expand category: terminal]: Terminate a persistent terminal session and release its process group.\n- **terminal_list**(): List active persistent terminal sessions.\n- **config_list**(category, limit): List LeapFlow's own writable settings (LLM model and endpoint, daemon, memory, perception, gateway, …) with current values. Use this to discover the exact key before changing anything. Optionally narrow by `category`. This is the only correct way to inspect LeapFlow configuration — never read config files from disk.\n- **config_get**(key): Read one LeapFlow setting by key (e.g. 'llm.model', 'llm.base_url', 'daemon.log_level'), returning its current value, type, scopes, and whether a change needs a daemon restart. There is no 'llm.provider' key: provider behavior is inferred from the OpenAI-compatible 'llm.base_url'. Never read LeapFlow config files from disk — use this.\n- **config_set**(key, value, scope) [capability_expand category: config]: Change one LeapFlow setting by key, e.g. switch an OpenAI-compatible LLM with key='llm.model' and key='llm.base_url'. There is no 'llm.provider' key: provider behavior is inferred from the endpoint. Values are validated and coerced; credentials are stored in the vault automatically. Call config_list or config_get first if unsure of the key. The result states whether a `leap daemon restart` is required. Never edit LeapFlow config files directly.\n- **web_fetch**(url, select, timeout, max_bytes): Read a URL over HTTP(S) and get back extracted, context-sized content: parsed JSON for API endpoints, readable text plus links for web pages. Use this for anything on the internet — prices, docs, releases, articles — instead of running curl through shell_run: it reports real HTTP status codes, retries rate limits on its own, and is a plain read so a retry is always safe. For JSON APIs pass `select` with a dotted path (e.g. 'chart.result.0.meta') to return just that part instead of the whole payload.\n- **memory_search**(query, limit): Search agent memory for relevant past experiences, observations, and facts.\n- **memory_add**(content, kind) [capability_expand category: write]: Store a new observation or insight in memory for future reference.\n- **research_note**(kind, text): Record a compact, structured note about the current task's state so it survives context compression on long / multi-step tasks. Use for durable findings, open questions still to resolve, decisions / excluded paths, and the immediate next step. One concise sentence per note.\n- **capability_expand**(category): Fetch the full callable schema for every tool in a capability category (e.g. 'hub', 'gateway', 'desktop', 'delegate', 'file', 'memory', 'skill'). The compact tool index always lists every registered tool by name and a one-line summary, but only a static low-risk subset is directly callable each turn. If you need a tool from the index that is not yet callable, call capability_expand with its category first; the matching tools become callable in this turn. Never invent a tool name — expand the category instead.\n- **delegate_task**(goal, context) [capability_expand category: delegate]: Delegate a complex sub-task to an isolated subagent. The subagent gets a fresh context and restricted tool access. Use when a task is self-contained and can be solved independently.\n- **schedule_reentry**(kind, reason, delay_seconds, event_match, max_reentries, deadline_seconds): Register a re-entry so this task can resume later from its current orientation (findings / open questions / next step). Use when work must pause and continue after a delay (kind=time) or when a matching platform event arrives (kind=event), instead of finishing now. The research-ledger state is carried over automatically.\n- **hub_push**(skill_name, visibility, version) [capability_expand category: hub]: Push a local skill to the ModelScope Hub for sharing or backup.\n- **hub_pull**(repo_id, version) [capability_expand category: hub]: Pull a skill from the ModelScope Hub to install locally.\n- **hub_search**(query) [capability_expand category: hub]: Search for skills on the Hub by keyword or description.\n- **hub_sync**(mode, dry_run) [capability_expand category: hub]: Preview or execute sync between local skills and Hub.\n- **platform_action**(platform, action, payload, backend_kind) [capability_expand category: gateway]: Execute an exact registered business action on an external platform through LeapFlow's App Connector layer. Actions must be copied from the App Connector Capability Index and are addressed as domain.operation, e.g. im.send_message or docs.create_markdown. All business fields (chat_id, text, query, etc.) MUST be placed inside `payload`, never at the top level. Example: {\"platform\":\"feishu\",\"action\":\"im.send_message\",\"payload\":{\"chat_id\":\"oc_xxx\",\"text\":\"hello\"}}. Do not invent action names, do not use management actions such as list/guide/connect/status here.\n- **platform_connect**(action, platform, credentials, options, checkpoint) [capability_expand category: gateway]: List, guide, connect, disconnect, remove, or check status for external platforms using the App Connector management namespace. Supports REST and CLI backends. Use this for management actions such as list/guide/preflight/connect/status; use platform_action only for exact registered business actions.\n- **gateway_send**(platform, chat_id, text, thread_id) [capability_expand category: gateway]: Send a message to a connected external platform (Feishu group, Telegram chat, DingTalk conversation, etc.). Requires the platform to be connected via gateway_connect first. Use gateway_connect with action='list' to see connected platforms and available chat IDs.\n- **gateway_connect**(action, platform, credentials, options) [capability_expand category: gateway]: Connect, configure, or manage external platform integrations (Feishu, DingTalk, Telegram, Slack, Discord, etc.). Conversational flow: 1) call 'guide' to get setup steps + required fields, 2) present the steps to the user and ask for ALL required credentials in a single message, 3) call 'connect' with the credentials. Goal: complete in 1–2 user turns. NEVER include credential values in your text response.\n- **plugin_list**(): List the live plugin registry across Tool/Gateway/LLM subsystems for plugin management: which plugins are loaded, their category and fiber lifecycle state. For questions about LeapFlow's own capabilities or whether it supports X, use self_describe instead.\n- **plugin_status**(plugin_id): Get detailed status of a specific plugin: its declared category, runtime dependencies, contributed tools, and fiber lifecycle state.\n- **plugin_versions**(plugin_id): List recorded source versions and active pointer for a profile-scoped plugin.\n- **plugin_propose**(requested_capability, plugin_id, proposed_tools, test_cases, risk_level, evidence): Create a side-effect-free PluginProposal from explicit capability-gap evidence. Use this before plugin_generate when a missing capability should be reviewed. Does not call an LLM, write files, or install anything.\n- **assess_compatibility**(manifest, source_path) [capability_expand category: plugin_management]: Assess whether a foreign plugin manifest or real DSH source bundle is compatible with LeapFlow. A source bundle assessment is static: runtime_ready stays false until restricted Node discovery during plugin_install. Returns component-level verdicts and limitations.\n- **plugin_generate**(plugin_id, description, proposal_id) [capability_expand category: system]: Generate a new ToolPlugin from a natural-language capability description. The LLM produces code that conforms to the ToolPlugin Protocol; it is then rigorously validated (syntax, structure, import, protocol conformance). The generated source is stored in CAS and receives explicit content approval. It DOES NOT install the plugin — installation requires a second, mutation-specific approval via plugin_install.\n- **plugin_install**(plugin_id, code, marketplace_name, source_path, proposal_id, version_label) [capability_expand category: system]: Install a plugin from exactly one source: validated Python code, the configured Python marketplace, or a real DSH source bundle. DSH bundles run restricted Node discovery before registration; only public host tools are installed and client UI limitations are reported. Writes profile-scoped state and mutates the process-global registry. REQUIRES APPROVAL.\n- **plugin_rollback**(plugin_id, version) [capability_expand category: system]: Rollback a profile-scoped plugin to a recorded source version and reload it. REQUIRES APPROVAL.\n- **plugin_reload**(plugin_id, version_label) [capability_expand category: system]: Hot-reload a plugin at runtime. Disposes the old plugin fiber, re-imports its module, and registers a fresh instance. Existing in-flight turns are unaffected (snapshot isolation). REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_disable**(plugin_id) [capability_expand category: system]: Disable a plugin by disposing its fiber, removing its tools from the runtime registry. Cannot disable self_management itself. REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_remove**(plugin_id, delete_source) [capability_expand category: system]: Terminally remove a plugin: dispose its fiber, unregister its tools, remove reload metadata, and optionally delete its profile-scoped source file. Cannot remove self_management itself. REQUIRES APPROVAL.\n- **plugin_unquarantine**(plugin_id) [capability_expand category: plugin_management]: Restore a quarantined plugin to probation status for re-evaluation. Unfreezes the trust ledger, transitions the proposal back to PROBATION, and reloads the plugin. REQUIRES APPROVAL.\n- **plugin_enable**(plugin_id) [capability_expand category: system]: Re-enable a previously disabled plugin by reloading its module and registering a fresh instance. REQUIRES APPROVAL.\n- **self_describe**(facet): Answer questions about LeapFlow ITSELF from live runtime evidence — its identity/version/model, what tools and plugins it has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of self-awareness. Prefer this over reading LeapFlow's own source code. Pick facet=capabilities for 'what can you do / do you support X', identity for version/model, runtime for context/posture, or all for a full self-check when explicitly requested.\n- **runtime_snapshot**(): Lightweight (~150 token) flat snapshot of current runtime state: model, context budget, posture, disclosure level, turn count, cache hit rate, uptime, tool count, and pending approvals.\n- **schedule_create**(trigger_expression, instruction, execution_mode, max_retries, delivery_target) [capability_expand category: scheduler]: Create a new scheduled task. Specify a trigger expression (e.g. '30m', 'every 2h', '0 9 * * *') and the instruction to execute on each trigger. Optionally provide an execution mode and a delivery target to receive notifications.\n- **schedule_list**() [capability_expand category: scheduler]: List all scheduled tasks with their current state, trigger, and next due time.\n- **schedule_status**(task_id) [capability_expand category: scheduler]: Get detailed status and recent execution history for a scheduled task.\n- **schedule_pause**(task_id) [capability_expand category: scheduler]: Pause a scheduled task so it stops firing without being cancelled.\n- **schedule_resume**(task_id) [capability_expand category: scheduler]: Resume a paused scheduled task — re-arms it and recalculates next due time.\n- **schedule_cancel**(task_id) [capability_expand category: scheduler]: Cancel a scheduled task permanently.\n- **tool_search**(query, max_results): Search all registered tools by keyword relevance. Returns a ranked list of matching tools with name, category, and summary. Use this when you need a tool but are unsure of its exact name or category.\n- **tool_describe**(tool_name): Get the full callable schema of a single tool by exact name. Returns the tool's description, parameters, and metadata. Use after tool_search to inspect a specific tool before calling it.\n- **fixture_echo**(text): Return the supplied text from a pre-built DSH package.\n- **session_search**(query, limit) [capability_expand category: unclassified]: Search past conversation sessions in the current workspace for relevant context.\n- **session_list**(limit): List recent conversation sessions in the current workspace with stable ids, titles, dates, and summaries. Use for browsing past tasks or when user asks to see history without specific search terms. No keywords needed — returns chronological list.\n- **session_detail**(session_id, limit, offset, include_inactive) [capability_expand category: unclassified]: Read a paginated persisted transcript for one past conversation session in the current workspace. Use after session_list or session_search returns a session_id.\n- **click**(element_index) [capability_expand category: desktop]: Click a UI element by its element_index (from the latest observe_ui snapshot)\n- **get_clipboard**() [capability_expand category: desktop]: Read current clipboard text content\n- **list_apps**(filter, running_only) [capability_expand category: desktop]: List available applications on this system. Use to discover correct bundle_id before switch_app.\n- **list_windows**() [capability_expand category: desktop]: List all top-level windows with pid, window_id, title, and per-window state (minimized, on-screen). Call this first to pick the pid and window_id that observe_ui and other window tools require.\n- **observe_ui**(pid, window_id, query) [capability_expand category: desktop]: Snapshot one window's actionable UI elements, each tagged with an element_index for click/right_click/read_text. Re-observe after actions — indices belong to one snapshot. Requires the window's pid and window_id from list_windows.\n- **open_url**(url, app_id) [capability_expand category: desktop]: Open a URL in the default or specified browser\n- **read_text**(element_index) [capability_expand category: desktop]: Read the text content of a specific UI element from the latest snapshot\n- **right_click**(element_index) [capability_expand category: desktop]: Right-click a UI element to open its context menu. Returns visible menu items.\n- **screenshot**(pid, window_id) [capability_expand category: desktop]: Capture a screenshot for visual verification. With pid + window_id captures that window (works across all displays); defaults to the last observed window, or the full desktop when no window has been observed.\n- **scroll**(element_index, direction, amount, pid, window_id) [capability_expand category: desktop]: Scroll a scrollable area of a window. Omit element_index to scroll the window's focused/page scroller; pass one to scroll an exact element from the latest snapshot.\n- **select_text**(element_index) [capability_expand category: desktop]: Select all text in a UI element (focus + select-all, for subsequent copy)\n- **set_clipboard**(text) [capability_expand category: desktop]: Write text to the clipboard\n- **shortcut**(keys) [capability_expand category: desktop]: Execute a keyboard shortcut\n- **switch_app**(app_id) [capability_expand category: desktop]: Switch to an app (launch if needed, activate, verify)\n- **type_text**(text) [capability_expand category: desktop]: Type text into the currently focused element\n- **wait**(seconds) [capability_expand category: desktop]: Wait for a specified duration before continuing\n- **wait_until**(condition, pid, window_id, timeout, poll_interval) [capability_expand category: desktop]: Wait until a UI condition is met (polls UI tree). Returns elements when found or on timeout.\n- **wait_until_stable**(timeout, poll_interval, pid, window_id) [capability_expand category: desktop]: Wait until the UI stops changing (element set stabilizes across polls).\n\n## App Connector Capability Index\nLeapFlow can onboard and manage external apps through `platform_connect` and execute exact registered business actions through `platform_action`.\nFor requests about connecting, setting up, configuring, enabling, or managing a supported app, use `platform_connect` first instead of generating SDK/Webhook sample code.\n`platform_connect.action` is the management namespace: list, guide, preflight, connect, disconnect, remove, status, events_start, events_stop, events_status.\n`platform_action.action` is only for exact registered platform business actions listed below, such as `im.send_message`; never use management actions like `list` or `guide` there.\nAll business fields MUST go inside `payload`; top-level keys are only `platform`, `action`, and `payload`.\nDo not invent platform IDs or platform action names. If the needed action is not listed, ask for discovery/clarification instead of guessing.\nUse `platform_connect` with `action='guide'` and the matching `platform` to start onboarding; use `action='list'` when the app is unclear.\nWhen a pending onboarding state is present, continue from that state with `platform_connect` instead of asking the user to restate the app.\nSupported apps:\n- `api_server`: API Server (OpenAI Compatible) (category=api; backend=adapter; platform_action actions=none registered)\n- `dingtalk`: 钉钉 (DingTalk) (category=im; backend=adapter; platform_action actions=none registered)\n- `feishu`: 飞书 (Feishu/Lark) (category=im; backend=cli)\n - `calendar.create_event` payload={summary*, start_time*, end_time*, attendees} [write/high]\n - `docs.create_markdown` payload={title*, markdown*, folder_token} [write/medium]\n - `drive.search` payload={query*, limit} [read/medium]\n - `im.add_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.download_resource` payload={message_id*, file_key*, type*} [read/low]\n - `im.get_messages` payload={message_ids*} [read/low]\n - `im.list_chats` payload={page_size} [read/low]\n - `im.list_messages` payload={chat_id*} [read/low]\n - `im.list_thread_messages` payload={thread*} [read/low]\n - `im.remove_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.reply_message` payload={message_id*, text*} [send/high]\n - `im.search_chats` payload={query*, page_size} [read/low]\n - `im.search_messages` payload={query*} [read/medium]\n - `im.send_message` payload={chat_id*, text*, thread_id} [send/high]\n - `im.update_card` payload={token*, card*} [write/medium]\n - `im.update_message` payload={message_id*, text*} [write/medium]\n - `mail.search_unread` payload={query, limit} [read/high]\n - `sheets.append_row` payload={spreadsheet_token*, sheet_id*, values*} [write/medium]\n - `task.create` payload={title*, description, due_time} [write/medium]\n- `telegram`: Telegram (category=social; backend=adapter; platform_action actions=none registered)\n- `webhook`: Webhook (Generic) (category=webhook; backend=adapter; platform_action actions=none registered)\n\n## Tool Usage\nTools are normally invoked through the native function-calling mechanism, not by writing JSON in your reply\ntext. Only if the provider signals that native function calling is unavailable for this turn, fall back to a\nsingle JSON code block: `{\"name\": \"tool_name\", \"arguments\": {\"key\": \"value\"}}` — use this fallback format\nonly, never both. Only call a tool whose exact name appears in the tool index above and is currently callable\n(or reachable via `capability_expand`). Never invent, rename, alias, or guess a tool name, platform ID, or\nplatform action from argument shape or wording — if the index does not list it, it does not exist.\n`platform_connect.action` (list/guide/preflight/connect/disconnect/remove/status/events_start/events_stop/\nevents_status) is the App Connector management namespace; `platform_action.action` only accepts exact\nregistered business actions such as `im.send_message` shown in the App Connector Capability Index — never mix\nthe two namespaces. If a tool call returns an unknown/unavailable result, use the returned suggestions or\navailable names for a single retry instead of trying further variations of the same guess.\n\n**Side-effect action rule** (`platform_action` with effect=send/write/execute):\n- Call each unique action+payload **exactly once**. Never include duplicates in the same turn.\n- Once the result returns `\"completed\": true`, that action is DONE for this task. Do NOT call it again\n in any subsequent turn — immediately summarize the result for the user instead.\n- The system enforces idempotency: duplicate calls are blocked and will not execute.\n- If the user explicitly requests sending/writing multiple times, use distinct payloads per call.\n\n**Resource identifier provenance rule**:\n- NEVER fabricate, guess, or infer resource identifiers (chat_id, message_id, file_key, user_id, etc.).\n Every resource ID used in a side-effect action MUST come from a successful API response in this session.\n- If a read/list action fails (e.g. authorization error), you do NOT have valid resource IDs.\n Report the failure to the user — do NOT attempt the dependent write/send action with a guessed ID.\n- When a tool result contains `\"llm_instruction\"`, follow it exactly.\n\n## Guidelines\n1. **Direct answers first**: If you already know the answer, respond directly without tools.\n2. **Avoid redundant tool calls**: Do not call the same tool with the same arguments more than once in the same user turn. When an existing tool result already answers the user's request, stop calling tools and answer directly.\n3. **Use tools proactively**: When the user asks about files, time, system state, or needs actions performed, use the appropriate tool.\n4. **Chain tools when needed**: You can call multiple tools in sequence (e.g., list files → read file → summarize).\n5. **Handle failures gracefully**: If a tool fails, explain what went wrong and suggest alternatives. If it failed because of an unknown tool/platform/action name, retry once with an exact name from the returned suggestions, then explain rather than keep guessing.\n6. **Summarize results naturally**: After tool execution, synthesize the results into a helpful answer rather than dumping raw output.\n7. **Stay conversational**: Maintain a natural, helpful tone. Acknowledge context from earlier in the conversation.\n8. **Recall past work**:\n - Broad queries (\"之前做了什么\", \"列出任务\"): answer from the \"Recent Task History\" section already in your context. If insufficient, call session_list.\n - Specific lookups (\"上次那个配置怎么改的\"): call session_search with relevant phrases (NOT single characters).\n - Do NOT call search tools repeatedly with keyword fragments. One well-phrased call is better than ten fragmented ones.\n\n## Coding & Verification\nWhen working with code, prefer the precise built-in tools over ad-hoc shell: use `repo_map` to orient in an\nunfamiliar project, `code_search`/`file_find` to locate and `code_intel` for symbols, `edit_file` (anchored\nsearch-replace, or a unified `diff`) to change files — never rewrite a whole file to change a few lines — and\n`git_query` to inspect diffs/log. After edits, check `syntax_ok` in the result and run `test_run`/`lint_check`\nbefore declaring the work done. Batch independent read-only calls (search/read/query) into a single turn — they\nrun in parallel.\n\n## Presentation Style\n1. **Polished Markdown only**: Format user-facing answers with clean Markdown headings, short paragraphs, and concise bullets. Use tables only when they improve comparison or scanning.\n2. **Terminal-friendly layout**: Keep lines readable in a TUI; avoid dense walls of text, deeply nested lists, oversized ASCII art, or heavy visual blocks.\n3. **Elegant emphasis**: Use bold text sparingly for key terms and conclusions. Avoid excessive emojis, decorative symbols, repeated separators, or visual noise.\n4. **Theme-safe colors**: Do not emit ANSI escape codes, HTML color tags, Rich markup, or hardcoded color names. Rely on the TUI theme to render Markdown professionally.\n5. **No leaked tool protocol**: Never show tool-call JSON, internal schemas, raw observations, tool result payloads, or hidden reasoning in the final answer unless the user explicitly asks for raw/debug output. Treat any prior `{\"name\": ..., \"arguments\": ...}` blocks and `Tool result (...)` messages as internal execution context only.\n6. **Professional closure**: End with a concise conclusion or next step when helpful; avoid rambling after the useful answer is complete.\n\nWhen finished with all tool calls, respond normally without a JSON block, tool-call transcript, or process log.\n\n## Task Contract\n- Task ID: turn-2\n- Original user request: Remove the hermetic DSH echo plugin completely.\n- Workspace root: \n- Allowed roots: \n- Treat relative project paths as relative to the workspace root; never infer `.` as the project root when a workspace root is provided.\n- Workspace boundary is enforced by tools: do not read, search, edit, or run commands against paths outside the allowed roots unless the user explicitly requests an external path and the tool/approval policy permits it.\n- LeapFlow workspace config is optional at `/.leapflow/config.yaml`; runtime config is loaded from `~/.leapflow/config/user.yaml` and `~/.leapflow/profiles//config/*.yaml`.\n- Preserve this task contract across summarization, compression, tool loops, and memory retrieval.\n" + }, + { + "role": "system", + "content": "\n## Recent Session Summary\n- [assistant] The DSH tool ran after restart.\n- [user] Remove the hermetic DSH echo plugin completely.\n" + }, + { + "role": "assistant", + "content": "The DSH tool ran after restart." + }, + { + "role": "user", + "content": "Remove the hermetic DSH echo plugin completely.\nRemove the hermetic DSH echo plugin completely." + }, + { + "role": "assistant", + "content": "", + "tool_calls": [ + "plugin_remove" + ] + }, + { + "role": "tool", + "content": "{\"ok\": true, \"action\": \"remove\", \"plugin_id\": \"r6_dsh_echo\", \"state\": \"disposed\", \"source_path\": \"\", \"source_deleted\": true, \"execution_id\": \"\", \"idempotency_key\": \"\", \"execution_policy\": \"mutating_once\", \"tool_call_id\": \"call_1\", \"execution_status\": \"completed\"}", + "tool_result": true + } + ], + "tools": [ + "capability_expand", + "code_intel", + "code_search", + "config_get", + "config_list", + "env_info", + "file_find", + "file_list", + "file_read", + "fixture_echo", + "git_query", + "lint_check", + "memory_search", + "plugin_list", + "plugin_propose", + "plugin_status", + "plugin_versions", + "repo_map", + "research_note", + "runtime_snapshot", + "schedule_reentry", + "self_describe", + "session_list", + "skill_view", + "skills_list", + "terminal_list", + "terminal_read", + "test_run", + "text_search", + "time_get", + "tool_describe", + "tool_search", + "web_fetch" + ] + }, + "responses": [ + { + "status": 200, + "content_type": "application/json", + "body": "{\"id\": \"chatcmpl-cassette\", \"object\": \"chat.completion\", \"model\": \"cassette-model\", \"choices\": [{\"index\": 0, \"message\": {\"role\": \"assistant\", \"content\": \"Removed the hermetic DSH plugin.\"}, \"finish_reason\": \"stop\"}], \"usage\": {\"prompt_tokens\": 64, \"completion_tokens\": 16, \"total_tokens\": 80}}" + } + ] +} diff --git a/tests/_fixtures/cassettes/r7_adaptive_plugin_loop/cassette-model-002e2be7b234dab4.cassette.json b/tests/_fixtures/cassettes/r7_adaptive_plugin_loop/cassette-model-002e2be7b234dab4.cassette.json deleted file mode 100644 index b0842844..00000000 --- a/tests/_fixtures/cassettes/r7_adaptive_plugin_loop/cassette-model-002e2be7b234dab4.cassette.json +++ /dev/null @@ -1,67 +0,0 @@ -{ - "fingerprint": "002e2be7b234dab48a1165e83474b2b6dc4fea6273cea935dd39aacfaf94cde8", - "note": "captured in seed mode", - "request": { - "model": "cassette-model", - "stream": false, - "messages": [ - { - "role": "system", - "content": "You are LeapFlow, an intelligent assistant that can both converse naturally and take real actions on the user's computer.\n\n## Capabilities\nThe tool index below lists **every** registered tool by name and a one-line summary — this is the complete\ncapability contract; nothing else exists. Only a subset is directly callable this turn (via native tool calling,\nnot a JSON block in your reply). If you need a tool from the index that is not yet callable, call\n`capability_expand` with its category name first — the matching tools become callable immediately after.\nWhen the user asks what LeapFlow itself supports, whether it supports plugins, or which runtime capabilities\nare available, use the live capability evidence exposed by `plugin_list` before making capability claims; report\nconfiguration-dependent or unavailable capabilities as limitations instead of inferring from documentation.\n- **text_search**(text, pattern): Search for a regex pattern in text.\n- **text_replace**(text, old, new, count) [capability_expand category: write]: Replace occurrences of a substring in text.\n- **time_get**(): Get current date and time.\n- **env_info**(): Get system environment information (OS, Python version, cwd).\n- **skills_list**(query, category, source): List available learned skills. Use when user asks about capabilities or you need a specific skill.\n- **skill_view**(name): View the full content of a specific skill document.\n- **code_intel**(path, operation): Precise document symbols (outline) for a source file: classes, functions, and methods with line ranges. Python uses an exact AST parse; other languages use a keyword-prefix scan. Prefer over file_read mode=symbols for accurate navigation before editing. Read-only.\n- **repo_map**(path): Compact project orientation for a repository root: languages, detected test/lint commands, top-level structure, entry points, manifest, and VCS branch. Call this first when entering an unfamiliar codebase. Read-only.\n- **scm_sync**(action, cwd, remote, pull_ref, push_ref, timeout) [capability_expand category: scm]: Run a typed git SCM action. Use this instead of shell_run for git pull/push/status. For 'pull origin main then push', set action='pull_then_push', remote='origin', pull_ref='main', and omit push_ref so LeapFlow pushes the current local branch.\n- **git_query**(action, cwd, ref, path, staged, max_count, stat): Read-only structured git inspection: action=diff|log|status|branch|show. Prefer over shell_run for reading repo state — output is clipped, redacted, and log/branch are parsed into structured fields. Use scm_sync for pull/push.\n- **git_write**(action, cwd, message, stage_all, name, ref, create) [capability_expand category: scm]: Mutating git actions: action=commit (message, stage_all), branch (create+switch), checkout (switch; create=true for -b). Approval-gated. Use scm_sync for pull/push and git_query for reads.\n- **test_run**(command, cwd, timeout): Run the project's test suite and return structured results (framework, passed/failed counts, failing tests). Auto-detects the runner (pytest/npm/go/cargo) or uses a configured/explicit command; executes via the governed shell. ok=true means the runner executed — see 'success' for pass/fail.\n- **lint_check**(command, cwd, timeout): Run the project's linter and return a structured clean/issue result. Auto-detects the linter (ruff/eslint/go vet/clippy) or uses a configured/explicit command; executes via the governed shell. ok=true means the linter ran — see 'clean'.\n- **file_list**(path, pattern, depth): List files and directories at a given path. Use depth=1 or depth=2 to get a recursive tree in one call instead of listing each sub-directory separately.\n- **file_read**(path, max_lines, start_line, max_chars, mode): Read text file content with adaptive context governance. For large or unfamiliar files, prefer mode='outline' or mode='symbols' first, then use mode='raw' with start_line/max_lines for the specific range you actually need. For LeapFlow's own settings, use config_list / config_get / config_set — its config files are outside the workspace and not readable here.\n- **file_write**(path, content, mode) [capability_expand category: write]: Write content to a file (overwrite or append).\n- **code_search**(pattern, patterns, path, glob, ignore_case, multiline, max_results, context_lines): Search file CONTENTS by regex pattern across a directory tree (ripgrep-backed). Requires a regex pattern. NOT for listing or browsing directory contents — use file_list for that. Prefer this over shell_run grep: faster, skips VCS/dependency/build dirs, and returns structured path:line:column matches. Batch related lookups into ONE call via `patterns` (OR-combined, single pass) instead of issuing several separate searches. Use file_read for the surrounding context of a hit.\n- **file_find**(glob, path, max_results): Find files by a recursive glob pattern under a base path (e.g. '**/test_*.py' or '*.md'). Prefer this over shell_run find; skips VCS/dependency/build dirs.\n- **edit_file**(path, edits, dry_run, diff) [capability_expand category: write]: Apply targeted, anchored search-replace edits to an EXISTING text file (use file_write to create/overwrite). Each edit is {original_text, new_text, replace_all?}; original_text must match exactly and uniquely (or set replace_all) — a non-unique or missing anchor is rejected so files are never corrupted. Set dry_run to preview. Alternatively pass a unified 'diff' to apply its hunks as anchored edits. Far cheaper and safer than rewriting a whole file.\n- **shell_run**(command, cwd, timeout) [capability_expand category: shell]: Execute a one-shot shell command with timeout protection. Runs in the active workspace; paths resolving outside it are refused. Reach for a structured tool first when one fits — web_fetch for anything over HTTP(S), git_query/scm_sync for git, code_search/file_find/file_read for the repo, config_get/config_set for LeapFlow's own settings — because those report typed results, while a failed shell command can only be diagnosed from its exit code and stderr. Every shell run counts as an external side effect, so a failure stops the rest of the batch and is not retried automatically.\n- **terminal_open**(command, cwd, shell) [capability_expand category: terminal]: Open a PERSISTENT shell session (REPL/dev server/watch), returning a session_id for terminal_send/read/close. Disabled unless tools.terminal_session_enabled is set. For one-shot commands use shell_run instead.\n- **terminal_send**(session_id, input, wait) [capability_expand category: terminal]: Send a line of input to a persistent terminal session and return output captured shortly after.\n- **terminal_read**(session_id, wait): Drain buffered output from a persistent terminal session (optionally waiting briefly first).\n- **terminal_close**(session_id) [capability_expand category: terminal]: Terminate a persistent terminal session and release its process group.\n- **terminal_list**(): List active persistent terminal sessions.\n- **config_list**(category, limit): List LeapFlow's own writable settings (LLM model and endpoint, daemon, memory, perception, gateway, …) with current values. Use this to discover the exact key before changing anything. Optionally narrow by `category`. This is the only correct way to inspect LeapFlow configuration — never read config files from disk.\n- **config_get**(key): Read one LeapFlow setting by key (e.g. 'llm.model', 'llm.base_url', 'daemon.log_level'), returning its current value, type, scopes, and whether a change needs a daemon restart. There is no 'llm.provider' key: provider behavior is inferred from the OpenAI-compatible 'llm.base_url'. Never read LeapFlow config files from disk — use this.\n- **config_set**(key, value, scope) [capability_expand category: config]: Change one LeapFlow setting by key, e.g. switch an OpenAI-compatible LLM with key='llm.model' and key='llm.base_url'. There is no 'llm.provider' key: provider behavior is inferred from the endpoint. Values are validated and coerced; credentials are stored in the vault automatically. Call config_list or config_get first if unsure of the key. The result states whether a `leap daemon restart` is required. Never edit LeapFlow config files directly.\n- **web_fetch**(url, select, timeout, max_bytes): Read a URL over HTTP(S) and get back extracted, context-sized content: parsed JSON for API endpoints, readable text plus links for web pages. Use this for anything on the internet — prices, docs, releases, articles — instead of running curl through shell_run: it reports real HTTP status codes, retries rate limits on its own, and is a plain read so a retry is always safe. For JSON APIs pass `select` with a dotted path (e.g. 'chart.result.0.meta') to return just that part instead of the whole payload.\n- **memory_search**(query, limit): Search agent memory for relevant past experiences, observations, and facts.\n- **memory_add**(content, kind) [capability_expand category: write]: Store a new observation or insight in memory for future reference.\n- **research_note**(kind, text): Record a compact, structured note about the current task's state so it survives context compression on long / multi-step tasks. Use for durable findings, open questions still to resolve, decisions / excluded paths, and the immediate next step. One concise sentence per note.\n- **capability_expand**(category): Fetch the full callable schema for every tool in a capability category (e.g. 'hub', 'gateway', 'desktop', 'delegate', 'file', 'memory', 'skill'). The compact tool index always lists every registered tool by name and a one-line summary, but only a static low-risk subset is directly callable each turn. If you need a tool from the index that is not yet callable, call capability_expand with its category first; the matching tools become callable in this turn. Never invent a tool name — expand the category instead.\n- **delegate_task**(goal, context) [capability_expand category: delegate]: Delegate a complex sub-task to an isolated subagent. The subagent gets a fresh context and restricted tool access. Use when a task is self-contained and can be solved independently.\n- **schedule_reentry**(kind, reason, delay_seconds, event_match, max_reentries, deadline_seconds): Register a re-entry so this task can resume later from its current orientation (findings / open questions / next step). Use when work must pause and continue after a delay (kind=time) or when a matching platform event arrives (kind=event), instead of finishing now. The research-ledger state is carried over automatically.\n- **hub_push**(skill_name, visibility, version) [capability_expand category: hub]: Push a local skill to the ModelScope Hub for sharing or backup.\n- **hub_pull**(repo_id, version) [capability_expand category: hub]: Pull a skill from the ModelScope Hub to install locally.\n- **hub_search**(query) [capability_expand category: hub]: Search for skills on the Hub by keyword or description.\n- **hub_sync**(mode, dry_run) [capability_expand category: hub]: Preview or execute sync between local skills and Hub.\n- **platform_action**(platform, action, payload, backend_kind) [capability_expand category: gateway]: Execute an exact registered business action on an external platform through LeapFlow's App Connector layer. Actions must be copied from the App Connector Capability Index and are addressed as domain.operation, e.g. im.send_message or docs.create_markdown. All business fields (chat_id, text, query, etc.) MUST be placed inside `payload`, never at the top level. Example: {\"platform\":\"feishu\",\"action\":\"im.send_message\",\"payload\":{\"chat_id\":\"oc_xxx\",\"text\":\"hello\"}}. Do not invent action names, do not use management actions such as list/guide/connect/status here.\n- **platform_connect**(action, platform, credentials, options, checkpoint) [capability_expand category: gateway]: List, guide, connect, disconnect, remove, or check status for external platforms using the App Connector management namespace. Supports REST and CLI backends. Use this for management actions such as list/guide/preflight/connect/status; use platform_action only for exact registered business actions.\n- **gateway_send**(platform, chat_id, text, thread_id) [capability_expand category: gateway]: Send a message to a connected external platform (Feishu group, Telegram chat, DingTalk conversation, etc.). Requires the platform to be connected via gateway_connect first. Use gateway_connect with action='list' to see connected platforms and available chat IDs.\n- **gateway_connect**(action, platform, credentials, options) [capability_expand category: gateway]: Connect, configure, or manage external platform integrations (Feishu, DingTalk, Telegram, Slack, Discord, etc.). Conversational flow: 1) call 'guide' to get setup steps + required fields, 2) present the steps to the user and ask for ALL required credentials in a single message, 3) call 'connect' with the credentials. Goal: complete in 1–2 user turns. NEVER include credential values in your text response.\n- **plugin_list**(): List the live plugin registry and cross-subsystem capability evidence. Use this before answering questions about whether LeapFlow supports plugins, self-evolution, plugin installation, hot reload, versioning, or other runtime capabilities.\n- **plugin_status**(plugin_id): Get detailed status of a specific plugin: its declared category, runtime dependencies, contributed tools, and fiber lifecycle state.\n- **plugin_versions**(plugin_id): List recorded source versions and active pointer for a profile-scoped plugin.\n- **plugin_propose**(requested_capability, plugin_id, proposed_tools, test_cases, risk_level, evidence): Create a side-effect-free PluginProposal from explicit capability-gap evidence. Use this before plugin_generate when a missing capability should be reviewed. Does not call an LLM, write files, or install anything.\n- **assess_compatibility**(manifest, source_path) [capability_expand category: plugin_management]: Assess whether a foreign plugin manifest or real DSH source bundle is compatible with LeapFlow. A source bundle assessment is static: runtime_ready stays false until restricted Node discovery during plugin_install. Returns component-level verdicts and limitations.\n- **plugin_generate**(plugin_id, description, proposal_id) [capability_expand category: system]: Generate a new ToolPlugin from a natural-language capability description. The LLM produces code that conforms to the ToolPlugin Protocol; it is then rigorously validated (syntax, structure, import, protocol conformance). The generated source is stored in CAS and receives explicit content approval. It DOES NOT install the plugin — installation requires a second, mutation-specific approval via plugin_install.\n- **plugin_install**(plugin_id, code, marketplace_name, source_path, proposal_id, version_label) [capability_expand category: system]: Install a plugin from exactly one source: validated Python code, the configured Python marketplace, or a real DSH source bundle. DSH bundles run restricted Node discovery before registration; only public host tools are installed and client UI limitations are reported. Writes profile-scoped state and mutates the process-global registry. REQUIRES APPROVAL.\n- **plugin_rollback**(plugin_id, version) [capability_expand category: system]: Rollback a profile-scoped plugin to a recorded source version and reload it. REQUIRES APPROVAL.\n- **plugin_reload**(plugin_id, version_label) [capability_expand category: system]: Hot-reload a plugin at runtime. Disposes the old plugin fiber, re-imports its module, and registers a fresh instance. Existing in-flight turns are unaffected (snapshot isolation). REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_disable**(plugin_id) [capability_expand category: system]: Disable a plugin by disposing its fiber, removing its tools from the runtime registry. Cannot disable self_management itself. REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_remove**(plugin_id, delete_source) [capability_expand category: system]: Terminally remove a plugin: dispose its fiber, unregister its tools, remove reload metadata, and optionally delete its profile-scoped source file. Cannot remove self_management itself. REQUIRES APPROVAL.\n- **plugin_unquarantine**(plugin_id) [capability_expand category: plugin_management]: Restore a quarantined plugin to probation status for re-evaluation. Unfreezes the trust ledger, transitions the proposal back to PROBATION, and reloads the plugin. REQUIRES APPROVAL.\n- **plugin_enable**(plugin_id) [capability_expand category: system]: Re-enable a previously disabled plugin by reloading its module and registering a fresh instance. REQUIRES APPROVAL.\n- **self_describe**(facet): Introspect LeapFlow's own identity, capabilities, runtime state, evolution metrics, or platform connections. Use facet='all' only when a comprehensive self-check is explicitly requested.\n- **runtime_snapshot**(): Lightweight (~150 token) flat snapshot of current runtime state: model, context budget, posture, disclosure level, turn count, cache hit rate, uptime, tool count, and pending approvals.\n- **schedule_create**(trigger_expression, instruction, execution_mode, max_retries, delivery_target) [capability_expand category: scheduler]: Create a new scheduled task. Specify a trigger expression (e.g. '30m', 'every 2h', '0 9 * * *') and the instruction to execute on each trigger. Optionally provide an execution mode and a delivery target to receive notifications.\n- **schedule_list**() [capability_expand category: scheduler]: List all scheduled tasks with their current state, trigger, and next due time.\n- **schedule_status**(task_id) [capability_expand category: scheduler]: Get detailed status and recent execution history for a scheduled task.\n- **schedule_pause**(task_id) [capability_expand category: scheduler]: Pause a scheduled task so it stops firing without being cancelled.\n- **schedule_resume**(task_id) [capability_expand category: scheduler]: Resume a paused scheduled task — re-arms it and recalculates next due time.\n- **schedule_cancel**(task_id) [capability_expand category: scheduler]: Cancel a scheduled task permanently.\n- **tool_search**(query, max_results): Search all registered tools by keyword relevance. Returns a ranked list of matching tools with name, category, and summary. Use this when you need a tool but are unsure of its exact name or category.\n- **tool_describe**(tool_name): Get the full callable schema of a single tool by exact name. Returns the tool's description, parameters, and metadata. Use after tool_search to inspect a specific tool before calling it.\n- **session_search**(query, limit) [capability_expand category: unclassified]: Search past conversation sessions in the current workspace for relevant context.\n- **session_list**(limit): List recent conversation sessions in the current workspace with stable ids, titles, dates, and summaries. Use for browsing past tasks or when user asks to see history without specific search terms. No keywords needed — returns chronological list.\n- **session_detail**(session_id, limit, offset, include_inactive) [capability_expand category: unclassified]: Read a paginated persisted transcript for one past conversation session in the current workspace. Use after session_list or session_search returns a session_id.\n- **click**(element_index) [capability_expand category: desktop]: Click a UI element by its element_index (from the latest observe_ui snapshot)\n- **get_clipboard**() [capability_expand category: desktop]: Read current clipboard text content\n- **list_apps**(filter, running_only) [capability_expand category: desktop]: List available applications on this system. Use to discover correct bundle_id before switch_app.\n- **list_windows**() [capability_expand category: desktop]: List all top-level windows with pid, window_id, title, and per-window state (minimized, on-screen). Call this first to pick the pid and window_id that observe_ui and other window tools require.\n- **observe_ui**(pid, window_id, query) [capability_expand category: desktop]: Snapshot one window's actionable UI elements, each tagged with an element_index for click/right_click/read_text. Re-observe after actions — indices belong to one snapshot. Requires the window's pid and window_id from list_windows.\n- **open_url**(url, app_id) [capability_expand category: desktop]: Open a URL in the default or specified browser\n- **read_text**(element_index) [capability_expand category: desktop]: Read the text content of a specific UI element from the latest snapshot\n- **right_click**(element_index) [capability_expand category: desktop]: Right-click a UI element to open its context menu. Returns visible menu items.\n- **screenshot**(pid, window_id) [capability_expand category: desktop]: Capture a screenshot for visual verification. With pid + window_id captures that window (works across all displays); defaults to the last observed window, or the full desktop when no window has been observed.\n- **scroll**(element_index, direction, amount, pid, window_id) [capability_expand category: desktop]: Scroll a scrollable area of a window. Omit element_index to scroll the window's focused/page scroller; pass one to scroll an exact element from the latest snapshot.\n- **select_text**(element_index) [capability_expand category: desktop]: Select all text in a UI element (focus + select-all, for subsequent copy)\n- **set_clipboard**(text) [capability_expand category: desktop]: Write text to the clipboard\n- **shortcut**(keys) [capability_expand category: desktop]: Execute a keyboard shortcut\n- **switch_app**(app_id) [capability_expand category: desktop]: Switch to an app (launch if needed, activate, verify)\n- **type_text**(text) [capability_expand category: desktop]: Type text into the currently focused element\n- **wait**(seconds) [capability_expand category: desktop]: Wait for a specified duration before continuing\n- **wait_until**(condition, pid, window_id, timeout, poll_interval) [capability_expand category: desktop]: Wait until a UI condition is met (polls UI tree). Returns elements when found or on timeout.\n- **wait_until_stable**(timeout, poll_interval, pid, window_id) [capability_expand category: desktop]: Wait until the UI stops changing (element set stabilizes across polls).\n\n## App Connector Capability Index\nLeapFlow can onboard and manage external apps through `platform_connect` and execute exact registered business actions through `platform_action`.\nFor requests about connecting, setting up, configuring, enabling, or managing a supported app, use `platform_connect` first instead of generating SDK/Webhook sample code.\n`platform_connect.action` is the management namespace: list, guide, preflight, connect, disconnect, remove, status, events_start, events_stop, events_status.\n`platform_action.action` is only for exact registered platform business actions listed below, such as `im.send_message`; never use management actions like `list` or `guide` there.\nAll business fields MUST go inside `payload`; top-level keys are only `platform`, `action`, and `payload`.\nDo not invent platform IDs or platform action names. If the needed action is not listed, ask for discovery/clarification instead of guessing.\nUse `platform_connect` with `action='guide'` and the matching `platform` to start onboarding; use `action='list'` when the app is unclear.\nWhen a pending onboarding state is present, continue from that state with `platform_connect` instead of asking the user to restate the app.\nSupported apps:\n- `api_server`: API Server (OpenAI Compatible) (category=api; backend=adapter; platform_action actions=none registered)\n- `dingtalk`: 钉钉 (DingTalk) (category=im; backend=adapter; platform_action actions=none registered)\n- `feishu`: 飞书 (Feishu/Lark) (category=im; backend=cli)\n - `calendar.create_event` payload={summary*, start_time*, end_time*, attendees} [write/high]\n - `docs.create_markdown` payload={title*, markdown*, folder_token} [write/medium]\n - `drive.search` payload={query*, limit} [read/medium]\n - `im.add_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.download_resource` payload={message_id*, file_key*, type*} [read/low]\n - `im.get_messages` payload={message_ids*} [read/low]\n - `im.list_chats` payload={page_size} [read/low]\n - `im.list_messages` payload={chat_id*} [read/low]\n - `im.list_thread_messages` payload={thread*} [read/low]\n - `im.remove_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.reply_message` payload={message_id*, text*} [send/high]\n - `im.search_chats` payload={query*, page_size} [read/low]\n - `im.search_messages` payload={query*} [read/medium]\n - `im.send_message` payload={chat_id*, text*, thread_id} [send/high]\n - `im.update_card` payload={token*, card*} [write/medium]\n - `im.update_message` payload={message_id*, text*} [write/medium]\n - `mail.search_unread` payload={query, limit} [read/high]\n - `sheets.append_row` payload={spreadsheet_token*, sheet_id*, values*} [write/medium]\n - `task.create` payload={title*, description, due_time} [write/medium]\n- `telegram`: Telegram (category=social; backend=adapter; platform_action actions=none registered)\n- `webhook`: Webhook (Generic) (category=webhook; backend=adapter; platform_action actions=none registered)\n\n## Tool Usage\nTools are normally invoked through the native function-calling mechanism, not by writing JSON in your reply\ntext. Only if the provider signals that native function calling is unavailable for this turn, fall back to a\nsingle JSON code block: `{\"name\": \"tool_name\", \"arguments\": {\"key\": \"value\"}}` — use this fallback format\nonly, never both. Only call a tool whose exact name appears in the tool index above and is currently callable\n(or reachable via `capability_expand`). Never invent, rename, alias, or guess a tool name, platform ID, or\nplatform action from argument shape or wording — if the index does not list it, it does not exist.\n`platform_connect.action` (list/guide/preflight/connect/disconnect/remove/status/events_start/events_stop/\nevents_status) is the App Connector management namespace; `platform_action.action` only accepts exact\nregistered business actions such as `im.send_message` shown in the App Connector Capability Index — never mix\nthe two namespaces. If a tool call returns an unknown/unavailable result, use the returned suggestions or\navailable names for a single retry instead of trying further variations of the same guess.\n\n**Side-effect action rule** (`platform_action` with effect=send/write/execute):\n- Call each unique action+payload **exactly once**. Never include duplicates in the same turn.\n- Once the result returns `\"completed\": true`, that action is DONE for this task. Do NOT call it again\n in any subsequent turn — immediately summarize the result for the user instead.\n- The system enforces idempotency: duplicate calls are blocked and will not execute.\n- If the user explicitly requests sending/writing multiple times, use distinct payloads per call.\n\n**Resource identifier provenance rule**:\n- NEVER fabricate, guess, or infer resource identifiers (chat_id, message_id, file_key, user_id, etc.).\n Every resource ID used in a side-effect action MUST come from a successful API response in this session.\n- If a read/list action fails (e.g. authorization error), you do NOT have valid resource IDs.\n Report the failure to the user — do NOT attempt the dependent write/send action with a guessed ID.\n- When a tool result contains `\"llm_instruction\"`, follow it exactly.\n\n## Guidelines\n1. **Direct answers first**: If you already know the answer, respond directly without tools.\n2. **Avoid redundant tool calls**: Do not call the same tool with the same arguments more than once in the same user turn. When an existing tool result already answers the user's request, stop calling tools and answer directly.\n3. **Use tools proactively**: When the user asks about files, time, system state, or needs actions performed, use the appropriate tool.\n4. **Chain tools when needed**: You can call multiple tools in sequence (e.g., list files → read file → summarize).\n5. **Handle failures gracefully**: If a tool fails, explain what went wrong and suggest alternatives. If it failed because of an unknown tool/platform/action name, retry once with an exact name from the returned suggestions, then explain rather than keep guessing.\n6. **Summarize results naturally**: After tool execution, synthesize the results into a helpful answer rather than dumping raw output.\n7. **Stay conversational**: Maintain a natural, helpful tone. Acknowledge context from earlier in the conversation.\n8. **Recall past work**:\n - Broad queries (\"之前做了什么\", \"列出任务\"): answer from the \"Recent Task History\" section already in your context. If insufficient, call session_list.\n - Specific lookups (\"上次那个配置怎么改的\"): call session_search with relevant phrases (NOT single characters).\n - Do NOT call search tools repeatedly with keyword fragments. One well-phrased call is better than ten fragmented ones.\n\n## Coding & Verification\nWhen working with code, prefer the precise built-in tools over ad-hoc shell: use `repo_map` to orient in an\nunfamiliar project, `code_search`/`file_find` to locate and `code_intel` for symbols, `edit_file` (anchored\nsearch-replace, or a unified `diff`) to change files — never rewrite a whole file to change a few lines — and\n`git_query` to inspect diffs/log. After edits, check `syntax_ok` in the result and run `test_run`/`lint_check`\nbefore declaring the work done. Batch independent read-only calls (search/read/query) into a single turn — they\nrun in parallel.\n\n## Presentation Style\n1. **Polished Markdown only**: Format user-facing answers with clean Markdown headings, short paragraphs, and concise bullets. Use tables only when they improve comparison or scanning.\n2. **Terminal-friendly layout**: Keep lines readable in a TUI; avoid dense walls of text, deeply nested lists, oversized ASCII art, or heavy visual blocks.\n3. **Elegant emphasis**: Use bold text sparingly for key terms and conclusions. Avoid excessive emojis, decorative symbols, repeated separators, or visual noise.\n4. **Theme-safe colors**: Do not emit ANSI escape codes, HTML color tags, Rich markup, or hardcoded color names. Rely on the TUI theme to render Markdown professionally.\n5. **No leaked tool protocol**: Never show tool-call JSON, internal schemas, raw observations, tool result payloads, or hidden reasoning in the final answer unless the user explicitly asks for raw/debug output. Treat any prior `{\"name\": ..., \"arguments\": ...}` blocks and `Tool result (...)` messages as internal execution context only.\n6. **Professional closure**: End with a concise conclusion or next step when helpful; avoid rambling after the useful answer is complete.\n\nWhen finished with all tool calls, respond normally without a JSON block, tool-call transcript, or process log.\n\n## Task Contract\n- Task ID: turn-2\n- Original user request: Install the prepared adaptive JSON pretty plugin.\n- Workspace root: \n- Allowed roots: \n- Treat relative project paths as relative to the workspace root; never infer `.` as the project root when a workspace root is provided.\n- Workspace boundary is enforced by tools: do not read, search, edit, or run commands against paths outside the allowed roots unless the user explicitly requests an external path and the tool/approval policy permits it.\n- LeapFlow workspace config is optional at `/.leapflow/config.yaml`; runtime config is loaded from `~/.leapflow/config/user.yaml` and `~/.leapflow/profiles//config/*.yaml`.\n- Preserve this task contract across summarization, compression, tool loops, and memory retrieval.\n" - }, - { - "role": "system", - "content": "\n## Recent Session Summary\n- [assistant] Observed the missing JSON pretty tool.\n- [user] Install the prepared adaptive JSON pretty plugin.\n" - }, - { - "role": "assistant", - "content": "Observed the missing JSON pretty tool." - }, - { - "role": "user", - "content": "Install the prepared adaptive JSON pretty plugin.\nInstall the prepared adaptive JSON pretty plugin." - } - ], - "tools": [ - "capability_expand", - "code_intel", - "code_search", - "config_get", - "config_list", - "env_info", - "file_find", - "file_list", - "file_read", - "git_query", - "lint_check", - "memory_search", - "plugin_list", - "plugin_propose", - "plugin_status", - "plugin_versions", - "repo_map", - "research_note", - "runtime_snapshot", - "schedule_reentry", - "self_describe", - "session_list", - "skill_view", - "skills_list", - "terminal_list", - "terminal_read", - "test_run", - "text_search", - "time_get", - "tool_describe", - "tool_search", - "web_fetch" - ] - }, - "responses": [ - { - "status": 200, - "content_type": "application/json", - "body": "{\"id\": \"chatcmpl-cassette\", \"object\": \"chat.completion\", \"model\": \"cassette-model\", \"choices\": [{\"index\": 0, \"message\": {\"role\": \"assistant\", \"content\": \"\", \"tool_calls\": [{\"id\": \"call_1\", \"type\": \"function\", \"function\": {\"name\": \"plugin_install\", \"arguments\": \"{\\\"plugin_id\\\": \\\"json_pretty_loop_e2e\\\", \\\"code\\\": \\\"from __future__ import annotations\\\\n\\\\nimport json\\\\nfrom typing import Any\\\\n\\\\nfrom leapflow.plugins.protocol import ToolMetadata\\\\n\\\\n\\\\nasync def json_pretty_loop_e2e(text: str = \\\\\\\"\\\\\\\", **kwargs: Any) -> dict[str, Any]:\\\\n payload = text or kwargs.get(\\\\\\\"payload\\\\\\\") or \\\\\\\"{}\\\\\\\"\\\\n try:\\\\n parsed = json.loads(str(payload))\\\\n except json.JSONDecodeError as exc:\\\\n return {\\\\\\\"ok\\\\\\\": False, \\\\\\\"error\\\\\\\": str(exc)}\\\\n return {\\\\\\\"ok\\\\\\\": True, \\\\\\\"content\\\\\\\": json.dumps(parsed, ensure_ascii=False, indent=2, sort_keys=True)}\\\\n\\\\n\\\\nclass JsonPrettyLoopE2EPlugin:\\\\n @property\\\\n def plugin_id(self) -> str:\\\\n return \\\\\\\"json_pretty_loop_e2e\\\\\\\"\\\\n\\\\n @property\\\\n def category(self) -> str:\\\\n return \\\\\\\"formatting\\\\\\\"\\\\n\\\\n @property\\\\n def dependencies(self) -> list[str]:\\\\n return []\\\\n\\\\n @property\\\\n def tools(self) -> list[ToolMetadata]:\\\\n return [\\\\n ToolMetadata(\\\\n name=\\\\\\\"json_pretty_loop_e2e\\\\\\\",\\\\n description=\\\\\\\"Pretty-print JSON for the adaptive closed-loop journey.\\\\\\\",\\\\n parameters_schema={\\\\n \\\\\\\"type\\\\\\\": \\\\\\\"object\\\\\\\",\\\\n \\\\\\\"properties\\\\\\\": {\\\\n \\\\\\\"text\\\\\\\": {\\\\\\\"type\\\\\\\": \\\\\\\"string\\\\\\\", \\\\\\\"description\\\\\\\": \\\\\\\"JSON text to format\\\\\\\"}\\\\n },\\\\n },\\\\n handler=json_pretty_loop_e2e,\\\\n x_leapflow={\\\\n \\\\\\\"category\\\\\\\": \\\\\\\"formatting\\\\\\\",\\\\n \\\\\\\"risk_level\\\\\\\": \\\\\\\"read_only\\\\\\\",\\\\n \\\\\\\"schema_cost\\\\\\\": \\\\\\\"low\\\\\\\",\\\\n \\\\\\\"requires_approval\\\\\\\": False,\\\\n },\\\\n provides_capabilities=(\\\\\\\"json.pretty\\\\\\\",),\\\\n requires_platform_capabilities=(\\\\\\\"file.ops\\\\\\\",),\\\\n )\\\\n ]\\\\n\\\\n def bind_runtime(self, **deps: Any) -> None:\\\\n return None\\\\n\\\\n\\\\nplugin = JsonPrettyLoopE2EPlugin()\\\\n\\\", \\\"version_label\\\": \\\"r7\\\"}\"}}]}, \"finish_reason\": \"tool_calls\"}], \"usage\": {\"prompt_tokens\": 64, \"completion_tokens\": 16, \"total_tokens\": 80}}" - } - ] -} diff --git a/tests/_fixtures/cassettes/r7_adaptive_plugin_loop/cassette-model-045af568ab039454.cassette.json b/tests/_fixtures/cassettes/r7_adaptive_plugin_loop/cassette-model-045af568ab039454.cassette.json deleted file mode 100644 index ae2747d0..00000000 --- a/tests/_fixtures/cassettes/r7_adaptive_plugin_loop/cassette-model-045af568ab039454.cassette.json +++ /dev/null @@ -1,79 +0,0 @@ -{ - "fingerprint": "045af568ab039454fb390488b41e2923fca09a79c62fd384b287c60b47c63750", - "note": "captured in seed mode", - "request": { - "model": "cassette-model", - "stream": false, - "messages": [ - { - "role": "system", - "content": "You are LeapFlow, an intelligent assistant that can both converse naturally and take real actions on the user's computer.\n\n## Capabilities\nThe tool index below lists **every** registered tool by name and a one-line summary — this is the complete\ncapability contract; nothing else exists. Only a subset is directly callable this turn (via native tool calling,\nnot a JSON block in your reply). If you need a tool from the index that is not yet callable, call\n`capability_expand` with its category name first — the matching tools become callable immediately after.\nWhen the user asks what LeapFlow itself supports, whether it supports plugins, or which runtime capabilities\nare available, use the live capability evidence exposed by `plugin_list` before making capability claims; report\nconfiguration-dependent or unavailable capabilities as limitations instead of inferring from documentation.\n- **text_search**(text, pattern): Search for a regex pattern in text.\n- **text_replace**(text, old, new, count) [capability_expand category: write]: Replace occurrences of a substring in text.\n- **time_get**(): Get current date and time.\n- **env_info**(): Get system environment information (OS, Python version, cwd).\n- **skills_list**(query, category, source): List available learned skills. Use when user asks about capabilities or you need a specific skill.\n- **skill_view**(name): View the full content of a specific skill document.\n- **code_intel**(path, operation): Precise document symbols (outline) for a source file: classes, functions, and methods with line ranges. Python uses an exact AST parse; other languages use a keyword-prefix scan. Prefer over file_read mode=symbols for accurate navigation before editing. Read-only.\n- **repo_map**(path): Compact project orientation for a repository root: languages, detected test/lint commands, top-level structure, entry points, manifest, and VCS branch. Call this first when entering an unfamiliar codebase. Read-only.\n- **scm_sync**(action, cwd, remote, pull_ref, push_ref, timeout) [capability_expand category: scm]: Run a typed git SCM action. Use this instead of shell_run for git pull/push/status. For 'pull origin main then push', set action='pull_then_push', remote='origin', pull_ref='main', and omit push_ref so LeapFlow pushes the current local branch.\n- **git_query**(action, cwd, ref, path, staged, max_count, stat): Read-only structured git inspection: action=diff|log|status|branch|show. Prefer over shell_run for reading repo state — output is clipped, redacted, and log/branch are parsed into structured fields. Use scm_sync for pull/push.\n- **git_write**(action, cwd, message, stage_all, name, ref, create) [capability_expand category: scm]: Mutating git actions: action=commit (message, stage_all), branch (create+switch), checkout (switch; create=true for -b). Approval-gated. Use scm_sync for pull/push and git_query for reads.\n- **test_run**(command, cwd, timeout): Run the project's test suite and return structured results (framework, passed/failed counts, failing tests). Auto-detects the runner (pytest/npm/go/cargo) or uses a configured/explicit command; executes via the governed shell. ok=true means the runner executed — see 'success' for pass/fail.\n- **lint_check**(command, cwd, timeout): Run the project's linter and return a structured clean/issue result. Auto-detects the linter (ruff/eslint/go vet/clippy) or uses a configured/explicit command; executes via the governed shell. ok=true means the linter ran — see 'clean'.\n- **file_list**(path, pattern, depth): List files and directories at a given path. Use depth=1 or depth=2 to get a recursive tree in one call instead of listing each sub-directory separately.\n- **file_read**(path, max_lines, start_line, max_chars, mode): Read text file content with adaptive context governance. For large or unfamiliar files, prefer mode='outline' or mode='symbols' first, then use mode='raw' with start_line/max_lines for the specific range you actually need. For LeapFlow's own settings, use config_list / config_get / config_set — its config files are outside the workspace and not readable here.\n- **file_write**(path, content, mode) [capability_expand category: write]: Write content to a file (overwrite or append).\n- **code_search**(pattern, patterns, path, glob, ignore_case, multiline, max_results, context_lines): Search file CONTENTS by regex pattern across a directory tree (ripgrep-backed). Requires a regex pattern. NOT for listing or browsing directory contents — use file_list for that. Prefer this over shell_run grep: faster, skips VCS/dependency/build dirs, and returns structured path:line:column matches. Batch related lookups into ONE call via `patterns` (OR-combined, single pass) instead of issuing several separate searches. Use file_read for the surrounding context of a hit.\n- **file_find**(glob, path, max_results): Find files by a recursive glob pattern under a base path (e.g. '**/test_*.py' or '*.md'). Prefer this over shell_run find; skips VCS/dependency/build dirs.\n- **edit_file**(path, edits, dry_run, diff) [capability_expand category: write]: Apply targeted, anchored search-replace edits to an EXISTING text file (use file_write to create/overwrite). Each edit is {original_text, new_text, replace_all?}; original_text must match exactly and uniquely (or set replace_all) — a non-unique or missing anchor is rejected so files are never corrupted. Set dry_run to preview. Alternatively pass a unified 'diff' to apply its hunks as anchored edits. Far cheaper and safer than rewriting a whole file.\n- **shell_run**(command, cwd, timeout) [capability_expand category: shell]: Execute a one-shot shell command with timeout protection. Runs in the active workspace; paths resolving outside it are refused. Reach for a structured tool first when one fits — web_fetch for anything over HTTP(S), git_query/scm_sync for git, code_search/file_find/file_read for the repo, config_get/config_set for LeapFlow's own settings — because those report typed results, while a failed shell command can only be diagnosed from its exit code and stderr. Every shell run counts as an external side effect, so a failure stops the rest of the batch and is not retried automatically.\n- **terminal_open**(command, cwd, shell) [capability_expand category: terminal]: Open a PERSISTENT shell session (REPL/dev server/watch), returning a session_id for terminal_send/read/close. Disabled unless tools.terminal_session_enabled is set. For one-shot commands use shell_run instead.\n- **terminal_send**(session_id, input, wait) [capability_expand category: terminal]: Send a line of input to a persistent terminal session and return output captured shortly after.\n- **terminal_read**(session_id, wait): Drain buffered output from a persistent terminal session (optionally waiting briefly first).\n- **terminal_close**(session_id) [capability_expand category: terminal]: Terminate a persistent terminal session and release its process group.\n- **terminal_list**(): List active persistent terminal sessions.\n- **config_list**(category, limit): List LeapFlow's own writable settings (LLM model and endpoint, daemon, memory, perception, gateway, …) with current values. Use this to discover the exact key before changing anything. Optionally narrow by `category`. This is the only correct way to inspect LeapFlow configuration — never read config files from disk.\n- **config_get**(key): Read one LeapFlow setting by key (e.g. 'llm.model', 'llm.base_url', 'daemon.log_level'), returning its current value, type, scopes, and whether a change needs a daemon restart. There is no 'llm.provider' key: provider behavior is inferred from the OpenAI-compatible 'llm.base_url'. Never read LeapFlow config files from disk — use this.\n- **config_set**(key, value, scope) [capability_expand category: config]: Change one LeapFlow setting by key, e.g. switch an OpenAI-compatible LLM with key='llm.model' and key='llm.base_url'. There is no 'llm.provider' key: provider behavior is inferred from the endpoint. Values are validated and coerced; credentials are stored in the vault automatically. Call config_list or config_get first if unsure of the key. The result states whether a `leap daemon restart` is required. Never edit LeapFlow config files directly.\n- **web_fetch**(url, select, timeout, max_bytes): Read a URL over HTTP(S) and get back extracted, context-sized content: parsed JSON for API endpoints, readable text plus links for web pages. Use this for anything on the internet — prices, docs, releases, articles — instead of running curl through shell_run: it reports real HTTP status codes, retries rate limits on its own, and is a plain read so a retry is always safe. For JSON APIs pass `select` with a dotted path (e.g. 'chart.result.0.meta') to return just that part instead of the whole payload.\n- **memory_search**(query, limit): Search agent memory for relevant past experiences, observations, and facts.\n- **memory_add**(content, kind) [capability_expand category: write]: Store a new observation or insight in memory for future reference.\n- **research_note**(kind, text): Record a compact, structured note about the current task's state so it survives context compression on long / multi-step tasks. Use for durable findings, open questions still to resolve, decisions / excluded paths, and the immediate next step. One concise sentence per note.\n- **capability_expand**(category): Fetch the full callable schema for every tool in a capability category (e.g. 'hub', 'gateway', 'desktop', 'delegate', 'file', 'memory', 'skill'). The compact tool index always lists every registered tool by name and a one-line summary, but only a static low-risk subset is directly callable each turn. If you need a tool from the index that is not yet callable, call capability_expand with its category first; the matching tools become callable in this turn. Never invent a tool name — expand the category instead.\n- **delegate_task**(goal, context) [capability_expand category: delegate]: Delegate a complex sub-task to an isolated subagent. The subagent gets a fresh context and restricted tool access. Use when a task is self-contained and can be solved independently.\n- **schedule_reentry**(kind, reason, delay_seconds, event_match, max_reentries, deadline_seconds): Register a re-entry so this task can resume later from its current orientation (findings / open questions / next step). Use when work must pause and continue after a delay (kind=time) or when a matching platform event arrives (kind=event), instead of finishing now. The research-ledger state is carried over automatically.\n- **hub_push**(skill_name, visibility, version) [capability_expand category: hub]: Push a local skill to the ModelScope Hub for sharing or backup.\n- **hub_pull**(repo_id, version) [capability_expand category: hub]: Pull a skill from the ModelScope Hub to install locally.\n- **hub_search**(query) [capability_expand category: hub]: Search for skills on the Hub by keyword or description.\n- **hub_sync**(mode, dry_run) [capability_expand category: hub]: Preview or execute sync between local skills and Hub.\n- **platform_action**(platform, action, payload, backend_kind) [capability_expand category: gateway]: Execute an exact registered business action on an external platform through LeapFlow's App Connector layer. Actions must be copied from the App Connector Capability Index and are addressed as domain.operation, e.g. im.send_message or docs.create_markdown. All business fields (chat_id, text, query, etc.) MUST be placed inside `payload`, never at the top level. Example: {\"platform\":\"feishu\",\"action\":\"im.send_message\",\"payload\":{\"chat_id\":\"oc_xxx\",\"text\":\"hello\"}}. Do not invent action names, do not use management actions such as list/guide/connect/status here.\n- **platform_connect**(action, platform, credentials, options, checkpoint) [capability_expand category: gateway]: List, guide, connect, disconnect, remove, or check status for external platforms using the App Connector management namespace. Supports REST and CLI backends. Use this for management actions such as list/guide/preflight/connect/status; use platform_action only for exact registered business actions.\n- **gateway_send**(platform, chat_id, text, thread_id) [capability_expand category: gateway]: Send a message to a connected external platform (Feishu group, Telegram chat, DingTalk conversation, etc.). Requires the platform to be connected via gateway_connect first. Use gateway_connect with action='list' to see connected platforms and available chat IDs.\n- **gateway_connect**(action, platform, credentials, options) [capability_expand category: gateway]: Connect, configure, or manage external platform integrations (Feishu, DingTalk, Telegram, Slack, Discord, etc.). Conversational flow: 1) call 'guide' to get setup steps + required fields, 2) present the steps to the user and ask for ALL required credentials in a single message, 3) call 'connect' with the credentials. Goal: complete in 1–2 user turns. NEVER include credential values in your text response.\n- **plugin_list**(): List the live plugin registry and cross-subsystem capability evidence. Use this before answering questions about whether LeapFlow supports plugins, self-evolution, plugin installation, hot reload, versioning, or other runtime capabilities.\n- **plugin_status**(plugin_id): Get detailed status of a specific plugin: its declared category, runtime dependencies, contributed tools, and fiber lifecycle state.\n- **plugin_versions**(plugin_id): List recorded source versions and active pointer for a profile-scoped plugin.\n- **plugin_propose**(requested_capability, plugin_id, proposed_tools, test_cases, risk_level, evidence): Create a side-effect-free PluginProposal from explicit capability-gap evidence. Use this before plugin_generate when a missing capability should be reviewed. Does not call an LLM, write files, or install anything.\n- **assess_compatibility**(manifest, source_path) [capability_expand category: plugin_management]: Assess whether a foreign plugin manifest or real DSH source bundle is compatible with LeapFlow. A source bundle assessment is static: runtime_ready stays false until restricted Node discovery during plugin_install. Returns component-level verdicts and limitations.\n- **plugin_generate**(plugin_id, description, proposal_id) [capability_expand category: system]: Generate a new ToolPlugin from a natural-language capability description. The LLM produces code that conforms to the ToolPlugin Protocol; it is then rigorously validated (syntax, structure, import, protocol conformance). The generated source is stored in CAS and receives explicit content approval. It DOES NOT install the plugin — installation requires a second, mutation-specific approval via plugin_install.\n- **plugin_install**(plugin_id, code, marketplace_name, source_path, proposal_id, version_label) [capability_expand category: system]: Install a plugin from exactly one source: validated Python code, the configured Python marketplace, or a real DSH source bundle. DSH bundles run restricted Node discovery before registration; only public host tools are installed and client UI limitations are reported. Writes profile-scoped state and mutates the process-global registry. REQUIRES APPROVAL.\n- **plugin_rollback**(plugin_id, version) [capability_expand category: system]: Rollback a profile-scoped plugin to a recorded source version and reload it. REQUIRES APPROVAL.\n- **plugin_reload**(plugin_id, version_label) [capability_expand category: system]: Hot-reload a plugin at runtime. Disposes the old plugin fiber, re-imports its module, and registers a fresh instance. Existing in-flight turns are unaffected (snapshot isolation). REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_disable**(plugin_id) [capability_expand category: system]: Disable a plugin by disposing its fiber, removing its tools from the runtime registry. Cannot disable self_management itself. REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_remove**(plugin_id, delete_source) [capability_expand category: system]: Terminally remove a plugin: dispose its fiber, unregister its tools, remove reload metadata, and optionally delete its profile-scoped source file. Cannot remove self_management itself. REQUIRES APPROVAL.\n- **plugin_unquarantine**(plugin_id) [capability_expand category: plugin_management]: Restore a quarantined plugin to probation status for re-evaluation. Unfreezes the trust ledger, transitions the proposal back to PROBATION, and reloads the plugin. REQUIRES APPROVAL.\n- **plugin_enable**(plugin_id) [capability_expand category: system]: Re-enable a previously disabled plugin by reloading its module and registering a fresh instance. REQUIRES APPROVAL.\n- **self_describe**(facet): Introspect LeapFlow's own identity, capabilities, runtime state, evolution metrics, or platform connections. Use facet='all' only when a comprehensive self-check is explicitly requested.\n- **runtime_snapshot**(): Lightweight (~150 token) flat snapshot of current runtime state: model, context budget, posture, disclosure level, turn count, cache hit rate, uptime, tool count, and pending approvals.\n- **schedule_create**(trigger_expression, instruction, execution_mode, max_retries, delivery_target) [capability_expand category: scheduler]: Create a new scheduled task. Specify a trigger expression (e.g. '30m', 'every 2h', '0 9 * * *') and the instruction to execute on each trigger. Optionally provide an execution mode and a delivery target to receive notifications.\n- **schedule_list**() [capability_expand category: scheduler]: List all scheduled tasks with their current state, trigger, and next due time.\n- **schedule_status**(task_id) [capability_expand category: scheduler]: Get detailed status and recent execution history for a scheduled task.\n- **schedule_pause**(task_id) [capability_expand category: scheduler]: Pause a scheduled task so it stops firing without being cancelled.\n- **schedule_resume**(task_id) [capability_expand category: scheduler]: Resume a paused scheduled task — re-arms it and recalculates next due time.\n- **schedule_cancel**(task_id) [capability_expand category: scheduler]: Cancel a scheduled task permanently.\n- **tool_search**(query, max_results): Search all registered tools by keyword relevance. Returns a ranked list of matching tools with name, category, and summary. Use this when you need a tool but are unsure of its exact name or category.\n- **tool_describe**(tool_name): Get the full callable schema of a single tool by exact name. Returns the tool's description, parameters, and metadata. Use after tool_search to inspect a specific tool before calling it.\n- **session_search**(query, limit) [capability_expand category: unclassified]: Search past conversation sessions in the current workspace for relevant context.\n- **session_list**(limit): List recent conversation sessions in the current workspace with stable ids, titles, dates, and summaries. Use for browsing past tasks or when user asks to see history without specific search terms. No keywords needed — returns chronological list.\n- **session_detail**(session_id, limit, offset, include_inactive) [capability_expand category: unclassified]: Read a paginated persisted transcript for one past conversation session in the current workspace. Use after session_list or session_search returns a session_id.\n- **click**(element_index) [capability_expand category: desktop]: Click a UI element by its element_index (from the latest observe_ui snapshot)\n- **get_clipboard**() [capability_expand category: desktop]: Read current clipboard text content\n- **list_apps**(filter, running_only) [capability_expand category: desktop]: List available applications on this system. Use to discover correct bundle_id before switch_app.\n- **list_windows**() [capability_expand category: desktop]: List all top-level windows with pid, window_id, title, and per-window state (minimized, on-screen). Call this first to pick the pid and window_id that observe_ui and other window tools require.\n- **observe_ui**(pid, window_id, query) [capability_expand category: desktop]: Snapshot one window's actionable UI elements, each tagged with an element_index for click/right_click/read_text. Re-observe after actions — indices belong to one snapshot. Requires the window's pid and window_id from list_windows.\n- **open_url**(url, app_id) [capability_expand category: desktop]: Open a URL in the default or specified browser\n- **read_text**(element_index) [capability_expand category: desktop]: Read the text content of a specific UI element from the latest snapshot\n- **right_click**(element_index) [capability_expand category: desktop]: Right-click a UI element to open its context menu. Returns visible menu items.\n- **screenshot**(pid, window_id) [capability_expand category: desktop]: Capture a screenshot for visual verification. With pid + window_id captures that window (works across all displays); defaults to the last observed window, or the full desktop when no window has been observed.\n- **scroll**(element_index, direction, amount, pid, window_id) [capability_expand category: desktop]: Scroll a scrollable area of a window. Omit element_index to scroll the window's focused/page scroller; pass one to scroll an exact element from the latest snapshot.\n- **select_text**(element_index) [capability_expand category: desktop]: Select all text in a UI element (focus + select-all, for subsequent copy)\n- **set_clipboard**(text) [capability_expand category: desktop]: Write text to the clipboard\n- **shortcut**(keys) [capability_expand category: desktop]: Execute a keyboard shortcut\n- **switch_app**(app_id) [capability_expand category: desktop]: Switch to an app (launch if needed, activate, verify)\n- **type_text**(text) [capability_expand category: desktop]: Type text into the currently focused element\n- **wait**(seconds) [capability_expand category: desktop]: Wait for a specified duration before continuing\n- **wait_until**(condition, pid, window_id, timeout, poll_interval) [capability_expand category: desktop]: Wait until a UI condition is met (polls UI tree). Returns elements when found or on timeout.\n- **wait_until_stable**(timeout, poll_interval, pid, window_id) [capability_expand category: desktop]: Wait until the UI stops changing (element set stabilizes across polls).\n\n## App Connector Capability Index\nLeapFlow can onboard and manage external apps through `platform_connect` and execute exact registered business actions through `platform_action`.\nFor requests about connecting, setting up, configuring, enabling, or managing a supported app, use `platform_connect` first instead of generating SDK/Webhook sample code.\n`platform_connect.action` is the management namespace: list, guide, preflight, connect, disconnect, remove, status, events_start, events_stop, events_status.\n`platform_action.action` is only for exact registered platform business actions listed below, such as `im.send_message`; never use management actions like `list` or `guide` there.\nAll business fields MUST go inside `payload`; top-level keys are only `platform`, `action`, and `payload`.\nDo not invent platform IDs or platform action names. If the needed action is not listed, ask for discovery/clarification instead of guessing.\nUse `platform_connect` with `action='guide'` and the matching `platform` to start onboarding; use `action='list'` when the app is unclear.\nWhen a pending onboarding state is present, continue from that state with `platform_connect` instead of asking the user to restate the app.\nSupported apps:\n- `api_server`: API Server (OpenAI Compatible) (category=api; backend=adapter; platform_action actions=none registered)\n- `dingtalk`: 钉钉 (DingTalk) (category=im; backend=adapter; platform_action actions=none registered)\n- `feishu`: 飞书 (Feishu/Lark) (category=im; backend=cli)\n - `calendar.create_event` payload={summary*, start_time*, end_time*, attendees} [write/high]\n - `docs.create_markdown` payload={title*, markdown*, folder_token} [write/medium]\n - `drive.search` payload={query*, limit} [read/medium]\n - `im.add_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.download_resource` payload={message_id*, file_key*, type*} [read/low]\n - `im.get_messages` payload={message_ids*} [read/low]\n - `im.list_chats` payload={page_size} [read/low]\n - `im.list_messages` payload={chat_id*} [read/low]\n - `im.list_thread_messages` payload={thread*} [read/low]\n - `im.remove_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.reply_message` payload={message_id*, text*} [send/high]\n - `im.search_chats` payload={query*, page_size} [read/low]\n - `im.search_messages` payload={query*} [read/medium]\n - `im.send_message` payload={chat_id*, text*, thread_id} [send/high]\n - `im.update_card` payload={token*, card*} [write/medium]\n - `im.update_message` payload={message_id*, text*} [write/medium]\n - `mail.search_unread` payload={query, limit} [read/high]\n - `sheets.append_row` payload={spreadsheet_token*, sheet_id*, values*} [write/medium]\n - `task.create` payload={title*, description, due_time} [write/medium]\n- `telegram`: Telegram (category=social; backend=adapter; platform_action actions=none registered)\n- `webhook`: Webhook (Generic) (category=webhook; backend=adapter; platform_action actions=none registered)\n\n## Tool Usage\nTools are normally invoked through the native function-calling mechanism, not by writing JSON in your reply\ntext. Only if the provider signals that native function calling is unavailable for this turn, fall back to a\nsingle JSON code block: `{\"name\": \"tool_name\", \"arguments\": {\"key\": \"value\"}}` — use this fallback format\nonly, never both. Only call a tool whose exact name appears in the tool index above and is currently callable\n(or reachable via `capability_expand`). Never invent, rename, alias, or guess a tool name, platform ID, or\nplatform action from argument shape or wording — if the index does not list it, it does not exist.\n`platform_connect.action` (list/guide/preflight/connect/disconnect/remove/status/events_start/events_stop/\nevents_status) is the App Connector management namespace; `platform_action.action` only accepts exact\nregistered business actions such as `im.send_message` shown in the App Connector Capability Index — never mix\nthe two namespaces. If a tool call returns an unknown/unavailable result, use the returned suggestions or\navailable names for a single retry instead of trying further variations of the same guess.\n\n**Side-effect action rule** (`platform_action` with effect=send/write/execute):\n- Call each unique action+payload **exactly once**. Never include duplicates in the same turn.\n- Once the result returns `\"completed\": true`, that action is DONE for this task. Do NOT call it again\n in any subsequent turn — immediately summarize the result for the user instead.\n- The system enforces idempotency: duplicate calls are blocked and will not execute.\n- If the user explicitly requests sending/writing multiple times, use distinct payloads per call.\n\n**Resource identifier provenance rule**:\n- NEVER fabricate, guess, or infer resource identifiers (chat_id, message_id, file_key, user_id, etc.).\n Every resource ID used in a side-effect action MUST come from a successful API response in this session.\n- If a read/list action fails (e.g. authorization error), you do NOT have valid resource IDs.\n Report the failure to the user — do NOT attempt the dependent write/send action with a guessed ID.\n- When a tool result contains `\"llm_instruction\"`, follow it exactly.\n\n## Guidelines\n1. **Direct answers first**: If you already know the answer, respond directly without tools.\n2. **Avoid redundant tool calls**: Do not call the same tool with the same arguments more than once in the same user turn. When an existing tool result already answers the user's request, stop calling tools and answer directly.\n3. **Use tools proactively**: When the user asks about files, time, system state, or needs actions performed, use the appropriate tool.\n4. **Chain tools when needed**: You can call multiple tools in sequence (e.g., list files → read file → summarize).\n5. **Handle failures gracefully**: If a tool fails, explain what went wrong and suggest alternatives. If it failed because of an unknown tool/platform/action name, retry once with an exact name from the returned suggestions, then explain rather than keep guessing.\n6. **Summarize results naturally**: After tool execution, synthesize the results into a helpful answer rather than dumping raw output.\n7. **Stay conversational**: Maintain a natural, helpful tone. Acknowledge context from earlier in the conversation.\n8. **Recall past work**:\n - Broad queries (\"之前做了什么\", \"列出任务\"): answer from the \"Recent Task History\" section already in your context. If insufficient, call session_list.\n - Specific lookups (\"上次那个配置怎么改的\"): call session_search with relevant phrases (NOT single characters).\n - Do NOT call search tools repeatedly with keyword fragments. One well-phrased call is better than ten fragmented ones.\n\n## Coding & Verification\nWhen working with code, prefer the precise built-in tools over ad-hoc shell: use `repo_map` to orient in an\nunfamiliar project, `code_search`/`file_find` to locate and `code_intel` for symbols, `edit_file` (anchored\nsearch-replace, or a unified `diff`) to change files — never rewrite a whole file to change a few lines — and\n`git_query` to inspect diffs/log. After edits, check `syntax_ok` in the result and run `test_run`/`lint_check`\nbefore declaring the work done. Batch independent read-only calls (search/read/query) into a single turn — they\nrun in parallel.\n\n## Presentation Style\n1. **Polished Markdown only**: Format user-facing answers with clean Markdown headings, short paragraphs, and concise bullets. Use tables only when they improve comparison or scanning.\n2. **Terminal-friendly layout**: Keep lines readable in a TUI; avoid dense walls of text, deeply nested lists, oversized ASCII art, or heavy visual blocks.\n3. **Elegant emphasis**: Use bold text sparingly for key terms and conclusions. Avoid excessive emojis, decorative symbols, repeated separators, or visual noise.\n4. **Theme-safe colors**: Do not emit ANSI escape codes, HTML color tags, Rich markup, or hardcoded color names. Rely on the TUI theme to render Markdown professionally.\n5. **No leaked tool protocol**: Never show tool-call JSON, internal schemas, raw observations, tool result payloads, or hidden reasoning in the final answer unless the user explicitly asks for raw/debug output. Treat any prior `{\"name\": ..., \"arguments\": ...}` blocks and `Tool result (...)` messages as internal execution context only.\n6. **Professional closure**: End with a concise conclusion or next step when helpful; avoid rambling after the useful answer is complete.\n\nWhen finished with all tool calls, respond normally without a JSON block, tool-call transcript, or process log.\n\n## Task Contract\n- Task ID: turn-2\n- Original user request: Install the prepared adaptive JSON pretty plugin.\n- Workspace root: \n- Allowed roots: \n- Treat relative project paths as relative to the workspace root; never infer `.` as the project root when a workspace root is provided.\n- Workspace boundary is enforced by tools: do not read, search, edit, or run commands against paths outside the allowed roots unless the user explicitly requests an external path and the tool/approval policy permits it.\n- LeapFlow workspace config is optional at `/.leapflow/config.yaml`; runtime config is loaded from `~/.leapflow/config/user.yaml` and `~/.leapflow/profiles//config/*.yaml`.\n- Preserve this task contract across summarization, compression, tool loops, and memory retrieval.\n" - }, - { - "role": "system", - "content": "\n## Recent Session Summary\n- [assistant] Observed the missing JSON pretty tool.\n- [user] Install the prepared adaptive JSON pretty plugin.\n" - }, - { - "role": "assistant", - "content": "Observed the missing JSON pretty tool." - }, - { - "role": "user", - "content": "Install the prepared adaptive JSON pretty plugin.\nInstall the prepared adaptive JSON pretty plugin." - }, - { - "role": "assistant", - "content": "", - "tool_calls": [ - "plugin_install" - ] - }, - { - "role": "tool", - "content": "{\"ok\": true, \"action\": \"install\", \"plugin_id\": \"json_pretty_loop_e2e\", \"installed_tools\": [\"json_pretty_loop_e2e\"], \"state\": \"active\", \"shadow_validated\": true, \"behavior_tests\": [], \"version\": \"r7\", \"execution_id\": \"\", \"idempotency_key\": \"\", \"execution_policy\": \"mutating_once\", \"tool_call_id\": \"call_1\", \"execution_status\": \"completed\"}", - "tool_result": true - } - ], - "tools": [ - "capability_expand", - "code_intel", - "code_search", - "config_get", - "config_list", - "env_info", - "file_find", - "file_list", - "file_read", - "git_query", - "lint_check", - "memory_search", - "plugin_list", - "plugin_propose", - "plugin_status", - "plugin_versions", - "repo_map", - "research_note", - "runtime_snapshot", - "schedule_reentry", - "self_describe", - "session_list", - "skill_view", - "skills_list", - "terminal_list", - "terminal_read", - "test_run", - "text_search", - "time_get", - "tool_describe", - "tool_search", - "web_fetch" - ] - }, - "responses": [ - { - "status": 200, - "content_type": "application/json", - "body": "{\"id\": \"chatcmpl-cassette\", \"object\": \"chat.completion\", \"model\": \"cassette-model\", \"choices\": [{\"index\": 0, \"message\": {\"role\": \"assistant\", \"content\": \"Installed json pretty plugin.\"}, \"finish_reason\": \"stop\"}], \"usage\": {\"prompt_tokens\": 64, \"completion_tokens\": 16, \"total_tokens\": 80}}" - } - ] -} diff --git a/tests/_fixtures/cassettes/r7_adaptive_plugin_loop/cassette-model-0c54665421c11f8a.cassette.json b/tests/_fixtures/cassettes/r7_adaptive_plugin_loop/cassette-model-0c54665421c11f8a.cassette.json new file mode 100644 index 00000000..306eeddb --- /dev/null +++ b/tests/_fixtures/cassettes/r7_adaptive_plugin_loop/cassette-model-0c54665421c11f8a.cassette.json @@ -0,0 +1,67 @@ +{ + "fingerprint": "0c54665421c11f8a092b8afe7b85529f39575772f0f484791940b39d4e4d47af", + "note": "captured in seed mode", + "request": { + "model": "cassette-model", + "stream": false, + "messages": [ + { + "role": "system", + "content": "You are LeapFlow, an intelligent assistant that can both converse naturally and take real actions on the user's computer.\n\n## Capabilities\nThe tool index below lists **every** registered tool by name and a one-line summary — this is the complete\ncapability contract; nothing else exists. Only a subset is directly callable this turn (via native tool calling,\nnot a JSON block in your reply). If you need a tool from the index that is not yet callable, call\n`capability_expand` with its category name first — the matching tools become callable immediately after.\nWhen the user asks about LeapFlow **itself** — its identity/version/model, what tools, plugins, or capabilities\nit has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of\nself-awareness — answer from live runtime evidence via `self_describe` (facet=capabilities for \"what can you\ndo / do you support X\", identity for version/model, runtime for context/posture, or `all` for a full self-check),\nnever by reading LeapFlow's own source code, documentation, or memory. Report configuration-dependent or\nunavailable capabilities as limitations rather than inferring them.\n- **text_search**(text, pattern): Search for a regex pattern in text.\n- **text_replace**(text, old, new, count) [capability_expand category: write]: Replace occurrences of a substring in text.\n- **time_get**(): Get current date and time.\n- **env_info**(): Get system environment information (OS, Python version, cwd).\n- **skills_list**(query, category, source): List available learned skills. Use when user asks about capabilities or you need a specific skill.\n- **skill_view**(name): View the full content of a specific skill document.\n- **skill_list_files**(name) [capability_expand category: skills]: List all support files in a skill directory (configs, templates, examples). Use before skill_file_read to discover available files.\n- **skill_file_read**(name, file_path) [capability_expand category: skills]: Read an individual support file from a skill directory. Use skill_list_files first to discover available files.\n- **code_intel**(path, operation): Precise document symbols (outline) for a source file: classes, functions, and methods with line ranges. Python uses an exact AST parse; other languages use a keyword-prefix scan. Prefer over file_read mode=symbols for accurate navigation before editing. Read-only.\n- **repo_map**(path): Compact project orientation for a repository root: languages, detected test/lint commands, top-level structure, entry points, manifest, and VCS branch. Call this first when entering an unfamiliar codebase. Read-only.\n- **scm_sync**(action, cwd, remote, pull_ref, push_ref, timeout) [capability_expand category: scm]: Run a typed git SCM action. Use this instead of shell_run for git pull/push/status. For 'pull origin main then push', set action='pull_then_push', remote='origin', pull_ref='main', and omit push_ref so LeapFlow pushes the current local branch.\n- **git_query**(action, cwd, ref, path, staged, max_count, stat): Read-only structured git inspection: action=diff|log|status|branch|show. Prefer over shell_run for reading repo state — output is clipped, redacted, and log/branch are parsed into structured fields. Use scm_sync for pull/push.\n- **git_write**(action, cwd, message, stage_all, name, ref, create) [capability_expand category: scm]: Mutating git actions: action=commit (message, stage_all), branch (create+switch), checkout (switch; create=true for -b). Approval-gated. Use scm_sync for pull/push and git_query for reads.\n- **test_run**(command, cwd, timeout): Run the project's test suite and return structured results (framework, passed/failed counts, failing tests). Auto-detects the runner (pytest/npm/go/cargo) or uses a configured/explicit command; executes via the governed shell. ok=true means the runner executed — see 'success' for pass/fail.\n- **lint_check**(command, cwd, timeout): Run the project's linter and return a structured clean/issue result. Auto-detects the linter (ruff/eslint/go vet/clippy) or uses a configured/explicit command; executes via the governed shell. ok=true means the linter ran — see 'clean'.\n- **file_list**(path, pattern, depth): List files and directories at a given path. Use depth=1 or depth=2 to get a recursive tree in one call instead of listing each sub-directory separately.\n- **file_read**(path, max_lines, start_line, max_chars, mode): Read text file content with adaptive context governance. For large or unfamiliar files, prefer mode='outline' or mode='symbols' first, then use mode='raw' with start_line/max_lines for the specific range you actually need. For LeapFlow's own settings, use config_list / config_get / config_set — its config files are outside the workspace and not readable here.\n- **file_write**(path, content, mode) [capability_expand category: write]: Write content to a file (overwrite or append).\n- **code_search**(pattern, patterns, path, glob, ignore_case, multiline, max_results, context_lines): Search file CONTENTS by regex pattern across a directory tree (ripgrep-backed). Requires a regex pattern. NOT for listing or browsing directory contents — use file_list for that. Prefer this over shell_run grep: faster, skips VCS/dependency/build dirs, and returns structured path:line:column matches. Batch related lookups into ONE call via `patterns` (OR-combined, single pass) instead of issuing several separate searches. Use file_read for the surrounding context of a hit.\n- **file_find**(glob, path, max_results): Find files by a recursive glob pattern under a base path (e.g. '**/test_*.py' or '*.md'). Prefer this over shell_run find; skips VCS/dependency/build dirs.\n- **edit_file**(path, edits, dry_run, diff) [capability_expand category: write]: Apply targeted, anchored search-replace edits to an EXISTING text file (use file_write to create/overwrite). Each edit is {original_text, new_text, replace_all?}; original_text must match exactly and uniquely (or set replace_all) — a non-unique or missing anchor is rejected so files are never corrupted. Set dry_run to preview. Alternatively pass a unified 'diff' to apply its hunks as anchored edits. Far cheaper and safer than rewriting a whole file.\n- **shell_run**(command, cwd, timeout) [capability_expand category: shell]: Execute a one-shot shell command with timeout protection. Runs in the active workspace; paths resolving outside it are refused. Reach for a structured tool first when one fits — web_fetch for anything over HTTP(S), git_query/scm_sync for git, code_search/file_find/file_read for the repo, config_get/config_set for LeapFlow's own settings — because those report typed results, while a failed shell command can only be diagnosed from its exit code and stderr. Every shell run counts as an external side effect, so a failure stops the rest of the batch and is not retried automatically.\n- **terminal_open**(command, cwd, shell) [capability_expand category: terminal]: Open a PERSISTENT shell session (REPL/dev server/watch), returning a session_id for terminal_send/read/close. Disabled unless tools.terminal_session_enabled is set. For one-shot commands use shell_run instead.\n- **terminal_send**(session_id, input, wait) [capability_expand category: terminal]: Send a line of input to a persistent terminal session and return output captured shortly after.\n- **terminal_read**(session_id, wait): Drain buffered output from a persistent terminal session (optionally waiting briefly first).\n- **terminal_close**(session_id) [capability_expand category: terminal]: Terminate a persistent terminal session and release its process group.\n- **terminal_list**(): List active persistent terminal sessions.\n- **config_list**(category, limit): List LeapFlow's own writable settings (LLM model and endpoint, daemon, memory, perception, gateway, …) with current values. Use this to discover the exact key before changing anything. Optionally narrow by `category`. This is the only correct way to inspect LeapFlow configuration — never read config files from disk.\n- **config_get**(key): Read one LeapFlow setting by key (e.g. 'llm.model', 'llm.base_url', 'daemon.log_level'), returning its current value, type, scopes, and whether a change needs a daemon restart. There is no 'llm.provider' key: provider behavior is inferred from the OpenAI-compatible 'llm.base_url'. Never read LeapFlow config files from disk — use this.\n- **config_set**(key, value, scope) [capability_expand category: config]: Change one LeapFlow setting by key, e.g. switch an OpenAI-compatible LLM with key='llm.model' and key='llm.base_url'. There is no 'llm.provider' key: provider behavior is inferred from the endpoint. Values are validated and coerced; credentials are stored in the vault automatically. Call config_list or config_get first if unsure of the key. The result states whether a `leap daemon restart` is required. Never edit LeapFlow config files directly.\n- **web_fetch**(url, select, timeout, max_bytes): Read a URL over HTTP(S) and get back extracted, context-sized content: parsed JSON for API endpoints, readable text plus links for web pages. Use this for anything on the internet — prices, docs, releases, articles — instead of running curl through shell_run: it reports real HTTP status codes, retries rate limits on its own, and is a plain read so a retry is always safe. For JSON APIs pass `select` with a dotted path (e.g. 'chart.result.0.meta') to return just that part instead of the whole payload.\n- **memory_search**(query, limit): Search agent memory for relevant past experiences, observations, and facts.\n- **memory_add**(content, kind) [capability_expand category: write]: Store a new observation or insight in memory for future reference.\n- **research_note**(kind, text): Record a compact, structured note about the current task's state so it survives context compression on long / multi-step tasks. Use for durable findings, open questions still to resolve, decisions / excluded paths, and the immediate next step. One concise sentence per note.\n- **capability_expand**(category): Fetch the full callable schema for every tool in a capability category (e.g. 'hub', 'gateway', 'desktop', 'delegate', 'file', 'memory', 'skill'). The compact tool index always lists every registered tool by name and a one-line summary, but only a static low-risk subset is directly callable each turn. If you need a tool from the index that is not yet callable, call capability_expand with its category first; the matching tools become callable in this turn. Never invent a tool name — expand the category instead.\n- **delegate_task**(goal, context) [capability_expand category: delegate]: Delegate a complex sub-task to an isolated subagent. The subagent gets a fresh context and restricted tool access. Use when a task is self-contained and can be solved independently.\n- **schedule_reentry**(kind, reason, delay_seconds, event_match, max_reentries, deadline_seconds): Register a re-entry so this task can resume later from its current orientation (findings / open questions / next step). Use when work must pause and continue after a delay (kind=time) or when a matching platform event arrives (kind=event), instead of finishing now. The research-ledger state is carried over automatically.\n- **hub_push**(skill_name, visibility, version) [capability_expand category: hub]: Push a local skill to the ModelScope Hub for sharing or backup.\n- **hub_pull**(repo_id, version) [capability_expand category: hub]: Pull a skill from the ModelScope Hub to install locally.\n- **hub_search**(query) [capability_expand category: hub]: Search for skills on the Hub by keyword or description.\n- **hub_sync**(mode, dry_run) [capability_expand category: hub]: Preview or execute sync between local skills and Hub.\n- **platform_action**(platform, action, payload, backend_kind) [capability_expand category: gateway]: Execute an exact registered business action on an external platform through LeapFlow's App Connector layer. Actions must be copied from the App Connector Capability Index and are addressed as domain.operation, e.g. im.send_message or docs.create_markdown. All business fields (chat_id, text, query, etc.) MUST be placed inside `payload`, never at the top level. Example: {\"platform\":\"feishu\",\"action\":\"im.send_message\",\"payload\":{\"chat_id\":\"oc_xxx\",\"text\":\"hello\"}}. Do not invent action names, do not use management actions such as list/guide/connect/status here.\n- **platform_connect**(action, platform, credentials, options, checkpoint) [capability_expand category: gateway]: List, guide, connect, disconnect, remove, or check status for external platforms using the App Connector management namespace. Supports REST and CLI backends. Use this for management actions such as list/guide/preflight/connect/status; use platform_action only for exact registered business actions.\n- **gateway_send**(platform, chat_id, text, thread_id) [capability_expand category: gateway]: Send a message to a connected external platform (Feishu group, Telegram chat, DingTalk conversation, etc.). Requires the platform to be connected via gateway_connect first. Use gateway_connect with action='list' to see connected platforms and available chat IDs.\n- **gateway_connect**(action, platform, credentials, options) [capability_expand category: gateway]: Connect, configure, or manage external platform integrations (Feishu, DingTalk, Telegram, Slack, Discord, etc.). Conversational flow: 1) call 'guide' to get setup steps + required fields, 2) present the steps to the user and ask for ALL required credentials in a single message, 3) call 'connect' with the credentials. Goal: complete in 1–2 user turns. NEVER include credential values in your text response.\n- **plugin_list**(): List the live plugin registry across Tool/Gateway/LLM subsystems for plugin management: which plugins are loaded, their category and fiber lifecycle state. For questions about LeapFlow's own capabilities or whether it supports X, use self_describe instead.\n- **plugin_status**(plugin_id): Get detailed status of a specific plugin: its declared category, runtime dependencies, contributed tools, and fiber lifecycle state.\n- **plugin_versions**(plugin_id): List recorded source versions and active pointer for a profile-scoped plugin.\n- **plugin_propose**(requested_capability, plugin_id, proposed_tools, test_cases, risk_level, evidence): Create a side-effect-free PluginProposal from explicit capability-gap evidence. Use this before plugin_generate when a missing capability should be reviewed. Does not call an LLM, write files, or install anything.\n- **assess_compatibility**(manifest, source_path) [capability_expand category: plugin_management]: Assess whether a foreign plugin manifest or real DSH source bundle is compatible with LeapFlow. A source bundle assessment is static: runtime_ready stays false until restricted Node discovery during plugin_install. Returns component-level verdicts and limitations.\n- **plugin_generate**(plugin_id, description, proposal_id) [capability_expand category: system]: Generate a new ToolPlugin from a natural-language capability description. The LLM produces code that conforms to the ToolPlugin Protocol; it is then rigorously validated (syntax, structure, import, protocol conformance). The generated source is stored in CAS and receives explicit content approval. It DOES NOT install the plugin — installation requires a second, mutation-specific approval via plugin_install.\n- **plugin_install**(plugin_id, code, marketplace_name, source_path, proposal_id, version_label) [capability_expand category: system]: Install a plugin from exactly one source: validated Python code, the configured Python marketplace, or a real DSH source bundle. DSH bundles run restricted Node discovery before registration; only public host tools are installed and client UI limitations are reported. Writes profile-scoped state and mutates the process-global registry. REQUIRES APPROVAL.\n- **plugin_rollback**(plugin_id, version) [capability_expand category: system]: Rollback a profile-scoped plugin to a recorded source version and reload it. REQUIRES APPROVAL.\n- **plugin_reload**(plugin_id, version_label) [capability_expand category: system]: Hot-reload a plugin at runtime. Disposes the old plugin fiber, re-imports its module, and registers a fresh instance. Existing in-flight turns are unaffected (snapshot isolation). REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_disable**(plugin_id) [capability_expand category: system]: Disable a plugin by disposing its fiber, removing its tools from the runtime registry. Cannot disable self_management itself. REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_remove**(plugin_id, delete_source) [capability_expand category: system]: Terminally remove a plugin: dispose its fiber, unregister its tools, remove reload metadata, and optionally delete its profile-scoped source file. Cannot remove self_management itself. REQUIRES APPROVAL.\n- **plugin_unquarantine**(plugin_id) [capability_expand category: plugin_management]: Restore a quarantined plugin to probation status for re-evaluation. Unfreezes the trust ledger, transitions the proposal back to PROBATION, and reloads the plugin. REQUIRES APPROVAL.\n- **plugin_enable**(plugin_id) [capability_expand category: system]: Re-enable a previously disabled plugin by reloading its module and registering a fresh instance. REQUIRES APPROVAL.\n- **self_describe**(facet): Answer questions about LeapFlow ITSELF from live runtime evidence — its identity/version/model, what tools and plugins it has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of self-awareness. Prefer this over reading LeapFlow's own source code. Pick facet=capabilities for 'what can you do / do you support X', identity for version/model, runtime for context/posture, or all for a full self-check when explicitly requested.\n- **runtime_snapshot**(): Lightweight (~150 token) flat snapshot of current runtime state: model, context budget, posture, disclosure level, turn count, cache hit rate, uptime, tool count, and pending approvals.\n- **schedule_create**(trigger_expression, instruction, execution_mode, max_retries, delivery_target) [capability_expand category: scheduler]: Create a new scheduled task. Specify a trigger expression (e.g. '30m', 'every 2h', '0 9 * * *') and the instruction to execute on each trigger. Optionally provide an execution mode and a delivery target to receive notifications.\n- **schedule_list**() [capability_expand category: scheduler]: List all scheduled tasks with their current state, trigger, and next due time.\n- **schedule_status**(task_id) [capability_expand category: scheduler]: Get detailed status and recent execution history for a scheduled task.\n- **schedule_pause**(task_id) [capability_expand category: scheduler]: Pause a scheduled task so it stops firing without being cancelled.\n- **schedule_resume**(task_id) [capability_expand category: scheduler]: Resume a paused scheduled task — re-arms it and recalculates next due time.\n- **schedule_cancel**(task_id) [capability_expand category: scheduler]: Cancel a scheduled task permanently.\n- **tool_search**(query, max_results): Search all registered tools by keyword relevance. Returns a ranked list of matching tools with name, category, and summary. Use this when you need a tool but are unsure of its exact name or category.\n- **tool_describe**(tool_name): Get the full callable schema of a single tool by exact name. Returns the tool's description, parameters, and metadata. Use after tool_search to inspect a specific tool before calling it.\n- **session_search**(query, limit) [capability_expand category: unclassified]: Search past conversation sessions in the current workspace for relevant context.\n- **session_list**(limit): List recent conversation sessions in the current workspace with stable ids, titles, dates, and summaries. Use for browsing past tasks or when user asks to see history without specific search terms. No keywords needed — returns chronological list.\n- **session_detail**(session_id, limit, offset, include_inactive) [capability_expand category: unclassified]: Read a paginated persisted transcript for one past conversation session in the current workspace. Use after session_list or session_search returns a session_id.\n- **click**(element_index) [capability_expand category: desktop]: Click a UI element by its element_index (from the latest observe_ui snapshot)\n- **get_clipboard**() [capability_expand category: desktop]: Read current clipboard text content\n- **list_apps**(filter, running_only) [capability_expand category: desktop]: List available applications on this system. Use to discover correct bundle_id before switch_app.\n- **list_windows**() [capability_expand category: desktop]: List all top-level windows with pid, window_id, title, and per-window state (minimized, on-screen). Call this first to pick the pid and window_id that observe_ui and other window tools require.\n- **observe_ui**(pid, window_id, query) [capability_expand category: desktop]: Snapshot one window's actionable UI elements, each tagged with an element_index for click/right_click/read_text. Re-observe after actions — indices belong to one snapshot. Requires the window's pid and window_id from list_windows.\n- **open_url**(url, app_id) [capability_expand category: desktop]: Open a URL in the default or specified browser\n- **read_text**(element_index) [capability_expand category: desktop]: Read the text content of a specific UI element from the latest snapshot\n- **right_click**(element_index) [capability_expand category: desktop]: Right-click a UI element to open its context menu. Returns visible menu items.\n- **screenshot**(pid, window_id) [capability_expand category: desktop]: Capture a screenshot for visual verification. With pid + window_id captures that window (works across all displays); defaults to the last observed window, or the full desktop when no window has been observed.\n- **scroll**(element_index, direction, amount, pid, window_id) [capability_expand category: desktop]: Scroll a scrollable area of a window. Omit element_index to scroll the window's focused/page scroller; pass one to scroll an exact element from the latest snapshot.\n- **select_text**(element_index) [capability_expand category: desktop]: Select all text in a UI element (focus + select-all, for subsequent copy)\n- **set_clipboard**(text) [capability_expand category: desktop]: Write text to the clipboard\n- **shortcut**(keys) [capability_expand category: desktop]: Execute a keyboard shortcut\n- **switch_app**(app_id) [capability_expand category: desktop]: Switch to an app (launch if needed, activate, verify)\n- **type_text**(text) [capability_expand category: desktop]: Type text into the currently focused element\n- **wait**(seconds) [capability_expand category: desktop]: Wait for a specified duration before continuing\n- **wait_until**(condition, pid, window_id, timeout, poll_interval) [capability_expand category: desktop]: Wait until a UI condition is met (polls UI tree). Returns elements when found or on timeout.\n- **wait_until_stable**(timeout, poll_interval, pid, window_id) [capability_expand category: desktop]: Wait until the UI stops changing (element set stabilizes across polls).\n\n## App Connector Capability Index\nLeapFlow can onboard and manage external apps through `platform_connect` and execute exact registered business actions through `platform_action`.\nFor requests about connecting, setting up, configuring, enabling, or managing a supported app, use `platform_connect` first instead of generating SDK/Webhook sample code.\n`platform_connect.action` is the management namespace: list, guide, preflight, connect, disconnect, remove, status, events_start, events_stop, events_status.\n`platform_action.action` is only for exact registered platform business actions listed below, such as `im.send_message`; never use management actions like `list` or `guide` there.\nAll business fields MUST go inside `payload`; top-level keys are only `platform`, `action`, and `payload`.\nDo not invent platform IDs or platform action names. If the needed action is not listed, ask for discovery/clarification instead of guessing.\nUse `platform_connect` with `action='guide'` and the matching `platform` to start onboarding; use `action='list'` when the app is unclear.\nWhen a pending onboarding state is present, continue from that state with `platform_connect` instead of asking the user to restate the app.\nSupported apps:\n- `api_server`: API Server (OpenAI Compatible) (category=api; backend=adapter; platform_action actions=none registered)\n- `dingtalk`: 钉钉 (DingTalk) (category=im; backend=adapter; platform_action actions=none registered)\n- `feishu`: 飞书 (Feishu/Lark) (category=im; backend=cli)\n - `calendar.create_event` payload={summary*, start_time*, end_time*, attendees} [write/high]\n - `docs.create_markdown` payload={title*, markdown*, folder_token} [write/medium]\n - `drive.search` payload={query*, limit} [read/medium]\n - `im.add_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.download_resource` payload={message_id*, file_key*, type*} [read/low]\n - `im.get_messages` payload={message_ids*} [read/low]\n - `im.list_chats` payload={page_size} [read/low]\n - `im.list_messages` payload={chat_id*} [read/low]\n - `im.list_thread_messages` payload={thread*} [read/low]\n - `im.remove_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.reply_message` payload={message_id*, text*} [send/high]\n - `im.search_chats` payload={query*, page_size} [read/low]\n - `im.search_messages` payload={query*} [read/medium]\n - `im.send_message` payload={chat_id*, text*, thread_id} [send/high]\n - `im.update_card` payload={token*, card*} [write/medium]\n - `im.update_message` payload={message_id*, text*} [write/medium]\n - `mail.search_unread` payload={query, limit} [read/high]\n - `sheets.append_row` payload={spreadsheet_token*, sheet_id*, values*} [write/medium]\n - `task.create` payload={title*, description, due_time} [write/medium]\n- `telegram`: Telegram (category=social; backend=adapter; platform_action actions=none registered)\n- `webhook`: Webhook (Generic) (category=webhook; backend=adapter; platform_action actions=none registered)\n\n## Tool Usage\nTools are normally invoked through the native function-calling mechanism, not by writing JSON in your reply\ntext. Only if the provider signals that native function calling is unavailable for this turn, fall back to a\nsingle JSON code block: `{\"name\": \"tool_name\", \"arguments\": {\"key\": \"value\"}}` — use this fallback format\nonly, never both. Only call a tool whose exact name appears in the tool index above and is currently callable\n(or reachable via `capability_expand`). Never invent, rename, alias, or guess a tool name, platform ID, or\nplatform action from argument shape or wording — if the index does not list it, it does not exist.\n`platform_connect.action` (list/guide/preflight/connect/disconnect/remove/status/events_start/events_stop/\nevents_status) is the App Connector management namespace; `platform_action.action` only accepts exact\nregistered business actions such as `im.send_message` shown in the App Connector Capability Index — never mix\nthe two namespaces. If a tool call returns an unknown/unavailable result, use the returned suggestions or\navailable names for a single retry instead of trying further variations of the same guess.\n\n**Side-effect action rule** (`platform_action` with effect=send/write/execute):\n- Call each unique action+payload **exactly once**. Never include duplicates in the same turn.\n- Once the result returns `\"completed\": true`, that action is DONE for this task. Do NOT call it again\n in any subsequent turn — immediately summarize the result for the user instead.\n- The system enforces idempotency: duplicate calls are blocked and will not execute.\n- If the user explicitly requests sending/writing multiple times, use distinct payloads per call.\n\n**Resource identifier provenance rule**:\n- NEVER fabricate, guess, or infer resource identifiers (chat_id, message_id, file_key, user_id, etc.).\n Every resource ID used in a side-effect action MUST come from a successful API response in this session.\n- If a read/list action fails (e.g. authorization error), you do NOT have valid resource IDs.\n Report the failure to the user — do NOT attempt the dependent write/send action with a guessed ID.\n- When a tool result contains `\"llm_instruction\"`, follow it exactly.\n\n## Guidelines\n1. **Direct answers first**: If you already know the answer, respond directly without tools.\n2. **Avoid redundant tool calls**: Do not call the same tool with the same arguments more than once in the same user turn. When an existing tool result already answers the user's request, stop calling tools and answer directly.\n3. **Use tools proactively**: When the user asks about files, time, system state, or needs actions performed, use the appropriate tool.\n4. **Chain tools when needed**: You can call multiple tools in sequence (e.g., list files → read file → summarize).\n5. **Handle failures gracefully**: If a tool fails, explain what went wrong and suggest alternatives. If it failed because of an unknown tool/platform/action name, retry once with an exact name from the returned suggestions, then explain rather than keep guessing.\n6. **Summarize results naturally**: After tool execution, synthesize the results into a helpful answer rather than dumping raw output.\n7. **Stay conversational**: Maintain a natural, helpful tone. Acknowledge context from earlier in the conversation.\n8. **Recall past work**:\n - Broad queries (\"之前做了什么\", \"列出任务\"): answer from the \"Recent Task History\" section already in your context. If insufficient, call session_list.\n - Specific lookups (\"上次那个配置怎么改的\"): call session_search with relevant phrases (NOT single characters).\n - Do NOT call search tools repeatedly with keyword fragments. One well-phrased call is better than ten fragmented ones.\n\n## Coding & Verification\nWhen working with code, prefer the precise built-in tools over ad-hoc shell: use `repo_map` to orient in an\nunfamiliar project, `code_search`/`file_find` to locate and `code_intel` for symbols, `edit_file` (anchored\nsearch-replace, or a unified `diff`) to change files — never rewrite a whole file to change a few lines — and\n`git_query` to inspect diffs/log. After edits, check `syntax_ok` in the result and run `test_run`/`lint_check`\nbefore declaring the work done. Batch independent read-only calls (search/read/query) into a single turn — they\nrun in parallel.\n\n## Presentation Style\n1. **Polished Markdown only**: Format user-facing answers with clean Markdown headings, short paragraphs, and concise bullets. Use tables only when they improve comparison or scanning.\n2. **Terminal-friendly layout**: Keep lines readable in a TUI; avoid dense walls of text, deeply nested lists, oversized ASCII art, or heavy visual blocks.\n3. **Elegant emphasis**: Use bold text sparingly for key terms and conclusions. Avoid excessive emojis, decorative symbols, repeated separators, or visual noise.\n4. **Theme-safe colors**: Do not emit ANSI escape codes, HTML color tags, Rich markup, or hardcoded color names. Rely on the TUI theme to render Markdown professionally.\n5. **No leaked tool protocol**: Never show tool-call JSON, internal schemas, raw observations, tool result payloads, or hidden reasoning in the final answer unless the user explicitly asks for raw/debug output. Treat any prior `{\"name\": ..., \"arguments\": ...}` blocks and `Tool result (...)` messages as internal execution context only.\n6. **Professional closure**: End with a concise conclusion or next step when helpful; avoid rambling after the useful answer is complete.\n\nWhen finished with all tool calls, respond normally without a JSON block, tool-call transcript, or process log.\n\n## Task Contract\n- Task ID: turn-4\n- Original user request: Remove the adaptive JSON pretty plugin completely.\n- Workspace root: \n- Allowed roots: \n- Treat relative project paths as relative to the workspace root; never infer `.` as the project root when a workspace root is provided.\n- Workspace boundary is enforced by tools: do not read, search, edit, or run commands against paths outside the allowed roots unless the user explicitly requests an external path and the tool/approval policy permits it.\n- LeapFlow workspace config is optional at `/.leapflow/config.yaml`; runtime config is loaded from `~/.leapflow/config/user.yaml` and `~/.leapflow/profiles//config/*.yaml`.\n- Preserve this task contract across summarization, compression, tool loops, and memory retrieval.\n" + }, + { + "role": "system", + "content": "\n## Recent Session Summary\n- [assistant] Formatted JSON with the new plugin.\n- [user] Remove the adaptive JSON pretty plugin completely.\n" + }, + { + "role": "assistant", + "content": "Formatted JSON with the new plugin." + }, + { + "role": "user", + "content": "Remove the adaptive JSON pretty plugin completely.\nRemove the adaptive JSON pretty plugin completely." + } + ], + "tools": [ + "capability_expand", + "code_intel", + "code_search", + "config_get", + "config_list", + "env_info", + "file_find", + "file_list", + "file_read", + "git_query", + "lint_check", + "memory_search", + "plugin_list", + "plugin_propose", + "plugin_status", + "plugin_versions", + "repo_map", + "research_note", + "runtime_snapshot", + "schedule_reentry", + "self_describe", + "session_list", + "skill_view", + "skills_list", + "terminal_list", + "terminal_read", + "test_run", + "text_search", + "time_get", + "tool_describe", + "tool_search", + "web_fetch" + ] + }, + "responses": [ + { + "status": 200, + "content_type": "application/json", + "body": "{\"id\": \"chatcmpl-cassette\", \"object\": \"chat.completion\", \"model\": \"cassette-model\", \"choices\": [{\"index\": 0, \"message\": {\"role\": \"assistant\", \"content\": \"\", \"tool_calls\": [{\"id\": \"call_1\", \"type\": \"function\", \"function\": {\"name\": \"plugin_remove\", \"arguments\": \"{\\\"plugin_id\\\": \\\"json_pretty_loop_e2e\\\", \\\"delete_source\\\": true}\"}}]}, \"finish_reason\": \"tool_calls\"}], \"usage\": {\"prompt_tokens\": 64, \"completion_tokens\": 16, \"total_tokens\": 80}}" + } + ] +} diff --git a/tests/_fixtures/cassettes/r7_adaptive_plugin_loop/cassette-model-1f2daf894a253f48.cassette.json b/tests/_fixtures/cassettes/r7_adaptive_plugin_loop/cassette-model-1f2daf894a253f48.cassette.json new file mode 100644 index 00000000..6f83ab67 --- /dev/null +++ b/tests/_fixtures/cassettes/r7_adaptive_plugin_loop/cassette-model-1f2daf894a253f48.cassette.json @@ -0,0 +1,68 @@ +{ + "fingerprint": "1f2daf894a253f48e11ee9e45693aa98cd8a2fbeedb5c85ac5be8ecc4046a6e8", + "note": "captured in seed mode", + "request": { + "model": "cassette-model", + "stream": false, + "messages": [ + { + "role": "system", + "content": "You are LeapFlow, an intelligent assistant that can both converse naturally and take real actions on the user's computer.\n\n## Capabilities\nThe tool index below lists **every** registered tool by name and a one-line summary — this is the complete\ncapability contract; nothing else exists. Only a subset is directly callable this turn (via native tool calling,\nnot a JSON block in your reply). If you need a tool from the index that is not yet callable, call\n`capability_expand` with its category name first — the matching tools become callable immediately after.\nWhen the user asks about LeapFlow **itself** — its identity/version/model, what tools, plugins, or capabilities\nit has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of\nself-awareness — answer from live runtime evidence via `self_describe` (facet=capabilities for \"what can you\ndo / do you support X\", identity for version/model, runtime for context/posture, or `all` for a full self-check),\nnever by reading LeapFlow's own source code, documentation, or memory. Report configuration-dependent or\nunavailable capabilities as limitations rather than inferring them.\n- **text_search**(text, pattern): Search for a regex pattern in text.\n- **text_replace**(text, old, new, count) [capability_expand category: write]: Replace occurrences of a substring in text.\n- **time_get**(): Get current date and time.\n- **env_info**(): Get system environment information (OS, Python version, cwd).\n- **skills_list**(query, category, source): List available learned skills. Use when user asks about capabilities or you need a specific skill.\n- **skill_view**(name): View the full content of a specific skill document.\n- **skill_list_files**(name) [capability_expand category: skills]: List all support files in a skill directory (configs, templates, examples). Use before skill_file_read to discover available files.\n- **skill_file_read**(name, file_path) [capability_expand category: skills]: Read an individual support file from a skill directory. Use skill_list_files first to discover available files.\n- **code_intel**(path, operation): Precise document symbols (outline) for a source file: classes, functions, and methods with line ranges. Python uses an exact AST parse; other languages use a keyword-prefix scan. Prefer over file_read mode=symbols for accurate navigation before editing. Read-only.\n- **repo_map**(path): Compact project orientation for a repository root: languages, detected test/lint commands, top-level structure, entry points, manifest, and VCS branch. Call this first when entering an unfamiliar codebase. Read-only.\n- **scm_sync**(action, cwd, remote, pull_ref, push_ref, timeout) [capability_expand category: scm]: Run a typed git SCM action. Use this instead of shell_run for git pull/push/status. For 'pull origin main then push', set action='pull_then_push', remote='origin', pull_ref='main', and omit push_ref so LeapFlow pushes the current local branch.\n- **git_query**(action, cwd, ref, path, staged, max_count, stat): Read-only structured git inspection: action=diff|log|status|branch|show. Prefer over shell_run for reading repo state — output is clipped, redacted, and log/branch are parsed into structured fields. Use scm_sync for pull/push.\n- **git_write**(action, cwd, message, stage_all, name, ref, create) [capability_expand category: scm]: Mutating git actions: action=commit (message, stage_all), branch (create+switch), checkout (switch; create=true for -b). Approval-gated. Use scm_sync for pull/push and git_query for reads.\n- **test_run**(command, cwd, timeout): Run the project's test suite and return structured results (framework, passed/failed counts, failing tests). Auto-detects the runner (pytest/npm/go/cargo) or uses a configured/explicit command; executes via the governed shell. ok=true means the runner executed — see 'success' for pass/fail.\n- **lint_check**(command, cwd, timeout): Run the project's linter and return a structured clean/issue result. Auto-detects the linter (ruff/eslint/go vet/clippy) or uses a configured/explicit command; executes via the governed shell. ok=true means the linter ran — see 'clean'.\n- **file_list**(path, pattern, depth): List files and directories at a given path. Use depth=1 or depth=2 to get a recursive tree in one call instead of listing each sub-directory separately.\n- **file_read**(path, max_lines, start_line, max_chars, mode): Read text file content with adaptive context governance. For large or unfamiliar files, prefer mode='outline' or mode='symbols' first, then use mode='raw' with start_line/max_lines for the specific range you actually need. For LeapFlow's own settings, use config_list / config_get / config_set — its config files are outside the workspace and not readable here.\n- **file_write**(path, content, mode) [capability_expand category: write]: Write content to a file (overwrite or append).\n- **code_search**(pattern, patterns, path, glob, ignore_case, multiline, max_results, context_lines): Search file CONTENTS by regex pattern across a directory tree (ripgrep-backed). Requires a regex pattern. NOT for listing or browsing directory contents — use file_list for that. Prefer this over shell_run grep: faster, skips VCS/dependency/build dirs, and returns structured path:line:column matches. Batch related lookups into ONE call via `patterns` (OR-combined, single pass) instead of issuing several separate searches. Use file_read for the surrounding context of a hit.\n- **file_find**(glob, path, max_results): Find files by a recursive glob pattern under a base path (e.g. '**/test_*.py' or '*.md'). Prefer this over shell_run find; skips VCS/dependency/build dirs.\n- **edit_file**(path, edits, dry_run, diff) [capability_expand category: write]: Apply targeted, anchored search-replace edits to an EXISTING text file (use file_write to create/overwrite). Each edit is {original_text, new_text, replace_all?}; original_text must match exactly and uniquely (or set replace_all) — a non-unique or missing anchor is rejected so files are never corrupted. Set dry_run to preview. Alternatively pass a unified 'diff' to apply its hunks as anchored edits. Far cheaper and safer than rewriting a whole file.\n- **shell_run**(command, cwd, timeout) [capability_expand category: shell]: Execute a one-shot shell command with timeout protection. Runs in the active workspace; paths resolving outside it are refused. Reach for a structured tool first when one fits — web_fetch for anything over HTTP(S), git_query/scm_sync for git, code_search/file_find/file_read for the repo, config_get/config_set for LeapFlow's own settings — because those report typed results, while a failed shell command can only be diagnosed from its exit code and stderr. Every shell run counts as an external side effect, so a failure stops the rest of the batch and is not retried automatically.\n- **terminal_open**(command, cwd, shell) [capability_expand category: terminal]: Open a PERSISTENT shell session (REPL/dev server/watch), returning a session_id for terminal_send/read/close. Disabled unless tools.terminal_session_enabled is set. For one-shot commands use shell_run instead.\n- **terminal_send**(session_id, input, wait) [capability_expand category: terminal]: Send a line of input to a persistent terminal session and return output captured shortly after.\n- **terminal_read**(session_id, wait): Drain buffered output from a persistent terminal session (optionally waiting briefly first).\n- **terminal_close**(session_id) [capability_expand category: terminal]: Terminate a persistent terminal session and release its process group.\n- **terminal_list**(): List active persistent terminal sessions.\n- **config_list**(category, limit): List LeapFlow's own writable settings (LLM model and endpoint, daemon, memory, perception, gateway, …) with current values. Use this to discover the exact key before changing anything. Optionally narrow by `category`. This is the only correct way to inspect LeapFlow configuration — never read config files from disk.\n- **config_get**(key): Read one LeapFlow setting by key (e.g. 'llm.model', 'llm.base_url', 'daemon.log_level'), returning its current value, type, scopes, and whether a change needs a daemon restart. There is no 'llm.provider' key: provider behavior is inferred from the OpenAI-compatible 'llm.base_url'. Never read LeapFlow config files from disk — use this.\n- **config_set**(key, value, scope) [capability_expand category: config]: Change one LeapFlow setting by key, e.g. switch an OpenAI-compatible LLM with key='llm.model' and key='llm.base_url'. There is no 'llm.provider' key: provider behavior is inferred from the endpoint. Values are validated and coerced; credentials are stored in the vault automatically. Call config_list or config_get first if unsure of the key. The result states whether a `leap daemon restart` is required. Never edit LeapFlow config files directly.\n- **web_fetch**(url, select, timeout, max_bytes): Read a URL over HTTP(S) and get back extracted, context-sized content: parsed JSON for API endpoints, readable text plus links for web pages. Use this for anything on the internet — prices, docs, releases, articles — instead of running curl through shell_run: it reports real HTTP status codes, retries rate limits on its own, and is a plain read so a retry is always safe. For JSON APIs pass `select` with a dotted path (e.g. 'chart.result.0.meta') to return just that part instead of the whole payload.\n- **memory_search**(query, limit): Search agent memory for relevant past experiences, observations, and facts.\n- **memory_add**(content, kind) [capability_expand category: write]: Store a new observation or insight in memory for future reference.\n- **research_note**(kind, text): Record a compact, structured note about the current task's state so it survives context compression on long / multi-step tasks. Use for durable findings, open questions still to resolve, decisions / excluded paths, and the immediate next step. One concise sentence per note.\n- **capability_expand**(category): Fetch the full callable schema for every tool in a capability category (e.g. 'hub', 'gateway', 'desktop', 'delegate', 'file', 'memory', 'skill'). The compact tool index always lists every registered tool by name and a one-line summary, but only a static low-risk subset is directly callable each turn. If you need a tool from the index that is not yet callable, call capability_expand with its category first; the matching tools become callable in this turn. Never invent a tool name — expand the category instead.\n- **delegate_task**(goal, context) [capability_expand category: delegate]: Delegate a complex sub-task to an isolated subagent. The subagent gets a fresh context and restricted tool access. Use when a task is self-contained and can be solved independently.\n- **schedule_reentry**(kind, reason, delay_seconds, event_match, max_reentries, deadline_seconds): Register a re-entry so this task can resume later from its current orientation (findings / open questions / next step). Use when work must pause and continue after a delay (kind=time) or when a matching platform event arrives (kind=event), instead of finishing now. The research-ledger state is carried over automatically.\n- **hub_push**(skill_name, visibility, version) [capability_expand category: hub]: Push a local skill to the ModelScope Hub for sharing or backup.\n- **hub_pull**(repo_id, version) [capability_expand category: hub]: Pull a skill from the ModelScope Hub to install locally.\n- **hub_search**(query) [capability_expand category: hub]: Search for skills on the Hub by keyword or description.\n- **hub_sync**(mode, dry_run) [capability_expand category: hub]: Preview or execute sync between local skills and Hub.\n- **platform_action**(platform, action, payload, backend_kind) [capability_expand category: gateway]: Execute an exact registered business action on an external platform through LeapFlow's App Connector layer. Actions must be copied from the App Connector Capability Index and are addressed as domain.operation, e.g. im.send_message or docs.create_markdown. All business fields (chat_id, text, query, etc.) MUST be placed inside `payload`, never at the top level. Example: {\"platform\":\"feishu\",\"action\":\"im.send_message\",\"payload\":{\"chat_id\":\"oc_xxx\",\"text\":\"hello\"}}. Do not invent action names, do not use management actions such as list/guide/connect/status here.\n- **platform_connect**(action, platform, credentials, options, checkpoint) [capability_expand category: gateway]: List, guide, connect, disconnect, remove, or check status for external platforms using the App Connector management namespace. Supports REST and CLI backends. Use this for management actions such as list/guide/preflight/connect/status; use platform_action only for exact registered business actions.\n- **gateway_send**(platform, chat_id, text, thread_id) [capability_expand category: gateway]: Send a message to a connected external platform (Feishu group, Telegram chat, DingTalk conversation, etc.). Requires the platform to be connected via gateway_connect first. Use gateway_connect with action='list' to see connected platforms and available chat IDs.\n- **gateway_connect**(action, platform, credentials, options) [capability_expand category: gateway]: Connect, configure, or manage external platform integrations (Feishu, DingTalk, Telegram, Slack, Discord, etc.). Conversational flow: 1) call 'guide' to get setup steps + required fields, 2) present the steps to the user and ask for ALL required credentials in a single message, 3) call 'connect' with the credentials. Goal: complete in 1–2 user turns. NEVER include credential values in your text response.\n- **plugin_list**(): List the live plugin registry across Tool/Gateway/LLM subsystems for plugin management: which plugins are loaded, their category and fiber lifecycle state. For questions about LeapFlow's own capabilities or whether it supports X, use self_describe instead.\n- **plugin_status**(plugin_id): Get detailed status of a specific plugin: its declared category, runtime dependencies, contributed tools, and fiber lifecycle state.\n- **plugin_versions**(plugin_id): List recorded source versions and active pointer for a profile-scoped plugin.\n- **plugin_propose**(requested_capability, plugin_id, proposed_tools, test_cases, risk_level, evidence): Create a side-effect-free PluginProposal from explicit capability-gap evidence. Use this before plugin_generate when a missing capability should be reviewed. Does not call an LLM, write files, or install anything.\n- **assess_compatibility**(manifest, source_path) [capability_expand category: plugin_management]: Assess whether a foreign plugin manifest or real DSH source bundle is compatible with LeapFlow. A source bundle assessment is static: runtime_ready stays false until restricted Node discovery during plugin_install. Returns component-level verdicts and limitations.\n- **plugin_generate**(plugin_id, description, proposal_id) [capability_expand category: system]: Generate a new ToolPlugin from a natural-language capability description. The LLM produces code that conforms to the ToolPlugin Protocol; it is then rigorously validated (syntax, structure, import, protocol conformance). The generated source is stored in CAS and receives explicit content approval. It DOES NOT install the plugin — installation requires a second, mutation-specific approval via plugin_install.\n- **plugin_install**(plugin_id, code, marketplace_name, source_path, proposal_id, version_label) [capability_expand category: system]: Install a plugin from exactly one source: validated Python code, the configured Python marketplace, or a real DSH source bundle. DSH bundles run restricted Node discovery before registration; only public host tools are installed and client UI limitations are reported. Writes profile-scoped state and mutates the process-global registry. REQUIRES APPROVAL.\n- **plugin_rollback**(plugin_id, version) [capability_expand category: system]: Rollback a profile-scoped plugin to a recorded source version and reload it. REQUIRES APPROVAL.\n- **plugin_reload**(plugin_id, version_label) [capability_expand category: system]: Hot-reload a plugin at runtime. Disposes the old plugin fiber, re-imports its module, and registers a fresh instance. Existing in-flight turns are unaffected (snapshot isolation). REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_disable**(plugin_id) [capability_expand category: system]: Disable a plugin by disposing its fiber, removing its tools from the runtime registry. Cannot disable self_management itself. REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_remove**(plugin_id, delete_source) [capability_expand category: system]: Terminally remove a plugin: dispose its fiber, unregister its tools, remove reload metadata, and optionally delete its profile-scoped source file. Cannot remove self_management itself. REQUIRES APPROVAL.\n- **plugin_unquarantine**(plugin_id) [capability_expand category: plugin_management]: Restore a quarantined plugin to probation status for re-evaluation. Unfreezes the trust ledger, transitions the proposal back to PROBATION, and reloads the plugin. REQUIRES APPROVAL.\n- **plugin_enable**(plugin_id) [capability_expand category: system]: Re-enable a previously disabled plugin by reloading its module and registering a fresh instance. REQUIRES APPROVAL.\n- **self_describe**(facet): Answer questions about LeapFlow ITSELF from live runtime evidence — its identity/version/model, what tools and plugins it has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of self-awareness. Prefer this over reading LeapFlow's own source code. Pick facet=capabilities for 'what can you do / do you support X', identity for version/model, runtime for context/posture, or all for a full self-check when explicitly requested.\n- **runtime_snapshot**(): Lightweight (~150 token) flat snapshot of current runtime state: model, context budget, posture, disclosure level, turn count, cache hit rate, uptime, tool count, and pending approvals.\n- **schedule_create**(trigger_expression, instruction, execution_mode, max_retries, delivery_target) [capability_expand category: scheduler]: Create a new scheduled task. Specify a trigger expression (e.g. '30m', 'every 2h', '0 9 * * *') and the instruction to execute on each trigger. Optionally provide an execution mode and a delivery target to receive notifications.\n- **schedule_list**() [capability_expand category: scheduler]: List all scheduled tasks with their current state, trigger, and next due time.\n- **schedule_status**(task_id) [capability_expand category: scheduler]: Get detailed status and recent execution history for a scheduled task.\n- **schedule_pause**(task_id) [capability_expand category: scheduler]: Pause a scheduled task so it stops firing without being cancelled.\n- **schedule_resume**(task_id) [capability_expand category: scheduler]: Resume a paused scheduled task — re-arms it and recalculates next due time.\n- **schedule_cancel**(task_id) [capability_expand category: scheduler]: Cancel a scheduled task permanently.\n- **tool_search**(query, max_results): Search all registered tools by keyword relevance. Returns a ranked list of matching tools with name, category, and summary. Use this when you need a tool but are unsure of its exact name or category.\n- **tool_describe**(tool_name): Get the full callable schema of a single tool by exact name. Returns the tool's description, parameters, and metadata. Use after tool_search to inspect a specific tool before calling it.\n- **session_search**(query, limit) [capability_expand category: unclassified]: Search past conversation sessions in the current workspace for relevant context.\n- **session_list**(limit): List recent conversation sessions in the current workspace with stable ids, titles, dates, and summaries. Use for browsing past tasks or when user asks to see history without specific search terms. No keywords needed — returns chronological list.\n- **session_detail**(session_id, limit, offset, include_inactive) [capability_expand category: unclassified]: Read a paginated persisted transcript for one past conversation session in the current workspace. Use after session_list or session_search returns a session_id.\n- **json_pretty_loop_e2e**(text): Pretty-print JSON for the adaptive closed-loop journey.\n- **click**(element_index) [capability_expand category: desktop]: Click a UI element by its element_index (from the latest observe_ui snapshot)\n- **get_clipboard**() [capability_expand category: desktop]: Read current clipboard text content\n- **list_apps**(filter, running_only) [capability_expand category: desktop]: List available applications on this system. Use to discover correct bundle_id before switch_app.\n- **list_windows**() [capability_expand category: desktop]: List all top-level windows with pid, window_id, title, and per-window state (minimized, on-screen). Call this first to pick the pid and window_id that observe_ui and other window tools require.\n- **observe_ui**(pid, window_id, query) [capability_expand category: desktop]: Snapshot one window's actionable UI elements, each tagged with an element_index for click/right_click/read_text. Re-observe after actions — indices belong to one snapshot. Requires the window's pid and window_id from list_windows.\n- **open_url**(url, app_id) [capability_expand category: desktop]: Open a URL in the default or specified browser\n- **read_text**(element_index) [capability_expand category: desktop]: Read the text content of a specific UI element from the latest snapshot\n- **right_click**(element_index) [capability_expand category: desktop]: Right-click a UI element to open its context menu. Returns visible menu items.\n- **screenshot**(pid, window_id) [capability_expand category: desktop]: Capture a screenshot for visual verification. With pid + window_id captures that window (works across all displays); defaults to the last observed window, or the full desktop when no window has been observed.\n- **scroll**(element_index, direction, amount, pid, window_id) [capability_expand category: desktop]: Scroll a scrollable area of a window. Omit element_index to scroll the window's focused/page scroller; pass one to scroll an exact element from the latest snapshot.\n- **select_text**(element_index) [capability_expand category: desktop]: Select all text in a UI element (focus + select-all, for subsequent copy)\n- **set_clipboard**(text) [capability_expand category: desktop]: Write text to the clipboard\n- **shortcut**(keys) [capability_expand category: desktop]: Execute a keyboard shortcut\n- **switch_app**(app_id) [capability_expand category: desktop]: Switch to an app (launch if needed, activate, verify)\n- **type_text**(text) [capability_expand category: desktop]: Type text into the currently focused element\n- **wait**(seconds) [capability_expand category: desktop]: Wait for a specified duration before continuing\n- **wait_until**(condition, pid, window_id, timeout, poll_interval) [capability_expand category: desktop]: Wait until a UI condition is met (polls UI tree). Returns elements when found or on timeout.\n- **wait_until_stable**(timeout, poll_interval, pid, window_id) [capability_expand category: desktop]: Wait until the UI stops changing (element set stabilizes across polls).\n\n## App Connector Capability Index\nLeapFlow can onboard and manage external apps through `platform_connect` and execute exact registered business actions through `platform_action`.\nFor requests about connecting, setting up, configuring, enabling, or managing a supported app, use `platform_connect` first instead of generating SDK/Webhook sample code.\n`platform_connect.action` is the management namespace: list, guide, preflight, connect, disconnect, remove, status, events_start, events_stop, events_status.\n`platform_action.action` is only for exact registered platform business actions listed below, such as `im.send_message`; never use management actions like `list` or `guide` there.\nAll business fields MUST go inside `payload`; top-level keys are only `platform`, `action`, and `payload`.\nDo not invent platform IDs or platform action names. If the needed action is not listed, ask for discovery/clarification instead of guessing.\nUse `platform_connect` with `action='guide'` and the matching `platform` to start onboarding; use `action='list'` when the app is unclear.\nWhen a pending onboarding state is present, continue from that state with `platform_connect` instead of asking the user to restate the app.\nSupported apps:\n- `api_server`: API Server (OpenAI Compatible) (category=api; backend=adapter; platform_action actions=none registered)\n- `dingtalk`: 钉钉 (DingTalk) (category=im; backend=adapter; platform_action actions=none registered)\n- `feishu`: 飞书 (Feishu/Lark) (category=im; backend=cli)\n - `calendar.create_event` payload={summary*, start_time*, end_time*, attendees} [write/high]\n - `docs.create_markdown` payload={title*, markdown*, folder_token} [write/medium]\n - `drive.search` payload={query*, limit} [read/medium]\n - `im.add_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.download_resource` payload={message_id*, file_key*, type*} [read/low]\n - `im.get_messages` payload={message_ids*} [read/low]\n - `im.list_chats` payload={page_size} [read/low]\n - `im.list_messages` payload={chat_id*} [read/low]\n - `im.list_thread_messages` payload={thread*} [read/low]\n - `im.remove_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.reply_message` payload={message_id*, text*} [send/high]\n - `im.search_chats` payload={query*, page_size} [read/low]\n - `im.search_messages` payload={query*} [read/medium]\n - `im.send_message` payload={chat_id*, text*, thread_id} [send/high]\n - `im.update_card` payload={token*, card*} [write/medium]\n - `im.update_message` payload={message_id*, text*} [write/medium]\n - `mail.search_unread` payload={query, limit} [read/high]\n - `sheets.append_row` payload={spreadsheet_token*, sheet_id*, values*} [write/medium]\n - `task.create` payload={title*, description, due_time} [write/medium]\n- `telegram`: Telegram (category=social; backend=adapter; platform_action actions=none registered)\n- `webhook`: Webhook (Generic) (category=webhook; backend=adapter; platform_action actions=none registered)\n\n## Tool Usage\nTools are normally invoked through the native function-calling mechanism, not by writing JSON in your reply\ntext. Only if the provider signals that native function calling is unavailable for this turn, fall back to a\nsingle JSON code block: `{\"name\": \"tool_name\", \"arguments\": {\"key\": \"value\"}}` — use this fallback format\nonly, never both. Only call a tool whose exact name appears in the tool index above and is currently callable\n(or reachable via `capability_expand`). Never invent, rename, alias, or guess a tool name, platform ID, or\nplatform action from argument shape or wording — if the index does not list it, it does not exist.\n`platform_connect.action` (list/guide/preflight/connect/disconnect/remove/status/events_start/events_stop/\nevents_status) is the App Connector management namespace; `platform_action.action` only accepts exact\nregistered business actions such as `im.send_message` shown in the App Connector Capability Index — never mix\nthe two namespaces. If a tool call returns an unknown/unavailable result, use the returned suggestions or\navailable names for a single retry instead of trying further variations of the same guess.\n\n**Side-effect action rule** (`platform_action` with effect=send/write/execute):\n- Call each unique action+payload **exactly once**. Never include duplicates in the same turn.\n- Once the result returns `\"completed\": true`, that action is DONE for this task. Do NOT call it again\n in any subsequent turn — immediately summarize the result for the user instead.\n- The system enforces idempotency: duplicate calls are blocked and will not execute.\n- If the user explicitly requests sending/writing multiple times, use distinct payloads per call.\n\n**Resource identifier provenance rule**:\n- NEVER fabricate, guess, or infer resource identifiers (chat_id, message_id, file_key, user_id, etc.).\n Every resource ID used in a side-effect action MUST come from a successful API response in this session.\n- If a read/list action fails (e.g. authorization error), you do NOT have valid resource IDs.\n Report the failure to the user — do NOT attempt the dependent write/send action with a guessed ID.\n- When a tool result contains `\"llm_instruction\"`, follow it exactly.\n\n## Guidelines\n1. **Direct answers first**: If you already know the answer, respond directly without tools.\n2. **Avoid redundant tool calls**: Do not call the same tool with the same arguments more than once in the same user turn. When an existing tool result already answers the user's request, stop calling tools and answer directly.\n3. **Use tools proactively**: When the user asks about files, time, system state, or needs actions performed, use the appropriate tool.\n4. **Chain tools when needed**: You can call multiple tools in sequence (e.g., list files → read file → summarize).\n5. **Handle failures gracefully**: If a tool fails, explain what went wrong and suggest alternatives. If it failed because of an unknown tool/platform/action name, retry once with an exact name from the returned suggestions, then explain rather than keep guessing.\n6. **Summarize results naturally**: After tool execution, synthesize the results into a helpful answer rather than dumping raw output.\n7. **Stay conversational**: Maintain a natural, helpful tone. Acknowledge context from earlier in the conversation.\n8. **Recall past work**:\n - Broad queries (\"之前做了什么\", \"列出任务\"): answer from the \"Recent Task History\" section already in your context. If insufficient, call session_list.\n - Specific lookups (\"上次那个配置怎么改的\"): call session_search with relevant phrases (NOT single characters).\n - Do NOT call search tools repeatedly with keyword fragments. One well-phrased call is better than ten fragmented ones.\n\n## Coding & Verification\nWhen working with code, prefer the precise built-in tools over ad-hoc shell: use `repo_map` to orient in an\nunfamiliar project, `code_search`/`file_find` to locate and `code_intel` for symbols, `edit_file` (anchored\nsearch-replace, or a unified `diff`) to change files — never rewrite a whole file to change a few lines — and\n`git_query` to inspect diffs/log. After edits, check `syntax_ok` in the result and run `test_run`/`lint_check`\nbefore declaring the work done. Batch independent read-only calls (search/read/query) into a single turn — they\nrun in parallel.\n\n## Presentation Style\n1. **Polished Markdown only**: Format user-facing answers with clean Markdown headings, short paragraphs, and concise bullets. Use tables only when they improve comparison or scanning.\n2. **Terminal-friendly layout**: Keep lines readable in a TUI; avoid dense walls of text, deeply nested lists, oversized ASCII art, or heavy visual blocks.\n3. **Elegant emphasis**: Use bold text sparingly for key terms and conclusions. Avoid excessive emojis, decorative symbols, repeated separators, or visual noise.\n4. **Theme-safe colors**: Do not emit ANSI escape codes, HTML color tags, Rich markup, or hardcoded color names. Rely on the TUI theme to render Markdown professionally.\n5. **No leaked tool protocol**: Never show tool-call JSON, internal schemas, raw observations, tool result payloads, or hidden reasoning in the final answer unless the user explicitly asks for raw/debug output. Treat any prior `{\"name\": ..., \"arguments\": ...}` blocks and `Tool result (...)` messages as internal execution context only.\n6. **Professional closure**: End with a concise conclusion or next step when helpful; avoid rambling after the useful answer is complete.\n\nWhen finished with all tool calls, respond normally without a JSON block, tool-call transcript, or process log.\n\n## Task Contract\n- Task ID: turn-3\n- Original user request: Use the json_pretty_loop_e2e tool to format the sample JSON.\n- Workspace root: \n- Allowed roots: \n- Treat relative project paths as relative to the workspace root; never infer `.` as the project root when a workspace root is provided.\n- Workspace boundary is enforced by tools: do not read, search, edit, or run commands against paths outside the allowed roots unless the user explicitly requests an external path and the tool/approval policy permits it.\n- LeapFlow workspace config is optional at `/.leapflow/config.yaml`; runtime config is loaded from `~/.leapflow/config/user.yaml` and `~/.leapflow/profiles//config/*.yaml`.\n- Preserve this task contract across summarization, compression, tool loops, and memory retrieval.\n" + }, + { + "role": "system", + "content": "\n## Recent Session Summary\n- [assistant] Installed json pretty plugin.\n- [user] Use the json_pretty_loop_e2e tool to format the sample JSON.\n" + }, + { + "role": "assistant", + "content": "Installed json pretty plugin." + }, + { + "role": "user", + "content": "Use the json_pretty_loop_e2e tool to format the sample JSON.\nUse the json_pretty_loop_e2e tool to format the sample JSON." + } + ], + "tools": [ + "capability_expand", + "code_intel", + "code_search", + "config_get", + "config_list", + "env_info", + "file_find", + "file_list", + "file_read", + "git_query", + "json_pretty_loop_e2e", + "lint_check", + "memory_search", + "plugin_list", + "plugin_propose", + "plugin_status", + "plugin_versions", + "repo_map", + "research_note", + "runtime_snapshot", + "schedule_reentry", + "self_describe", + "session_list", + "skill_view", + "skills_list", + "terminal_list", + "terminal_read", + "test_run", + "text_search", + "time_get", + "tool_describe", + "tool_search", + "web_fetch" + ] + }, + "responses": [ + { + "status": 200, + "content_type": "application/json", + "body": "{\"id\": \"chatcmpl-cassette\", \"object\": \"chat.completion\", \"model\": \"cassette-model\", \"choices\": [{\"index\": 0, \"message\": {\"role\": \"assistant\", \"content\": \"\", \"tool_calls\": [{\"id\": \"call_1\", \"type\": \"function\", \"function\": {\"name\": \"json_pretty_loop_e2e\", \"arguments\": \"{\\\"text\\\": \\\"{\\\\\\\"b\\\\\\\":2,\\\\\\\"a\\\\\\\":1}\\\"}\"}}]}, \"finish_reason\": \"tool_calls\"}], \"usage\": {\"prompt_tokens\": 64, \"completion_tokens\": 16, \"total_tokens\": 80}}" + } + ] +} diff --git a/tests/_fixtures/cassettes/r7_adaptive_plugin_loop/cassette-model-3a06db86fba7f34f.cassette.json b/tests/_fixtures/cassettes/r7_adaptive_plugin_loop/cassette-model-3a06db86fba7f34f.cassette.json deleted file mode 100644 index 5f37db6a..00000000 --- a/tests/_fixtures/cassettes/r7_adaptive_plugin_loop/cassette-model-3a06db86fba7f34f.cassette.json +++ /dev/null @@ -1,79 +0,0 @@ -{ - "fingerprint": "3a06db86fba7f34f75ec78ac7a1295e016a9a7e2043fea9613f5b508299eaebc", - "note": "captured in seed mode", - "request": { - "model": "cassette-model", - "stream": false, - "messages": [ - { - "role": "system", - "content": "You are LeapFlow, an intelligent assistant that can both converse naturally and take real actions on the user's computer.\n\n## Capabilities\nThe tool index below lists **every** registered tool by name and a one-line summary — this is the complete\ncapability contract; nothing else exists. Only a subset is directly callable this turn (via native tool calling,\nnot a JSON block in your reply). If you need a tool from the index that is not yet callable, call\n`capability_expand` with its category name first — the matching tools become callable immediately after.\nWhen the user asks what LeapFlow itself supports, whether it supports plugins, or which runtime capabilities\nare available, use the live capability evidence exposed by `plugin_list` before making capability claims; report\nconfiguration-dependent or unavailable capabilities as limitations instead of inferring from documentation.\n- **text_search**(text, pattern): Search for a regex pattern in text.\n- **text_replace**(text, old, new, count) [capability_expand category: write]: Replace occurrences of a substring in text.\n- **time_get**(): Get current date and time.\n- **env_info**(): Get system environment information (OS, Python version, cwd).\n- **skills_list**(query, category, source): List available learned skills. Use when user asks about capabilities or you need a specific skill.\n- **skill_view**(name): View the full content of a specific skill document.\n- **code_intel**(path, operation): Precise document symbols (outline) for a source file: classes, functions, and methods with line ranges. Python uses an exact AST parse; other languages use a keyword-prefix scan. Prefer over file_read mode=symbols for accurate navigation before editing. Read-only.\n- **repo_map**(path): Compact project orientation for a repository root: languages, detected test/lint commands, top-level structure, entry points, manifest, and VCS branch. Call this first when entering an unfamiliar codebase. Read-only.\n- **scm_sync**(action, cwd, remote, pull_ref, push_ref, timeout) [capability_expand category: scm]: Run a typed git SCM action. Use this instead of shell_run for git pull/push/status. For 'pull origin main then push', set action='pull_then_push', remote='origin', pull_ref='main', and omit push_ref so LeapFlow pushes the current local branch.\n- **git_query**(action, cwd, ref, path, staged, max_count, stat): Read-only structured git inspection: action=diff|log|status|branch|show. Prefer over shell_run for reading repo state — output is clipped, redacted, and log/branch are parsed into structured fields. Use scm_sync for pull/push.\n- **git_write**(action, cwd, message, stage_all, name, ref, create) [capability_expand category: scm]: Mutating git actions: action=commit (message, stage_all), branch (create+switch), checkout (switch; create=true for -b). Approval-gated. Use scm_sync for pull/push and git_query for reads.\n- **test_run**(command, cwd, timeout): Run the project's test suite and return structured results (framework, passed/failed counts, failing tests). Auto-detects the runner (pytest/npm/go/cargo) or uses a configured/explicit command; executes via the governed shell. ok=true means the runner executed — see 'success' for pass/fail.\n- **lint_check**(command, cwd, timeout): Run the project's linter and return a structured clean/issue result. Auto-detects the linter (ruff/eslint/go vet/clippy) or uses a configured/explicit command; executes via the governed shell. ok=true means the linter ran — see 'clean'.\n- **file_list**(path, pattern, depth): List files and directories at a given path. Use depth=1 or depth=2 to get a recursive tree in one call instead of listing each sub-directory separately.\n- **file_read**(path, max_lines, start_line, max_chars, mode): Read text file content with adaptive context governance. For large or unfamiliar files, prefer mode='outline' or mode='symbols' first, then use mode='raw' with start_line/max_lines for the specific range you actually need. For LeapFlow's own settings, use config_list / config_get / config_set — its config files are outside the workspace and not readable here.\n- **file_write**(path, content, mode) [capability_expand category: write]: Write content to a file (overwrite or append).\n- **code_search**(pattern, patterns, path, glob, ignore_case, multiline, max_results, context_lines): Search file CONTENTS by regex pattern across a directory tree (ripgrep-backed). Requires a regex pattern. NOT for listing or browsing directory contents — use file_list for that. Prefer this over shell_run grep: faster, skips VCS/dependency/build dirs, and returns structured path:line:column matches. Batch related lookups into ONE call via `patterns` (OR-combined, single pass) instead of issuing several separate searches. Use file_read for the surrounding context of a hit.\n- **file_find**(glob, path, max_results): Find files by a recursive glob pattern under a base path (e.g. '**/test_*.py' or '*.md'). Prefer this over shell_run find; skips VCS/dependency/build dirs.\n- **edit_file**(path, edits, dry_run, diff) [capability_expand category: write]: Apply targeted, anchored search-replace edits to an EXISTING text file (use file_write to create/overwrite). Each edit is {original_text, new_text, replace_all?}; original_text must match exactly and uniquely (or set replace_all) — a non-unique or missing anchor is rejected so files are never corrupted. Set dry_run to preview. Alternatively pass a unified 'diff' to apply its hunks as anchored edits. Far cheaper and safer than rewriting a whole file.\n- **shell_run**(command, cwd, timeout) [capability_expand category: shell]: Execute a one-shot shell command with timeout protection. Runs in the active workspace; paths resolving outside it are refused. Reach for a structured tool first when one fits — web_fetch for anything over HTTP(S), git_query/scm_sync for git, code_search/file_find/file_read for the repo, config_get/config_set for LeapFlow's own settings — because those report typed results, while a failed shell command can only be diagnosed from its exit code and stderr. Every shell run counts as an external side effect, so a failure stops the rest of the batch and is not retried automatically.\n- **terminal_open**(command, cwd, shell) [capability_expand category: terminal]: Open a PERSISTENT shell session (REPL/dev server/watch), returning a session_id for terminal_send/read/close. Disabled unless tools.terminal_session_enabled is set. For one-shot commands use shell_run instead.\n- **terminal_send**(session_id, input, wait) [capability_expand category: terminal]: Send a line of input to a persistent terminal session and return output captured shortly after.\n- **terminal_read**(session_id, wait): Drain buffered output from a persistent terminal session (optionally waiting briefly first).\n- **terminal_close**(session_id) [capability_expand category: terminal]: Terminate a persistent terminal session and release its process group.\n- **terminal_list**(): List active persistent terminal sessions.\n- **config_list**(category, limit): List LeapFlow's own writable settings (LLM model and endpoint, daemon, memory, perception, gateway, …) with current values. Use this to discover the exact key before changing anything. Optionally narrow by `category`. This is the only correct way to inspect LeapFlow configuration — never read config files from disk.\n- **config_get**(key): Read one LeapFlow setting by key (e.g. 'llm.model', 'llm.base_url', 'daemon.log_level'), returning its current value, type, scopes, and whether a change needs a daemon restart. There is no 'llm.provider' key: provider behavior is inferred from the OpenAI-compatible 'llm.base_url'. Never read LeapFlow config files from disk — use this.\n- **config_set**(key, value, scope) [capability_expand category: config]: Change one LeapFlow setting by key, e.g. switch an OpenAI-compatible LLM with key='llm.model' and key='llm.base_url'. There is no 'llm.provider' key: provider behavior is inferred from the endpoint. Values are validated and coerced; credentials are stored in the vault automatically. Call config_list or config_get first if unsure of the key. The result states whether a `leap daemon restart` is required. Never edit LeapFlow config files directly.\n- **web_fetch**(url, select, timeout, max_bytes): Read a URL over HTTP(S) and get back extracted, context-sized content: parsed JSON for API endpoints, readable text plus links for web pages. Use this for anything on the internet — prices, docs, releases, articles — instead of running curl through shell_run: it reports real HTTP status codes, retries rate limits on its own, and is a plain read so a retry is always safe. For JSON APIs pass `select` with a dotted path (e.g. 'chart.result.0.meta') to return just that part instead of the whole payload.\n- **memory_search**(query, limit): Search agent memory for relevant past experiences, observations, and facts.\n- **memory_add**(content, kind) [capability_expand category: write]: Store a new observation or insight in memory for future reference.\n- **research_note**(kind, text): Record a compact, structured note about the current task's state so it survives context compression on long / multi-step tasks. Use for durable findings, open questions still to resolve, decisions / excluded paths, and the immediate next step. One concise sentence per note.\n- **capability_expand**(category): Fetch the full callable schema for every tool in a capability category (e.g. 'hub', 'gateway', 'desktop', 'delegate', 'file', 'memory', 'skill'). The compact tool index always lists every registered tool by name and a one-line summary, but only a static low-risk subset is directly callable each turn. If you need a tool from the index that is not yet callable, call capability_expand with its category first; the matching tools become callable in this turn. Never invent a tool name — expand the category instead.\n- **delegate_task**(goal, context) [capability_expand category: delegate]: Delegate a complex sub-task to an isolated subagent. The subagent gets a fresh context and restricted tool access. Use when a task is self-contained and can be solved independently.\n- **schedule_reentry**(kind, reason, delay_seconds, event_match, max_reentries, deadline_seconds): Register a re-entry so this task can resume later from its current orientation (findings / open questions / next step). Use when work must pause and continue after a delay (kind=time) or when a matching platform event arrives (kind=event), instead of finishing now. The research-ledger state is carried over automatically.\n- **hub_push**(skill_name, visibility, version) [capability_expand category: hub]: Push a local skill to the ModelScope Hub for sharing or backup.\n- **hub_pull**(repo_id, version) [capability_expand category: hub]: Pull a skill from the ModelScope Hub to install locally.\n- **hub_search**(query) [capability_expand category: hub]: Search for skills on the Hub by keyword or description.\n- **hub_sync**(mode, dry_run) [capability_expand category: hub]: Preview or execute sync between local skills and Hub.\n- **platform_action**(platform, action, payload, backend_kind) [capability_expand category: gateway]: Execute an exact registered business action on an external platform through LeapFlow's App Connector layer. Actions must be copied from the App Connector Capability Index and are addressed as domain.operation, e.g. im.send_message or docs.create_markdown. All business fields (chat_id, text, query, etc.) MUST be placed inside `payload`, never at the top level. Example: {\"platform\":\"feishu\",\"action\":\"im.send_message\",\"payload\":{\"chat_id\":\"oc_xxx\",\"text\":\"hello\"}}. Do not invent action names, do not use management actions such as list/guide/connect/status here.\n- **platform_connect**(action, platform, credentials, options, checkpoint) [capability_expand category: gateway]: List, guide, connect, disconnect, remove, or check status for external platforms using the App Connector management namespace. Supports REST and CLI backends. Use this for management actions such as list/guide/preflight/connect/status; use platform_action only for exact registered business actions.\n- **gateway_send**(platform, chat_id, text, thread_id) [capability_expand category: gateway]: Send a message to a connected external platform (Feishu group, Telegram chat, DingTalk conversation, etc.). Requires the platform to be connected via gateway_connect first. Use gateway_connect with action='list' to see connected platforms and available chat IDs.\n- **gateway_connect**(action, platform, credentials, options) [capability_expand category: gateway]: Connect, configure, or manage external platform integrations (Feishu, DingTalk, Telegram, Slack, Discord, etc.). Conversational flow: 1) call 'guide' to get setup steps + required fields, 2) present the steps to the user and ask for ALL required credentials in a single message, 3) call 'connect' with the credentials. Goal: complete in 1–2 user turns. NEVER include credential values in your text response.\n- **plugin_list**(): List the live plugin registry and cross-subsystem capability evidence. Use this before answering questions about whether LeapFlow supports plugins, self-evolution, plugin installation, hot reload, versioning, or other runtime capabilities.\n- **plugin_status**(plugin_id): Get detailed status of a specific plugin: its declared category, runtime dependencies, contributed tools, and fiber lifecycle state.\n- **plugin_versions**(plugin_id): List recorded source versions and active pointer for a profile-scoped plugin.\n- **plugin_propose**(requested_capability, plugin_id, proposed_tools, test_cases, risk_level, evidence): Create a side-effect-free PluginProposal from explicit capability-gap evidence. Use this before plugin_generate when a missing capability should be reviewed. Does not call an LLM, write files, or install anything.\n- **assess_compatibility**(manifest, source_path) [capability_expand category: plugin_management]: Assess whether a foreign plugin manifest or real DSH source bundle is compatible with LeapFlow. A source bundle assessment is static: runtime_ready stays false until restricted Node discovery during plugin_install. Returns component-level verdicts and limitations.\n- **plugin_generate**(plugin_id, description, proposal_id) [capability_expand category: system]: Generate a new ToolPlugin from a natural-language capability description. The LLM produces code that conforms to the ToolPlugin Protocol; it is then rigorously validated (syntax, structure, import, protocol conformance). The generated source is stored in CAS and receives explicit content approval. It DOES NOT install the plugin — installation requires a second, mutation-specific approval via plugin_install.\n- **plugin_install**(plugin_id, code, marketplace_name, source_path, proposal_id, version_label) [capability_expand category: system]: Install a plugin from exactly one source: validated Python code, the configured Python marketplace, or a real DSH source bundle. DSH bundles run restricted Node discovery before registration; only public host tools are installed and client UI limitations are reported. Writes profile-scoped state and mutates the process-global registry. REQUIRES APPROVAL.\n- **plugin_rollback**(plugin_id, version) [capability_expand category: system]: Rollback a profile-scoped plugin to a recorded source version and reload it. REQUIRES APPROVAL.\n- **plugin_reload**(plugin_id, version_label) [capability_expand category: system]: Hot-reload a plugin at runtime. Disposes the old plugin fiber, re-imports its module, and registers a fresh instance. Existing in-flight turns are unaffected (snapshot isolation). REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_disable**(plugin_id) [capability_expand category: system]: Disable a plugin by disposing its fiber, removing its tools from the runtime registry. Cannot disable self_management itself. REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_remove**(plugin_id, delete_source) [capability_expand category: system]: Terminally remove a plugin: dispose its fiber, unregister its tools, remove reload metadata, and optionally delete its profile-scoped source file. Cannot remove self_management itself. REQUIRES APPROVAL.\n- **plugin_unquarantine**(plugin_id) [capability_expand category: plugin_management]: Restore a quarantined plugin to probation status for re-evaluation. Unfreezes the trust ledger, transitions the proposal back to PROBATION, and reloads the plugin. REQUIRES APPROVAL.\n- **plugin_enable**(plugin_id) [capability_expand category: system]: Re-enable a previously disabled plugin by reloading its module and registering a fresh instance. REQUIRES APPROVAL.\n- **self_describe**(facet): Introspect LeapFlow's own identity, capabilities, runtime state, evolution metrics, or platform connections. Use facet='all' only when a comprehensive self-check is explicitly requested.\n- **runtime_snapshot**(): Lightweight (~150 token) flat snapshot of current runtime state: model, context budget, posture, disclosure level, turn count, cache hit rate, uptime, tool count, and pending approvals.\n- **schedule_create**(trigger_expression, instruction, execution_mode, max_retries, delivery_target) [capability_expand category: scheduler]: Create a new scheduled task. Specify a trigger expression (e.g. '30m', 'every 2h', '0 9 * * *') and the instruction to execute on each trigger. Optionally provide an execution mode and a delivery target to receive notifications.\n- **schedule_list**() [capability_expand category: scheduler]: List all scheduled tasks with their current state, trigger, and next due time.\n- **schedule_status**(task_id) [capability_expand category: scheduler]: Get detailed status and recent execution history for a scheduled task.\n- **schedule_pause**(task_id) [capability_expand category: scheduler]: Pause a scheduled task so it stops firing without being cancelled.\n- **schedule_resume**(task_id) [capability_expand category: scheduler]: Resume a paused scheduled task — re-arms it and recalculates next due time.\n- **schedule_cancel**(task_id) [capability_expand category: scheduler]: Cancel a scheduled task permanently.\n- **tool_search**(query, max_results): Search all registered tools by keyword relevance. Returns a ranked list of matching tools with name, category, and summary. Use this when you need a tool but are unsure of its exact name or category.\n- **tool_describe**(tool_name): Get the full callable schema of a single tool by exact name. Returns the tool's description, parameters, and metadata. Use after tool_search to inspect a specific tool before calling it.\n- **session_search**(query, limit) [capability_expand category: unclassified]: Search past conversation sessions in the current workspace for relevant context.\n- **session_list**(limit): List recent conversation sessions in the current workspace with stable ids, titles, dates, and summaries. Use for browsing past tasks or when user asks to see history without specific search terms. No keywords needed — returns chronological list.\n- **session_detail**(session_id, limit, offset, include_inactive) [capability_expand category: unclassified]: Read a paginated persisted transcript for one past conversation session in the current workspace. Use after session_list or session_search returns a session_id.\n- **click**(element_index) [capability_expand category: desktop]: Click a UI element by its element_index (from the latest observe_ui snapshot)\n- **get_clipboard**() [capability_expand category: desktop]: Read current clipboard text content\n- **list_apps**(filter, running_only) [capability_expand category: desktop]: List available applications on this system. Use to discover correct bundle_id before switch_app.\n- **list_windows**() [capability_expand category: desktop]: List all top-level windows with pid, window_id, title, and per-window state (minimized, on-screen). Call this first to pick the pid and window_id that observe_ui and other window tools require.\n- **observe_ui**(pid, window_id, query) [capability_expand category: desktop]: Snapshot one window's actionable UI elements, each tagged with an element_index for click/right_click/read_text. Re-observe after actions — indices belong to one snapshot. Requires the window's pid and window_id from list_windows.\n- **open_url**(url, app_id) [capability_expand category: desktop]: Open a URL in the default or specified browser\n- **read_text**(element_index) [capability_expand category: desktop]: Read the text content of a specific UI element from the latest snapshot\n- **right_click**(element_index) [capability_expand category: desktop]: Right-click a UI element to open its context menu. Returns visible menu items.\n- **screenshot**(pid, window_id) [capability_expand category: desktop]: Capture a screenshot for visual verification. With pid + window_id captures that window (works across all displays); defaults to the last observed window, or the full desktop when no window has been observed.\n- **scroll**(element_index, direction, amount, pid, window_id) [capability_expand category: desktop]: Scroll a scrollable area of a window. Omit element_index to scroll the window's focused/page scroller; pass one to scroll an exact element from the latest snapshot.\n- **select_text**(element_index) [capability_expand category: desktop]: Select all text in a UI element (focus + select-all, for subsequent copy)\n- **set_clipboard**(text) [capability_expand category: desktop]: Write text to the clipboard\n- **shortcut**(keys) [capability_expand category: desktop]: Execute a keyboard shortcut\n- **switch_app**(app_id) [capability_expand category: desktop]: Switch to an app (launch if needed, activate, verify)\n- **type_text**(text) [capability_expand category: desktop]: Type text into the currently focused element\n- **wait**(seconds) [capability_expand category: desktop]: Wait for a specified duration before continuing\n- **wait_until**(condition, pid, window_id, timeout, poll_interval) [capability_expand category: desktop]: Wait until a UI condition is met (polls UI tree). Returns elements when found or on timeout.\n- **wait_until_stable**(timeout, poll_interval, pid, window_id) [capability_expand category: desktop]: Wait until the UI stops changing (element set stabilizes across polls).\n\n## App Connector Capability Index\nLeapFlow can onboard and manage external apps through `platform_connect` and execute exact registered business actions through `platform_action`.\nFor requests about connecting, setting up, configuring, enabling, or managing a supported app, use `platform_connect` first instead of generating SDK/Webhook sample code.\n`platform_connect.action` is the management namespace: list, guide, preflight, connect, disconnect, remove, status, events_start, events_stop, events_status.\n`platform_action.action` is only for exact registered platform business actions listed below, such as `im.send_message`; never use management actions like `list` or `guide` there.\nAll business fields MUST go inside `payload`; top-level keys are only `platform`, `action`, and `payload`.\nDo not invent platform IDs or platform action names. If the needed action is not listed, ask for discovery/clarification instead of guessing.\nUse `platform_connect` with `action='guide'` and the matching `platform` to start onboarding; use `action='list'` when the app is unclear.\nWhen a pending onboarding state is present, continue from that state with `platform_connect` instead of asking the user to restate the app.\nSupported apps:\n- `api_server`: API Server (OpenAI Compatible) (category=api; backend=adapter; platform_action actions=none registered)\n- `dingtalk`: 钉钉 (DingTalk) (category=im; backend=adapter; platform_action actions=none registered)\n- `feishu`: 飞书 (Feishu/Lark) (category=im; backend=cli)\n - `calendar.create_event` payload={summary*, start_time*, end_time*, attendees} [write/high]\n - `docs.create_markdown` payload={title*, markdown*, folder_token} [write/medium]\n - `drive.search` payload={query*, limit} [read/medium]\n - `im.add_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.download_resource` payload={message_id*, file_key*, type*} [read/low]\n - `im.get_messages` payload={message_ids*} [read/low]\n - `im.list_chats` payload={page_size} [read/low]\n - `im.list_messages` payload={chat_id*} [read/low]\n - `im.list_thread_messages` payload={thread*} [read/low]\n - `im.remove_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.reply_message` payload={message_id*, text*} [send/high]\n - `im.search_chats` payload={query*, page_size} [read/low]\n - `im.search_messages` payload={query*} [read/medium]\n - `im.send_message` payload={chat_id*, text*, thread_id} [send/high]\n - `im.update_card` payload={token*, card*} [write/medium]\n - `im.update_message` payload={message_id*, text*} [write/medium]\n - `mail.search_unread` payload={query, limit} [read/high]\n - `sheets.append_row` payload={spreadsheet_token*, sheet_id*, values*} [write/medium]\n - `task.create` payload={title*, description, due_time} [write/medium]\n- `telegram`: Telegram (category=social; backend=adapter; platform_action actions=none registered)\n- `webhook`: Webhook (Generic) (category=webhook; backend=adapter; platform_action actions=none registered)\n\n## Tool Usage\nTools are normally invoked through the native function-calling mechanism, not by writing JSON in your reply\ntext. Only if the provider signals that native function calling is unavailable for this turn, fall back to a\nsingle JSON code block: `{\"name\": \"tool_name\", \"arguments\": {\"key\": \"value\"}}` — use this fallback format\nonly, never both. Only call a tool whose exact name appears in the tool index above and is currently callable\n(or reachable via `capability_expand`). Never invent, rename, alias, or guess a tool name, platform ID, or\nplatform action from argument shape or wording — if the index does not list it, it does not exist.\n`platform_connect.action` (list/guide/preflight/connect/disconnect/remove/status/events_start/events_stop/\nevents_status) is the App Connector management namespace; `platform_action.action` only accepts exact\nregistered business actions such as `im.send_message` shown in the App Connector Capability Index — never mix\nthe two namespaces. If a tool call returns an unknown/unavailable result, use the returned suggestions or\navailable names for a single retry instead of trying further variations of the same guess.\n\n**Side-effect action rule** (`platform_action` with effect=send/write/execute):\n- Call each unique action+payload **exactly once**. Never include duplicates in the same turn.\n- Once the result returns `\"completed\": true`, that action is DONE for this task. Do NOT call it again\n in any subsequent turn — immediately summarize the result for the user instead.\n- The system enforces idempotency: duplicate calls are blocked and will not execute.\n- If the user explicitly requests sending/writing multiple times, use distinct payloads per call.\n\n**Resource identifier provenance rule**:\n- NEVER fabricate, guess, or infer resource identifiers (chat_id, message_id, file_key, user_id, etc.).\n Every resource ID used in a side-effect action MUST come from a successful API response in this session.\n- If a read/list action fails (e.g. authorization error), you do NOT have valid resource IDs.\n Report the failure to the user — do NOT attempt the dependent write/send action with a guessed ID.\n- When a tool result contains `\"llm_instruction\"`, follow it exactly.\n\n## Guidelines\n1. **Direct answers first**: If you already know the answer, respond directly without tools.\n2. **Avoid redundant tool calls**: Do not call the same tool with the same arguments more than once in the same user turn. When an existing tool result already answers the user's request, stop calling tools and answer directly.\n3. **Use tools proactively**: When the user asks about files, time, system state, or needs actions performed, use the appropriate tool.\n4. **Chain tools when needed**: You can call multiple tools in sequence (e.g., list files → read file → summarize).\n5. **Handle failures gracefully**: If a tool fails, explain what went wrong and suggest alternatives. If it failed because of an unknown tool/platform/action name, retry once with an exact name from the returned suggestions, then explain rather than keep guessing.\n6. **Summarize results naturally**: After tool execution, synthesize the results into a helpful answer rather than dumping raw output.\n7. **Stay conversational**: Maintain a natural, helpful tone. Acknowledge context from earlier in the conversation.\n8. **Recall past work**:\n - Broad queries (\"之前做了什么\", \"列出任务\"): answer from the \"Recent Task History\" section already in your context. If insufficient, call session_list.\n - Specific lookups (\"上次那个配置怎么改的\"): call session_search with relevant phrases (NOT single characters).\n - Do NOT call search tools repeatedly with keyword fragments. One well-phrased call is better than ten fragmented ones.\n\n## Coding & Verification\nWhen working with code, prefer the precise built-in tools over ad-hoc shell: use `repo_map` to orient in an\nunfamiliar project, `code_search`/`file_find` to locate and `code_intel` for symbols, `edit_file` (anchored\nsearch-replace, or a unified `diff`) to change files — never rewrite a whole file to change a few lines — and\n`git_query` to inspect diffs/log. After edits, check `syntax_ok` in the result and run `test_run`/`lint_check`\nbefore declaring the work done. Batch independent read-only calls (search/read/query) into a single turn — they\nrun in parallel.\n\n## Presentation Style\n1. **Polished Markdown only**: Format user-facing answers with clean Markdown headings, short paragraphs, and concise bullets. Use tables only when they improve comparison or scanning.\n2. **Terminal-friendly layout**: Keep lines readable in a TUI; avoid dense walls of text, deeply nested lists, oversized ASCII art, or heavy visual blocks.\n3. **Elegant emphasis**: Use bold text sparingly for key terms and conclusions. Avoid excessive emojis, decorative symbols, repeated separators, or visual noise.\n4. **Theme-safe colors**: Do not emit ANSI escape codes, HTML color tags, Rich markup, or hardcoded color names. Rely on the TUI theme to render Markdown professionally.\n5. **No leaked tool protocol**: Never show tool-call JSON, internal schemas, raw observations, tool result payloads, or hidden reasoning in the final answer unless the user explicitly asks for raw/debug output. Treat any prior `{\"name\": ..., \"arguments\": ...}` blocks and `Tool result (...)` messages as internal execution context only.\n6. **Professional closure**: End with a concise conclusion or next step when helpful; avoid rambling after the useful answer is complete.\n\nWhen finished with all tool calls, respond normally without a JSON block, tool-call transcript, or process log.\n\n## Task Contract\n- Task ID: turn-4\n- Original user request: Remove the adaptive JSON pretty plugin completely.\n- Workspace root: \n- Allowed roots: \n- Treat relative project paths as relative to the workspace root; never infer `.` as the project root when a workspace root is provided.\n- Workspace boundary is enforced by tools: do not read, search, edit, or run commands against paths outside the allowed roots unless the user explicitly requests an external path and the tool/approval policy permits it.\n- LeapFlow workspace config is optional at `/.leapflow/config.yaml`; runtime config is loaded from `~/.leapflow/config/user.yaml` and `~/.leapflow/profiles//config/*.yaml`.\n- Preserve this task contract across summarization, compression, tool loops, and memory retrieval.\n" - }, - { - "role": "system", - "content": "\n## Recent Session Summary\n- [assistant] Formatted JSON with the new plugin.\n- [user] Remove the adaptive JSON pretty plugin completely.\n" - }, - { - "role": "assistant", - "content": "Formatted JSON with the new plugin." - }, - { - "role": "user", - "content": "Remove the adaptive JSON pretty plugin completely.\nRemove the adaptive JSON pretty plugin completely." - }, - { - "role": "assistant", - "content": "", - "tool_calls": [ - "plugin_remove" - ] - }, - { - "role": "tool", - "content": "{\"ok\": true, \"action\": \"remove\", \"plugin_id\": \"json_pretty_loop_e2e\", \"state\": \"disposed\", \"source_path\": \"\", \"source_deleted\": true, \"execution_id\": \"\", \"idempotency_key\": \"\", \"execution_policy\": \"mutating_once\", \"tool_call_id\": \"call_1\", \"execution_status\": \"completed\"}", - "tool_result": true - } - ], - "tools": [ - "capability_expand", - "code_intel", - "code_search", - "config_get", - "config_list", - "env_info", - "file_find", - "file_list", - "file_read", - "git_query", - "lint_check", - "memory_search", - "plugin_list", - "plugin_propose", - "plugin_status", - "plugin_versions", - "repo_map", - "research_note", - "runtime_snapshot", - "schedule_reentry", - "self_describe", - "session_list", - "skill_view", - "skills_list", - "terminal_list", - "terminal_read", - "test_run", - "text_search", - "time_get", - "tool_describe", - "tool_search", - "web_fetch" - ] - }, - "responses": [ - { - "status": 200, - "content_type": "application/json", - "body": "{\"id\": \"chatcmpl-cassette\", \"object\": \"chat.completion\", \"model\": \"cassette-model\", \"choices\": [{\"index\": 0, \"message\": {\"role\": \"assistant\", \"content\": \"Removed json pretty plugin.\"}, \"finish_reason\": \"stop\"}], \"usage\": {\"prompt_tokens\": 64, \"completion_tokens\": 16, \"total_tokens\": 80}}" - } - ] -} diff --git a/tests/_fixtures/cassettes/r7_adaptive_plugin_loop/cassette-model-4b7eca2c226e3ad7.cassette.json b/tests/_fixtures/cassettes/r7_adaptive_plugin_loop/cassette-model-4b7eca2c226e3ad7.cassette.json deleted file mode 100644 index 6207b1a4..00000000 --- a/tests/_fixtures/cassettes/r7_adaptive_plugin_loop/cassette-model-4b7eca2c226e3ad7.cassette.json +++ /dev/null @@ -1,68 +0,0 @@ -{ - "fingerprint": "4b7eca2c226e3ad700c4ba3c1989a12c26f7690522c6d1b13f4348efe4d43f10", - "note": "captured in seed mode", - "request": { - "model": "cassette-model", - "stream": false, - "messages": [ - { - "role": "system", - "content": "You are LeapFlow, an intelligent assistant that can both converse naturally and take real actions on the user's computer.\n\n## Capabilities\nThe tool index below lists **every** registered tool by name and a one-line summary — this is the complete\ncapability contract; nothing else exists. Only a subset is directly callable this turn (via native tool calling,\nnot a JSON block in your reply). If you need a tool from the index that is not yet callable, call\n`capability_expand` with its category name first — the matching tools become callable immediately after.\nWhen the user asks what LeapFlow itself supports, whether it supports plugins, or which runtime capabilities\nare available, use the live capability evidence exposed by `plugin_list` before making capability claims; report\nconfiguration-dependent or unavailable capabilities as limitations instead of inferring from documentation.\n- **text_search**(text, pattern): Search for a regex pattern in text.\n- **text_replace**(text, old, new, count) [capability_expand category: write]: Replace occurrences of a substring in text.\n- **time_get**(): Get current date and time.\n- **env_info**(): Get system environment information (OS, Python version, cwd).\n- **skills_list**(query, category, source): List available learned skills. Use when user asks about capabilities or you need a specific skill.\n- **skill_view**(name): View the full content of a specific skill document.\n- **code_intel**(path, operation): Precise document symbols (outline) for a source file: classes, functions, and methods with line ranges. Python uses an exact AST parse; other languages use a keyword-prefix scan. Prefer over file_read mode=symbols for accurate navigation before editing. Read-only.\n- **repo_map**(path): Compact project orientation for a repository root: languages, detected test/lint commands, top-level structure, entry points, manifest, and VCS branch. Call this first when entering an unfamiliar codebase. Read-only.\n- **scm_sync**(action, cwd, remote, pull_ref, push_ref, timeout) [capability_expand category: scm]: Run a typed git SCM action. Use this instead of shell_run for git pull/push/status. For 'pull origin main then push', set action='pull_then_push', remote='origin', pull_ref='main', and omit push_ref so LeapFlow pushes the current local branch.\n- **git_query**(action, cwd, ref, path, staged, max_count, stat): Read-only structured git inspection: action=diff|log|status|branch|show. Prefer over shell_run for reading repo state — output is clipped, redacted, and log/branch are parsed into structured fields. Use scm_sync for pull/push.\n- **git_write**(action, cwd, message, stage_all, name, ref, create) [capability_expand category: scm]: Mutating git actions: action=commit (message, stage_all), branch (create+switch), checkout (switch; create=true for -b). Approval-gated. Use scm_sync for pull/push and git_query for reads.\n- **test_run**(command, cwd, timeout): Run the project's test suite and return structured results (framework, passed/failed counts, failing tests). Auto-detects the runner (pytest/npm/go/cargo) or uses a configured/explicit command; executes via the governed shell. ok=true means the runner executed — see 'success' for pass/fail.\n- **lint_check**(command, cwd, timeout): Run the project's linter and return a structured clean/issue result. Auto-detects the linter (ruff/eslint/go vet/clippy) or uses a configured/explicit command; executes via the governed shell. ok=true means the linter ran — see 'clean'.\n- **file_list**(path, pattern, depth): List files and directories at a given path. Use depth=1 or depth=2 to get a recursive tree in one call instead of listing each sub-directory separately.\n- **file_read**(path, max_lines, start_line, max_chars, mode): Read text file content with adaptive context governance. For large or unfamiliar files, prefer mode='outline' or mode='symbols' first, then use mode='raw' with start_line/max_lines for the specific range you actually need. For LeapFlow's own settings, use config_list / config_get / config_set — its config files are outside the workspace and not readable here.\n- **file_write**(path, content, mode) [capability_expand category: write]: Write content to a file (overwrite or append).\n- **code_search**(pattern, patterns, path, glob, ignore_case, multiline, max_results, context_lines): Search file CONTENTS by regex pattern across a directory tree (ripgrep-backed). Requires a regex pattern. NOT for listing or browsing directory contents — use file_list for that. Prefer this over shell_run grep: faster, skips VCS/dependency/build dirs, and returns structured path:line:column matches. Batch related lookups into ONE call via `patterns` (OR-combined, single pass) instead of issuing several separate searches. Use file_read for the surrounding context of a hit.\n- **file_find**(glob, path, max_results): Find files by a recursive glob pattern under a base path (e.g. '**/test_*.py' or '*.md'). Prefer this over shell_run find; skips VCS/dependency/build dirs.\n- **edit_file**(path, edits, dry_run, diff) [capability_expand category: write]: Apply targeted, anchored search-replace edits to an EXISTING text file (use file_write to create/overwrite). Each edit is {original_text, new_text, replace_all?}; original_text must match exactly and uniquely (or set replace_all) — a non-unique or missing anchor is rejected so files are never corrupted. Set dry_run to preview. Alternatively pass a unified 'diff' to apply its hunks as anchored edits. Far cheaper and safer than rewriting a whole file.\n- **shell_run**(command, cwd, timeout) [capability_expand category: shell]: Execute a one-shot shell command with timeout protection. Runs in the active workspace; paths resolving outside it are refused. Reach for a structured tool first when one fits — web_fetch for anything over HTTP(S), git_query/scm_sync for git, code_search/file_find/file_read for the repo, config_get/config_set for LeapFlow's own settings — because those report typed results, while a failed shell command can only be diagnosed from its exit code and stderr. Every shell run counts as an external side effect, so a failure stops the rest of the batch and is not retried automatically.\n- **terminal_open**(command, cwd, shell) [capability_expand category: terminal]: Open a PERSISTENT shell session (REPL/dev server/watch), returning a session_id for terminal_send/read/close. Disabled unless tools.terminal_session_enabled is set. For one-shot commands use shell_run instead.\n- **terminal_send**(session_id, input, wait) [capability_expand category: terminal]: Send a line of input to a persistent terminal session and return output captured shortly after.\n- **terminal_read**(session_id, wait): Drain buffered output from a persistent terminal session (optionally waiting briefly first).\n- **terminal_close**(session_id) [capability_expand category: terminal]: Terminate a persistent terminal session and release its process group.\n- **terminal_list**(): List active persistent terminal sessions.\n- **config_list**(category, limit): List LeapFlow's own writable settings (LLM model and endpoint, daemon, memory, perception, gateway, …) with current values. Use this to discover the exact key before changing anything. Optionally narrow by `category`. This is the only correct way to inspect LeapFlow configuration — never read config files from disk.\n- **config_get**(key): Read one LeapFlow setting by key (e.g. 'llm.model', 'llm.base_url', 'daemon.log_level'), returning its current value, type, scopes, and whether a change needs a daemon restart. There is no 'llm.provider' key: provider behavior is inferred from the OpenAI-compatible 'llm.base_url'. Never read LeapFlow config files from disk — use this.\n- **config_set**(key, value, scope) [capability_expand category: config]: Change one LeapFlow setting by key, e.g. switch an OpenAI-compatible LLM with key='llm.model' and key='llm.base_url'. There is no 'llm.provider' key: provider behavior is inferred from the endpoint. Values are validated and coerced; credentials are stored in the vault automatically. Call config_list or config_get first if unsure of the key. The result states whether a `leap daemon restart` is required. Never edit LeapFlow config files directly.\n- **web_fetch**(url, select, timeout, max_bytes): Read a URL over HTTP(S) and get back extracted, context-sized content: parsed JSON for API endpoints, readable text plus links for web pages. Use this for anything on the internet — prices, docs, releases, articles — instead of running curl through shell_run: it reports real HTTP status codes, retries rate limits on its own, and is a plain read so a retry is always safe. For JSON APIs pass `select` with a dotted path (e.g. 'chart.result.0.meta') to return just that part instead of the whole payload.\n- **memory_search**(query, limit): Search agent memory for relevant past experiences, observations, and facts.\n- **memory_add**(content, kind) [capability_expand category: write]: Store a new observation or insight in memory for future reference.\n- **research_note**(kind, text): Record a compact, structured note about the current task's state so it survives context compression on long / multi-step tasks. Use for durable findings, open questions still to resolve, decisions / excluded paths, and the immediate next step. One concise sentence per note.\n- **capability_expand**(category): Fetch the full callable schema for every tool in a capability category (e.g. 'hub', 'gateway', 'desktop', 'delegate', 'file', 'memory', 'skill'). The compact tool index always lists every registered tool by name and a one-line summary, but only a static low-risk subset is directly callable each turn. If you need a tool from the index that is not yet callable, call capability_expand with its category first; the matching tools become callable in this turn. Never invent a tool name — expand the category instead.\n- **delegate_task**(goal, context) [capability_expand category: delegate]: Delegate a complex sub-task to an isolated subagent. The subagent gets a fresh context and restricted tool access. Use when a task is self-contained and can be solved independently.\n- **schedule_reentry**(kind, reason, delay_seconds, event_match, max_reentries, deadline_seconds): Register a re-entry so this task can resume later from its current orientation (findings / open questions / next step). Use when work must pause and continue after a delay (kind=time) or when a matching platform event arrives (kind=event), instead of finishing now. The research-ledger state is carried over automatically.\n- **hub_push**(skill_name, visibility, version) [capability_expand category: hub]: Push a local skill to the ModelScope Hub for sharing or backup.\n- **hub_pull**(repo_id, version) [capability_expand category: hub]: Pull a skill from the ModelScope Hub to install locally.\n- **hub_search**(query) [capability_expand category: hub]: Search for skills on the Hub by keyword or description.\n- **hub_sync**(mode, dry_run) [capability_expand category: hub]: Preview or execute sync between local skills and Hub.\n- **platform_action**(platform, action, payload, backend_kind) [capability_expand category: gateway]: Execute an exact registered business action on an external platform through LeapFlow's App Connector layer. Actions must be copied from the App Connector Capability Index and are addressed as domain.operation, e.g. im.send_message or docs.create_markdown. All business fields (chat_id, text, query, etc.) MUST be placed inside `payload`, never at the top level. Example: {\"platform\":\"feishu\",\"action\":\"im.send_message\",\"payload\":{\"chat_id\":\"oc_xxx\",\"text\":\"hello\"}}. Do not invent action names, do not use management actions such as list/guide/connect/status here.\n- **platform_connect**(action, platform, credentials, options, checkpoint) [capability_expand category: gateway]: List, guide, connect, disconnect, remove, or check status for external platforms using the App Connector management namespace. Supports REST and CLI backends. Use this for management actions such as list/guide/preflight/connect/status; use platform_action only for exact registered business actions.\n- **gateway_send**(platform, chat_id, text, thread_id) [capability_expand category: gateway]: Send a message to a connected external platform (Feishu group, Telegram chat, DingTalk conversation, etc.). Requires the platform to be connected via gateway_connect first. Use gateway_connect with action='list' to see connected platforms and available chat IDs.\n- **gateway_connect**(action, platform, credentials, options) [capability_expand category: gateway]: Connect, configure, or manage external platform integrations (Feishu, DingTalk, Telegram, Slack, Discord, etc.). Conversational flow: 1) call 'guide' to get setup steps + required fields, 2) present the steps to the user and ask for ALL required credentials in a single message, 3) call 'connect' with the credentials. Goal: complete in 1–2 user turns. NEVER include credential values in your text response.\n- **plugin_list**(): List the live plugin registry and cross-subsystem capability evidence. Use this before answering questions about whether LeapFlow supports plugins, self-evolution, plugin installation, hot reload, versioning, or other runtime capabilities.\n- **plugin_status**(plugin_id): Get detailed status of a specific plugin: its declared category, runtime dependencies, contributed tools, and fiber lifecycle state.\n- **plugin_versions**(plugin_id): List recorded source versions and active pointer for a profile-scoped plugin.\n- **plugin_propose**(requested_capability, plugin_id, proposed_tools, test_cases, risk_level, evidence): Create a side-effect-free PluginProposal from explicit capability-gap evidence. Use this before plugin_generate when a missing capability should be reviewed. Does not call an LLM, write files, or install anything.\n- **assess_compatibility**(manifest, source_path) [capability_expand category: plugin_management]: Assess whether a foreign plugin manifest or real DSH source bundle is compatible with LeapFlow. A source bundle assessment is static: runtime_ready stays false until restricted Node discovery during plugin_install. Returns component-level verdicts and limitations.\n- **plugin_generate**(plugin_id, description, proposal_id) [capability_expand category: system]: Generate a new ToolPlugin from a natural-language capability description. The LLM produces code that conforms to the ToolPlugin Protocol; it is then rigorously validated (syntax, structure, import, protocol conformance). The generated source is stored in CAS and receives explicit content approval. It DOES NOT install the plugin — installation requires a second, mutation-specific approval via plugin_install.\n- **plugin_install**(plugin_id, code, marketplace_name, source_path, proposal_id, version_label) [capability_expand category: system]: Install a plugin from exactly one source: validated Python code, the configured Python marketplace, or a real DSH source bundle. DSH bundles run restricted Node discovery before registration; only public host tools are installed and client UI limitations are reported. Writes profile-scoped state and mutates the process-global registry. REQUIRES APPROVAL.\n- **plugin_rollback**(plugin_id, version) [capability_expand category: system]: Rollback a profile-scoped plugin to a recorded source version and reload it. REQUIRES APPROVAL.\n- **plugin_reload**(plugin_id, version_label) [capability_expand category: system]: Hot-reload a plugin at runtime. Disposes the old plugin fiber, re-imports its module, and registers a fresh instance. Existing in-flight turns are unaffected (snapshot isolation). REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_disable**(plugin_id) [capability_expand category: system]: Disable a plugin by disposing its fiber, removing its tools from the runtime registry. Cannot disable self_management itself. REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_remove**(plugin_id, delete_source) [capability_expand category: system]: Terminally remove a plugin: dispose its fiber, unregister its tools, remove reload metadata, and optionally delete its profile-scoped source file. Cannot remove self_management itself. REQUIRES APPROVAL.\n- **plugin_unquarantine**(plugin_id) [capability_expand category: plugin_management]: Restore a quarantined plugin to probation status for re-evaluation. Unfreezes the trust ledger, transitions the proposal back to PROBATION, and reloads the plugin. REQUIRES APPROVAL.\n- **plugin_enable**(plugin_id) [capability_expand category: system]: Re-enable a previously disabled plugin by reloading its module and registering a fresh instance. REQUIRES APPROVAL.\n- **self_describe**(facet): Introspect LeapFlow's own identity, capabilities, runtime state, evolution metrics, or platform connections. Use facet='all' only when a comprehensive self-check is explicitly requested.\n- **runtime_snapshot**(): Lightweight (~150 token) flat snapshot of current runtime state: model, context budget, posture, disclosure level, turn count, cache hit rate, uptime, tool count, and pending approvals.\n- **schedule_create**(trigger_expression, instruction, execution_mode, max_retries, delivery_target) [capability_expand category: scheduler]: Create a new scheduled task. Specify a trigger expression (e.g. '30m', 'every 2h', '0 9 * * *') and the instruction to execute on each trigger. Optionally provide an execution mode and a delivery target to receive notifications.\n- **schedule_list**() [capability_expand category: scheduler]: List all scheduled tasks with their current state, trigger, and next due time.\n- **schedule_status**(task_id) [capability_expand category: scheduler]: Get detailed status and recent execution history for a scheduled task.\n- **schedule_pause**(task_id) [capability_expand category: scheduler]: Pause a scheduled task so it stops firing without being cancelled.\n- **schedule_resume**(task_id) [capability_expand category: scheduler]: Resume a paused scheduled task — re-arms it and recalculates next due time.\n- **schedule_cancel**(task_id) [capability_expand category: scheduler]: Cancel a scheduled task permanently.\n- **tool_search**(query, max_results): Search all registered tools by keyword relevance. Returns a ranked list of matching tools with name, category, and summary. Use this when you need a tool but are unsure of its exact name or category.\n- **tool_describe**(tool_name): Get the full callable schema of a single tool by exact name. Returns the tool's description, parameters, and metadata. Use after tool_search to inspect a specific tool before calling it.\n- **session_search**(query, limit) [capability_expand category: unclassified]: Search past conversation sessions in the current workspace for relevant context.\n- **session_list**(limit): List recent conversation sessions in the current workspace with stable ids, titles, dates, and summaries. Use for browsing past tasks or when user asks to see history without specific search terms. No keywords needed — returns chronological list.\n- **session_detail**(session_id, limit, offset, include_inactive) [capability_expand category: unclassified]: Read a paginated persisted transcript for one past conversation session in the current workspace. Use after session_list or session_search returns a session_id.\n- **json_pretty_loop_e2e**(text): Pretty-print JSON for the adaptive closed-loop journey.\n- **click**(element_index) [capability_expand category: desktop]: Click a UI element by its element_index (from the latest observe_ui snapshot)\n- **get_clipboard**() [capability_expand category: desktop]: Read current clipboard text content\n- **list_apps**(filter, running_only) [capability_expand category: desktop]: List available applications on this system. Use to discover correct bundle_id before switch_app.\n- **list_windows**() [capability_expand category: desktop]: List all top-level windows with pid, window_id, title, and per-window state (minimized, on-screen). Call this first to pick the pid and window_id that observe_ui and other window tools require.\n- **observe_ui**(pid, window_id, query) [capability_expand category: desktop]: Snapshot one window's actionable UI elements, each tagged with an element_index for click/right_click/read_text. Re-observe after actions — indices belong to one snapshot. Requires the window's pid and window_id from list_windows.\n- **open_url**(url, app_id) [capability_expand category: desktop]: Open a URL in the default or specified browser\n- **read_text**(element_index) [capability_expand category: desktop]: Read the text content of a specific UI element from the latest snapshot\n- **right_click**(element_index) [capability_expand category: desktop]: Right-click a UI element to open its context menu. Returns visible menu items.\n- **screenshot**(pid, window_id) [capability_expand category: desktop]: Capture a screenshot for visual verification. With pid + window_id captures that window (works across all displays); defaults to the last observed window, or the full desktop when no window has been observed.\n- **scroll**(element_index, direction, amount, pid, window_id) [capability_expand category: desktop]: Scroll a scrollable area of a window. Omit element_index to scroll the window's focused/page scroller; pass one to scroll an exact element from the latest snapshot.\n- **select_text**(element_index) [capability_expand category: desktop]: Select all text in a UI element (focus + select-all, for subsequent copy)\n- **set_clipboard**(text) [capability_expand category: desktop]: Write text to the clipboard\n- **shortcut**(keys) [capability_expand category: desktop]: Execute a keyboard shortcut\n- **switch_app**(app_id) [capability_expand category: desktop]: Switch to an app (launch if needed, activate, verify)\n- **type_text**(text) [capability_expand category: desktop]: Type text into the currently focused element\n- **wait**(seconds) [capability_expand category: desktop]: Wait for a specified duration before continuing\n- **wait_until**(condition, pid, window_id, timeout, poll_interval) [capability_expand category: desktop]: Wait until a UI condition is met (polls UI tree). Returns elements when found or on timeout.\n- **wait_until_stable**(timeout, poll_interval, pid, window_id) [capability_expand category: desktop]: Wait until the UI stops changing (element set stabilizes across polls).\n\n## App Connector Capability Index\nLeapFlow can onboard and manage external apps through `platform_connect` and execute exact registered business actions through `platform_action`.\nFor requests about connecting, setting up, configuring, enabling, or managing a supported app, use `platform_connect` first instead of generating SDK/Webhook sample code.\n`platform_connect.action` is the management namespace: list, guide, preflight, connect, disconnect, remove, status, events_start, events_stop, events_status.\n`platform_action.action` is only for exact registered platform business actions listed below, such as `im.send_message`; never use management actions like `list` or `guide` there.\nAll business fields MUST go inside `payload`; top-level keys are only `platform`, `action`, and `payload`.\nDo not invent platform IDs or platform action names. If the needed action is not listed, ask for discovery/clarification instead of guessing.\nUse `platform_connect` with `action='guide'` and the matching `platform` to start onboarding; use `action='list'` when the app is unclear.\nWhen a pending onboarding state is present, continue from that state with `platform_connect` instead of asking the user to restate the app.\nSupported apps:\n- `api_server`: API Server (OpenAI Compatible) (category=api; backend=adapter; platform_action actions=none registered)\n- `dingtalk`: 钉钉 (DingTalk) (category=im; backend=adapter; platform_action actions=none registered)\n- `feishu`: 飞书 (Feishu/Lark) (category=im; backend=cli)\n - `calendar.create_event` payload={summary*, start_time*, end_time*, attendees} [write/high]\n - `docs.create_markdown` payload={title*, markdown*, folder_token} [write/medium]\n - `drive.search` payload={query*, limit} [read/medium]\n - `im.add_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.download_resource` payload={message_id*, file_key*, type*} [read/low]\n - `im.get_messages` payload={message_ids*} [read/low]\n - `im.list_chats` payload={page_size} [read/low]\n - `im.list_messages` payload={chat_id*} [read/low]\n - `im.list_thread_messages` payload={thread*} [read/low]\n - `im.remove_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.reply_message` payload={message_id*, text*} [send/high]\n - `im.search_chats` payload={query*, page_size} [read/low]\n - `im.search_messages` payload={query*} [read/medium]\n - `im.send_message` payload={chat_id*, text*, thread_id} [send/high]\n - `im.update_card` payload={token*, card*} [write/medium]\n - `im.update_message` payload={message_id*, text*} [write/medium]\n - `mail.search_unread` payload={query, limit} [read/high]\n - `sheets.append_row` payload={spreadsheet_token*, sheet_id*, values*} [write/medium]\n - `task.create` payload={title*, description, due_time} [write/medium]\n- `telegram`: Telegram (category=social; backend=adapter; platform_action actions=none registered)\n- `webhook`: Webhook (Generic) (category=webhook; backend=adapter; platform_action actions=none registered)\n\n## Tool Usage\nTools are normally invoked through the native function-calling mechanism, not by writing JSON in your reply\ntext. Only if the provider signals that native function calling is unavailable for this turn, fall back to a\nsingle JSON code block: `{\"name\": \"tool_name\", \"arguments\": {\"key\": \"value\"}}` — use this fallback format\nonly, never both. Only call a tool whose exact name appears in the tool index above and is currently callable\n(or reachable via `capability_expand`). Never invent, rename, alias, or guess a tool name, platform ID, or\nplatform action from argument shape or wording — if the index does not list it, it does not exist.\n`platform_connect.action` (list/guide/preflight/connect/disconnect/remove/status/events_start/events_stop/\nevents_status) is the App Connector management namespace; `platform_action.action` only accepts exact\nregistered business actions such as `im.send_message` shown in the App Connector Capability Index — never mix\nthe two namespaces. If a tool call returns an unknown/unavailable result, use the returned suggestions or\navailable names for a single retry instead of trying further variations of the same guess.\n\n**Side-effect action rule** (`platform_action` with effect=send/write/execute):\n- Call each unique action+payload **exactly once**. Never include duplicates in the same turn.\n- Once the result returns `\"completed\": true`, that action is DONE for this task. Do NOT call it again\n in any subsequent turn — immediately summarize the result for the user instead.\n- The system enforces idempotency: duplicate calls are blocked and will not execute.\n- If the user explicitly requests sending/writing multiple times, use distinct payloads per call.\n\n**Resource identifier provenance rule**:\n- NEVER fabricate, guess, or infer resource identifiers (chat_id, message_id, file_key, user_id, etc.).\n Every resource ID used in a side-effect action MUST come from a successful API response in this session.\n- If a read/list action fails (e.g. authorization error), you do NOT have valid resource IDs.\n Report the failure to the user — do NOT attempt the dependent write/send action with a guessed ID.\n- When a tool result contains `\"llm_instruction\"`, follow it exactly.\n\n## Guidelines\n1. **Direct answers first**: If you already know the answer, respond directly without tools.\n2. **Avoid redundant tool calls**: Do not call the same tool with the same arguments more than once in the same user turn. When an existing tool result already answers the user's request, stop calling tools and answer directly.\n3. **Use tools proactively**: When the user asks about files, time, system state, or needs actions performed, use the appropriate tool.\n4. **Chain tools when needed**: You can call multiple tools in sequence (e.g., list files → read file → summarize).\n5. **Handle failures gracefully**: If a tool fails, explain what went wrong and suggest alternatives. If it failed because of an unknown tool/platform/action name, retry once with an exact name from the returned suggestions, then explain rather than keep guessing.\n6. **Summarize results naturally**: After tool execution, synthesize the results into a helpful answer rather than dumping raw output.\n7. **Stay conversational**: Maintain a natural, helpful tone. Acknowledge context from earlier in the conversation.\n8. **Recall past work**:\n - Broad queries (\"之前做了什么\", \"列出任务\"): answer from the \"Recent Task History\" section already in your context. If insufficient, call session_list.\n - Specific lookups (\"上次那个配置怎么改的\"): call session_search with relevant phrases (NOT single characters).\n - Do NOT call search tools repeatedly with keyword fragments. One well-phrased call is better than ten fragmented ones.\n\n## Coding & Verification\nWhen working with code, prefer the precise built-in tools over ad-hoc shell: use `repo_map` to orient in an\nunfamiliar project, `code_search`/`file_find` to locate and `code_intel` for symbols, `edit_file` (anchored\nsearch-replace, or a unified `diff`) to change files — never rewrite a whole file to change a few lines — and\n`git_query` to inspect diffs/log. After edits, check `syntax_ok` in the result and run `test_run`/`lint_check`\nbefore declaring the work done. Batch independent read-only calls (search/read/query) into a single turn — they\nrun in parallel.\n\n## Presentation Style\n1. **Polished Markdown only**: Format user-facing answers with clean Markdown headings, short paragraphs, and concise bullets. Use tables only when they improve comparison or scanning.\n2. **Terminal-friendly layout**: Keep lines readable in a TUI; avoid dense walls of text, deeply nested lists, oversized ASCII art, or heavy visual blocks.\n3. **Elegant emphasis**: Use bold text sparingly for key terms and conclusions. Avoid excessive emojis, decorative symbols, repeated separators, or visual noise.\n4. **Theme-safe colors**: Do not emit ANSI escape codes, HTML color tags, Rich markup, or hardcoded color names. Rely on the TUI theme to render Markdown professionally.\n5. **No leaked tool protocol**: Never show tool-call JSON, internal schemas, raw observations, tool result payloads, or hidden reasoning in the final answer unless the user explicitly asks for raw/debug output. Treat any prior `{\"name\": ..., \"arguments\": ...}` blocks and `Tool result (...)` messages as internal execution context only.\n6. **Professional closure**: End with a concise conclusion or next step when helpful; avoid rambling after the useful answer is complete.\n\nWhen finished with all tool calls, respond normally without a JSON block, tool-call transcript, or process log.\n\n## Task Contract\n- Task ID: turn-3\n- Original user request: Use the json_pretty_loop_e2e tool to format the sample JSON.\n- Workspace root: \n- Allowed roots: \n- Treat relative project paths as relative to the workspace root; never infer `.` as the project root when a workspace root is provided.\n- Workspace boundary is enforced by tools: do not read, search, edit, or run commands against paths outside the allowed roots unless the user explicitly requests an external path and the tool/approval policy permits it.\n- LeapFlow workspace config is optional at `/.leapflow/config.yaml`; runtime config is loaded from `~/.leapflow/config/user.yaml` and `~/.leapflow/profiles//config/*.yaml`.\n- Preserve this task contract across summarization, compression, tool loops, and memory retrieval.\n" - }, - { - "role": "system", - "content": "\n## Recent Session Summary\n- [assistant] Installed json pretty plugin.\n- [user] Use the json_pretty_loop_e2e tool to format the sample JSON.\n" - }, - { - "role": "assistant", - "content": "Installed json pretty plugin." - }, - { - "role": "user", - "content": "Use the json_pretty_loop_e2e tool to format the sample JSON.\nUse the json_pretty_loop_e2e tool to format the sample JSON." - } - ], - "tools": [ - "capability_expand", - "code_intel", - "code_search", - "config_get", - "config_list", - "env_info", - "file_find", - "file_list", - "file_read", - "git_query", - "json_pretty_loop_e2e", - "lint_check", - "memory_search", - "plugin_list", - "plugin_propose", - "plugin_status", - "plugin_versions", - "repo_map", - "research_note", - "runtime_snapshot", - "schedule_reentry", - "self_describe", - "session_list", - "skill_view", - "skills_list", - "terminal_list", - "terminal_read", - "test_run", - "text_search", - "time_get", - "tool_describe", - "tool_search", - "web_fetch" - ] - }, - "responses": [ - { - "status": 200, - "content_type": "application/json", - "body": "{\"id\": \"chatcmpl-cassette\", \"object\": \"chat.completion\", \"model\": \"cassette-model\", \"choices\": [{\"index\": 0, \"message\": {\"role\": \"assistant\", \"content\": \"\", \"tool_calls\": [{\"id\": \"call_1\", \"type\": \"function\", \"function\": {\"name\": \"json_pretty_loop_e2e\", \"arguments\": \"{\\\"text\\\": \\\"{\\\\\\\"b\\\\\\\":2,\\\\\\\"a\\\\\\\":1}\\\"}\"}}]}, \"finish_reason\": \"tool_calls\"}], \"usage\": {\"prompt_tokens\": 64, \"completion_tokens\": 16, \"total_tokens\": 80}}" - } - ] -} diff --git a/tests/_fixtures/cassettes/r7_adaptive_plugin_loop/cassette-model-4fbfcb945a4ef6a0.cassette.json b/tests/_fixtures/cassettes/r7_adaptive_plugin_loop/cassette-model-4fbfcb945a4ef6a0.cassette.json new file mode 100644 index 00000000..91b6e740 --- /dev/null +++ b/tests/_fixtures/cassettes/r7_adaptive_plugin_loop/cassette-model-4fbfcb945a4ef6a0.cassette.json @@ -0,0 +1,140 @@ +{ + "fingerprint": "4fbfcb945a4ef6a0bc1017c69a15257c5cd01764b6ccd7d964bcfc56d22b6b87", + "note": "captured in seed mode", + "request": { + "model": "cassette-model", + "stream": false, + "messages": [ + { + "role": "system", + "content": "You are LeapFlow, an intelligent assistant that can both converse naturally and take real actions on the user's computer.\n\n## Capabilities\nThe tool index below lists **every** registered tool by name and a one-line summary — this is the complete\ncapability contract; nothing else exists. Only a subset is directly callable this turn (via native tool calling,\nnot a JSON block in your reply). If you need a tool from the index that is not yet callable, call\n`capability_expand` with its category name first — the matching tools become callable immediately after.\nWhen the user asks about LeapFlow **itself** — its identity/version/model, what tools, plugins, or capabilities\nit has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of\nself-awareness — answer from live runtime evidence via `self_describe` (facet=capabilities for \"what can you\ndo / do you support X\", identity for version/model, runtime for context/posture, or `all` for a full self-check),\nnever by reading LeapFlow's own source code, documentation, or memory. Report configuration-dependent or\nunavailable capabilities as limitations rather than inferring them.\n- **text_search**(text, pattern): Search for a regex pattern in text.\n- **text_replace**(text, old, new, count) [capability_expand category: write]: Replace occurrences of a substring in text.\n- **time_get**(): Get current date and time.\n- **env_info**(): Get system environment information (OS, Python version, cwd).\n- **skills_list**(query, category, source): List available learned skills. Use when user asks about capabilities or you need a specific skill.\n- **skill_view**(name): View the full content of a specific skill document.\n- **skill_list_files**(name) [capability_expand category: skills]: List all support files in a skill directory (configs, templates, examples). Use before skill_file_read to discover available files.\n- **skill_file_read**(name, file_path) [capability_expand category: skills]: Read an individual support file from a skill directory. Use skill_list_files first to discover available files.\n- **code_intel**(path, operation): Precise document symbols (outline) for a source file: classes, functions, and methods with line ranges. Python uses an exact AST parse; other languages use a keyword-prefix scan. Prefer over file_read mode=symbols for accurate navigation before editing. Read-only.\n- **repo_map**(path): Compact project orientation for a repository root: languages, detected test/lint commands, top-level structure, entry points, manifest, and VCS branch. Call this first when entering an unfamiliar codebase. Read-only.\n- **scm_sync**(action, cwd, remote, pull_ref, push_ref, timeout) [capability_expand category: scm]: Run a typed git SCM action. Use this instead of shell_run for git pull/push/status. For 'pull origin main then push', set action='pull_then_push', remote='origin', pull_ref='main', and omit push_ref so LeapFlow pushes the current local branch.\n- **git_query**(action, cwd, ref, path, staged, max_count, stat): Read-only structured git inspection: action=diff|log|status|branch|show. Prefer over shell_run for reading repo state — output is clipped, redacted, and log/branch are parsed into structured fields. Use scm_sync for pull/push.\n- **git_write**(action, cwd, message, stage_all, name, ref, create) [capability_expand category: scm]: Mutating git actions: action=commit (message, stage_all), branch (create+switch), checkout (switch; create=true for -b). Approval-gated. Use scm_sync for pull/push and git_query for reads.\n- **test_run**(command, cwd, timeout): Run the project's test suite and return structured results (framework, passed/failed counts, failing tests). Auto-detects the runner (pytest/npm/go/cargo) or uses a configured/explicit command; executes via the governed shell. ok=true means the runner executed — see 'success' for pass/fail.\n- **lint_check**(command, cwd, timeout): Run the project's linter and return a structured clean/issue result. Auto-detects the linter (ruff/eslint/go vet/clippy) or uses a configured/explicit command; executes via the governed shell. ok=true means the linter ran — see 'clean'.\n- **file_list**(path, pattern, depth): List files and directories at a given path. Use depth=1 or depth=2 to get a recursive tree in one call instead of listing each sub-directory separately.\n- **file_read**(path, max_lines, start_line, max_chars, mode): Read text file content with adaptive context governance. For large or unfamiliar files, prefer mode='outline' or mode='symbols' first, then use mode='raw' with start_line/max_lines for the specific range you actually need. For LeapFlow's own settings, use config_list / config_get / config_set — its config files are outside the workspace and not readable here.\n- **file_write**(path, content, mode) [capability_expand category: write]: Write content to a file (overwrite or append).\n- **code_search**(pattern, patterns, path, glob, ignore_case, multiline, max_results, context_lines): Search file CONTENTS by regex pattern across a directory tree (ripgrep-backed). Requires a regex pattern. NOT for listing or browsing directory contents — use file_list for that. Prefer this over shell_run grep: faster, skips VCS/dependency/build dirs, and returns structured path:line:column matches. Batch related lookups into ONE call via `patterns` (OR-combined, single pass) instead of issuing several separate searches. Use file_read for the surrounding context of a hit.\n- **file_find**(glob, path, max_results): Find files by a recursive glob pattern under a base path (e.g. '**/test_*.py' or '*.md'). Prefer this over shell_run find; skips VCS/dependency/build dirs.\n- **edit_file**(path, edits, dry_run, diff) [capability_expand category: write]: Apply targeted, anchored search-replace edits to an EXISTING text file (use file_write to create/overwrite). Each edit is {original_text, new_text, replace_all?}; original_text must match exactly and uniquely (or set replace_all) — a non-unique or missing anchor is rejected so files are never corrupted. Set dry_run to preview. Alternatively pass a unified 'diff' to apply its hunks as anchored edits. Far cheaper and safer than rewriting a whole file.\n- **shell_run**(command, cwd, timeout) [capability_expand category: shell]: Execute a one-shot shell command with timeout protection. Runs in the active workspace; paths resolving outside it are refused. Reach for a structured tool first when one fits — web_fetch for anything over HTTP(S), git_query/scm_sync for git, code_search/file_find/file_read for the repo, config_get/config_set for LeapFlow's own settings — because those report typed results, while a failed shell command can only be diagnosed from its exit code and stderr. Every shell run counts as an external side effect, so a failure stops the rest of the batch and is not retried automatically.\n- **terminal_open**(command, cwd, shell) [capability_expand category: terminal]: Open a PERSISTENT shell session (REPL/dev server/watch), returning a session_id for terminal_send/read/close. Disabled unless tools.terminal_session_enabled is set. For one-shot commands use shell_run instead.\n- **terminal_send**(session_id, input, wait) [capability_expand category: terminal]: Send a line of input to a persistent terminal session and return output captured shortly after.\n- **terminal_read**(session_id, wait): Drain buffered output from a persistent terminal session (optionally waiting briefly first).\n- **terminal_close**(session_id) [capability_expand category: terminal]: Terminate a persistent terminal session and release its process group.\n- **terminal_list**(): List active persistent terminal sessions.\n- **config_list**(category, limit): List LeapFlow's own writable settings (LLM model and endpoint, daemon, memory, perception, gateway, …) with current values. Use this to discover the exact key before changing anything. Optionally narrow by `category`. This is the only correct way to inspect LeapFlow configuration — never read config files from disk.\n- **config_get**(key): Read one LeapFlow setting by key (e.g. 'llm.model', 'llm.base_url', 'daemon.log_level'), returning its current value, type, scopes, and whether a change needs a daemon restart. There is no 'llm.provider' key: provider behavior is inferred from the OpenAI-compatible 'llm.base_url'. Never read LeapFlow config files from disk — use this.\n- **config_set**(key, value, scope) [capability_expand category: config]: Change one LeapFlow setting by key, e.g. switch an OpenAI-compatible LLM with key='llm.model' and key='llm.base_url'. There is no 'llm.provider' key: provider behavior is inferred from the endpoint. Values are validated and coerced; credentials are stored in the vault automatically. Call config_list or config_get first if unsure of the key. The result states whether a `leap daemon restart` is required. Never edit LeapFlow config files directly.\n- **web_fetch**(url, select, timeout, max_bytes): Read a URL over HTTP(S) and get back extracted, context-sized content: parsed JSON for API endpoints, readable text plus links for web pages. Use this for anything on the internet — prices, docs, releases, articles — instead of running curl through shell_run: it reports real HTTP status codes, retries rate limits on its own, and is a plain read so a retry is always safe. For JSON APIs pass `select` with a dotted path (e.g. 'chart.result.0.meta') to return just that part instead of the whole payload.\n- **memory_search**(query, limit): Search agent memory for relevant past experiences, observations, and facts.\n- **memory_add**(content, kind) [capability_expand category: write]: Store a new observation or insight in memory for future reference.\n- **research_note**(kind, text): Record a compact, structured note about the current task's state so it survives context compression on long / multi-step tasks. Use for durable findings, open questions still to resolve, decisions / excluded paths, and the immediate next step. One concise sentence per note.\n- **capability_expand**(category): Fetch the full callable schema for every tool in a capability category (e.g. 'hub', 'gateway', 'desktop', 'delegate', 'file', 'memory', 'skill'). The compact tool index always lists every registered tool by name and a one-line summary, but only a static low-risk subset is directly callable each turn. If you need a tool from the index that is not yet callable, call capability_expand with its category first; the matching tools become callable in this turn. Never invent a tool name — expand the category instead.\n- **delegate_task**(goal, context) [capability_expand category: delegate]: Delegate a complex sub-task to an isolated subagent. The subagent gets a fresh context and restricted tool access. Use when a task is self-contained and can be solved independently.\n- **schedule_reentry**(kind, reason, delay_seconds, event_match, max_reentries, deadline_seconds): Register a re-entry so this task can resume later from its current orientation (findings / open questions / next step). Use when work must pause and continue after a delay (kind=time) or when a matching platform event arrives (kind=event), instead of finishing now. The research-ledger state is carried over automatically.\n- **hub_push**(skill_name, visibility, version) [capability_expand category: hub]: Push a local skill to the ModelScope Hub for sharing or backup.\n- **hub_pull**(repo_id, version) [capability_expand category: hub]: Pull a skill from the ModelScope Hub to install locally.\n- **hub_search**(query) [capability_expand category: hub]: Search for skills on the Hub by keyword or description.\n- **hub_sync**(mode, dry_run) [capability_expand category: hub]: Preview or execute sync between local skills and Hub.\n- **platform_action**(platform, action, payload, backend_kind) [capability_expand category: gateway]: Execute an exact registered business action on an external platform through LeapFlow's App Connector layer. Actions must be copied from the App Connector Capability Index and are addressed as domain.operation, e.g. im.send_message or docs.create_markdown. All business fields (chat_id, text, query, etc.) MUST be placed inside `payload`, never at the top level. Example: {\"platform\":\"feishu\",\"action\":\"im.send_message\",\"payload\":{\"chat_id\":\"oc_xxx\",\"text\":\"hello\"}}. Do not invent action names, do not use management actions such as list/guide/connect/status here.\n- **platform_connect**(action, platform, credentials, options, checkpoint) [capability_expand category: gateway]: List, guide, connect, disconnect, remove, or check status for external platforms using the App Connector management namespace. Supports REST and CLI backends. Use this for management actions such as list/guide/preflight/connect/status; use platform_action only for exact registered business actions.\n- **gateway_send**(platform, chat_id, text, thread_id) [capability_expand category: gateway]: Send a message to a connected external platform (Feishu group, Telegram chat, DingTalk conversation, etc.). Requires the platform to be connected via gateway_connect first. Use gateway_connect with action='list' to see connected platforms and available chat IDs.\n- **gateway_connect**(action, platform, credentials, options) [capability_expand category: gateway]: Connect, configure, or manage external platform integrations (Feishu, DingTalk, Telegram, Slack, Discord, etc.). Conversational flow: 1) call 'guide' to get setup steps + required fields, 2) present the steps to the user and ask for ALL required credentials in a single message, 3) call 'connect' with the credentials. Goal: complete in 1–2 user turns. NEVER include credential values in your text response.\n- **plugin_list**(): List the live plugin registry across Tool/Gateway/LLM subsystems for plugin management: which plugins are loaded, their category and fiber lifecycle state. For questions about LeapFlow's own capabilities or whether it supports X, use self_describe instead.\n- **plugin_status**(plugin_id): Get detailed status of a specific plugin: its declared category, runtime dependencies, contributed tools, and fiber lifecycle state.\n- **plugin_versions**(plugin_id): List recorded source versions and active pointer for a profile-scoped plugin.\n- **plugin_propose**(requested_capability, plugin_id, proposed_tools, test_cases, risk_level, evidence): Create a side-effect-free PluginProposal from explicit capability-gap evidence. Use this before plugin_generate when a missing capability should be reviewed. Does not call an LLM, write files, or install anything.\n- **assess_compatibility**(manifest, source_path) [capability_expand category: plugin_management]: Assess whether a foreign plugin manifest or real DSH source bundle is compatible with LeapFlow. A source bundle assessment is static: runtime_ready stays false until restricted Node discovery during plugin_install. Returns component-level verdicts and limitations.\n- **plugin_generate**(plugin_id, description, proposal_id) [capability_expand category: system]: Generate a new ToolPlugin from a natural-language capability description. The LLM produces code that conforms to the ToolPlugin Protocol; it is then rigorously validated (syntax, structure, import, protocol conformance). The generated source is stored in CAS and receives explicit content approval. It DOES NOT install the plugin — installation requires a second, mutation-specific approval via plugin_install.\n- **plugin_install**(plugin_id, code, marketplace_name, source_path, proposal_id, version_label) [capability_expand category: system]: Install a plugin from exactly one source: validated Python code, the configured Python marketplace, or a real DSH source bundle. DSH bundles run restricted Node discovery before registration; only public host tools are installed and client UI limitations are reported. Writes profile-scoped state and mutates the process-global registry. REQUIRES APPROVAL.\n- **plugin_rollback**(plugin_id, version) [capability_expand category: system]: Rollback a profile-scoped plugin to a recorded source version and reload it. REQUIRES APPROVAL.\n- **plugin_reload**(plugin_id, version_label) [capability_expand category: system]: Hot-reload a plugin at runtime. Disposes the old plugin fiber, re-imports its module, and registers a fresh instance. Existing in-flight turns are unaffected (snapshot isolation). REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_disable**(plugin_id) [capability_expand category: system]: Disable a plugin by disposing its fiber, removing its tools from the runtime registry. Cannot disable self_management itself. REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_remove**(plugin_id, delete_source) [capability_expand category: system]: Terminally remove a plugin: dispose its fiber, unregister its tools, remove reload metadata, and optionally delete its profile-scoped source file. Cannot remove self_management itself. REQUIRES APPROVAL.\n- **plugin_unquarantine**(plugin_id) [capability_expand category: plugin_management]: Restore a quarantined plugin to probation status for re-evaluation. Unfreezes the trust ledger, transitions the proposal back to PROBATION, and reloads the plugin. REQUIRES APPROVAL.\n- **plugin_enable**(plugin_id) [capability_expand category: system]: Re-enable a previously disabled plugin by reloading its module and registering a fresh instance. REQUIRES APPROVAL.\n- **self_describe**(facet): Answer questions about LeapFlow ITSELF from live runtime evidence — its identity/version/model, what tools and plugins it has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of self-awareness. Prefer this over reading LeapFlow's own source code. Pick facet=capabilities for 'what can you do / do you support X', identity for version/model, runtime for context/posture, or all for a full self-check when explicitly requested.\n- **runtime_snapshot**(): Lightweight (~150 token) flat snapshot of current runtime state: model, context budget, posture, disclosure level, turn count, cache hit rate, uptime, tool count, and pending approvals.\n- **schedule_create**(trigger_expression, instruction, execution_mode, max_retries, delivery_target) [capability_expand category: scheduler]: Create a new scheduled task. Specify a trigger expression (e.g. '30m', 'every 2h', '0 9 * * *') and the instruction to execute on each trigger. Optionally provide an execution mode and a delivery target to receive notifications.\n- **schedule_list**() [capability_expand category: scheduler]: List all scheduled tasks with their current state, trigger, and next due time.\n- **schedule_status**(task_id) [capability_expand category: scheduler]: Get detailed status and recent execution history for a scheduled task.\n- **schedule_pause**(task_id) [capability_expand category: scheduler]: Pause a scheduled task so it stops firing without being cancelled.\n- **schedule_resume**(task_id) [capability_expand category: scheduler]: Resume a paused scheduled task — re-arms it and recalculates next due time.\n- **schedule_cancel**(task_id) [capability_expand category: scheduler]: Cancel a scheduled task permanently.\n- **tool_search**(query, max_results): Search all registered tools by keyword relevance. Returns a ranked list of matching tools with name, category, and summary. Use this when you need a tool but are unsure of its exact name or category.\n- **tool_describe**(tool_name): Get the full callable schema of a single tool by exact name. Returns the tool's description, parameters, and metadata. Use after tool_search to inspect a specific tool before calling it.\n- **session_search**(query, limit) [capability_expand category: unclassified]: Search past conversation sessions in the current workspace for relevant context.\n- **session_list**(limit): List recent conversation sessions in the current workspace with stable ids, titles, dates, and summaries. Use for browsing past tasks or when user asks to see history without specific search terms. No keywords needed — returns chronological list.\n- **session_detail**(session_id, limit, offset, include_inactive) [capability_expand category: unclassified]: Read a paginated persisted transcript for one past conversation session in the current workspace. Use after session_list or session_search returns a session_id.\n- **click**(element_index) [capability_expand category: desktop]: Click a UI element by its element_index (from the latest observe_ui snapshot)\n- **get_clipboard**() [capability_expand category: desktop]: Read current clipboard text content\n- **list_apps**(filter, running_only) [capability_expand category: desktop]: List available applications on this system. Use to discover correct bundle_id before switch_app.\n- **list_windows**() [capability_expand category: desktop]: List all top-level windows with pid, window_id, title, and per-window state (minimized, on-screen). Call this first to pick the pid and window_id that observe_ui and other window tools require.\n- **observe_ui**(pid, window_id, query) [capability_expand category: desktop]: Snapshot one window's actionable UI elements, each tagged with an element_index for click/right_click/read_text. Re-observe after actions — indices belong to one snapshot. Requires the window's pid and window_id from list_windows.\n- **open_url**(url, app_id) [capability_expand category: desktop]: Open a URL in the default or specified browser\n- **read_text**(element_index) [capability_expand category: desktop]: Read the text content of a specific UI element from the latest snapshot\n- **right_click**(element_index) [capability_expand category: desktop]: Right-click a UI element to open its context menu. Returns visible menu items.\n- **screenshot**(pid, window_id) [capability_expand category: desktop]: Capture a screenshot for visual verification. With pid + window_id captures that window (works across all displays); defaults to the last observed window, or the full desktop when no window has been observed.\n- **scroll**(element_index, direction, amount, pid, window_id) [capability_expand category: desktop]: Scroll a scrollable area of a window. Omit element_index to scroll the window's focused/page scroller; pass one to scroll an exact element from the latest snapshot.\n- **select_text**(element_index) [capability_expand category: desktop]: Select all text in a UI element (focus + select-all, for subsequent copy)\n- **set_clipboard**(text) [capability_expand category: desktop]: Write text to the clipboard\n- **shortcut**(keys) [capability_expand category: desktop]: Execute a keyboard shortcut\n- **switch_app**(app_id) [capability_expand category: desktop]: Switch to an app (launch if needed, activate, verify)\n- **type_text**(text) [capability_expand category: desktop]: Type text into the currently focused element\n- **wait**(seconds) [capability_expand category: desktop]: Wait for a specified duration before continuing\n- **wait_until**(condition, pid, window_id, timeout, poll_interval) [capability_expand category: desktop]: Wait until a UI condition is met (polls UI tree). Returns elements when found or on timeout.\n- **wait_until_stable**(timeout, poll_interval, pid, window_id) [capability_expand category: desktop]: Wait until the UI stops changing (element set stabilizes across polls).\n\n## App Connector Capability Index\nLeapFlow can onboard and manage external apps through `platform_connect` and execute exact registered business actions through `platform_action`.\nFor requests about connecting, setting up, configuring, enabling, or managing a supported app, use `platform_connect` first instead of generating SDK/Webhook sample code.\n`platform_connect.action` is the management namespace: list, guide, preflight, connect, disconnect, remove, status, events_start, events_stop, events_status.\n`platform_action.action` is only for exact registered platform business actions listed below, such as `im.send_message`; never use management actions like `list` or `guide` there.\nAll business fields MUST go inside `payload`; top-level keys are only `platform`, `action`, and `payload`.\nDo not invent platform IDs or platform action names. If the needed action is not listed, ask for discovery/clarification instead of guessing.\nUse `platform_connect` with `action='guide'` and the matching `platform` to start onboarding; use `action='list'` when the app is unclear.\nWhen a pending onboarding state is present, continue from that state with `platform_connect` instead of asking the user to restate the app.\nSupported apps:\n- `api_server`: API Server (OpenAI Compatible) (category=api; backend=adapter; platform_action actions=none registered)\n- `dingtalk`: 钉钉 (DingTalk) (category=im; backend=adapter; platform_action actions=none registered)\n- `feishu`: 飞书 (Feishu/Lark) (category=im; backend=cli)\n - `calendar.create_event` payload={summary*, start_time*, end_time*, attendees} [write/high]\n - `docs.create_markdown` payload={title*, markdown*, folder_token} [write/medium]\n - `drive.search` payload={query*, limit} [read/medium]\n - `im.add_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.download_resource` payload={message_id*, file_key*, type*} [read/low]\n - `im.get_messages` payload={message_ids*} [read/low]\n - `im.list_chats` payload={page_size} [read/low]\n - `im.list_messages` payload={chat_id*} [read/low]\n - `im.list_thread_messages` payload={thread*} [read/low]\n - `im.remove_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.reply_message` payload={message_id*, text*} [send/high]\n - `im.search_chats` payload={query*, page_size} [read/low]\n - `im.search_messages` payload={query*} [read/medium]\n - `im.send_message` payload={chat_id*, text*, thread_id} [send/high]\n - `im.update_card` payload={token*, card*} [write/medium]\n - `im.update_message` payload={message_id*, text*} [write/medium]\n - `mail.search_unread` payload={query, limit} [read/high]\n - `sheets.append_row` payload={spreadsheet_token*, sheet_id*, values*} [write/medium]\n - `task.create` payload={title*, description, due_time} [write/medium]\n- `telegram`: Telegram (category=social; backend=adapter; platform_action actions=none registered)\n- `webhook`: Webhook (Generic) (category=webhook; backend=adapter; platform_action actions=none registered)\n\n## Tool Usage\nTools are normally invoked through the native function-calling mechanism, not by writing JSON in your reply\ntext. Only if the provider signals that native function calling is unavailable for this turn, fall back to a\nsingle JSON code block: `{\"name\": \"tool_name\", \"arguments\": {\"key\": \"value\"}}` — use this fallback format\nonly, never both. Only call a tool whose exact name appears in the tool index above and is currently callable\n(or reachable via `capability_expand`). Never invent, rename, alias, or guess a tool name, platform ID, or\nplatform action from argument shape or wording — if the index does not list it, it does not exist.\n`platform_connect.action` (list/guide/preflight/connect/disconnect/remove/status/events_start/events_stop/\nevents_status) is the App Connector management namespace; `platform_action.action` only accepts exact\nregistered business actions such as `im.send_message` shown in the App Connector Capability Index — never mix\nthe two namespaces. If a tool call returns an unknown/unavailable result, use the returned suggestions or\navailable names for a single retry instead of trying further variations of the same guess.\n\n**Side-effect action rule** (`platform_action` with effect=send/write/execute):\n- Call each unique action+payload **exactly once**. Never include duplicates in the same turn.\n- Once the result returns `\"completed\": true`, that action is DONE for this task. Do NOT call it again\n in any subsequent turn — immediately summarize the result for the user instead.\n- The system enforces idempotency: duplicate calls are blocked and will not execute.\n- If the user explicitly requests sending/writing multiple times, use distinct payloads per call.\n\n**Resource identifier provenance rule**:\n- NEVER fabricate, guess, or infer resource identifiers (chat_id, message_id, file_key, user_id, etc.).\n Every resource ID used in a side-effect action MUST come from a successful API response in this session.\n- If a read/list action fails (e.g. authorization error), you do NOT have valid resource IDs.\n Report the failure to the user — do NOT attempt the dependent write/send action with a guessed ID.\n- When a tool result contains `\"llm_instruction\"`, follow it exactly.\n\n## Guidelines\n1. **Direct answers first**: If you already know the answer, respond directly without tools.\n2. **Avoid redundant tool calls**: Do not call the same tool with the same arguments more than once in the same user turn. When an existing tool result already answers the user's request, stop calling tools and answer directly.\n3. **Use tools proactively**: When the user asks about files, time, system state, or needs actions performed, use the appropriate tool.\n4. **Chain tools when needed**: You can call multiple tools in sequence (e.g., list files → read file → summarize).\n5. **Handle failures gracefully**: If a tool fails, explain what went wrong and suggest alternatives. If it failed because of an unknown tool/platform/action name, retry once with an exact name from the returned suggestions, then explain rather than keep guessing.\n6. **Summarize results naturally**: After tool execution, synthesize the results into a helpful answer rather than dumping raw output.\n7. **Stay conversational**: Maintain a natural, helpful tone. Acknowledge context from earlier in the conversation.\n8. **Recall past work**:\n - Broad queries (\"之前做了什么\", \"列出任务\"): answer from the \"Recent Task History\" section already in your context. If insufficient, call session_list.\n - Specific lookups (\"上次那个配置怎么改的\"): call session_search with relevant phrases (NOT single characters).\n - Do NOT call search tools repeatedly with keyword fragments. One well-phrased call is better than ten fragmented ones.\n\n## Coding & Verification\nWhen working with code, prefer the precise built-in tools over ad-hoc shell: use `repo_map` to orient in an\nunfamiliar project, `code_search`/`file_find` to locate and `code_intel` for symbols, `edit_file` (anchored\nsearch-replace, or a unified `diff`) to change files — never rewrite a whole file to change a few lines — and\n`git_query` to inspect diffs/log. After edits, check `syntax_ok` in the result and run `test_run`/`lint_check`\nbefore declaring the work done. Batch independent read-only calls (search/read/query) into a single turn — they\nrun in parallel.\n\n## Presentation Style\n1. **Polished Markdown only**: Format user-facing answers with clean Markdown headings, short paragraphs, and concise bullets. Use tables only when they improve comparison or scanning.\n2. **Terminal-friendly layout**: Keep lines readable in a TUI; avoid dense walls of text, deeply nested lists, oversized ASCII art, or heavy visual blocks.\n3. **Elegant emphasis**: Use bold text sparingly for key terms and conclusions. Avoid excessive emojis, decorative symbols, repeated separators, or visual noise.\n4. **Theme-safe colors**: Do not emit ANSI escape codes, HTML color tags, Rich markup, or hardcoded color names. Rely on the TUI theme to render Markdown professionally.\n5. **No leaked tool protocol**: Never show tool-call JSON, internal schemas, raw observations, tool result payloads, or hidden reasoning in the final answer unless the user explicitly asks for raw/debug output. Treat any prior `{\"name\": ..., \"arguments\": ...}` blocks and `Tool result (...)` messages as internal execution context only.\n6. **Professional closure**: End with a concise conclusion or next step when helpful; avoid rambling after the useful answer is complete.\n\nWhen finished with all tool calls, respond normally without a JSON block, tool-call transcript, or process log.\n\n## Task Contract\n- Task ID: turn-1\n- Original user request: Try the missing_json_pretty_e2e tool so LeapFlow records a capability gap.\n- Workspace root: \n- Allowed roots: \n- Treat relative project paths as relative to the workspace root; never infer `.` as the project root when a workspace root is provided.\n- Workspace boundary is enforced by tools: do not read, search, edit, or run commands against paths outside the allowed roots unless the user explicitly requests an external path and the tool/approval policy permits it.\n- LeapFlow workspace config is optional at `/.leapflow/config.yaml`; runtime config is loaded from `~/.leapflow/config/user.yaml` and `~/.leapflow/profiles//config/*.yaml`.\n- Preserve this task contract across summarization, compression, tool loops, and memory retrieval.\n" + }, + { + "role": "system", + "content": "\n## Recent Session Summary\n- [user] Try the missing_json_pretty_e2e tool so LeapFlow records a capability gap.\n" + }, + { + "role": "user", + "content": "Try the missing_json_pretty_e2e tool so LeapFlow records a capability gap.\nTry the missing_json_pretty_e2e tool so LeapFlow records a capability gap." + }, + { + "role": "assistant", + "content": "", + "tool_calls": [ + "missing_json_pretty_e2e" + ] + }, + { + "role": "tool", + "content": "{\"ok\": false, \"error\": \"Unknown tool: missing_json_pretty_e2e\", \"error_type\": \"unknown_tool\", \"retryable\": true}", + "tool_result": true + }, + { + "role": "user", + "content": "SYSTEM: The previous tool call used an unavailable tool name. Original tool: missing_json_pretty_e2e. Resolution: unknown (no exact canonical tool name match). Suggested canonical tools: text_search, text_replace, research_note, gateway_send. Available tools include: assess_compatibility, capability_expand, code_intel, code_search, config_get, config_list, config_set, delegate_task, edit_file, env_info, file_find, file_list. Retry once using an exact canonical tool name from the available list and valid arguments. Do not invent tool names, use aliases, or infer a tool from argument shape; answer without a tool if no exact tool fits." + }, + { + "role": "user", + "content": "SYSTEM: Context utilization is high (25,656/32,768 estimated tokens, round 2). Prefer summaries, targeted reads, and final synthesis." + } + ], + "tools": [ + "assess_compatibility", + "capability_expand", + "click", + "code_intel", + "code_search", + "config_get", + "config_list", + "config_set", + "delegate_task", + "edit_file", + "env_info", + "file_find", + "file_list", + "file_read", + "file_write", + "gateway_connect", + "gateway_send", + "get_clipboard", + "git_query", + "git_write", + "hub_pull", + "hub_push", + "hub_search", + "hub_sync", + "lint_check", + "list_apps", + "list_windows", + "memory_add", + "memory_search", + "observe_ui", + "open_url", + "platform_action", + "platform_connect", + "plugin_disable", + "plugin_enable", + "plugin_generate", + "plugin_install", + "plugin_list", + "plugin_propose", + "plugin_reload", + "plugin_remove", + "plugin_rollback", + "plugin_status", + "plugin_unquarantine", + "plugin_versions", + "read_text", + "repo_map", + "research_note", + "right_click", + "runtime_snapshot", + "schedule_cancel", + "schedule_create", + "schedule_list", + "schedule_pause", + "schedule_reentry", + "schedule_resume", + "schedule_status", + "scm_sync", + "screenshot", + "scroll", + "select_text", + "self_describe", + "session_detail", + "session_list", + "session_search", + "set_clipboard", + "shell_run", + "shortcut", + "skill_file_read", + "skill_list_files", + "skill_view", + "skills_list", + "switch_app", + "terminal_close", + "terminal_list", + "terminal_open", + "terminal_read", + "terminal_send", + "test_run", + "text_replace", + "text_search", + "time_get", + "tool_describe", + "tool_search", + "type_text", + "wait", + "wait_until", + "wait_until_stable", + "web_fetch" + ] + }, + "responses": [ + { + "status": 200, + "content_type": "application/json", + "body": "{\"id\": \"chatcmpl-cassette\", \"object\": \"chat.completion\", \"model\": \"cassette-model\", \"choices\": [{\"index\": 0, \"message\": {\"role\": \"assistant\", \"content\": \"Observed the missing JSON pretty tool.\"}, \"finish_reason\": \"stop\"}], \"usage\": {\"prompt_tokens\": 64, \"completion_tokens\": 16, \"total_tokens\": 80}}" + } + ] +} diff --git a/tests/_fixtures/cassettes/r7_adaptive_plugin_loop/cassette-model-55a7650db1502566.cassette.json b/tests/_fixtures/cassettes/r7_adaptive_plugin_loop/cassette-model-55a7650db1502566.cassette.json deleted file mode 100644 index d2510476..00000000 --- a/tests/_fixtures/cassettes/r7_adaptive_plugin_loop/cassette-model-55a7650db1502566.cassette.json +++ /dev/null @@ -1,67 +0,0 @@ -{ - "fingerprint": "55a7650db150256684975db96e1149c516772d0d2d19c4333610abaece9371af", - "note": "captured in seed mode", - "request": { - "model": "cassette-model", - "stream": false, - "messages": [ - { - "role": "system", - "content": "You are LeapFlow, an intelligent assistant that can both converse naturally and take real actions on the user's computer.\n\n## Capabilities\nThe tool index below lists **every** registered tool by name and a one-line summary — this is the complete\ncapability contract; nothing else exists. Only a subset is directly callable this turn (via native tool calling,\nnot a JSON block in your reply). If you need a tool from the index that is not yet callable, call\n`capability_expand` with its category name first — the matching tools become callable immediately after.\nWhen the user asks what LeapFlow itself supports, whether it supports plugins, or which runtime capabilities\nare available, use the live capability evidence exposed by `plugin_list` before making capability claims; report\nconfiguration-dependent or unavailable capabilities as limitations instead of inferring from documentation.\n- **text_search**(text, pattern): Search for a regex pattern in text.\n- **text_replace**(text, old, new, count) [capability_expand category: write]: Replace occurrences of a substring in text.\n- **time_get**(): Get current date and time.\n- **env_info**(): Get system environment information (OS, Python version, cwd).\n- **skills_list**(query, category, source): List available learned skills. Use when user asks about capabilities or you need a specific skill.\n- **skill_view**(name): View the full content of a specific skill document.\n- **code_intel**(path, operation): Precise document symbols (outline) for a source file: classes, functions, and methods with line ranges. Python uses an exact AST parse; other languages use a keyword-prefix scan. Prefer over file_read mode=symbols for accurate navigation before editing. Read-only.\n- **repo_map**(path): Compact project orientation for a repository root: languages, detected test/lint commands, top-level structure, entry points, manifest, and VCS branch. Call this first when entering an unfamiliar codebase. Read-only.\n- **scm_sync**(action, cwd, remote, pull_ref, push_ref, timeout) [capability_expand category: scm]: Run a typed git SCM action. Use this instead of shell_run for git pull/push/status. For 'pull origin main then push', set action='pull_then_push', remote='origin', pull_ref='main', and omit push_ref so LeapFlow pushes the current local branch.\n- **git_query**(action, cwd, ref, path, staged, max_count, stat): Read-only structured git inspection: action=diff|log|status|branch|show. Prefer over shell_run for reading repo state — output is clipped, redacted, and log/branch are parsed into structured fields. Use scm_sync for pull/push.\n- **git_write**(action, cwd, message, stage_all, name, ref, create) [capability_expand category: scm]: Mutating git actions: action=commit (message, stage_all), branch (create+switch), checkout (switch; create=true for -b). Approval-gated. Use scm_sync for pull/push and git_query for reads.\n- **test_run**(command, cwd, timeout): Run the project's test suite and return structured results (framework, passed/failed counts, failing tests). Auto-detects the runner (pytest/npm/go/cargo) or uses a configured/explicit command; executes via the governed shell. ok=true means the runner executed — see 'success' for pass/fail.\n- **lint_check**(command, cwd, timeout): Run the project's linter and return a structured clean/issue result. Auto-detects the linter (ruff/eslint/go vet/clippy) or uses a configured/explicit command; executes via the governed shell. ok=true means the linter ran — see 'clean'.\n- **file_list**(path, pattern, depth): List files and directories at a given path. Use depth=1 or depth=2 to get a recursive tree in one call instead of listing each sub-directory separately.\n- **file_read**(path, max_lines, start_line, max_chars, mode): Read text file content with adaptive context governance. For large or unfamiliar files, prefer mode='outline' or mode='symbols' first, then use mode='raw' with start_line/max_lines for the specific range you actually need. For LeapFlow's own settings, use config_list / config_get / config_set — its config files are outside the workspace and not readable here.\n- **file_write**(path, content, mode) [capability_expand category: write]: Write content to a file (overwrite or append).\n- **code_search**(pattern, patterns, path, glob, ignore_case, multiline, max_results, context_lines): Search file CONTENTS by regex pattern across a directory tree (ripgrep-backed). Requires a regex pattern. NOT for listing or browsing directory contents — use file_list for that. Prefer this over shell_run grep: faster, skips VCS/dependency/build dirs, and returns structured path:line:column matches. Batch related lookups into ONE call via `patterns` (OR-combined, single pass) instead of issuing several separate searches. Use file_read for the surrounding context of a hit.\n- **file_find**(glob, path, max_results): Find files by a recursive glob pattern under a base path (e.g. '**/test_*.py' or '*.md'). Prefer this over shell_run find; skips VCS/dependency/build dirs.\n- **edit_file**(path, edits, dry_run, diff) [capability_expand category: write]: Apply targeted, anchored search-replace edits to an EXISTING text file (use file_write to create/overwrite). Each edit is {original_text, new_text, replace_all?}; original_text must match exactly and uniquely (or set replace_all) — a non-unique or missing anchor is rejected so files are never corrupted. Set dry_run to preview. Alternatively pass a unified 'diff' to apply its hunks as anchored edits. Far cheaper and safer than rewriting a whole file.\n- **shell_run**(command, cwd, timeout) [capability_expand category: shell]: Execute a one-shot shell command with timeout protection. Runs in the active workspace; paths resolving outside it are refused. Reach for a structured tool first when one fits — web_fetch for anything over HTTP(S), git_query/scm_sync for git, code_search/file_find/file_read for the repo, config_get/config_set for LeapFlow's own settings — because those report typed results, while a failed shell command can only be diagnosed from its exit code and stderr. Every shell run counts as an external side effect, so a failure stops the rest of the batch and is not retried automatically.\n- **terminal_open**(command, cwd, shell) [capability_expand category: terminal]: Open a PERSISTENT shell session (REPL/dev server/watch), returning a session_id for terminal_send/read/close. Disabled unless tools.terminal_session_enabled is set. For one-shot commands use shell_run instead.\n- **terminal_send**(session_id, input, wait) [capability_expand category: terminal]: Send a line of input to a persistent terminal session and return output captured shortly after.\n- **terminal_read**(session_id, wait): Drain buffered output from a persistent terminal session (optionally waiting briefly first).\n- **terminal_close**(session_id) [capability_expand category: terminal]: Terminate a persistent terminal session and release its process group.\n- **terminal_list**(): List active persistent terminal sessions.\n- **config_list**(category, limit): List LeapFlow's own writable settings (LLM model and endpoint, daemon, memory, perception, gateway, …) with current values. Use this to discover the exact key before changing anything. Optionally narrow by `category`. This is the only correct way to inspect LeapFlow configuration — never read config files from disk.\n- **config_get**(key): Read one LeapFlow setting by key (e.g. 'llm.model', 'llm.base_url', 'daemon.log_level'), returning its current value, type, scopes, and whether a change needs a daemon restart. There is no 'llm.provider' key: provider behavior is inferred from the OpenAI-compatible 'llm.base_url'. Never read LeapFlow config files from disk — use this.\n- **config_set**(key, value, scope) [capability_expand category: config]: Change one LeapFlow setting by key, e.g. switch an OpenAI-compatible LLM with key='llm.model' and key='llm.base_url'. There is no 'llm.provider' key: provider behavior is inferred from the endpoint. Values are validated and coerced; credentials are stored in the vault automatically. Call config_list or config_get first if unsure of the key. The result states whether a `leap daemon restart` is required. Never edit LeapFlow config files directly.\n- **web_fetch**(url, select, timeout, max_bytes): Read a URL over HTTP(S) and get back extracted, context-sized content: parsed JSON for API endpoints, readable text plus links for web pages. Use this for anything on the internet — prices, docs, releases, articles — instead of running curl through shell_run: it reports real HTTP status codes, retries rate limits on its own, and is a plain read so a retry is always safe. For JSON APIs pass `select` with a dotted path (e.g. 'chart.result.0.meta') to return just that part instead of the whole payload.\n- **memory_search**(query, limit): Search agent memory for relevant past experiences, observations, and facts.\n- **memory_add**(content, kind) [capability_expand category: write]: Store a new observation or insight in memory for future reference.\n- **research_note**(kind, text): Record a compact, structured note about the current task's state so it survives context compression on long / multi-step tasks. Use for durable findings, open questions still to resolve, decisions / excluded paths, and the immediate next step. One concise sentence per note.\n- **capability_expand**(category): Fetch the full callable schema for every tool in a capability category (e.g. 'hub', 'gateway', 'desktop', 'delegate', 'file', 'memory', 'skill'). The compact tool index always lists every registered tool by name and a one-line summary, but only a static low-risk subset is directly callable each turn. If you need a tool from the index that is not yet callable, call capability_expand with its category first; the matching tools become callable in this turn. Never invent a tool name — expand the category instead.\n- **delegate_task**(goal, context) [capability_expand category: delegate]: Delegate a complex sub-task to an isolated subagent. The subagent gets a fresh context and restricted tool access. Use when a task is self-contained and can be solved independently.\n- **schedule_reentry**(kind, reason, delay_seconds, event_match, max_reentries, deadline_seconds): Register a re-entry so this task can resume later from its current orientation (findings / open questions / next step). Use when work must pause and continue after a delay (kind=time) or when a matching platform event arrives (kind=event), instead of finishing now. The research-ledger state is carried over automatically.\n- **hub_push**(skill_name, visibility, version) [capability_expand category: hub]: Push a local skill to the ModelScope Hub for sharing or backup.\n- **hub_pull**(repo_id, version) [capability_expand category: hub]: Pull a skill from the ModelScope Hub to install locally.\n- **hub_search**(query) [capability_expand category: hub]: Search for skills on the Hub by keyword or description.\n- **hub_sync**(mode, dry_run) [capability_expand category: hub]: Preview or execute sync between local skills and Hub.\n- **platform_action**(platform, action, payload, backend_kind) [capability_expand category: gateway]: Execute an exact registered business action on an external platform through LeapFlow's App Connector layer. Actions must be copied from the App Connector Capability Index and are addressed as domain.operation, e.g. im.send_message or docs.create_markdown. All business fields (chat_id, text, query, etc.) MUST be placed inside `payload`, never at the top level. Example: {\"platform\":\"feishu\",\"action\":\"im.send_message\",\"payload\":{\"chat_id\":\"oc_xxx\",\"text\":\"hello\"}}. Do not invent action names, do not use management actions such as list/guide/connect/status here.\n- **platform_connect**(action, platform, credentials, options, checkpoint) [capability_expand category: gateway]: List, guide, connect, disconnect, remove, or check status for external platforms using the App Connector management namespace. Supports REST and CLI backends. Use this for management actions such as list/guide/preflight/connect/status; use platform_action only for exact registered business actions.\n- **gateway_send**(platform, chat_id, text, thread_id) [capability_expand category: gateway]: Send a message to a connected external platform (Feishu group, Telegram chat, DingTalk conversation, etc.). Requires the platform to be connected via gateway_connect first. Use gateway_connect with action='list' to see connected platforms and available chat IDs.\n- **gateway_connect**(action, platform, credentials, options) [capability_expand category: gateway]: Connect, configure, or manage external platform integrations (Feishu, DingTalk, Telegram, Slack, Discord, etc.). Conversational flow: 1) call 'guide' to get setup steps + required fields, 2) present the steps to the user and ask for ALL required credentials in a single message, 3) call 'connect' with the credentials. Goal: complete in 1–2 user turns. NEVER include credential values in your text response.\n- **plugin_list**(): List the live plugin registry and cross-subsystem capability evidence. Use this before answering questions about whether LeapFlow supports plugins, self-evolution, plugin installation, hot reload, versioning, or other runtime capabilities.\n- **plugin_status**(plugin_id): Get detailed status of a specific plugin: its declared category, runtime dependencies, contributed tools, and fiber lifecycle state.\n- **plugin_versions**(plugin_id): List recorded source versions and active pointer for a profile-scoped plugin.\n- **plugin_propose**(requested_capability, plugin_id, proposed_tools, test_cases, risk_level, evidence): Create a side-effect-free PluginProposal from explicit capability-gap evidence. Use this before plugin_generate when a missing capability should be reviewed. Does not call an LLM, write files, or install anything.\n- **assess_compatibility**(manifest, source_path) [capability_expand category: plugin_management]: Assess whether a foreign plugin manifest or real DSH source bundle is compatible with LeapFlow. A source bundle assessment is static: runtime_ready stays false until restricted Node discovery during plugin_install. Returns component-level verdicts and limitations.\n- **plugin_generate**(plugin_id, description, proposal_id) [capability_expand category: system]: Generate a new ToolPlugin from a natural-language capability description. The LLM produces code that conforms to the ToolPlugin Protocol; it is then rigorously validated (syntax, structure, import, protocol conformance). The generated source is stored in CAS and receives explicit content approval. It DOES NOT install the plugin — installation requires a second, mutation-specific approval via plugin_install.\n- **plugin_install**(plugin_id, code, marketplace_name, source_path, proposal_id, version_label) [capability_expand category: system]: Install a plugin from exactly one source: validated Python code, the configured Python marketplace, or a real DSH source bundle. DSH bundles run restricted Node discovery before registration; only public host tools are installed and client UI limitations are reported. Writes profile-scoped state and mutates the process-global registry. REQUIRES APPROVAL.\n- **plugin_rollback**(plugin_id, version) [capability_expand category: system]: Rollback a profile-scoped plugin to a recorded source version and reload it. REQUIRES APPROVAL.\n- **plugin_reload**(plugin_id, version_label) [capability_expand category: system]: Hot-reload a plugin at runtime. Disposes the old plugin fiber, re-imports its module, and registers a fresh instance. Existing in-flight turns are unaffected (snapshot isolation). REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_disable**(plugin_id) [capability_expand category: system]: Disable a plugin by disposing its fiber, removing its tools from the runtime registry. Cannot disable self_management itself. REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_remove**(plugin_id, delete_source) [capability_expand category: system]: Terminally remove a plugin: dispose its fiber, unregister its tools, remove reload metadata, and optionally delete its profile-scoped source file. Cannot remove self_management itself. REQUIRES APPROVAL.\n- **plugin_unquarantine**(plugin_id) [capability_expand category: plugin_management]: Restore a quarantined plugin to probation status for re-evaluation. Unfreezes the trust ledger, transitions the proposal back to PROBATION, and reloads the plugin. REQUIRES APPROVAL.\n- **plugin_enable**(plugin_id) [capability_expand category: system]: Re-enable a previously disabled plugin by reloading its module and registering a fresh instance. REQUIRES APPROVAL.\n- **self_describe**(facet): Introspect LeapFlow's own identity, capabilities, runtime state, evolution metrics, or platform connections. Use facet='all' only when a comprehensive self-check is explicitly requested.\n- **runtime_snapshot**(): Lightweight (~150 token) flat snapshot of current runtime state: model, context budget, posture, disclosure level, turn count, cache hit rate, uptime, tool count, and pending approvals.\n- **schedule_create**(trigger_expression, instruction, execution_mode, max_retries, delivery_target) [capability_expand category: scheduler]: Create a new scheduled task. Specify a trigger expression (e.g. '30m', 'every 2h', '0 9 * * *') and the instruction to execute on each trigger. Optionally provide an execution mode and a delivery target to receive notifications.\n- **schedule_list**() [capability_expand category: scheduler]: List all scheduled tasks with their current state, trigger, and next due time.\n- **schedule_status**(task_id) [capability_expand category: scheduler]: Get detailed status and recent execution history for a scheduled task.\n- **schedule_pause**(task_id) [capability_expand category: scheduler]: Pause a scheduled task so it stops firing without being cancelled.\n- **schedule_resume**(task_id) [capability_expand category: scheduler]: Resume a paused scheduled task — re-arms it and recalculates next due time.\n- **schedule_cancel**(task_id) [capability_expand category: scheduler]: Cancel a scheduled task permanently.\n- **tool_search**(query, max_results): Search all registered tools by keyword relevance. Returns a ranked list of matching tools with name, category, and summary. Use this when you need a tool but are unsure of its exact name or category.\n- **tool_describe**(tool_name): Get the full callable schema of a single tool by exact name. Returns the tool's description, parameters, and metadata. Use after tool_search to inspect a specific tool before calling it.\n- **session_search**(query, limit) [capability_expand category: unclassified]: Search past conversation sessions in the current workspace for relevant context.\n- **session_list**(limit): List recent conversation sessions in the current workspace with stable ids, titles, dates, and summaries. Use for browsing past tasks or when user asks to see history without specific search terms. No keywords needed — returns chronological list.\n- **session_detail**(session_id, limit, offset, include_inactive) [capability_expand category: unclassified]: Read a paginated persisted transcript for one past conversation session in the current workspace. Use after session_list or session_search returns a session_id.\n- **click**(element_index) [capability_expand category: desktop]: Click a UI element by its element_index (from the latest observe_ui snapshot)\n- **get_clipboard**() [capability_expand category: desktop]: Read current clipboard text content\n- **list_apps**(filter, running_only) [capability_expand category: desktop]: List available applications on this system. Use to discover correct bundle_id before switch_app.\n- **list_windows**() [capability_expand category: desktop]: List all top-level windows with pid, window_id, title, and per-window state (minimized, on-screen). Call this first to pick the pid and window_id that observe_ui and other window tools require.\n- **observe_ui**(pid, window_id, query) [capability_expand category: desktop]: Snapshot one window's actionable UI elements, each tagged with an element_index for click/right_click/read_text. Re-observe after actions — indices belong to one snapshot. Requires the window's pid and window_id from list_windows.\n- **open_url**(url, app_id) [capability_expand category: desktop]: Open a URL in the default or specified browser\n- **read_text**(element_index) [capability_expand category: desktop]: Read the text content of a specific UI element from the latest snapshot\n- **right_click**(element_index) [capability_expand category: desktop]: Right-click a UI element to open its context menu. Returns visible menu items.\n- **screenshot**(pid, window_id) [capability_expand category: desktop]: Capture a screenshot for visual verification. With pid + window_id captures that window (works across all displays); defaults to the last observed window, or the full desktop when no window has been observed.\n- **scroll**(element_index, direction, amount, pid, window_id) [capability_expand category: desktop]: Scroll a scrollable area of a window. Omit element_index to scroll the window's focused/page scroller; pass one to scroll an exact element from the latest snapshot.\n- **select_text**(element_index) [capability_expand category: desktop]: Select all text in a UI element (focus + select-all, for subsequent copy)\n- **set_clipboard**(text) [capability_expand category: desktop]: Write text to the clipboard\n- **shortcut**(keys) [capability_expand category: desktop]: Execute a keyboard shortcut\n- **switch_app**(app_id) [capability_expand category: desktop]: Switch to an app (launch if needed, activate, verify)\n- **type_text**(text) [capability_expand category: desktop]: Type text into the currently focused element\n- **wait**(seconds) [capability_expand category: desktop]: Wait for a specified duration before continuing\n- **wait_until**(condition, pid, window_id, timeout, poll_interval) [capability_expand category: desktop]: Wait until a UI condition is met (polls UI tree). Returns elements when found or on timeout.\n- **wait_until_stable**(timeout, poll_interval, pid, window_id) [capability_expand category: desktop]: Wait until the UI stops changing (element set stabilizes across polls).\n\n## App Connector Capability Index\nLeapFlow can onboard and manage external apps through `platform_connect` and execute exact registered business actions through `platform_action`.\nFor requests about connecting, setting up, configuring, enabling, or managing a supported app, use `platform_connect` first instead of generating SDK/Webhook sample code.\n`platform_connect.action` is the management namespace: list, guide, preflight, connect, disconnect, remove, status, events_start, events_stop, events_status.\n`platform_action.action` is only for exact registered platform business actions listed below, such as `im.send_message`; never use management actions like `list` or `guide` there.\nAll business fields MUST go inside `payload`; top-level keys are only `platform`, `action`, and `payload`.\nDo not invent platform IDs or platform action names. If the needed action is not listed, ask for discovery/clarification instead of guessing.\nUse `platform_connect` with `action='guide'` and the matching `platform` to start onboarding; use `action='list'` when the app is unclear.\nWhen a pending onboarding state is present, continue from that state with `platform_connect` instead of asking the user to restate the app.\nSupported apps:\n- `api_server`: API Server (OpenAI Compatible) (category=api; backend=adapter; platform_action actions=none registered)\n- `dingtalk`: 钉钉 (DingTalk) (category=im; backend=adapter; platform_action actions=none registered)\n- `feishu`: 飞书 (Feishu/Lark) (category=im; backend=cli)\n - `calendar.create_event` payload={summary*, start_time*, end_time*, attendees} [write/high]\n - `docs.create_markdown` payload={title*, markdown*, folder_token} [write/medium]\n - `drive.search` payload={query*, limit} [read/medium]\n - `im.add_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.download_resource` payload={message_id*, file_key*, type*} [read/low]\n - `im.get_messages` payload={message_ids*} [read/low]\n - `im.list_chats` payload={page_size} [read/low]\n - `im.list_messages` payload={chat_id*} [read/low]\n - `im.list_thread_messages` payload={thread*} [read/low]\n - `im.remove_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.reply_message` payload={message_id*, text*} [send/high]\n - `im.search_chats` payload={query*, page_size} [read/low]\n - `im.search_messages` payload={query*} [read/medium]\n - `im.send_message` payload={chat_id*, text*, thread_id} [send/high]\n - `im.update_card` payload={token*, card*} [write/medium]\n - `im.update_message` payload={message_id*, text*} [write/medium]\n - `mail.search_unread` payload={query, limit} [read/high]\n - `sheets.append_row` payload={spreadsheet_token*, sheet_id*, values*} [write/medium]\n - `task.create` payload={title*, description, due_time} [write/medium]\n- `telegram`: Telegram (category=social; backend=adapter; platform_action actions=none registered)\n- `webhook`: Webhook (Generic) (category=webhook; backend=adapter; platform_action actions=none registered)\n\n## Tool Usage\nTools are normally invoked through the native function-calling mechanism, not by writing JSON in your reply\ntext. Only if the provider signals that native function calling is unavailable for this turn, fall back to a\nsingle JSON code block: `{\"name\": \"tool_name\", \"arguments\": {\"key\": \"value\"}}` — use this fallback format\nonly, never both. Only call a tool whose exact name appears in the tool index above and is currently callable\n(or reachable via `capability_expand`). Never invent, rename, alias, or guess a tool name, platform ID, or\nplatform action from argument shape or wording — if the index does not list it, it does not exist.\n`platform_connect.action` (list/guide/preflight/connect/disconnect/remove/status/events_start/events_stop/\nevents_status) is the App Connector management namespace; `platform_action.action` only accepts exact\nregistered business actions such as `im.send_message` shown in the App Connector Capability Index — never mix\nthe two namespaces. If a tool call returns an unknown/unavailable result, use the returned suggestions or\navailable names for a single retry instead of trying further variations of the same guess.\n\n**Side-effect action rule** (`platform_action` with effect=send/write/execute):\n- Call each unique action+payload **exactly once**. Never include duplicates in the same turn.\n- Once the result returns `\"completed\": true`, that action is DONE for this task. Do NOT call it again\n in any subsequent turn — immediately summarize the result for the user instead.\n- The system enforces idempotency: duplicate calls are blocked and will not execute.\n- If the user explicitly requests sending/writing multiple times, use distinct payloads per call.\n\n**Resource identifier provenance rule**:\n- NEVER fabricate, guess, or infer resource identifiers (chat_id, message_id, file_key, user_id, etc.).\n Every resource ID used in a side-effect action MUST come from a successful API response in this session.\n- If a read/list action fails (e.g. authorization error), you do NOT have valid resource IDs.\n Report the failure to the user — do NOT attempt the dependent write/send action with a guessed ID.\n- When a tool result contains `\"llm_instruction\"`, follow it exactly.\n\n## Guidelines\n1. **Direct answers first**: If you already know the answer, respond directly without tools.\n2. **Avoid redundant tool calls**: Do not call the same tool with the same arguments more than once in the same user turn. When an existing tool result already answers the user's request, stop calling tools and answer directly.\n3. **Use tools proactively**: When the user asks about files, time, system state, or needs actions performed, use the appropriate tool.\n4. **Chain tools when needed**: You can call multiple tools in sequence (e.g., list files → read file → summarize).\n5. **Handle failures gracefully**: If a tool fails, explain what went wrong and suggest alternatives. If it failed because of an unknown tool/platform/action name, retry once with an exact name from the returned suggestions, then explain rather than keep guessing.\n6. **Summarize results naturally**: After tool execution, synthesize the results into a helpful answer rather than dumping raw output.\n7. **Stay conversational**: Maintain a natural, helpful tone. Acknowledge context from earlier in the conversation.\n8. **Recall past work**:\n - Broad queries (\"之前做了什么\", \"列出任务\"): answer from the \"Recent Task History\" section already in your context. If insufficient, call session_list.\n - Specific lookups (\"上次那个配置怎么改的\"): call session_search with relevant phrases (NOT single characters).\n - Do NOT call search tools repeatedly with keyword fragments. One well-phrased call is better than ten fragmented ones.\n\n## Coding & Verification\nWhen working with code, prefer the precise built-in tools over ad-hoc shell: use `repo_map` to orient in an\nunfamiliar project, `code_search`/`file_find` to locate and `code_intel` for symbols, `edit_file` (anchored\nsearch-replace, or a unified `diff`) to change files — never rewrite a whole file to change a few lines — and\n`git_query` to inspect diffs/log. After edits, check `syntax_ok` in the result and run `test_run`/`lint_check`\nbefore declaring the work done. Batch independent read-only calls (search/read/query) into a single turn — they\nrun in parallel.\n\n## Presentation Style\n1. **Polished Markdown only**: Format user-facing answers with clean Markdown headings, short paragraphs, and concise bullets. Use tables only when they improve comparison or scanning.\n2. **Terminal-friendly layout**: Keep lines readable in a TUI; avoid dense walls of text, deeply nested lists, oversized ASCII art, or heavy visual blocks.\n3. **Elegant emphasis**: Use bold text sparingly for key terms and conclusions. Avoid excessive emojis, decorative symbols, repeated separators, or visual noise.\n4. **Theme-safe colors**: Do not emit ANSI escape codes, HTML color tags, Rich markup, or hardcoded color names. Rely on the TUI theme to render Markdown professionally.\n5. **No leaked tool protocol**: Never show tool-call JSON, internal schemas, raw observations, tool result payloads, or hidden reasoning in the final answer unless the user explicitly asks for raw/debug output. Treat any prior `{\"name\": ..., \"arguments\": ...}` blocks and `Tool result (...)` messages as internal execution context only.\n6. **Professional closure**: End with a concise conclusion or next step when helpful; avoid rambling after the useful answer is complete.\n\nWhen finished with all tool calls, respond normally without a JSON block, tool-call transcript, or process log.\n\n## Task Contract\n- Task ID: turn-4\n- Original user request: Remove the adaptive JSON pretty plugin completely.\n- Workspace root: \n- Allowed roots: \n- Treat relative project paths as relative to the workspace root; never infer `.` as the project root when a workspace root is provided.\n- Workspace boundary is enforced by tools: do not read, search, edit, or run commands against paths outside the allowed roots unless the user explicitly requests an external path and the tool/approval policy permits it.\n- LeapFlow workspace config is optional at `/.leapflow/config.yaml`; runtime config is loaded from `~/.leapflow/config/user.yaml` and `~/.leapflow/profiles//config/*.yaml`.\n- Preserve this task contract across summarization, compression, tool loops, and memory retrieval.\n" - }, - { - "role": "system", - "content": "\n## Recent Session Summary\n- [assistant] Formatted JSON with the new plugin.\n- [user] Remove the adaptive JSON pretty plugin completely.\n" - }, - { - "role": "assistant", - "content": "Formatted JSON with the new plugin." - }, - { - "role": "user", - "content": "Remove the adaptive JSON pretty plugin completely.\nRemove the adaptive JSON pretty plugin completely." - } - ], - "tools": [ - "capability_expand", - "code_intel", - "code_search", - "config_get", - "config_list", - "env_info", - "file_find", - "file_list", - "file_read", - "git_query", - "lint_check", - "memory_search", - "plugin_list", - "plugin_propose", - "plugin_status", - "plugin_versions", - "repo_map", - "research_note", - "runtime_snapshot", - "schedule_reentry", - "self_describe", - "session_list", - "skill_view", - "skills_list", - "terminal_list", - "terminal_read", - "test_run", - "text_search", - "time_get", - "tool_describe", - "tool_search", - "web_fetch" - ] - }, - "responses": [ - { - "status": 200, - "content_type": "application/json", - "body": "{\"id\": \"chatcmpl-cassette\", \"object\": \"chat.completion\", \"model\": \"cassette-model\", \"choices\": [{\"index\": 0, \"message\": {\"role\": \"assistant\", \"content\": \"\", \"tool_calls\": [{\"id\": \"call_1\", \"type\": \"function\", \"function\": {\"name\": \"plugin_remove\", \"arguments\": \"{\\\"plugin_id\\\": \\\"json_pretty_loop_e2e\\\", \\\"delete_source\\\": true}\"}}]}, \"finish_reason\": \"tool_calls\"}], \"usage\": {\"prompt_tokens\": 64, \"completion_tokens\": 16, \"total_tokens\": 80}}" - } - ] -} diff --git a/tests/_fixtures/cassettes/r7_adaptive_plugin_loop/cassette-model-5e0f084389286456.cassette.json b/tests/_fixtures/cassettes/r7_adaptive_plugin_loop/cassette-model-5e0f084389286456.cassette.json deleted file mode 100644 index 1a632c6e..00000000 --- a/tests/_fixtures/cassettes/r7_adaptive_plugin_loop/cassette-model-5e0f084389286456.cassette.json +++ /dev/null @@ -1,63 +0,0 @@ -{ - "fingerprint": "5e0f084389286456c840af713b8c2c1d4a889d8182fba1cf6615ca0870aa1749", - "note": "captured in seed mode", - "request": { - "model": "cassette-model", - "stream": false, - "messages": [ - { - "role": "system", - "content": "You are LeapFlow, an intelligent assistant that can both converse naturally and take real actions on the user's computer.\n\n## Capabilities\nThe tool index below lists **every** registered tool by name and a one-line summary — this is the complete\ncapability contract; nothing else exists. Only a subset is directly callable this turn (via native tool calling,\nnot a JSON block in your reply). If you need a tool from the index that is not yet callable, call\n`capability_expand` with its category name first — the matching tools become callable immediately after.\nWhen the user asks what LeapFlow itself supports, whether it supports plugins, or which runtime capabilities\nare available, use the live capability evidence exposed by `plugin_list` before making capability claims; report\nconfiguration-dependent or unavailable capabilities as limitations instead of inferring from documentation.\n- **text_search**(text, pattern): Search for a regex pattern in text.\n- **text_replace**(text, old, new, count) [capability_expand category: write]: Replace occurrences of a substring in text.\n- **time_get**(): Get current date and time.\n- **env_info**(): Get system environment information (OS, Python version, cwd).\n- **skills_list**(query, category, source): List available learned skills. Use when user asks about capabilities or you need a specific skill.\n- **skill_view**(name): View the full content of a specific skill document.\n- **code_intel**(path, operation): Precise document symbols (outline) for a source file: classes, functions, and methods with line ranges. Python uses an exact AST parse; other languages use a keyword-prefix scan. Prefer over file_read mode=symbols for accurate navigation before editing. Read-only.\n- **repo_map**(path): Compact project orientation for a repository root: languages, detected test/lint commands, top-level structure, entry points, manifest, and VCS branch. Call this first when entering an unfamiliar codebase. Read-only.\n- **scm_sync**(action, cwd, remote, pull_ref, push_ref, timeout) [capability_expand category: scm]: Run a typed git SCM action. Use this instead of shell_run for git pull/push/status. For 'pull origin main then push', set action='pull_then_push', remote='origin', pull_ref='main', and omit push_ref so LeapFlow pushes the current local branch.\n- **git_query**(action, cwd, ref, path, staged, max_count, stat): Read-only structured git inspection: action=diff|log|status|branch|show. Prefer over shell_run for reading repo state — output is clipped, redacted, and log/branch are parsed into structured fields. Use scm_sync for pull/push.\n- **git_write**(action, cwd, message, stage_all, name, ref, create) [capability_expand category: scm]: Mutating git actions: action=commit (message, stage_all), branch (create+switch), checkout (switch; create=true for -b). Approval-gated. Use scm_sync for pull/push and git_query for reads.\n- **test_run**(command, cwd, timeout): Run the project's test suite and return structured results (framework, passed/failed counts, failing tests). Auto-detects the runner (pytest/npm/go/cargo) or uses a configured/explicit command; executes via the governed shell. ok=true means the runner executed — see 'success' for pass/fail.\n- **lint_check**(command, cwd, timeout): Run the project's linter and return a structured clean/issue result. Auto-detects the linter (ruff/eslint/go vet/clippy) or uses a configured/explicit command; executes via the governed shell. ok=true means the linter ran — see 'clean'.\n- **file_list**(path, pattern, depth): List files and directories at a given path. Use depth=1 or depth=2 to get a recursive tree in one call instead of listing each sub-directory separately.\n- **file_read**(path, max_lines, start_line, max_chars, mode): Read text file content with adaptive context governance. For large or unfamiliar files, prefer mode='outline' or mode='symbols' first, then use mode='raw' with start_line/max_lines for the specific range you actually need. For LeapFlow's own settings, use config_list / config_get / config_set — its config files are outside the workspace and not readable here.\n- **file_write**(path, content, mode) [capability_expand category: write]: Write content to a file (overwrite or append).\n- **code_search**(pattern, patterns, path, glob, ignore_case, multiline, max_results, context_lines): Search file CONTENTS by regex pattern across a directory tree (ripgrep-backed). Requires a regex pattern. NOT for listing or browsing directory contents — use file_list for that. Prefer this over shell_run grep: faster, skips VCS/dependency/build dirs, and returns structured path:line:column matches. Batch related lookups into ONE call via `patterns` (OR-combined, single pass) instead of issuing several separate searches. Use file_read for the surrounding context of a hit.\n- **file_find**(glob, path, max_results): Find files by a recursive glob pattern under a base path (e.g. '**/test_*.py' or '*.md'). Prefer this over shell_run find; skips VCS/dependency/build dirs.\n- **edit_file**(path, edits, dry_run, diff) [capability_expand category: write]: Apply targeted, anchored search-replace edits to an EXISTING text file (use file_write to create/overwrite). Each edit is {original_text, new_text, replace_all?}; original_text must match exactly and uniquely (or set replace_all) — a non-unique or missing anchor is rejected so files are never corrupted. Set dry_run to preview. Alternatively pass a unified 'diff' to apply its hunks as anchored edits. Far cheaper and safer than rewriting a whole file.\n- **shell_run**(command, cwd, timeout) [capability_expand category: shell]: Execute a one-shot shell command with timeout protection. Runs in the active workspace; paths resolving outside it are refused. Reach for a structured tool first when one fits — web_fetch for anything over HTTP(S), git_query/scm_sync for git, code_search/file_find/file_read for the repo, config_get/config_set for LeapFlow's own settings — because those report typed results, while a failed shell command can only be diagnosed from its exit code and stderr. Every shell run counts as an external side effect, so a failure stops the rest of the batch and is not retried automatically.\n- **terminal_open**(command, cwd, shell) [capability_expand category: terminal]: Open a PERSISTENT shell session (REPL/dev server/watch), returning a session_id for terminal_send/read/close. Disabled unless tools.terminal_session_enabled is set. For one-shot commands use shell_run instead.\n- **terminal_send**(session_id, input, wait) [capability_expand category: terminal]: Send a line of input to a persistent terminal session and return output captured shortly after.\n- **terminal_read**(session_id, wait): Drain buffered output from a persistent terminal session (optionally waiting briefly first).\n- **terminal_close**(session_id) [capability_expand category: terminal]: Terminate a persistent terminal session and release its process group.\n- **terminal_list**(): List active persistent terminal sessions.\n- **config_list**(category, limit): List LeapFlow's own writable settings (LLM model and endpoint, daemon, memory, perception, gateway, …) with current values. Use this to discover the exact key before changing anything. Optionally narrow by `category`. This is the only correct way to inspect LeapFlow configuration — never read config files from disk.\n- **config_get**(key): Read one LeapFlow setting by key (e.g. 'llm.model', 'llm.base_url', 'daemon.log_level'), returning its current value, type, scopes, and whether a change needs a daemon restart. There is no 'llm.provider' key: provider behavior is inferred from the OpenAI-compatible 'llm.base_url'. Never read LeapFlow config files from disk — use this.\n- **config_set**(key, value, scope) [capability_expand category: config]: Change one LeapFlow setting by key, e.g. switch an OpenAI-compatible LLM with key='llm.model' and key='llm.base_url'. There is no 'llm.provider' key: provider behavior is inferred from the endpoint. Values are validated and coerced; credentials are stored in the vault automatically. Call config_list or config_get first if unsure of the key. The result states whether a `leap daemon restart` is required. Never edit LeapFlow config files directly.\n- **web_fetch**(url, select, timeout, max_bytes): Read a URL over HTTP(S) and get back extracted, context-sized content: parsed JSON for API endpoints, readable text plus links for web pages. Use this for anything on the internet — prices, docs, releases, articles — instead of running curl through shell_run: it reports real HTTP status codes, retries rate limits on its own, and is a plain read so a retry is always safe. For JSON APIs pass `select` with a dotted path (e.g. 'chart.result.0.meta') to return just that part instead of the whole payload.\n- **memory_search**(query, limit): Search agent memory for relevant past experiences, observations, and facts.\n- **memory_add**(content, kind) [capability_expand category: write]: Store a new observation or insight in memory for future reference.\n- **research_note**(kind, text): Record a compact, structured note about the current task's state so it survives context compression on long / multi-step tasks. Use for durable findings, open questions still to resolve, decisions / excluded paths, and the immediate next step. One concise sentence per note.\n- **capability_expand**(category): Fetch the full callable schema for every tool in a capability category (e.g. 'hub', 'gateway', 'desktop', 'delegate', 'file', 'memory', 'skill'). The compact tool index always lists every registered tool by name and a one-line summary, but only a static low-risk subset is directly callable each turn. If you need a tool from the index that is not yet callable, call capability_expand with its category first; the matching tools become callable in this turn. Never invent a tool name — expand the category instead.\n- **delegate_task**(goal, context) [capability_expand category: delegate]: Delegate a complex sub-task to an isolated subagent. The subagent gets a fresh context and restricted tool access. Use when a task is self-contained and can be solved independently.\n- **schedule_reentry**(kind, reason, delay_seconds, event_match, max_reentries, deadline_seconds): Register a re-entry so this task can resume later from its current orientation (findings / open questions / next step). Use when work must pause and continue after a delay (kind=time) or when a matching platform event arrives (kind=event), instead of finishing now. The research-ledger state is carried over automatically.\n- **hub_push**(skill_name, visibility, version) [capability_expand category: hub]: Push a local skill to the ModelScope Hub for sharing or backup.\n- **hub_pull**(repo_id, version) [capability_expand category: hub]: Pull a skill from the ModelScope Hub to install locally.\n- **hub_search**(query) [capability_expand category: hub]: Search for skills on the Hub by keyword or description.\n- **hub_sync**(mode, dry_run) [capability_expand category: hub]: Preview or execute sync between local skills and Hub.\n- **platform_action**(platform, action, payload, backend_kind) [capability_expand category: gateway]: Execute an exact registered business action on an external platform through LeapFlow's App Connector layer. Actions must be copied from the App Connector Capability Index and are addressed as domain.operation, e.g. im.send_message or docs.create_markdown. All business fields (chat_id, text, query, etc.) MUST be placed inside `payload`, never at the top level. Example: {\"platform\":\"feishu\",\"action\":\"im.send_message\",\"payload\":{\"chat_id\":\"oc_xxx\",\"text\":\"hello\"}}. Do not invent action names, do not use management actions such as list/guide/connect/status here.\n- **platform_connect**(action, platform, credentials, options, checkpoint) [capability_expand category: gateway]: List, guide, connect, disconnect, remove, or check status for external platforms using the App Connector management namespace. Supports REST and CLI backends. Use this for management actions such as list/guide/preflight/connect/status; use platform_action only for exact registered business actions.\n- **gateway_send**(platform, chat_id, text, thread_id) [capability_expand category: gateway]: Send a message to a connected external platform (Feishu group, Telegram chat, DingTalk conversation, etc.). Requires the platform to be connected via gateway_connect first. Use gateway_connect with action='list' to see connected platforms and available chat IDs.\n- **gateway_connect**(action, platform, credentials, options) [capability_expand category: gateway]: Connect, configure, or manage external platform integrations (Feishu, DingTalk, Telegram, Slack, Discord, etc.). Conversational flow: 1) call 'guide' to get setup steps + required fields, 2) present the steps to the user and ask for ALL required credentials in a single message, 3) call 'connect' with the credentials. Goal: complete in 1–2 user turns. NEVER include credential values in your text response.\n- **plugin_list**(): List the live plugin registry and cross-subsystem capability evidence. Use this before answering questions about whether LeapFlow supports plugins, self-evolution, plugin installation, hot reload, versioning, or other runtime capabilities.\n- **plugin_status**(plugin_id): Get detailed status of a specific plugin: its declared category, runtime dependencies, contributed tools, and fiber lifecycle state.\n- **plugin_versions**(plugin_id): List recorded source versions and active pointer for a profile-scoped plugin.\n- **plugin_propose**(requested_capability, plugin_id, proposed_tools, test_cases, risk_level, evidence): Create a side-effect-free PluginProposal from explicit capability-gap evidence. Use this before plugin_generate when a missing capability should be reviewed. Does not call an LLM, write files, or install anything.\n- **assess_compatibility**(manifest, source_path) [capability_expand category: plugin_management]: Assess whether a foreign plugin manifest or real DSH source bundle is compatible with LeapFlow. A source bundle assessment is static: runtime_ready stays false until restricted Node discovery during plugin_install. Returns component-level verdicts and limitations.\n- **plugin_generate**(plugin_id, description, proposal_id) [capability_expand category: system]: Generate a new ToolPlugin from a natural-language capability description. The LLM produces code that conforms to the ToolPlugin Protocol; it is then rigorously validated (syntax, structure, import, protocol conformance). The generated source is stored in CAS and receives explicit content approval. It DOES NOT install the plugin — installation requires a second, mutation-specific approval via plugin_install.\n- **plugin_install**(plugin_id, code, marketplace_name, source_path, proposal_id, version_label) [capability_expand category: system]: Install a plugin from exactly one source: validated Python code, the configured Python marketplace, or a real DSH source bundle. DSH bundles run restricted Node discovery before registration; only public host tools are installed and client UI limitations are reported. Writes profile-scoped state and mutates the process-global registry. REQUIRES APPROVAL.\n- **plugin_rollback**(plugin_id, version) [capability_expand category: system]: Rollback a profile-scoped plugin to a recorded source version and reload it. REQUIRES APPROVAL.\n- **plugin_reload**(plugin_id, version_label) [capability_expand category: system]: Hot-reload a plugin at runtime. Disposes the old plugin fiber, re-imports its module, and registers a fresh instance. Existing in-flight turns are unaffected (snapshot isolation). REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_disable**(plugin_id) [capability_expand category: system]: Disable a plugin by disposing its fiber, removing its tools from the runtime registry. Cannot disable self_management itself. REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_remove**(plugin_id, delete_source) [capability_expand category: system]: Terminally remove a plugin: dispose its fiber, unregister its tools, remove reload metadata, and optionally delete its profile-scoped source file. Cannot remove self_management itself. REQUIRES APPROVAL.\n- **plugin_unquarantine**(plugin_id) [capability_expand category: plugin_management]: Restore a quarantined plugin to probation status for re-evaluation. Unfreezes the trust ledger, transitions the proposal back to PROBATION, and reloads the plugin. REQUIRES APPROVAL.\n- **plugin_enable**(plugin_id) [capability_expand category: system]: Re-enable a previously disabled plugin by reloading its module and registering a fresh instance. REQUIRES APPROVAL.\n- **self_describe**(facet): Introspect LeapFlow's own identity, capabilities, runtime state, evolution metrics, or platform connections. Use facet='all' only when a comprehensive self-check is explicitly requested.\n- **runtime_snapshot**(): Lightweight (~150 token) flat snapshot of current runtime state: model, context budget, posture, disclosure level, turn count, cache hit rate, uptime, tool count, and pending approvals.\n- **schedule_create**(trigger_expression, instruction, execution_mode, max_retries, delivery_target) [capability_expand category: scheduler]: Create a new scheduled task. Specify a trigger expression (e.g. '30m', 'every 2h', '0 9 * * *') and the instruction to execute on each trigger. Optionally provide an execution mode and a delivery target to receive notifications.\n- **schedule_list**() [capability_expand category: scheduler]: List all scheduled tasks with their current state, trigger, and next due time.\n- **schedule_status**(task_id) [capability_expand category: scheduler]: Get detailed status and recent execution history for a scheduled task.\n- **schedule_pause**(task_id) [capability_expand category: scheduler]: Pause a scheduled task so it stops firing without being cancelled.\n- **schedule_resume**(task_id) [capability_expand category: scheduler]: Resume a paused scheduled task — re-arms it and recalculates next due time.\n- **schedule_cancel**(task_id) [capability_expand category: scheduler]: Cancel a scheduled task permanently.\n- **tool_search**(query, max_results): Search all registered tools by keyword relevance. Returns a ranked list of matching tools with name, category, and summary. Use this when you need a tool but are unsure of its exact name or category.\n- **tool_describe**(tool_name): Get the full callable schema of a single tool by exact name. Returns the tool's description, parameters, and metadata. Use after tool_search to inspect a specific tool before calling it.\n- **session_search**(query, limit) [capability_expand category: unclassified]: Search past conversation sessions in the current workspace for relevant context.\n- **session_list**(limit): List recent conversation sessions in the current workspace with stable ids, titles, dates, and summaries. Use for browsing past tasks or when user asks to see history without specific search terms. No keywords needed — returns chronological list.\n- **session_detail**(session_id, limit, offset, include_inactive) [capability_expand category: unclassified]: Read a paginated persisted transcript for one past conversation session in the current workspace. Use after session_list or session_search returns a session_id.\n- **click**(element_index) [capability_expand category: desktop]: Click a UI element by its element_index (from the latest observe_ui snapshot)\n- **get_clipboard**() [capability_expand category: desktop]: Read current clipboard text content\n- **list_apps**(filter, running_only) [capability_expand category: desktop]: List available applications on this system. Use to discover correct bundle_id before switch_app.\n- **list_windows**() [capability_expand category: desktop]: List all top-level windows with pid, window_id, title, and per-window state (minimized, on-screen). Call this first to pick the pid and window_id that observe_ui and other window tools require.\n- **observe_ui**(pid, window_id, query) [capability_expand category: desktop]: Snapshot one window's actionable UI elements, each tagged with an element_index for click/right_click/read_text. Re-observe after actions — indices belong to one snapshot. Requires the window's pid and window_id from list_windows.\n- **open_url**(url, app_id) [capability_expand category: desktop]: Open a URL in the default or specified browser\n- **read_text**(element_index) [capability_expand category: desktop]: Read the text content of a specific UI element from the latest snapshot\n- **right_click**(element_index) [capability_expand category: desktop]: Right-click a UI element to open its context menu. Returns visible menu items.\n- **screenshot**(pid, window_id) [capability_expand category: desktop]: Capture a screenshot for visual verification. With pid + window_id captures that window (works across all displays); defaults to the last observed window, or the full desktop when no window has been observed.\n- **scroll**(element_index, direction, amount, pid, window_id) [capability_expand category: desktop]: Scroll a scrollable area of a window. Omit element_index to scroll the window's focused/page scroller; pass one to scroll an exact element from the latest snapshot.\n- **select_text**(element_index) [capability_expand category: desktop]: Select all text in a UI element (focus + select-all, for subsequent copy)\n- **set_clipboard**(text) [capability_expand category: desktop]: Write text to the clipboard\n- **shortcut**(keys) [capability_expand category: desktop]: Execute a keyboard shortcut\n- **switch_app**(app_id) [capability_expand category: desktop]: Switch to an app (launch if needed, activate, verify)\n- **type_text**(text) [capability_expand category: desktop]: Type text into the currently focused element\n- **wait**(seconds) [capability_expand category: desktop]: Wait for a specified duration before continuing\n- **wait_until**(condition, pid, window_id, timeout, poll_interval) [capability_expand category: desktop]: Wait until a UI condition is met (polls UI tree). Returns elements when found or on timeout.\n- **wait_until_stable**(timeout, poll_interval, pid, window_id) [capability_expand category: desktop]: Wait until the UI stops changing (element set stabilizes across polls).\n\n## App Connector Capability Index\nLeapFlow can onboard and manage external apps through `platform_connect` and execute exact registered business actions through `platform_action`.\nFor requests about connecting, setting up, configuring, enabling, or managing a supported app, use `platform_connect` first instead of generating SDK/Webhook sample code.\n`platform_connect.action` is the management namespace: list, guide, preflight, connect, disconnect, remove, status, events_start, events_stop, events_status.\n`platform_action.action` is only for exact registered platform business actions listed below, such as `im.send_message`; never use management actions like `list` or `guide` there.\nAll business fields MUST go inside `payload`; top-level keys are only `platform`, `action`, and `payload`.\nDo not invent platform IDs or platform action names. If the needed action is not listed, ask for discovery/clarification instead of guessing.\nUse `platform_connect` with `action='guide'` and the matching `platform` to start onboarding; use `action='list'` when the app is unclear.\nWhen a pending onboarding state is present, continue from that state with `platform_connect` instead of asking the user to restate the app.\nSupported apps:\n- `api_server`: API Server (OpenAI Compatible) (category=api; backend=adapter; platform_action actions=none registered)\n- `dingtalk`: 钉钉 (DingTalk) (category=im; backend=adapter; platform_action actions=none registered)\n- `feishu`: 飞书 (Feishu/Lark) (category=im; backend=cli)\n - `calendar.create_event` payload={summary*, start_time*, end_time*, attendees} [write/high]\n - `docs.create_markdown` payload={title*, markdown*, folder_token} [write/medium]\n - `drive.search` payload={query*, limit} [read/medium]\n - `im.add_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.download_resource` payload={message_id*, file_key*, type*} [read/low]\n - `im.get_messages` payload={message_ids*} [read/low]\n - `im.list_chats` payload={page_size} [read/low]\n - `im.list_messages` payload={chat_id*} [read/low]\n - `im.list_thread_messages` payload={thread*} [read/low]\n - `im.remove_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.reply_message` payload={message_id*, text*} [send/high]\n - `im.search_chats` payload={query*, page_size} [read/low]\n - `im.search_messages` payload={query*} [read/medium]\n - `im.send_message` payload={chat_id*, text*, thread_id} [send/high]\n - `im.update_card` payload={token*, card*} [write/medium]\n - `im.update_message` payload={message_id*, text*} [write/medium]\n - `mail.search_unread` payload={query, limit} [read/high]\n - `sheets.append_row` payload={spreadsheet_token*, sheet_id*, values*} [write/medium]\n - `task.create` payload={title*, description, due_time} [write/medium]\n- `telegram`: Telegram (category=social; backend=adapter; platform_action actions=none registered)\n- `webhook`: Webhook (Generic) (category=webhook; backend=adapter; platform_action actions=none registered)\n\n## Tool Usage\nTools are normally invoked through the native function-calling mechanism, not by writing JSON in your reply\ntext. Only if the provider signals that native function calling is unavailable for this turn, fall back to a\nsingle JSON code block: `{\"name\": \"tool_name\", \"arguments\": {\"key\": \"value\"}}` — use this fallback format\nonly, never both. Only call a tool whose exact name appears in the tool index above and is currently callable\n(or reachable via `capability_expand`). Never invent, rename, alias, or guess a tool name, platform ID, or\nplatform action from argument shape or wording — if the index does not list it, it does not exist.\n`platform_connect.action` (list/guide/preflight/connect/disconnect/remove/status/events_start/events_stop/\nevents_status) is the App Connector management namespace; `platform_action.action` only accepts exact\nregistered business actions such as `im.send_message` shown in the App Connector Capability Index — never mix\nthe two namespaces. If a tool call returns an unknown/unavailable result, use the returned suggestions or\navailable names for a single retry instead of trying further variations of the same guess.\n\n**Side-effect action rule** (`platform_action` with effect=send/write/execute):\n- Call each unique action+payload **exactly once**. Never include duplicates in the same turn.\n- Once the result returns `\"completed\": true`, that action is DONE for this task. Do NOT call it again\n in any subsequent turn — immediately summarize the result for the user instead.\n- The system enforces idempotency: duplicate calls are blocked and will not execute.\n- If the user explicitly requests sending/writing multiple times, use distinct payloads per call.\n\n**Resource identifier provenance rule**:\n- NEVER fabricate, guess, or infer resource identifiers (chat_id, message_id, file_key, user_id, etc.).\n Every resource ID used in a side-effect action MUST come from a successful API response in this session.\n- If a read/list action fails (e.g. authorization error), you do NOT have valid resource IDs.\n Report the failure to the user — do NOT attempt the dependent write/send action with a guessed ID.\n- When a tool result contains `\"llm_instruction\"`, follow it exactly.\n\n## Guidelines\n1. **Direct answers first**: If you already know the answer, respond directly without tools.\n2. **Avoid redundant tool calls**: Do not call the same tool with the same arguments more than once in the same user turn. When an existing tool result already answers the user's request, stop calling tools and answer directly.\n3. **Use tools proactively**: When the user asks about files, time, system state, or needs actions performed, use the appropriate tool.\n4. **Chain tools when needed**: You can call multiple tools in sequence (e.g., list files → read file → summarize).\n5. **Handle failures gracefully**: If a tool fails, explain what went wrong and suggest alternatives. If it failed because of an unknown tool/platform/action name, retry once with an exact name from the returned suggestions, then explain rather than keep guessing.\n6. **Summarize results naturally**: After tool execution, synthesize the results into a helpful answer rather than dumping raw output.\n7. **Stay conversational**: Maintain a natural, helpful tone. Acknowledge context from earlier in the conversation.\n8. **Recall past work**:\n - Broad queries (\"之前做了什么\", \"列出任务\"): answer from the \"Recent Task History\" section already in your context. If insufficient, call session_list.\n - Specific lookups (\"上次那个配置怎么改的\"): call session_search with relevant phrases (NOT single characters).\n - Do NOT call search tools repeatedly with keyword fragments. One well-phrased call is better than ten fragmented ones.\n\n## Coding & Verification\nWhen working with code, prefer the precise built-in tools over ad-hoc shell: use `repo_map` to orient in an\nunfamiliar project, `code_search`/`file_find` to locate and `code_intel` for symbols, `edit_file` (anchored\nsearch-replace, or a unified `diff`) to change files — never rewrite a whole file to change a few lines — and\n`git_query` to inspect diffs/log. After edits, check `syntax_ok` in the result and run `test_run`/`lint_check`\nbefore declaring the work done. Batch independent read-only calls (search/read/query) into a single turn — they\nrun in parallel.\n\n## Presentation Style\n1. **Polished Markdown only**: Format user-facing answers with clean Markdown headings, short paragraphs, and concise bullets. Use tables only when they improve comparison or scanning.\n2. **Terminal-friendly layout**: Keep lines readable in a TUI; avoid dense walls of text, deeply nested lists, oversized ASCII art, or heavy visual blocks.\n3. **Elegant emphasis**: Use bold text sparingly for key terms and conclusions. Avoid excessive emojis, decorative symbols, repeated separators, or visual noise.\n4. **Theme-safe colors**: Do not emit ANSI escape codes, HTML color tags, Rich markup, or hardcoded color names. Rely on the TUI theme to render Markdown professionally.\n5. **No leaked tool protocol**: Never show tool-call JSON, internal schemas, raw observations, tool result payloads, or hidden reasoning in the final answer unless the user explicitly asks for raw/debug output. Treat any prior `{\"name\": ..., \"arguments\": ...}` blocks and `Tool result (...)` messages as internal execution context only.\n6. **Professional closure**: End with a concise conclusion or next step when helpful; avoid rambling after the useful answer is complete.\n\nWhen finished with all tool calls, respond normally without a JSON block, tool-call transcript, or process log.\n\n## Task Contract\n- Task ID: turn-1\n- Original user request: Try the missing_json_pretty_e2e tool so LeapFlow records a capability gap.\n- Workspace root: \n- Allowed roots: \n- Treat relative project paths as relative to the workspace root; never infer `.` as the project root when a workspace root is provided.\n- Workspace boundary is enforced by tools: do not read, search, edit, or run commands against paths outside the allowed roots unless the user explicitly requests an external path and the tool/approval policy permits it.\n- LeapFlow workspace config is optional at `/.leapflow/config.yaml`; runtime config is loaded from `~/.leapflow/config/user.yaml` and `~/.leapflow/profiles//config/*.yaml`.\n- Preserve this task contract across summarization, compression, tool loops, and memory retrieval.\n" - }, - { - "role": "system", - "content": "\n## Recent Session Summary\n- [user] Try the missing_json_pretty_e2e tool so LeapFlow records a capability gap.\n" - }, - { - "role": "user", - "content": "Try the missing_json_pretty_e2e tool so LeapFlow records a capability gap.\nTry the missing_json_pretty_e2e tool so LeapFlow records a capability gap." - } - ], - "tools": [ - "capability_expand", - "code_intel", - "code_search", - "config_get", - "config_list", - "env_info", - "file_find", - "file_list", - "file_read", - "git_query", - "lint_check", - "memory_search", - "plugin_list", - "plugin_propose", - "plugin_status", - "plugin_versions", - "repo_map", - "research_note", - "runtime_snapshot", - "schedule_reentry", - "self_describe", - "session_list", - "skill_view", - "skills_list", - "terminal_list", - "terminal_read", - "test_run", - "text_search", - "time_get", - "tool_describe", - "tool_search", - "web_fetch" - ] - }, - "responses": [ - { - "status": 200, - "content_type": "application/json", - "body": "{\"id\": \"chatcmpl-cassette\", \"object\": \"chat.completion\", \"model\": \"cassette-model\", \"choices\": [{\"index\": 0, \"message\": {\"role\": \"assistant\", \"content\": \"\", \"tool_calls\": [{\"id\": \"call_1\", \"type\": \"function\", \"function\": {\"name\": \"missing_json_pretty_e2e\", \"arguments\": \"{\\\"text\\\": \\\"{\\\\\\\"b\\\\\\\":2,\\\\\\\"a\\\\\\\":1}\\\"}\"}}]}, \"finish_reason\": \"tool_calls\"}], \"usage\": {\"prompt_tokens\": 64, \"completion_tokens\": 16, \"total_tokens\": 80}}" - } - ] -} diff --git a/tests/_fixtures/cassettes/r7_adaptive_plugin_loop/cassette-model-87b870c95fc18e67.cassette.json b/tests/_fixtures/cassettes/r7_adaptive_plugin_loop/cassette-model-87b870c95fc18e67.cassette.json new file mode 100644 index 00000000..1071faad --- /dev/null +++ b/tests/_fixtures/cassettes/r7_adaptive_plugin_loop/cassette-model-87b870c95fc18e67.cassette.json @@ -0,0 +1,67 @@ +{ + "fingerprint": "87b870c95fc18e674825012038c405571e9d0de6594df057703eaec475a906d2", + "note": "captured in seed mode", + "request": { + "model": "cassette-model", + "stream": false, + "messages": [ + { + "role": "system", + "content": "You are LeapFlow, an intelligent assistant that can both converse naturally and take real actions on the user's computer.\n\n## Capabilities\nThe tool index below lists **every** registered tool by name and a one-line summary — this is the complete\ncapability contract; nothing else exists. Only a subset is directly callable this turn (via native tool calling,\nnot a JSON block in your reply). If you need a tool from the index that is not yet callable, call\n`capability_expand` with its category name first — the matching tools become callable immediately after.\nWhen the user asks about LeapFlow **itself** — its identity/version/model, what tools, plugins, or capabilities\nit has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of\nself-awareness — answer from live runtime evidence via `self_describe` (facet=capabilities for \"what can you\ndo / do you support X\", identity for version/model, runtime for context/posture, or `all` for a full self-check),\nnever by reading LeapFlow's own source code, documentation, or memory. Report configuration-dependent or\nunavailable capabilities as limitations rather than inferring them.\n- **text_search**(text, pattern): Search for a regex pattern in text.\n- **text_replace**(text, old, new, count) [capability_expand category: write]: Replace occurrences of a substring in text.\n- **time_get**(): Get current date and time.\n- **env_info**(): Get system environment information (OS, Python version, cwd).\n- **skills_list**(query, category, source): List available learned skills. Use when user asks about capabilities or you need a specific skill.\n- **skill_view**(name): View the full content of a specific skill document.\n- **skill_list_files**(name) [capability_expand category: skills]: List all support files in a skill directory (configs, templates, examples). Use before skill_file_read to discover available files.\n- **skill_file_read**(name, file_path) [capability_expand category: skills]: Read an individual support file from a skill directory. Use skill_list_files first to discover available files.\n- **code_intel**(path, operation): Precise document symbols (outline) for a source file: classes, functions, and methods with line ranges. Python uses an exact AST parse; other languages use a keyword-prefix scan. Prefer over file_read mode=symbols for accurate navigation before editing. Read-only.\n- **repo_map**(path): Compact project orientation for a repository root: languages, detected test/lint commands, top-level structure, entry points, manifest, and VCS branch. Call this first when entering an unfamiliar codebase. Read-only.\n- **scm_sync**(action, cwd, remote, pull_ref, push_ref, timeout) [capability_expand category: scm]: Run a typed git SCM action. Use this instead of shell_run for git pull/push/status. For 'pull origin main then push', set action='pull_then_push', remote='origin', pull_ref='main', and omit push_ref so LeapFlow pushes the current local branch.\n- **git_query**(action, cwd, ref, path, staged, max_count, stat): Read-only structured git inspection: action=diff|log|status|branch|show. Prefer over shell_run for reading repo state — output is clipped, redacted, and log/branch are parsed into structured fields. Use scm_sync for pull/push.\n- **git_write**(action, cwd, message, stage_all, name, ref, create) [capability_expand category: scm]: Mutating git actions: action=commit (message, stage_all), branch (create+switch), checkout (switch; create=true for -b). Approval-gated. Use scm_sync for pull/push and git_query for reads.\n- **test_run**(command, cwd, timeout): Run the project's test suite and return structured results (framework, passed/failed counts, failing tests). Auto-detects the runner (pytest/npm/go/cargo) or uses a configured/explicit command; executes via the governed shell. ok=true means the runner executed — see 'success' for pass/fail.\n- **lint_check**(command, cwd, timeout): Run the project's linter and return a structured clean/issue result. Auto-detects the linter (ruff/eslint/go vet/clippy) or uses a configured/explicit command; executes via the governed shell. ok=true means the linter ran — see 'clean'.\n- **file_list**(path, pattern, depth): List files and directories at a given path. Use depth=1 or depth=2 to get a recursive tree in one call instead of listing each sub-directory separately.\n- **file_read**(path, max_lines, start_line, max_chars, mode): Read text file content with adaptive context governance. For large or unfamiliar files, prefer mode='outline' or mode='symbols' first, then use mode='raw' with start_line/max_lines for the specific range you actually need. For LeapFlow's own settings, use config_list / config_get / config_set — its config files are outside the workspace and not readable here.\n- **file_write**(path, content, mode) [capability_expand category: write]: Write content to a file (overwrite or append).\n- **code_search**(pattern, patterns, path, glob, ignore_case, multiline, max_results, context_lines): Search file CONTENTS by regex pattern across a directory tree (ripgrep-backed). Requires a regex pattern. NOT for listing or browsing directory contents — use file_list for that. Prefer this over shell_run grep: faster, skips VCS/dependency/build dirs, and returns structured path:line:column matches. Batch related lookups into ONE call via `patterns` (OR-combined, single pass) instead of issuing several separate searches. Use file_read for the surrounding context of a hit.\n- **file_find**(glob, path, max_results): Find files by a recursive glob pattern under a base path (e.g. '**/test_*.py' or '*.md'). Prefer this over shell_run find; skips VCS/dependency/build dirs.\n- **edit_file**(path, edits, dry_run, diff) [capability_expand category: write]: Apply targeted, anchored search-replace edits to an EXISTING text file (use file_write to create/overwrite). Each edit is {original_text, new_text, replace_all?}; original_text must match exactly and uniquely (or set replace_all) — a non-unique or missing anchor is rejected so files are never corrupted. Set dry_run to preview. Alternatively pass a unified 'diff' to apply its hunks as anchored edits. Far cheaper and safer than rewriting a whole file.\n- **shell_run**(command, cwd, timeout) [capability_expand category: shell]: Execute a one-shot shell command with timeout protection. Runs in the active workspace; paths resolving outside it are refused. Reach for a structured tool first when one fits — web_fetch for anything over HTTP(S), git_query/scm_sync for git, code_search/file_find/file_read for the repo, config_get/config_set for LeapFlow's own settings — because those report typed results, while a failed shell command can only be diagnosed from its exit code and stderr. Every shell run counts as an external side effect, so a failure stops the rest of the batch and is not retried automatically.\n- **terminal_open**(command, cwd, shell) [capability_expand category: terminal]: Open a PERSISTENT shell session (REPL/dev server/watch), returning a session_id for terminal_send/read/close. Disabled unless tools.terminal_session_enabled is set. For one-shot commands use shell_run instead.\n- **terminal_send**(session_id, input, wait) [capability_expand category: terminal]: Send a line of input to a persistent terminal session and return output captured shortly after.\n- **terminal_read**(session_id, wait): Drain buffered output from a persistent terminal session (optionally waiting briefly first).\n- **terminal_close**(session_id) [capability_expand category: terminal]: Terminate a persistent terminal session and release its process group.\n- **terminal_list**(): List active persistent terminal sessions.\n- **config_list**(category, limit): List LeapFlow's own writable settings (LLM model and endpoint, daemon, memory, perception, gateway, …) with current values. Use this to discover the exact key before changing anything. Optionally narrow by `category`. This is the only correct way to inspect LeapFlow configuration — never read config files from disk.\n- **config_get**(key): Read one LeapFlow setting by key (e.g. 'llm.model', 'llm.base_url', 'daemon.log_level'), returning its current value, type, scopes, and whether a change needs a daemon restart. There is no 'llm.provider' key: provider behavior is inferred from the OpenAI-compatible 'llm.base_url'. Never read LeapFlow config files from disk — use this.\n- **config_set**(key, value, scope) [capability_expand category: config]: Change one LeapFlow setting by key, e.g. switch an OpenAI-compatible LLM with key='llm.model' and key='llm.base_url'. There is no 'llm.provider' key: provider behavior is inferred from the endpoint. Values are validated and coerced; credentials are stored in the vault automatically. Call config_list or config_get first if unsure of the key. The result states whether a `leap daemon restart` is required. Never edit LeapFlow config files directly.\n- **web_fetch**(url, select, timeout, max_bytes): Read a URL over HTTP(S) and get back extracted, context-sized content: parsed JSON for API endpoints, readable text plus links for web pages. Use this for anything on the internet — prices, docs, releases, articles — instead of running curl through shell_run: it reports real HTTP status codes, retries rate limits on its own, and is a plain read so a retry is always safe. For JSON APIs pass `select` with a dotted path (e.g. 'chart.result.0.meta') to return just that part instead of the whole payload.\n- **memory_search**(query, limit): Search agent memory for relevant past experiences, observations, and facts.\n- **memory_add**(content, kind) [capability_expand category: write]: Store a new observation or insight in memory for future reference.\n- **research_note**(kind, text): Record a compact, structured note about the current task's state so it survives context compression on long / multi-step tasks. Use for durable findings, open questions still to resolve, decisions / excluded paths, and the immediate next step. One concise sentence per note.\n- **capability_expand**(category): Fetch the full callable schema for every tool in a capability category (e.g. 'hub', 'gateway', 'desktop', 'delegate', 'file', 'memory', 'skill'). The compact tool index always lists every registered tool by name and a one-line summary, but only a static low-risk subset is directly callable each turn. If you need a tool from the index that is not yet callable, call capability_expand with its category first; the matching tools become callable in this turn. Never invent a tool name — expand the category instead.\n- **delegate_task**(goal, context) [capability_expand category: delegate]: Delegate a complex sub-task to an isolated subagent. The subagent gets a fresh context and restricted tool access. Use when a task is self-contained and can be solved independently.\n- **schedule_reentry**(kind, reason, delay_seconds, event_match, max_reentries, deadline_seconds): Register a re-entry so this task can resume later from its current orientation (findings / open questions / next step). Use when work must pause and continue after a delay (kind=time) or when a matching platform event arrives (kind=event), instead of finishing now. The research-ledger state is carried over automatically.\n- **hub_push**(skill_name, visibility, version) [capability_expand category: hub]: Push a local skill to the ModelScope Hub for sharing or backup.\n- **hub_pull**(repo_id, version) [capability_expand category: hub]: Pull a skill from the ModelScope Hub to install locally.\n- **hub_search**(query) [capability_expand category: hub]: Search for skills on the Hub by keyword or description.\n- **hub_sync**(mode, dry_run) [capability_expand category: hub]: Preview or execute sync between local skills and Hub.\n- **platform_action**(platform, action, payload, backend_kind) [capability_expand category: gateway]: Execute an exact registered business action on an external platform through LeapFlow's App Connector layer. Actions must be copied from the App Connector Capability Index and are addressed as domain.operation, e.g. im.send_message or docs.create_markdown. All business fields (chat_id, text, query, etc.) MUST be placed inside `payload`, never at the top level. Example: {\"platform\":\"feishu\",\"action\":\"im.send_message\",\"payload\":{\"chat_id\":\"oc_xxx\",\"text\":\"hello\"}}. Do not invent action names, do not use management actions such as list/guide/connect/status here.\n- **platform_connect**(action, platform, credentials, options, checkpoint) [capability_expand category: gateway]: List, guide, connect, disconnect, remove, or check status for external platforms using the App Connector management namespace. Supports REST and CLI backends. Use this for management actions such as list/guide/preflight/connect/status; use platform_action only for exact registered business actions.\n- **gateway_send**(platform, chat_id, text, thread_id) [capability_expand category: gateway]: Send a message to a connected external platform (Feishu group, Telegram chat, DingTalk conversation, etc.). Requires the platform to be connected via gateway_connect first. Use gateway_connect with action='list' to see connected platforms and available chat IDs.\n- **gateway_connect**(action, platform, credentials, options) [capability_expand category: gateway]: Connect, configure, or manage external platform integrations (Feishu, DingTalk, Telegram, Slack, Discord, etc.). Conversational flow: 1) call 'guide' to get setup steps + required fields, 2) present the steps to the user and ask for ALL required credentials in a single message, 3) call 'connect' with the credentials. Goal: complete in 1–2 user turns. NEVER include credential values in your text response.\n- **plugin_list**(): List the live plugin registry across Tool/Gateway/LLM subsystems for plugin management: which plugins are loaded, their category and fiber lifecycle state. For questions about LeapFlow's own capabilities or whether it supports X, use self_describe instead.\n- **plugin_status**(plugin_id): Get detailed status of a specific plugin: its declared category, runtime dependencies, contributed tools, and fiber lifecycle state.\n- **plugin_versions**(plugin_id): List recorded source versions and active pointer for a profile-scoped plugin.\n- **plugin_propose**(requested_capability, plugin_id, proposed_tools, test_cases, risk_level, evidence): Create a side-effect-free PluginProposal from explicit capability-gap evidence. Use this before plugin_generate when a missing capability should be reviewed. Does not call an LLM, write files, or install anything.\n- **assess_compatibility**(manifest, source_path) [capability_expand category: plugin_management]: Assess whether a foreign plugin manifest or real DSH source bundle is compatible with LeapFlow. A source bundle assessment is static: runtime_ready stays false until restricted Node discovery during plugin_install. Returns component-level verdicts and limitations.\n- **plugin_generate**(plugin_id, description, proposal_id) [capability_expand category: system]: Generate a new ToolPlugin from a natural-language capability description. The LLM produces code that conforms to the ToolPlugin Protocol; it is then rigorously validated (syntax, structure, import, protocol conformance). The generated source is stored in CAS and receives explicit content approval. It DOES NOT install the plugin — installation requires a second, mutation-specific approval via plugin_install.\n- **plugin_install**(plugin_id, code, marketplace_name, source_path, proposal_id, version_label) [capability_expand category: system]: Install a plugin from exactly one source: validated Python code, the configured Python marketplace, or a real DSH source bundle. DSH bundles run restricted Node discovery before registration; only public host tools are installed and client UI limitations are reported. Writes profile-scoped state and mutates the process-global registry. REQUIRES APPROVAL.\n- **plugin_rollback**(plugin_id, version) [capability_expand category: system]: Rollback a profile-scoped plugin to a recorded source version and reload it. REQUIRES APPROVAL.\n- **plugin_reload**(plugin_id, version_label) [capability_expand category: system]: Hot-reload a plugin at runtime. Disposes the old plugin fiber, re-imports its module, and registers a fresh instance. Existing in-flight turns are unaffected (snapshot isolation). REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_disable**(plugin_id) [capability_expand category: system]: Disable a plugin by disposing its fiber, removing its tools from the runtime registry. Cannot disable self_management itself. REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_remove**(plugin_id, delete_source) [capability_expand category: system]: Terminally remove a plugin: dispose its fiber, unregister its tools, remove reload metadata, and optionally delete its profile-scoped source file. Cannot remove self_management itself. REQUIRES APPROVAL.\n- **plugin_unquarantine**(plugin_id) [capability_expand category: plugin_management]: Restore a quarantined plugin to probation status for re-evaluation. Unfreezes the trust ledger, transitions the proposal back to PROBATION, and reloads the plugin. REQUIRES APPROVAL.\n- **plugin_enable**(plugin_id) [capability_expand category: system]: Re-enable a previously disabled plugin by reloading its module and registering a fresh instance. REQUIRES APPROVAL.\n- **self_describe**(facet): Answer questions about LeapFlow ITSELF from live runtime evidence — its identity/version/model, what tools and plugins it has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of self-awareness. Prefer this over reading LeapFlow's own source code. Pick facet=capabilities for 'what can you do / do you support X', identity for version/model, runtime for context/posture, or all for a full self-check when explicitly requested.\n- **runtime_snapshot**(): Lightweight (~150 token) flat snapshot of current runtime state: model, context budget, posture, disclosure level, turn count, cache hit rate, uptime, tool count, and pending approvals.\n- **schedule_create**(trigger_expression, instruction, execution_mode, max_retries, delivery_target) [capability_expand category: scheduler]: Create a new scheduled task. Specify a trigger expression (e.g. '30m', 'every 2h', '0 9 * * *') and the instruction to execute on each trigger. Optionally provide an execution mode and a delivery target to receive notifications.\n- **schedule_list**() [capability_expand category: scheduler]: List all scheduled tasks with their current state, trigger, and next due time.\n- **schedule_status**(task_id) [capability_expand category: scheduler]: Get detailed status and recent execution history for a scheduled task.\n- **schedule_pause**(task_id) [capability_expand category: scheduler]: Pause a scheduled task so it stops firing without being cancelled.\n- **schedule_resume**(task_id) [capability_expand category: scheduler]: Resume a paused scheduled task — re-arms it and recalculates next due time.\n- **schedule_cancel**(task_id) [capability_expand category: scheduler]: Cancel a scheduled task permanently.\n- **tool_search**(query, max_results): Search all registered tools by keyword relevance. Returns a ranked list of matching tools with name, category, and summary. Use this when you need a tool but are unsure of its exact name or category.\n- **tool_describe**(tool_name): Get the full callable schema of a single tool by exact name. Returns the tool's description, parameters, and metadata. Use after tool_search to inspect a specific tool before calling it.\n- **session_search**(query, limit) [capability_expand category: unclassified]: Search past conversation sessions in the current workspace for relevant context.\n- **session_list**(limit): List recent conversation sessions in the current workspace with stable ids, titles, dates, and summaries. Use for browsing past tasks or when user asks to see history without specific search terms. No keywords needed — returns chronological list.\n- **session_detail**(session_id, limit, offset, include_inactive) [capability_expand category: unclassified]: Read a paginated persisted transcript for one past conversation session in the current workspace. Use after session_list or session_search returns a session_id.\n- **click**(element_index) [capability_expand category: desktop]: Click a UI element by its element_index (from the latest observe_ui snapshot)\n- **get_clipboard**() [capability_expand category: desktop]: Read current clipboard text content\n- **list_apps**(filter, running_only) [capability_expand category: desktop]: List available applications on this system. Use to discover correct bundle_id before switch_app.\n- **list_windows**() [capability_expand category: desktop]: List all top-level windows with pid, window_id, title, and per-window state (minimized, on-screen). Call this first to pick the pid and window_id that observe_ui and other window tools require.\n- **observe_ui**(pid, window_id, query) [capability_expand category: desktop]: Snapshot one window's actionable UI elements, each tagged with an element_index for click/right_click/read_text. Re-observe after actions — indices belong to one snapshot. Requires the window's pid and window_id from list_windows.\n- **open_url**(url, app_id) [capability_expand category: desktop]: Open a URL in the default or specified browser\n- **read_text**(element_index) [capability_expand category: desktop]: Read the text content of a specific UI element from the latest snapshot\n- **right_click**(element_index) [capability_expand category: desktop]: Right-click a UI element to open its context menu. Returns visible menu items.\n- **screenshot**(pid, window_id) [capability_expand category: desktop]: Capture a screenshot for visual verification. With pid + window_id captures that window (works across all displays); defaults to the last observed window, or the full desktop when no window has been observed.\n- **scroll**(element_index, direction, amount, pid, window_id) [capability_expand category: desktop]: Scroll a scrollable area of a window. Omit element_index to scroll the window's focused/page scroller; pass one to scroll an exact element from the latest snapshot.\n- **select_text**(element_index) [capability_expand category: desktop]: Select all text in a UI element (focus + select-all, for subsequent copy)\n- **set_clipboard**(text) [capability_expand category: desktop]: Write text to the clipboard\n- **shortcut**(keys) [capability_expand category: desktop]: Execute a keyboard shortcut\n- **switch_app**(app_id) [capability_expand category: desktop]: Switch to an app (launch if needed, activate, verify)\n- **type_text**(text) [capability_expand category: desktop]: Type text into the currently focused element\n- **wait**(seconds) [capability_expand category: desktop]: Wait for a specified duration before continuing\n- **wait_until**(condition, pid, window_id, timeout, poll_interval) [capability_expand category: desktop]: Wait until a UI condition is met (polls UI tree). Returns elements when found or on timeout.\n- **wait_until_stable**(timeout, poll_interval, pid, window_id) [capability_expand category: desktop]: Wait until the UI stops changing (element set stabilizes across polls).\n\n## App Connector Capability Index\nLeapFlow can onboard and manage external apps through `platform_connect` and execute exact registered business actions through `platform_action`.\nFor requests about connecting, setting up, configuring, enabling, or managing a supported app, use `platform_connect` first instead of generating SDK/Webhook sample code.\n`platform_connect.action` is the management namespace: list, guide, preflight, connect, disconnect, remove, status, events_start, events_stop, events_status.\n`platform_action.action` is only for exact registered platform business actions listed below, such as `im.send_message`; never use management actions like `list` or `guide` there.\nAll business fields MUST go inside `payload`; top-level keys are only `platform`, `action`, and `payload`.\nDo not invent platform IDs or platform action names. If the needed action is not listed, ask for discovery/clarification instead of guessing.\nUse `platform_connect` with `action='guide'` and the matching `platform` to start onboarding; use `action='list'` when the app is unclear.\nWhen a pending onboarding state is present, continue from that state with `platform_connect` instead of asking the user to restate the app.\nSupported apps:\n- `api_server`: API Server (OpenAI Compatible) (category=api; backend=adapter; platform_action actions=none registered)\n- `dingtalk`: 钉钉 (DingTalk) (category=im; backend=adapter; platform_action actions=none registered)\n- `feishu`: 飞书 (Feishu/Lark) (category=im; backend=cli)\n - `calendar.create_event` payload={summary*, start_time*, end_time*, attendees} [write/high]\n - `docs.create_markdown` payload={title*, markdown*, folder_token} [write/medium]\n - `drive.search` payload={query*, limit} [read/medium]\n - `im.add_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.download_resource` payload={message_id*, file_key*, type*} [read/low]\n - `im.get_messages` payload={message_ids*} [read/low]\n - `im.list_chats` payload={page_size} [read/low]\n - `im.list_messages` payload={chat_id*} [read/low]\n - `im.list_thread_messages` payload={thread*} [read/low]\n - `im.remove_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.reply_message` payload={message_id*, text*} [send/high]\n - `im.search_chats` payload={query*, page_size} [read/low]\n - `im.search_messages` payload={query*} [read/medium]\n - `im.send_message` payload={chat_id*, text*, thread_id} [send/high]\n - `im.update_card` payload={token*, card*} [write/medium]\n - `im.update_message` payload={message_id*, text*} [write/medium]\n - `mail.search_unread` payload={query, limit} [read/high]\n - `sheets.append_row` payload={spreadsheet_token*, sheet_id*, values*} [write/medium]\n - `task.create` payload={title*, description, due_time} [write/medium]\n- `telegram`: Telegram (category=social; backend=adapter; platform_action actions=none registered)\n- `webhook`: Webhook (Generic) (category=webhook; backend=adapter; platform_action actions=none registered)\n\n## Tool Usage\nTools are normally invoked through the native function-calling mechanism, not by writing JSON in your reply\ntext. Only if the provider signals that native function calling is unavailable for this turn, fall back to a\nsingle JSON code block: `{\"name\": \"tool_name\", \"arguments\": {\"key\": \"value\"}}` — use this fallback format\nonly, never both. Only call a tool whose exact name appears in the tool index above and is currently callable\n(or reachable via `capability_expand`). Never invent, rename, alias, or guess a tool name, platform ID, or\nplatform action from argument shape or wording — if the index does not list it, it does not exist.\n`platform_connect.action` (list/guide/preflight/connect/disconnect/remove/status/events_start/events_stop/\nevents_status) is the App Connector management namespace; `platform_action.action` only accepts exact\nregistered business actions such as `im.send_message` shown in the App Connector Capability Index — never mix\nthe two namespaces. If a tool call returns an unknown/unavailable result, use the returned suggestions or\navailable names for a single retry instead of trying further variations of the same guess.\n\n**Side-effect action rule** (`platform_action` with effect=send/write/execute):\n- Call each unique action+payload **exactly once**. Never include duplicates in the same turn.\n- Once the result returns `\"completed\": true`, that action is DONE for this task. Do NOT call it again\n in any subsequent turn — immediately summarize the result for the user instead.\n- The system enforces idempotency: duplicate calls are blocked and will not execute.\n- If the user explicitly requests sending/writing multiple times, use distinct payloads per call.\n\n**Resource identifier provenance rule**:\n- NEVER fabricate, guess, or infer resource identifiers (chat_id, message_id, file_key, user_id, etc.).\n Every resource ID used in a side-effect action MUST come from a successful API response in this session.\n- If a read/list action fails (e.g. authorization error), you do NOT have valid resource IDs.\n Report the failure to the user — do NOT attempt the dependent write/send action with a guessed ID.\n- When a tool result contains `\"llm_instruction\"`, follow it exactly.\n\n## Guidelines\n1. **Direct answers first**: If you already know the answer, respond directly without tools.\n2. **Avoid redundant tool calls**: Do not call the same tool with the same arguments more than once in the same user turn. When an existing tool result already answers the user's request, stop calling tools and answer directly.\n3. **Use tools proactively**: When the user asks about files, time, system state, or needs actions performed, use the appropriate tool.\n4. **Chain tools when needed**: You can call multiple tools in sequence (e.g., list files → read file → summarize).\n5. **Handle failures gracefully**: If a tool fails, explain what went wrong and suggest alternatives. If it failed because of an unknown tool/platform/action name, retry once with an exact name from the returned suggestions, then explain rather than keep guessing.\n6. **Summarize results naturally**: After tool execution, synthesize the results into a helpful answer rather than dumping raw output.\n7. **Stay conversational**: Maintain a natural, helpful tone. Acknowledge context from earlier in the conversation.\n8. **Recall past work**:\n - Broad queries (\"之前做了什么\", \"列出任务\"): answer from the \"Recent Task History\" section already in your context. If insufficient, call session_list.\n - Specific lookups (\"上次那个配置怎么改的\"): call session_search with relevant phrases (NOT single characters).\n - Do NOT call search tools repeatedly with keyword fragments. One well-phrased call is better than ten fragmented ones.\n\n## Coding & Verification\nWhen working with code, prefer the precise built-in tools over ad-hoc shell: use `repo_map` to orient in an\nunfamiliar project, `code_search`/`file_find` to locate and `code_intel` for symbols, `edit_file` (anchored\nsearch-replace, or a unified `diff`) to change files — never rewrite a whole file to change a few lines — and\n`git_query` to inspect diffs/log. After edits, check `syntax_ok` in the result and run `test_run`/`lint_check`\nbefore declaring the work done. Batch independent read-only calls (search/read/query) into a single turn — they\nrun in parallel.\n\n## Presentation Style\n1. **Polished Markdown only**: Format user-facing answers with clean Markdown headings, short paragraphs, and concise bullets. Use tables only when they improve comparison or scanning.\n2. **Terminal-friendly layout**: Keep lines readable in a TUI; avoid dense walls of text, deeply nested lists, oversized ASCII art, or heavy visual blocks.\n3. **Elegant emphasis**: Use bold text sparingly for key terms and conclusions. Avoid excessive emojis, decorative symbols, repeated separators, or visual noise.\n4. **Theme-safe colors**: Do not emit ANSI escape codes, HTML color tags, Rich markup, or hardcoded color names. Rely on the TUI theme to render Markdown professionally.\n5. **No leaked tool protocol**: Never show tool-call JSON, internal schemas, raw observations, tool result payloads, or hidden reasoning in the final answer unless the user explicitly asks for raw/debug output. Treat any prior `{\"name\": ..., \"arguments\": ...}` blocks and `Tool result (...)` messages as internal execution context only.\n6. **Professional closure**: End with a concise conclusion or next step when helpful; avoid rambling after the useful answer is complete.\n\nWhen finished with all tool calls, respond normally without a JSON block, tool-call transcript, or process log.\n\n## Task Contract\n- Task ID: turn-2\n- Original user request: Install the prepared adaptive JSON pretty plugin.\n- Workspace root: \n- Allowed roots: \n- Treat relative project paths as relative to the workspace root; never infer `.` as the project root when a workspace root is provided.\n- Workspace boundary is enforced by tools: do not read, search, edit, or run commands against paths outside the allowed roots unless the user explicitly requests an external path and the tool/approval policy permits it.\n- LeapFlow workspace config is optional at `/.leapflow/config.yaml`; runtime config is loaded from `~/.leapflow/config/user.yaml` and `~/.leapflow/profiles//config/*.yaml`.\n- Preserve this task contract across summarization, compression, tool loops, and memory retrieval.\n" + }, + { + "role": "system", + "content": "\n## Recent Session Summary\n- [assistant] Observed the missing JSON pretty tool.\n- [user] Install the prepared adaptive JSON pretty plugin.\n" + }, + { + "role": "assistant", + "content": "Observed the missing JSON pretty tool." + }, + { + "role": "user", + "content": "Install the prepared adaptive JSON pretty plugin.\nInstall the prepared adaptive JSON pretty plugin." + } + ], + "tools": [ + "capability_expand", + "code_intel", + "code_search", + "config_get", + "config_list", + "env_info", + "file_find", + "file_list", + "file_read", + "git_query", + "lint_check", + "memory_search", + "plugin_list", + "plugin_propose", + "plugin_status", + "plugin_versions", + "repo_map", + "research_note", + "runtime_snapshot", + "schedule_reentry", + "self_describe", + "session_list", + "skill_view", + "skills_list", + "terminal_list", + "terminal_read", + "test_run", + "text_search", + "time_get", + "tool_describe", + "tool_search", + "web_fetch" + ] + }, + "responses": [ + { + "status": 200, + "content_type": "application/json", + "body": "{\"id\": \"chatcmpl-cassette\", \"object\": \"chat.completion\", \"model\": \"cassette-model\", \"choices\": [{\"index\": 0, \"message\": {\"role\": \"assistant\", \"content\": \"\", \"tool_calls\": [{\"id\": \"call_1\", \"type\": \"function\", \"function\": {\"name\": \"plugin_install\", \"arguments\": \"{\\\"plugin_id\\\": \\\"json_pretty_loop_e2e\\\", \\\"code\\\": \\\"from __future__ import annotations\\\\n\\\\nimport json\\\\nfrom typing import Any\\\\n\\\\nfrom leapflow.plugins.protocol import ToolMetadata\\\\n\\\\n\\\\nasync def json_pretty_loop_e2e(text: str = \\\\\\\"\\\\\\\", **kwargs: Any) -> dict[str, Any]:\\\\n payload = text or kwargs.get(\\\\\\\"payload\\\\\\\") or \\\\\\\"{}\\\\\\\"\\\\n try:\\\\n parsed = json.loads(str(payload))\\\\n except json.JSONDecodeError as exc:\\\\n return {\\\\\\\"ok\\\\\\\": False, \\\\\\\"error\\\\\\\": str(exc)}\\\\n return {\\\\\\\"ok\\\\\\\": True, \\\\\\\"content\\\\\\\": json.dumps(parsed, ensure_ascii=False, indent=2, sort_keys=True)}\\\\n\\\\n\\\\nclass JsonPrettyLoopE2EPlugin:\\\\n @property\\\\n def plugin_id(self) -> str:\\\\n return \\\\\\\"json_pretty_loop_e2e\\\\\\\"\\\\n\\\\n @property\\\\n def category(self) -> str:\\\\n return \\\\\\\"formatting\\\\\\\"\\\\n\\\\n @property\\\\n def dependencies(self) -> list[str]:\\\\n return []\\\\n\\\\n @property\\\\n def tools(self) -> list[ToolMetadata]:\\\\n return [\\\\n ToolMetadata(\\\\n name=\\\\\\\"json_pretty_loop_e2e\\\\\\\",\\\\n description=\\\\\\\"Pretty-print JSON for the adaptive closed-loop journey.\\\\\\\",\\\\n parameters_schema={\\\\n \\\\\\\"type\\\\\\\": \\\\\\\"object\\\\\\\",\\\\n \\\\\\\"properties\\\\\\\": {\\\\n \\\\\\\"text\\\\\\\": {\\\\\\\"type\\\\\\\": \\\\\\\"string\\\\\\\", \\\\\\\"description\\\\\\\": \\\\\\\"JSON text to format\\\\\\\"}\\\\n },\\\\n },\\\\n handler=json_pretty_loop_e2e,\\\\n x_leapflow={\\\\n \\\\\\\"category\\\\\\\": \\\\\\\"formatting\\\\\\\",\\\\n \\\\\\\"risk_level\\\\\\\": \\\\\\\"read_only\\\\\\\",\\\\n \\\\\\\"schema_cost\\\\\\\": \\\\\\\"low\\\\\\\",\\\\n \\\\\\\"requires_approval\\\\\\\": False,\\\\n },\\\\n provides_capabilities=(\\\\\\\"json.pretty\\\\\\\",),\\\\n requires_platform_capabilities=(\\\\\\\"file.ops\\\\\\\",),\\\\n )\\\\n ]\\\\n\\\\n def bind_runtime(self, **deps: Any) -> None:\\\\n return None\\\\n\\\\n\\\\nplugin = JsonPrettyLoopE2EPlugin()\\\\n\\\", \\\"version_label\\\": \\\"r7\\\"}\"}}]}, \"finish_reason\": \"tool_calls\"}], \"usage\": {\"prompt_tokens\": 64, \"completion_tokens\": 16, \"total_tokens\": 80}}" + } + ] +} diff --git a/tests/_fixtures/cassettes/r7_adaptive_plugin_loop/cassette-model-9518e5ff224e4b45.cassette.json b/tests/_fixtures/cassettes/r7_adaptive_plugin_loop/cassette-model-9518e5ff224e4b45.cassette.json new file mode 100644 index 00000000..d46c823f --- /dev/null +++ b/tests/_fixtures/cassettes/r7_adaptive_plugin_loop/cassette-model-9518e5ff224e4b45.cassette.json @@ -0,0 +1,63 @@ +{ + "fingerprint": "9518e5ff224e4b457869c018b569427f7f40a40fc980d036f45ed0587e490eba", + "note": "captured in seed mode", + "request": { + "model": "cassette-model", + "stream": false, + "messages": [ + { + "role": "system", + "content": "You are LeapFlow, an intelligent assistant that can both converse naturally and take real actions on the user's computer.\n\n## Capabilities\nThe tool index below lists **every** registered tool by name and a one-line summary — this is the complete\ncapability contract; nothing else exists. Only a subset is directly callable this turn (via native tool calling,\nnot a JSON block in your reply). If you need a tool from the index that is not yet callable, call\n`capability_expand` with its category name first — the matching tools become callable immediately after.\nWhen the user asks about LeapFlow **itself** — its identity/version/model, what tools, plugins, or capabilities\nit has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of\nself-awareness — answer from live runtime evidence via `self_describe` (facet=capabilities for \"what can you\ndo / do you support X\", identity for version/model, runtime for context/posture, or `all` for a full self-check),\nnever by reading LeapFlow's own source code, documentation, or memory. Report configuration-dependent or\nunavailable capabilities as limitations rather than inferring them.\n- **text_search**(text, pattern): Search for a regex pattern in text.\n- **text_replace**(text, old, new, count) [capability_expand category: write]: Replace occurrences of a substring in text.\n- **time_get**(): Get current date and time.\n- **env_info**(): Get system environment information (OS, Python version, cwd).\n- **skills_list**(query, category, source): List available learned skills. Use when user asks about capabilities or you need a specific skill.\n- **skill_view**(name): View the full content of a specific skill document.\n- **skill_list_files**(name) [capability_expand category: skills]: List all support files in a skill directory (configs, templates, examples). Use before skill_file_read to discover available files.\n- **skill_file_read**(name, file_path) [capability_expand category: skills]: Read an individual support file from a skill directory. Use skill_list_files first to discover available files.\n- **code_intel**(path, operation): Precise document symbols (outline) for a source file: classes, functions, and methods with line ranges. Python uses an exact AST parse; other languages use a keyword-prefix scan. Prefer over file_read mode=symbols for accurate navigation before editing. Read-only.\n- **repo_map**(path): Compact project orientation for a repository root: languages, detected test/lint commands, top-level structure, entry points, manifest, and VCS branch. Call this first when entering an unfamiliar codebase. Read-only.\n- **scm_sync**(action, cwd, remote, pull_ref, push_ref, timeout) [capability_expand category: scm]: Run a typed git SCM action. Use this instead of shell_run for git pull/push/status. For 'pull origin main then push', set action='pull_then_push', remote='origin', pull_ref='main', and omit push_ref so LeapFlow pushes the current local branch.\n- **git_query**(action, cwd, ref, path, staged, max_count, stat): Read-only structured git inspection: action=diff|log|status|branch|show. Prefer over shell_run for reading repo state — output is clipped, redacted, and log/branch are parsed into structured fields. Use scm_sync for pull/push.\n- **git_write**(action, cwd, message, stage_all, name, ref, create) [capability_expand category: scm]: Mutating git actions: action=commit (message, stage_all), branch (create+switch), checkout (switch; create=true for -b). Approval-gated. Use scm_sync for pull/push and git_query for reads.\n- **test_run**(command, cwd, timeout): Run the project's test suite and return structured results (framework, passed/failed counts, failing tests). Auto-detects the runner (pytest/npm/go/cargo) or uses a configured/explicit command; executes via the governed shell. ok=true means the runner executed — see 'success' for pass/fail.\n- **lint_check**(command, cwd, timeout): Run the project's linter and return a structured clean/issue result. Auto-detects the linter (ruff/eslint/go vet/clippy) or uses a configured/explicit command; executes via the governed shell. ok=true means the linter ran — see 'clean'.\n- **file_list**(path, pattern, depth): List files and directories at a given path. Use depth=1 or depth=2 to get a recursive tree in one call instead of listing each sub-directory separately.\n- **file_read**(path, max_lines, start_line, max_chars, mode): Read text file content with adaptive context governance. For large or unfamiliar files, prefer mode='outline' or mode='symbols' first, then use mode='raw' with start_line/max_lines for the specific range you actually need. For LeapFlow's own settings, use config_list / config_get / config_set — its config files are outside the workspace and not readable here.\n- **file_write**(path, content, mode) [capability_expand category: write]: Write content to a file (overwrite or append).\n- **code_search**(pattern, patterns, path, glob, ignore_case, multiline, max_results, context_lines): Search file CONTENTS by regex pattern across a directory tree (ripgrep-backed). Requires a regex pattern. NOT for listing or browsing directory contents — use file_list for that. Prefer this over shell_run grep: faster, skips VCS/dependency/build dirs, and returns structured path:line:column matches. Batch related lookups into ONE call via `patterns` (OR-combined, single pass) instead of issuing several separate searches. Use file_read for the surrounding context of a hit.\n- **file_find**(glob, path, max_results): Find files by a recursive glob pattern under a base path (e.g. '**/test_*.py' or '*.md'). Prefer this over shell_run find; skips VCS/dependency/build dirs.\n- **edit_file**(path, edits, dry_run, diff) [capability_expand category: write]: Apply targeted, anchored search-replace edits to an EXISTING text file (use file_write to create/overwrite). Each edit is {original_text, new_text, replace_all?}; original_text must match exactly and uniquely (or set replace_all) — a non-unique or missing anchor is rejected so files are never corrupted. Set dry_run to preview. Alternatively pass a unified 'diff' to apply its hunks as anchored edits. Far cheaper and safer than rewriting a whole file.\n- **shell_run**(command, cwd, timeout) [capability_expand category: shell]: Execute a one-shot shell command with timeout protection. Runs in the active workspace; paths resolving outside it are refused. Reach for a structured tool first when one fits — web_fetch for anything over HTTP(S), git_query/scm_sync for git, code_search/file_find/file_read for the repo, config_get/config_set for LeapFlow's own settings — because those report typed results, while a failed shell command can only be diagnosed from its exit code and stderr. Every shell run counts as an external side effect, so a failure stops the rest of the batch and is not retried automatically.\n- **terminal_open**(command, cwd, shell) [capability_expand category: terminal]: Open a PERSISTENT shell session (REPL/dev server/watch), returning a session_id for terminal_send/read/close. Disabled unless tools.terminal_session_enabled is set. For one-shot commands use shell_run instead.\n- **terminal_send**(session_id, input, wait) [capability_expand category: terminal]: Send a line of input to a persistent terminal session and return output captured shortly after.\n- **terminal_read**(session_id, wait): Drain buffered output from a persistent terminal session (optionally waiting briefly first).\n- **terminal_close**(session_id) [capability_expand category: terminal]: Terminate a persistent terminal session and release its process group.\n- **terminal_list**(): List active persistent terminal sessions.\n- **config_list**(category, limit): List LeapFlow's own writable settings (LLM model and endpoint, daemon, memory, perception, gateway, …) with current values. Use this to discover the exact key before changing anything. Optionally narrow by `category`. This is the only correct way to inspect LeapFlow configuration — never read config files from disk.\n- **config_get**(key): Read one LeapFlow setting by key (e.g. 'llm.model', 'llm.base_url', 'daemon.log_level'), returning its current value, type, scopes, and whether a change needs a daemon restart. There is no 'llm.provider' key: provider behavior is inferred from the OpenAI-compatible 'llm.base_url'. Never read LeapFlow config files from disk — use this.\n- **config_set**(key, value, scope) [capability_expand category: config]: Change one LeapFlow setting by key, e.g. switch an OpenAI-compatible LLM with key='llm.model' and key='llm.base_url'. There is no 'llm.provider' key: provider behavior is inferred from the endpoint. Values are validated and coerced; credentials are stored in the vault automatically. Call config_list or config_get first if unsure of the key. The result states whether a `leap daemon restart` is required. Never edit LeapFlow config files directly.\n- **web_fetch**(url, select, timeout, max_bytes): Read a URL over HTTP(S) and get back extracted, context-sized content: parsed JSON for API endpoints, readable text plus links for web pages. Use this for anything on the internet — prices, docs, releases, articles — instead of running curl through shell_run: it reports real HTTP status codes, retries rate limits on its own, and is a plain read so a retry is always safe. For JSON APIs pass `select` with a dotted path (e.g. 'chart.result.0.meta') to return just that part instead of the whole payload.\n- **memory_search**(query, limit): Search agent memory for relevant past experiences, observations, and facts.\n- **memory_add**(content, kind) [capability_expand category: write]: Store a new observation or insight in memory for future reference.\n- **research_note**(kind, text): Record a compact, structured note about the current task's state so it survives context compression on long / multi-step tasks. Use for durable findings, open questions still to resolve, decisions / excluded paths, and the immediate next step. One concise sentence per note.\n- **capability_expand**(category): Fetch the full callable schema for every tool in a capability category (e.g. 'hub', 'gateway', 'desktop', 'delegate', 'file', 'memory', 'skill'). The compact tool index always lists every registered tool by name and a one-line summary, but only a static low-risk subset is directly callable each turn. If you need a tool from the index that is not yet callable, call capability_expand with its category first; the matching tools become callable in this turn. Never invent a tool name — expand the category instead.\n- **delegate_task**(goal, context) [capability_expand category: delegate]: Delegate a complex sub-task to an isolated subagent. The subagent gets a fresh context and restricted tool access. Use when a task is self-contained and can be solved independently.\n- **schedule_reentry**(kind, reason, delay_seconds, event_match, max_reentries, deadline_seconds): Register a re-entry so this task can resume later from its current orientation (findings / open questions / next step). Use when work must pause and continue after a delay (kind=time) or when a matching platform event arrives (kind=event), instead of finishing now. The research-ledger state is carried over automatically.\n- **hub_push**(skill_name, visibility, version) [capability_expand category: hub]: Push a local skill to the ModelScope Hub for sharing or backup.\n- **hub_pull**(repo_id, version) [capability_expand category: hub]: Pull a skill from the ModelScope Hub to install locally.\n- **hub_search**(query) [capability_expand category: hub]: Search for skills on the Hub by keyword or description.\n- **hub_sync**(mode, dry_run) [capability_expand category: hub]: Preview or execute sync between local skills and Hub.\n- **platform_action**(platform, action, payload, backend_kind) [capability_expand category: gateway]: Execute an exact registered business action on an external platform through LeapFlow's App Connector layer. Actions must be copied from the App Connector Capability Index and are addressed as domain.operation, e.g. im.send_message or docs.create_markdown. All business fields (chat_id, text, query, etc.) MUST be placed inside `payload`, never at the top level. Example: {\"platform\":\"feishu\",\"action\":\"im.send_message\",\"payload\":{\"chat_id\":\"oc_xxx\",\"text\":\"hello\"}}. Do not invent action names, do not use management actions such as list/guide/connect/status here.\n- **platform_connect**(action, platform, credentials, options, checkpoint) [capability_expand category: gateway]: List, guide, connect, disconnect, remove, or check status for external platforms using the App Connector management namespace. Supports REST and CLI backends. Use this for management actions such as list/guide/preflight/connect/status; use platform_action only for exact registered business actions.\n- **gateway_send**(platform, chat_id, text, thread_id) [capability_expand category: gateway]: Send a message to a connected external platform (Feishu group, Telegram chat, DingTalk conversation, etc.). Requires the platform to be connected via gateway_connect first. Use gateway_connect with action='list' to see connected platforms and available chat IDs.\n- **gateway_connect**(action, platform, credentials, options) [capability_expand category: gateway]: Connect, configure, or manage external platform integrations (Feishu, DingTalk, Telegram, Slack, Discord, etc.). Conversational flow: 1) call 'guide' to get setup steps + required fields, 2) present the steps to the user and ask for ALL required credentials in a single message, 3) call 'connect' with the credentials. Goal: complete in 1–2 user turns. NEVER include credential values in your text response.\n- **plugin_list**(): List the live plugin registry across Tool/Gateway/LLM subsystems for plugin management: which plugins are loaded, their category and fiber lifecycle state. For questions about LeapFlow's own capabilities or whether it supports X, use self_describe instead.\n- **plugin_status**(plugin_id): Get detailed status of a specific plugin: its declared category, runtime dependencies, contributed tools, and fiber lifecycle state.\n- **plugin_versions**(plugin_id): List recorded source versions and active pointer for a profile-scoped plugin.\n- **plugin_propose**(requested_capability, plugin_id, proposed_tools, test_cases, risk_level, evidence): Create a side-effect-free PluginProposal from explicit capability-gap evidence. Use this before plugin_generate when a missing capability should be reviewed. Does not call an LLM, write files, or install anything.\n- **assess_compatibility**(manifest, source_path) [capability_expand category: plugin_management]: Assess whether a foreign plugin manifest or real DSH source bundle is compatible with LeapFlow. A source bundle assessment is static: runtime_ready stays false until restricted Node discovery during plugin_install. Returns component-level verdicts and limitations.\n- **plugin_generate**(plugin_id, description, proposal_id) [capability_expand category: system]: Generate a new ToolPlugin from a natural-language capability description. The LLM produces code that conforms to the ToolPlugin Protocol; it is then rigorously validated (syntax, structure, import, protocol conformance). The generated source is stored in CAS and receives explicit content approval. It DOES NOT install the plugin — installation requires a second, mutation-specific approval via plugin_install.\n- **plugin_install**(plugin_id, code, marketplace_name, source_path, proposal_id, version_label) [capability_expand category: system]: Install a plugin from exactly one source: validated Python code, the configured Python marketplace, or a real DSH source bundle. DSH bundles run restricted Node discovery before registration; only public host tools are installed and client UI limitations are reported. Writes profile-scoped state and mutates the process-global registry. REQUIRES APPROVAL.\n- **plugin_rollback**(plugin_id, version) [capability_expand category: system]: Rollback a profile-scoped plugin to a recorded source version and reload it. REQUIRES APPROVAL.\n- **plugin_reload**(plugin_id, version_label) [capability_expand category: system]: Hot-reload a plugin at runtime. Disposes the old plugin fiber, re-imports its module, and registers a fresh instance. Existing in-flight turns are unaffected (snapshot isolation). REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_disable**(plugin_id) [capability_expand category: system]: Disable a plugin by disposing its fiber, removing its tools from the runtime registry. Cannot disable self_management itself. REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_remove**(plugin_id, delete_source) [capability_expand category: system]: Terminally remove a plugin: dispose its fiber, unregister its tools, remove reload metadata, and optionally delete its profile-scoped source file. Cannot remove self_management itself. REQUIRES APPROVAL.\n- **plugin_unquarantine**(plugin_id) [capability_expand category: plugin_management]: Restore a quarantined plugin to probation status for re-evaluation. Unfreezes the trust ledger, transitions the proposal back to PROBATION, and reloads the plugin. REQUIRES APPROVAL.\n- **plugin_enable**(plugin_id) [capability_expand category: system]: Re-enable a previously disabled plugin by reloading its module and registering a fresh instance. REQUIRES APPROVAL.\n- **self_describe**(facet): Answer questions about LeapFlow ITSELF from live runtime evidence — its identity/version/model, what tools and plugins it has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of self-awareness. Prefer this over reading LeapFlow's own source code. Pick facet=capabilities for 'what can you do / do you support X', identity for version/model, runtime for context/posture, or all for a full self-check when explicitly requested.\n- **runtime_snapshot**(): Lightweight (~150 token) flat snapshot of current runtime state: model, context budget, posture, disclosure level, turn count, cache hit rate, uptime, tool count, and pending approvals.\n- **schedule_create**(trigger_expression, instruction, execution_mode, max_retries, delivery_target) [capability_expand category: scheduler]: Create a new scheduled task. Specify a trigger expression (e.g. '30m', 'every 2h', '0 9 * * *') and the instruction to execute on each trigger. Optionally provide an execution mode and a delivery target to receive notifications.\n- **schedule_list**() [capability_expand category: scheduler]: List all scheduled tasks with their current state, trigger, and next due time.\n- **schedule_status**(task_id) [capability_expand category: scheduler]: Get detailed status and recent execution history for a scheduled task.\n- **schedule_pause**(task_id) [capability_expand category: scheduler]: Pause a scheduled task so it stops firing without being cancelled.\n- **schedule_resume**(task_id) [capability_expand category: scheduler]: Resume a paused scheduled task — re-arms it and recalculates next due time.\n- **schedule_cancel**(task_id) [capability_expand category: scheduler]: Cancel a scheduled task permanently.\n- **tool_search**(query, max_results): Search all registered tools by keyword relevance. Returns a ranked list of matching tools with name, category, and summary. Use this when you need a tool but are unsure of its exact name or category.\n- **tool_describe**(tool_name): Get the full callable schema of a single tool by exact name. Returns the tool's description, parameters, and metadata. Use after tool_search to inspect a specific tool before calling it.\n- **session_search**(query, limit) [capability_expand category: unclassified]: Search past conversation sessions in the current workspace for relevant context.\n- **session_list**(limit): List recent conversation sessions in the current workspace with stable ids, titles, dates, and summaries. Use for browsing past tasks or when user asks to see history without specific search terms. No keywords needed — returns chronological list.\n- **session_detail**(session_id, limit, offset, include_inactive) [capability_expand category: unclassified]: Read a paginated persisted transcript for one past conversation session in the current workspace. Use after session_list or session_search returns a session_id.\n- **click**(element_index) [capability_expand category: desktop]: Click a UI element by its element_index (from the latest observe_ui snapshot)\n- **get_clipboard**() [capability_expand category: desktop]: Read current clipboard text content\n- **list_apps**(filter, running_only) [capability_expand category: desktop]: List available applications on this system. Use to discover correct bundle_id before switch_app.\n- **list_windows**() [capability_expand category: desktop]: List all top-level windows with pid, window_id, title, and per-window state (minimized, on-screen). Call this first to pick the pid and window_id that observe_ui and other window tools require.\n- **observe_ui**(pid, window_id, query) [capability_expand category: desktop]: Snapshot one window's actionable UI elements, each tagged with an element_index for click/right_click/read_text. Re-observe after actions — indices belong to one snapshot. Requires the window's pid and window_id from list_windows.\n- **open_url**(url, app_id) [capability_expand category: desktop]: Open a URL in the default or specified browser\n- **read_text**(element_index) [capability_expand category: desktop]: Read the text content of a specific UI element from the latest snapshot\n- **right_click**(element_index) [capability_expand category: desktop]: Right-click a UI element to open its context menu. Returns visible menu items.\n- **screenshot**(pid, window_id) [capability_expand category: desktop]: Capture a screenshot for visual verification. With pid + window_id captures that window (works across all displays); defaults to the last observed window, or the full desktop when no window has been observed.\n- **scroll**(element_index, direction, amount, pid, window_id) [capability_expand category: desktop]: Scroll a scrollable area of a window. Omit element_index to scroll the window's focused/page scroller; pass one to scroll an exact element from the latest snapshot.\n- **select_text**(element_index) [capability_expand category: desktop]: Select all text in a UI element (focus + select-all, for subsequent copy)\n- **set_clipboard**(text) [capability_expand category: desktop]: Write text to the clipboard\n- **shortcut**(keys) [capability_expand category: desktop]: Execute a keyboard shortcut\n- **switch_app**(app_id) [capability_expand category: desktop]: Switch to an app (launch if needed, activate, verify)\n- **type_text**(text) [capability_expand category: desktop]: Type text into the currently focused element\n- **wait**(seconds) [capability_expand category: desktop]: Wait for a specified duration before continuing\n- **wait_until**(condition, pid, window_id, timeout, poll_interval) [capability_expand category: desktop]: Wait until a UI condition is met (polls UI tree). Returns elements when found or on timeout.\n- **wait_until_stable**(timeout, poll_interval, pid, window_id) [capability_expand category: desktop]: Wait until the UI stops changing (element set stabilizes across polls).\n\n## App Connector Capability Index\nLeapFlow can onboard and manage external apps through `platform_connect` and execute exact registered business actions through `platform_action`.\nFor requests about connecting, setting up, configuring, enabling, or managing a supported app, use `platform_connect` first instead of generating SDK/Webhook sample code.\n`platform_connect.action` is the management namespace: list, guide, preflight, connect, disconnect, remove, status, events_start, events_stop, events_status.\n`platform_action.action` is only for exact registered platform business actions listed below, such as `im.send_message`; never use management actions like `list` or `guide` there.\nAll business fields MUST go inside `payload`; top-level keys are only `platform`, `action`, and `payload`.\nDo not invent platform IDs or platform action names. If the needed action is not listed, ask for discovery/clarification instead of guessing.\nUse `platform_connect` with `action='guide'` and the matching `platform` to start onboarding; use `action='list'` when the app is unclear.\nWhen a pending onboarding state is present, continue from that state with `platform_connect` instead of asking the user to restate the app.\nSupported apps:\n- `api_server`: API Server (OpenAI Compatible) (category=api; backend=adapter; platform_action actions=none registered)\n- `dingtalk`: 钉钉 (DingTalk) (category=im; backend=adapter; platform_action actions=none registered)\n- `feishu`: 飞书 (Feishu/Lark) (category=im; backend=cli)\n - `calendar.create_event` payload={summary*, start_time*, end_time*, attendees} [write/high]\n - `docs.create_markdown` payload={title*, markdown*, folder_token} [write/medium]\n - `drive.search` payload={query*, limit} [read/medium]\n - `im.add_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.download_resource` payload={message_id*, file_key*, type*} [read/low]\n - `im.get_messages` payload={message_ids*} [read/low]\n - `im.list_chats` payload={page_size} [read/low]\n - `im.list_messages` payload={chat_id*} [read/low]\n - `im.list_thread_messages` payload={thread*} [read/low]\n - `im.remove_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.reply_message` payload={message_id*, text*} [send/high]\n - `im.search_chats` payload={query*, page_size} [read/low]\n - `im.search_messages` payload={query*} [read/medium]\n - `im.send_message` payload={chat_id*, text*, thread_id} [send/high]\n - `im.update_card` payload={token*, card*} [write/medium]\n - `im.update_message` payload={message_id*, text*} [write/medium]\n - `mail.search_unread` payload={query, limit} [read/high]\n - `sheets.append_row` payload={spreadsheet_token*, sheet_id*, values*} [write/medium]\n - `task.create` payload={title*, description, due_time} [write/medium]\n- `telegram`: Telegram (category=social; backend=adapter; platform_action actions=none registered)\n- `webhook`: Webhook (Generic) (category=webhook; backend=adapter; platform_action actions=none registered)\n\n## Tool Usage\nTools are normally invoked through the native function-calling mechanism, not by writing JSON in your reply\ntext. Only if the provider signals that native function calling is unavailable for this turn, fall back to a\nsingle JSON code block: `{\"name\": \"tool_name\", \"arguments\": {\"key\": \"value\"}}` — use this fallback format\nonly, never both. Only call a tool whose exact name appears in the tool index above and is currently callable\n(or reachable via `capability_expand`). Never invent, rename, alias, or guess a tool name, platform ID, or\nplatform action from argument shape or wording — if the index does not list it, it does not exist.\n`platform_connect.action` (list/guide/preflight/connect/disconnect/remove/status/events_start/events_stop/\nevents_status) is the App Connector management namespace; `platform_action.action` only accepts exact\nregistered business actions such as `im.send_message` shown in the App Connector Capability Index — never mix\nthe two namespaces. If a tool call returns an unknown/unavailable result, use the returned suggestions or\navailable names for a single retry instead of trying further variations of the same guess.\n\n**Side-effect action rule** (`platform_action` with effect=send/write/execute):\n- Call each unique action+payload **exactly once**. Never include duplicates in the same turn.\n- Once the result returns `\"completed\": true`, that action is DONE for this task. Do NOT call it again\n in any subsequent turn — immediately summarize the result for the user instead.\n- The system enforces idempotency: duplicate calls are blocked and will not execute.\n- If the user explicitly requests sending/writing multiple times, use distinct payloads per call.\n\n**Resource identifier provenance rule**:\n- NEVER fabricate, guess, or infer resource identifiers (chat_id, message_id, file_key, user_id, etc.).\n Every resource ID used in a side-effect action MUST come from a successful API response in this session.\n- If a read/list action fails (e.g. authorization error), you do NOT have valid resource IDs.\n Report the failure to the user — do NOT attempt the dependent write/send action with a guessed ID.\n- When a tool result contains `\"llm_instruction\"`, follow it exactly.\n\n## Guidelines\n1. **Direct answers first**: If you already know the answer, respond directly without tools.\n2. **Avoid redundant tool calls**: Do not call the same tool with the same arguments more than once in the same user turn. When an existing tool result already answers the user's request, stop calling tools and answer directly.\n3. **Use tools proactively**: When the user asks about files, time, system state, or needs actions performed, use the appropriate tool.\n4. **Chain tools when needed**: You can call multiple tools in sequence (e.g., list files → read file → summarize).\n5. **Handle failures gracefully**: If a tool fails, explain what went wrong and suggest alternatives. If it failed because of an unknown tool/platform/action name, retry once with an exact name from the returned suggestions, then explain rather than keep guessing.\n6. **Summarize results naturally**: After tool execution, synthesize the results into a helpful answer rather than dumping raw output.\n7. **Stay conversational**: Maintain a natural, helpful tone. Acknowledge context from earlier in the conversation.\n8. **Recall past work**:\n - Broad queries (\"之前做了什么\", \"列出任务\"): answer from the \"Recent Task History\" section already in your context. If insufficient, call session_list.\n - Specific lookups (\"上次那个配置怎么改的\"): call session_search with relevant phrases (NOT single characters).\n - Do NOT call search tools repeatedly with keyword fragments. One well-phrased call is better than ten fragmented ones.\n\n## Coding & Verification\nWhen working with code, prefer the precise built-in tools over ad-hoc shell: use `repo_map` to orient in an\nunfamiliar project, `code_search`/`file_find` to locate and `code_intel` for symbols, `edit_file` (anchored\nsearch-replace, or a unified `diff`) to change files — never rewrite a whole file to change a few lines — and\n`git_query` to inspect diffs/log. After edits, check `syntax_ok` in the result and run `test_run`/`lint_check`\nbefore declaring the work done. Batch independent read-only calls (search/read/query) into a single turn — they\nrun in parallel.\n\n## Presentation Style\n1. **Polished Markdown only**: Format user-facing answers with clean Markdown headings, short paragraphs, and concise bullets. Use tables only when they improve comparison or scanning.\n2. **Terminal-friendly layout**: Keep lines readable in a TUI; avoid dense walls of text, deeply nested lists, oversized ASCII art, or heavy visual blocks.\n3. **Elegant emphasis**: Use bold text sparingly for key terms and conclusions. Avoid excessive emojis, decorative symbols, repeated separators, or visual noise.\n4. **Theme-safe colors**: Do not emit ANSI escape codes, HTML color tags, Rich markup, or hardcoded color names. Rely on the TUI theme to render Markdown professionally.\n5. **No leaked tool protocol**: Never show tool-call JSON, internal schemas, raw observations, tool result payloads, or hidden reasoning in the final answer unless the user explicitly asks for raw/debug output. Treat any prior `{\"name\": ..., \"arguments\": ...}` blocks and `Tool result (...)` messages as internal execution context only.\n6. **Professional closure**: End with a concise conclusion or next step when helpful; avoid rambling after the useful answer is complete.\n\nWhen finished with all tool calls, respond normally without a JSON block, tool-call transcript, or process log.\n\n## Task Contract\n- Task ID: turn-1\n- Original user request: Try the missing_json_pretty_e2e tool so LeapFlow records a capability gap.\n- Workspace root: \n- Allowed roots: \n- Treat relative project paths as relative to the workspace root; never infer `.` as the project root when a workspace root is provided.\n- Workspace boundary is enforced by tools: do not read, search, edit, or run commands against paths outside the allowed roots unless the user explicitly requests an external path and the tool/approval policy permits it.\n- LeapFlow workspace config is optional at `/.leapflow/config.yaml`; runtime config is loaded from `~/.leapflow/config/user.yaml` and `~/.leapflow/profiles//config/*.yaml`.\n- Preserve this task contract across summarization, compression, tool loops, and memory retrieval.\n" + }, + { + "role": "system", + "content": "\n## Recent Session Summary\n- [user] Try the missing_json_pretty_e2e tool so LeapFlow records a capability gap.\n" + }, + { + "role": "user", + "content": "Try the missing_json_pretty_e2e tool so LeapFlow records a capability gap.\nTry the missing_json_pretty_e2e tool so LeapFlow records a capability gap." + } + ], + "tools": [ + "capability_expand", + "code_intel", + "code_search", + "config_get", + "config_list", + "env_info", + "file_find", + "file_list", + "file_read", + "git_query", + "lint_check", + "memory_search", + "plugin_list", + "plugin_propose", + "plugin_status", + "plugin_versions", + "repo_map", + "research_note", + "runtime_snapshot", + "schedule_reentry", + "self_describe", + "session_list", + "skill_view", + "skills_list", + "terminal_list", + "terminal_read", + "test_run", + "text_search", + "time_get", + "tool_describe", + "tool_search", + "web_fetch" + ] + }, + "responses": [ + { + "status": 200, + "content_type": "application/json", + "body": "{\"id\": \"chatcmpl-cassette\", \"object\": \"chat.completion\", \"model\": \"cassette-model\", \"choices\": [{\"index\": 0, \"message\": {\"role\": \"assistant\", \"content\": \"\", \"tool_calls\": [{\"id\": \"call_1\", \"type\": \"function\", \"function\": {\"name\": \"missing_json_pretty_e2e\", \"arguments\": \"{\\\"text\\\": \\\"{\\\\\\\"b\\\\\\\":2,\\\\\\\"a\\\\\\\":1}\\\"}\"}}]}, \"finish_reason\": \"tool_calls\"}], \"usage\": {\"prompt_tokens\": 64, \"completion_tokens\": 16, \"total_tokens\": 80}}" + } + ] +} diff --git a/tests/_fixtures/cassettes/r7_adaptive_plugin_loop/cassette-model-99947561cfa4cffa.cassette.json b/tests/_fixtures/cassettes/r7_adaptive_plugin_loop/cassette-model-99947561cfa4cffa.cassette.json deleted file mode 100644 index 8a7ea716..00000000 --- a/tests/_fixtures/cassettes/r7_adaptive_plugin_loop/cassette-model-99947561cfa4cffa.cassette.json +++ /dev/null @@ -1,138 +0,0 @@ -{ - "fingerprint": "99947561cfa4cffa08cbc008c701190959b85047e837c5a1b05dd88327be367b", - "note": "captured in seed mode", - "request": { - "model": "cassette-model", - "stream": false, - "messages": [ - { - "role": "system", - "content": "You are LeapFlow, an intelligent assistant that can both converse naturally and take real actions on the user's computer.\n\n## Capabilities\nThe tool index below lists **every** registered tool by name and a one-line summary — this is the complete\ncapability contract; nothing else exists. Only a subset is directly callable this turn (via native tool calling,\nnot a JSON block in your reply). If you need a tool from the index that is not yet callable, call\n`capability_expand` with its category name first — the matching tools become callable immediately after.\nWhen the user asks what LeapFlow itself supports, whether it supports plugins, or which runtime capabilities\nare available, use the live capability evidence exposed by `plugin_list` before making capability claims; report\nconfiguration-dependent or unavailable capabilities as limitations instead of inferring from documentation.\n- **text_search**(text, pattern): Search for a regex pattern in text.\n- **text_replace**(text, old, new, count) [capability_expand category: write]: Replace occurrences of a substring in text.\n- **time_get**(): Get current date and time.\n- **env_info**(): Get system environment information (OS, Python version, cwd).\n- **skills_list**(query, category, source): List available learned skills. Use when user asks about capabilities or you need a specific skill.\n- **skill_view**(name): View the full content of a specific skill document.\n- **code_intel**(path, operation): Precise document symbols (outline) for a source file: classes, functions, and methods with line ranges. Python uses an exact AST parse; other languages use a keyword-prefix scan. Prefer over file_read mode=symbols for accurate navigation before editing. Read-only.\n- **repo_map**(path): Compact project orientation for a repository root: languages, detected test/lint commands, top-level structure, entry points, manifest, and VCS branch. Call this first when entering an unfamiliar codebase. Read-only.\n- **scm_sync**(action, cwd, remote, pull_ref, push_ref, timeout) [capability_expand category: scm]: Run a typed git SCM action. Use this instead of shell_run for git pull/push/status. For 'pull origin main then push', set action='pull_then_push', remote='origin', pull_ref='main', and omit push_ref so LeapFlow pushes the current local branch.\n- **git_query**(action, cwd, ref, path, staged, max_count, stat): Read-only structured git inspection: action=diff|log|status|branch|show. Prefer over shell_run for reading repo state — output is clipped, redacted, and log/branch are parsed into structured fields. Use scm_sync for pull/push.\n- **git_write**(action, cwd, message, stage_all, name, ref, create) [capability_expand category: scm]: Mutating git actions: action=commit (message, stage_all), branch (create+switch), checkout (switch; create=true for -b). Approval-gated. Use scm_sync for pull/push and git_query for reads.\n- **test_run**(command, cwd, timeout): Run the project's test suite and return structured results (framework, passed/failed counts, failing tests). Auto-detects the runner (pytest/npm/go/cargo) or uses a configured/explicit command; executes via the governed shell. ok=true means the runner executed — see 'success' for pass/fail.\n- **lint_check**(command, cwd, timeout): Run the project's linter and return a structured clean/issue result. Auto-detects the linter (ruff/eslint/go vet/clippy) or uses a configured/explicit command; executes via the governed shell. ok=true means the linter ran — see 'clean'.\n- **file_list**(path, pattern, depth): List files and directories at a given path. Use depth=1 or depth=2 to get a recursive tree in one call instead of listing each sub-directory separately.\n- **file_read**(path, max_lines, start_line, max_chars, mode): Read text file content with adaptive context governance. For large or unfamiliar files, prefer mode='outline' or mode='symbols' first, then use mode='raw' with start_line/max_lines for the specific range you actually need. For LeapFlow's own settings, use config_list / config_get / config_set — its config files are outside the workspace and not readable here.\n- **file_write**(path, content, mode) [capability_expand category: write]: Write content to a file (overwrite or append).\n- **code_search**(pattern, patterns, path, glob, ignore_case, multiline, max_results, context_lines): Search file CONTENTS by regex pattern across a directory tree (ripgrep-backed). Requires a regex pattern. NOT for listing or browsing directory contents — use file_list for that. Prefer this over shell_run grep: faster, skips VCS/dependency/build dirs, and returns structured path:line:column matches. Batch related lookups into ONE call via `patterns` (OR-combined, single pass) instead of issuing several separate searches. Use file_read for the surrounding context of a hit.\n- **file_find**(glob, path, max_results): Find files by a recursive glob pattern under a base path (e.g. '**/test_*.py' or '*.md'). Prefer this over shell_run find; skips VCS/dependency/build dirs.\n- **edit_file**(path, edits, dry_run, diff) [capability_expand category: write]: Apply targeted, anchored search-replace edits to an EXISTING text file (use file_write to create/overwrite). Each edit is {original_text, new_text, replace_all?}; original_text must match exactly and uniquely (or set replace_all) — a non-unique or missing anchor is rejected so files are never corrupted. Set dry_run to preview. Alternatively pass a unified 'diff' to apply its hunks as anchored edits. Far cheaper and safer than rewriting a whole file.\n- **shell_run**(command, cwd, timeout) [capability_expand category: shell]: Execute a one-shot shell command with timeout protection. Runs in the active workspace; paths resolving outside it are refused. Reach for a structured tool first when one fits — web_fetch for anything over HTTP(S), git_query/scm_sync for git, code_search/file_find/file_read for the repo, config_get/config_set for LeapFlow's own settings — because those report typed results, while a failed shell command can only be diagnosed from its exit code and stderr. Every shell run counts as an external side effect, so a failure stops the rest of the batch and is not retried automatically.\n- **terminal_open**(command, cwd, shell) [capability_expand category: terminal]: Open a PERSISTENT shell session (REPL/dev server/watch), returning a session_id for terminal_send/read/close. Disabled unless tools.terminal_session_enabled is set. For one-shot commands use shell_run instead.\n- **terminal_send**(session_id, input, wait) [capability_expand category: terminal]: Send a line of input to a persistent terminal session and return output captured shortly after.\n- **terminal_read**(session_id, wait): Drain buffered output from a persistent terminal session (optionally waiting briefly first).\n- **terminal_close**(session_id) [capability_expand category: terminal]: Terminate a persistent terminal session and release its process group.\n- **terminal_list**(): List active persistent terminal sessions.\n- **config_list**(category, limit): List LeapFlow's own writable settings (LLM model and endpoint, daemon, memory, perception, gateway, …) with current values. Use this to discover the exact key before changing anything. Optionally narrow by `category`. This is the only correct way to inspect LeapFlow configuration — never read config files from disk.\n- **config_get**(key): Read one LeapFlow setting by key (e.g. 'llm.model', 'llm.base_url', 'daemon.log_level'), returning its current value, type, scopes, and whether a change needs a daemon restart. There is no 'llm.provider' key: provider behavior is inferred from the OpenAI-compatible 'llm.base_url'. Never read LeapFlow config files from disk — use this.\n- **config_set**(key, value, scope) [capability_expand category: config]: Change one LeapFlow setting by key, e.g. switch an OpenAI-compatible LLM with key='llm.model' and key='llm.base_url'. There is no 'llm.provider' key: provider behavior is inferred from the endpoint. Values are validated and coerced; credentials are stored in the vault automatically. Call config_list or config_get first if unsure of the key. The result states whether a `leap daemon restart` is required. Never edit LeapFlow config files directly.\n- **web_fetch**(url, select, timeout, max_bytes): Read a URL over HTTP(S) and get back extracted, context-sized content: parsed JSON for API endpoints, readable text plus links for web pages. Use this for anything on the internet — prices, docs, releases, articles — instead of running curl through shell_run: it reports real HTTP status codes, retries rate limits on its own, and is a plain read so a retry is always safe. For JSON APIs pass `select` with a dotted path (e.g. 'chart.result.0.meta') to return just that part instead of the whole payload.\n- **memory_search**(query, limit): Search agent memory for relevant past experiences, observations, and facts.\n- **memory_add**(content, kind) [capability_expand category: write]: Store a new observation or insight in memory for future reference.\n- **research_note**(kind, text): Record a compact, structured note about the current task's state so it survives context compression on long / multi-step tasks. Use for durable findings, open questions still to resolve, decisions / excluded paths, and the immediate next step. One concise sentence per note.\n- **capability_expand**(category): Fetch the full callable schema for every tool in a capability category (e.g. 'hub', 'gateway', 'desktop', 'delegate', 'file', 'memory', 'skill'). The compact tool index always lists every registered tool by name and a one-line summary, but only a static low-risk subset is directly callable each turn. If you need a tool from the index that is not yet callable, call capability_expand with its category first; the matching tools become callable in this turn. Never invent a tool name — expand the category instead.\n- **delegate_task**(goal, context) [capability_expand category: delegate]: Delegate a complex sub-task to an isolated subagent. The subagent gets a fresh context and restricted tool access. Use when a task is self-contained and can be solved independently.\n- **schedule_reentry**(kind, reason, delay_seconds, event_match, max_reentries, deadline_seconds): Register a re-entry so this task can resume later from its current orientation (findings / open questions / next step). Use when work must pause and continue after a delay (kind=time) or when a matching platform event arrives (kind=event), instead of finishing now. The research-ledger state is carried over automatically.\n- **hub_push**(skill_name, visibility, version) [capability_expand category: hub]: Push a local skill to the ModelScope Hub for sharing or backup.\n- **hub_pull**(repo_id, version) [capability_expand category: hub]: Pull a skill from the ModelScope Hub to install locally.\n- **hub_search**(query) [capability_expand category: hub]: Search for skills on the Hub by keyword or description.\n- **hub_sync**(mode, dry_run) [capability_expand category: hub]: Preview or execute sync between local skills and Hub.\n- **platform_action**(platform, action, payload, backend_kind) [capability_expand category: gateway]: Execute an exact registered business action on an external platform through LeapFlow's App Connector layer. Actions must be copied from the App Connector Capability Index and are addressed as domain.operation, e.g. im.send_message or docs.create_markdown. All business fields (chat_id, text, query, etc.) MUST be placed inside `payload`, never at the top level. Example: {\"platform\":\"feishu\",\"action\":\"im.send_message\",\"payload\":{\"chat_id\":\"oc_xxx\",\"text\":\"hello\"}}. Do not invent action names, do not use management actions such as list/guide/connect/status here.\n- **platform_connect**(action, platform, credentials, options, checkpoint) [capability_expand category: gateway]: List, guide, connect, disconnect, remove, or check status for external platforms using the App Connector management namespace. Supports REST and CLI backends. Use this for management actions such as list/guide/preflight/connect/status; use platform_action only for exact registered business actions.\n- **gateway_send**(platform, chat_id, text, thread_id) [capability_expand category: gateway]: Send a message to a connected external platform (Feishu group, Telegram chat, DingTalk conversation, etc.). Requires the platform to be connected via gateway_connect first. Use gateway_connect with action='list' to see connected platforms and available chat IDs.\n- **gateway_connect**(action, platform, credentials, options) [capability_expand category: gateway]: Connect, configure, or manage external platform integrations (Feishu, DingTalk, Telegram, Slack, Discord, etc.). Conversational flow: 1) call 'guide' to get setup steps + required fields, 2) present the steps to the user and ask for ALL required credentials in a single message, 3) call 'connect' with the credentials. Goal: complete in 1–2 user turns. NEVER include credential values in your text response.\n- **plugin_list**(): List the live plugin registry and cross-subsystem capability evidence. Use this before answering questions about whether LeapFlow supports plugins, self-evolution, plugin installation, hot reload, versioning, or other runtime capabilities.\n- **plugin_status**(plugin_id): Get detailed status of a specific plugin: its declared category, runtime dependencies, contributed tools, and fiber lifecycle state.\n- **plugin_versions**(plugin_id): List recorded source versions and active pointer for a profile-scoped plugin.\n- **plugin_propose**(requested_capability, plugin_id, proposed_tools, test_cases, risk_level, evidence): Create a side-effect-free PluginProposal from explicit capability-gap evidence. Use this before plugin_generate when a missing capability should be reviewed. Does not call an LLM, write files, or install anything.\n- **assess_compatibility**(manifest, source_path) [capability_expand category: plugin_management]: Assess whether a foreign plugin manifest or real DSH source bundle is compatible with LeapFlow. A source bundle assessment is static: runtime_ready stays false until restricted Node discovery during plugin_install. Returns component-level verdicts and limitations.\n- **plugin_generate**(plugin_id, description, proposal_id) [capability_expand category: system]: Generate a new ToolPlugin from a natural-language capability description. The LLM produces code that conforms to the ToolPlugin Protocol; it is then rigorously validated (syntax, structure, import, protocol conformance). The generated source is stored in CAS and receives explicit content approval. It DOES NOT install the plugin — installation requires a second, mutation-specific approval via plugin_install.\n- **plugin_install**(plugin_id, code, marketplace_name, source_path, proposal_id, version_label) [capability_expand category: system]: Install a plugin from exactly one source: validated Python code, the configured Python marketplace, or a real DSH source bundle. DSH bundles run restricted Node discovery before registration; only public host tools are installed and client UI limitations are reported. Writes profile-scoped state and mutates the process-global registry. REQUIRES APPROVAL.\n- **plugin_rollback**(plugin_id, version) [capability_expand category: system]: Rollback a profile-scoped plugin to a recorded source version and reload it. REQUIRES APPROVAL.\n- **plugin_reload**(plugin_id, version_label) [capability_expand category: system]: Hot-reload a plugin at runtime. Disposes the old plugin fiber, re-imports its module, and registers a fresh instance. Existing in-flight turns are unaffected (snapshot isolation). REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_disable**(plugin_id) [capability_expand category: system]: Disable a plugin by disposing its fiber, removing its tools from the runtime registry. Cannot disable self_management itself. REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_remove**(plugin_id, delete_source) [capability_expand category: system]: Terminally remove a plugin: dispose its fiber, unregister its tools, remove reload metadata, and optionally delete its profile-scoped source file. Cannot remove self_management itself. REQUIRES APPROVAL.\n- **plugin_unquarantine**(plugin_id) [capability_expand category: plugin_management]: Restore a quarantined plugin to probation status for re-evaluation. Unfreezes the trust ledger, transitions the proposal back to PROBATION, and reloads the plugin. REQUIRES APPROVAL.\n- **plugin_enable**(plugin_id) [capability_expand category: system]: Re-enable a previously disabled plugin by reloading its module and registering a fresh instance. REQUIRES APPROVAL.\n- **self_describe**(facet): Introspect LeapFlow's own identity, capabilities, runtime state, evolution metrics, or platform connections. Use facet='all' only when a comprehensive self-check is explicitly requested.\n- **runtime_snapshot**(): Lightweight (~150 token) flat snapshot of current runtime state: model, context budget, posture, disclosure level, turn count, cache hit rate, uptime, tool count, and pending approvals.\n- **schedule_create**(trigger_expression, instruction, execution_mode, max_retries, delivery_target) [capability_expand category: scheduler]: Create a new scheduled task. Specify a trigger expression (e.g. '30m', 'every 2h', '0 9 * * *') and the instruction to execute on each trigger. Optionally provide an execution mode and a delivery target to receive notifications.\n- **schedule_list**() [capability_expand category: scheduler]: List all scheduled tasks with their current state, trigger, and next due time.\n- **schedule_status**(task_id) [capability_expand category: scheduler]: Get detailed status and recent execution history for a scheduled task.\n- **schedule_pause**(task_id) [capability_expand category: scheduler]: Pause a scheduled task so it stops firing without being cancelled.\n- **schedule_resume**(task_id) [capability_expand category: scheduler]: Resume a paused scheduled task — re-arms it and recalculates next due time.\n- **schedule_cancel**(task_id) [capability_expand category: scheduler]: Cancel a scheduled task permanently.\n- **tool_search**(query, max_results): Search all registered tools by keyword relevance. Returns a ranked list of matching tools with name, category, and summary. Use this when you need a tool but are unsure of its exact name or category.\n- **tool_describe**(tool_name): Get the full callable schema of a single tool by exact name. Returns the tool's description, parameters, and metadata. Use after tool_search to inspect a specific tool before calling it.\n- **session_search**(query, limit) [capability_expand category: unclassified]: Search past conversation sessions in the current workspace for relevant context.\n- **session_list**(limit): List recent conversation sessions in the current workspace with stable ids, titles, dates, and summaries. Use for browsing past tasks or when user asks to see history without specific search terms. No keywords needed — returns chronological list.\n- **session_detail**(session_id, limit, offset, include_inactive) [capability_expand category: unclassified]: Read a paginated persisted transcript for one past conversation session in the current workspace. Use after session_list or session_search returns a session_id.\n- **click**(element_index) [capability_expand category: desktop]: Click a UI element by its element_index (from the latest observe_ui snapshot)\n- **get_clipboard**() [capability_expand category: desktop]: Read current clipboard text content\n- **list_apps**(filter, running_only) [capability_expand category: desktop]: List available applications on this system. Use to discover correct bundle_id before switch_app.\n- **list_windows**() [capability_expand category: desktop]: List all top-level windows with pid, window_id, title, and per-window state (minimized, on-screen). Call this first to pick the pid and window_id that observe_ui and other window tools require.\n- **observe_ui**(pid, window_id, query) [capability_expand category: desktop]: Snapshot one window's actionable UI elements, each tagged with an element_index for click/right_click/read_text. Re-observe after actions — indices belong to one snapshot. Requires the window's pid and window_id from list_windows.\n- **open_url**(url, app_id) [capability_expand category: desktop]: Open a URL in the default or specified browser\n- **read_text**(element_index) [capability_expand category: desktop]: Read the text content of a specific UI element from the latest snapshot\n- **right_click**(element_index) [capability_expand category: desktop]: Right-click a UI element to open its context menu. Returns visible menu items.\n- **screenshot**(pid, window_id) [capability_expand category: desktop]: Capture a screenshot for visual verification. With pid + window_id captures that window (works across all displays); defaults to the last observed window, or the full desktop when no window has been observed.\n- **scroll**(element_index, direction, amount, pid, window_id) [capability_expand category: desktop]: Scroll a scrollable area of a window. Omit element_index to scroll the window's focused/page scroller; pass one to scroll an exact element from the latest snapshot.\n- **select_text**(element_index) [capability_expand category: desktop]: Select all text in a UI element (focus + select-all, for subsequent copy)\n- **set_clipboard**(text) [capability_expand category: desktop]: Write text to the clipboard\n- **shortcut**(keys) [capability_expand category: desktop]: Execute a keyboard shortcut\n- **switch_app**(app_id) [capability_expand category: desktop]: Switch to an app (launch if needed, activate, verify)\n- **type_text**(text) [capability_expand category: desktop]: Type text into the currently focused element\n- **wait**(seconds) [capability_expand category: desktop]: Wait for a specified duration before continuing\n- **wait_until**(condition, pid, window_id, timeout, poll_interval) [capability_expand category: desktop]: Wait until a UI condition is met (polls UI tree). Returns elements when found or on timeout.\n- **wait_until_stable**(timeout, poll_interval, pid, window_id) [capability_expand category: desktop]: Wait until the UI stops changing (element set stabilizes across polls).\n\n## App Connector Capability Index\nLeapFlow can onboard and manage external apps through `platform_connect` and execute exact registered business actions through `platform_action`.\nFor requests about connecting, setting up, configuring, enabling, or managing a supported app, use `platform_connect` first instead of generating SDK/Webhook sample code.\n`platform_connect.action` is the management namespace: list, guide, preflight, connect, disconnect, remove, status, events_start, events_stop, events_status.\n`platform_action.action` is only for exact registered platform business actions listed below, such as `im.send_message`; never use management actions like `list` or `guide` there.\nAll business fields MUST go inside `payload`; top-level keys are only `platform`, `action`, and `payload`.\nDo not invent platform IDs or platform action names. If the needed action is not listed, ask for discovery/clarification instead of guessing.\nUse `platform_connect` with `action='guide'` and the matching `platform` to start onboarding; use `action='list'` when the app is unclear.\nWhen a pending onboarding state is present, continue from that state with `platform_connect` instead of asking the user to restate the app.\nSupported apps:\n- `api_server`: API Server (OpenAI Compatible) (category=api; backend=adapter; platform_action actions=none registered)\n- `dingtalk`: 钉钉 (DingTalk) (category=im; backend=adapter; platform_action actions=none registered)\n- `feishu`: 飞书 (Feishu/Lark) (category=im; backend=cli)\n - `calendar.create_event` payload={summary*, start_time*, end_time*, attendees} [write/high]\n - `docs.create_markdown` payload={title*, markdown*, folder_token} [write/medium]\n - `drive.search` payload={query*, limit} [read/medium]\n - `im.add_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.download_resource` payload={message_id*, file_key*, type*} [read/low]\n - `im.get_messages` payload={message_ids*} [read/low]\n - `im.list_chats` payload={page_size} [read/low]\n - `im.list_messages` payload={chat_id*} [read/low]\n - `im.list_thread_messages` payload={thread*} [read/low]\n - `im.remove_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.reply_message` payload={message_id*, text*} [send/high]\n - `im.search_chats` payload={query*, page_size} [read/low]\n - `im.search_messages` payload={query*} [read/medium]\n - `im.send_message` payload={chat_id*, text*, thread_id} [send/high]\n - `im.update_card` payload={token*, card*} [write/medium]\n - `im.update_message` payload={message_id*, text*} [write/medium]\n - `mail.search_unread` payload={query, limit} [read/high]\n - `sheets.append_row` payload={spreadsheet_token*, sheet_id*, values*} [write/medium]\n - `task.create` payload={title*, description, due_time} [write/medium]\n- `telegram`: Telegram (category=social; backend=adapter; platform_action actions=none registered)\n- `webhook`: Webhook (Generic) (category=webhook; backend=adapter; platform_action actions=none registered)\n\n## Tool Usage\nTools are normally invoked through the native function-calling mechanism, not by writing JSON in your reply\ntext. Only if the provider signals that native function calling is unavailable for this turn, fall back to a\nsingle JSON code block: `{\"name\": \"tool_name\", \"arguments\": {\"key\": \"value\"}}` — use this fallback format\nonly, never both. Only call a tool whose exact name appears in the tool index above and is currently callable\n(or reachable via `capability_expand`). Never invent, rename, alias, or guess a tool name, platform ID, or\nplatform action from argument shape or wording — if the index does not list it, it does not exist.\n`platform_connect.action` (list/guide/preflight/connect/disconnect/remove/status/events_start/events_stop/\nevents_status) is the App Connector management namespace; `platform_action.action` only accepts exact\nregistered business actions such as `im.send_message` shown in the App Connector Capability Index — never mix\nthe two namespaces. If a tool call returns an unknown/unavailable result, use the returned suggestions or\navailable names for a single retry instead of trying further variations of the same guess.\n\n**Side-effect action rule** (`platform_action` with effect=send/write/execute):\n- Call each unique action+payload **exactly once**. Never include duplicates in the same turn.\n- Once the result returns `\"completed\": true`, that action is DONE for this task. Do NOT call it again\n in any subsequent turn — immediately summarize the result for the user instead.\n- The system enforces idempotency: duplicate calls are blocked and will not execute.\n- If the user explicitly requests sending/writing multiple times, use distinct payloads per call.\n\n**Resource identifier provenance rule**:\n- NEVER fabricate, guess, or infer resource identifiers (chat_id, message_id, file_key, user_id, etc.).\n Every resource ID used in a side-effect action MUST come from a successful API response in this session.\n- If a read/list action fails (e.g. authorization error), you do NOT have valid resource IDs.\n Report the failure to the user — do NOT attempt the dependent write/send action with a guessed ID.\n- When a tool result contains `\"llm_instruction\"`, follow it exactly.\n\n## Guidelines\n1. **Direct answers first**: If you already know the answer, respond directly without tools.\n2. **Avoid redundant tool calls**: Do not call the same tool with the same arguments more than once in the same user turn. When an existing tool result already answers the user's request, stop calling tools and answer directly.\n3. **Use tools proactively**: When the user asks about files, time, system state, or needs actions performed, use the appropriate tool.\n4. **Chain tools when needed**: You can call multiple tools in sequence (e.g., list files → read file → summarize).\n5. **Handle failures gracefully**: If a tool fails, explain what went wrong and suggest alternatives. If it failed because of an unknown tool/platform/action name, retry once with an exact name from the returned suggestions, then explain rather than keep guessing.\n6. **Summarize results naturally**: After tool execution, synthesize the results into a helpful answer rather than dumping raw output.\n7. **Stay conversational**: Maintain a natural, helpful tone. Acknowledge context from earlier in the conversation.\n8. **Recall past work**:\n - Broad queries (\"之前做了什么\", \"列出任务\"): answer from the \"Recent Task History\" section already in your context. If insufficient, call session_list.\n - Specific lookups (\"上次那个配置怎么改的\"): call session_search with relevant phrases (NOT single characters).\n - Do NOT call search tools repeatedly with keyword fragments. One well-phrased call is better than ten fragmented ones.\n\n## Coding & Verification\nWhen working with code, prefer the precise built-in tools over ad-hoc shell: use `repo_map` to orient in an\nunfamiliar project, `code_search`/`file_find` to locate and `code_intel` for symbols, `edit_file` (anchored\nsearch-replace, or a unified `diff`) to change files — never rewrite a whole file to change a few lines — and\n`git_query` to inspect diffs/log. After edits, check `syntax_ok` in the result and run `test_run`/`lint_check`\nbefore declaring the work done. Batch independent read-only calls (search/read/query) into a single turn — they\nrun in parallel.\n\n## Presentation Style\n1. **Polished Markdown only**: Format user-facing answers with clean Markdown headings, short paragraphs, and concise bullets. Use tables only when they improve comparison or scanning.\n2. **Terminal-friendly layout**: Keep lines readable in a TUI; avoid dense walls of text, deeply nested lists, oversized ASCII art, or heavy visual blocks.\n3. **Elegant emphasis**: Use bold text sparingly for key terms and conclusions. Avoid excessive emojis, decorative symbols, repeated separators, or visual noise.\n4. **Theme-safe colors**: Do not emit ANSI escape codes, HTML color tags, Rich markup, or hardcoded color names. Rely on the TUI theme to render Markdown professionally.\n5. **No leaked tool protocol**: Never show tool-call JSON, internal schemas, raw observations, tool result payloads, or hidden reasoning in the final answer unless the user explicitly asks for raw/debug output. Treat any prior `{\"name\": ..., \"arguments\": ...}` blocks and `Tool result (...)` messages as internal execution context only.\n6. **Professional closure**: End with a concise conclusion or next step when helpful; avoid rambling after the useful answer is complete.\n\nWhen finished with all tool calls, respond normally without a JSON block, tool-call transcript, or process log.\n\n## Task Contract\n- Task ID: turn-1\n- Original user request: Try the missing_json_pretty_e2e tool so LeapFlow records a capability gap.\n- Workspace root: \n- Allowed roots: \n- Treat relative project paths as relative to the workspace root; never infer `.` as the project root when a workspace root is provided.\n- Workspace boundary is enforced by tools: do not read, search, edit, or run commands against paths outside the allowed roots unless the user explicitly requests an external path and the tool/approval policy permits it.\n- LeapFlow workspace config is optional at `/.leapflow/config.yaml`; runtime config is loaded from `~/.leapflow/config/user.yaml` and `~/.leapflow/profiles//config/*.yaml`.\n- Preserve this task contract across summarization, compression, tool loops, and memory retrieval.\n" - }, - { - "role": "system", - "content": "\n## Recent Session Summary\n- [user] Try the missing_json_pretty_e2e tool so LeapFlow records a capability gap.\n" - }, - { - "role": "user", - "content": "Try the missing_json_pretty_e2e tool so LeapFlow records a capability gap.\nTry the missing_json_pretty_e2e tool so LeapFlow records a capability gap." - }, - { - "role": "assistant", - "content": "", - "tool_calls": [ - "missing_json_pretty_e2e" - ] - }, - { - "role": "tool", - "content": "{\"ok\": false, \"error\": \"Unknown tool: missing_json_pretty_e2e\", \"error_type\": \"unknown_tool\", \"retryable\": true}", - "tool_result": true - }, - { - "role": "user", - "content": "SYSTEM: The previous tool call used an unavailable tool name. Original tool: missing_json_pretty_e2e. Resolution: unknown (no exact canonical tool name match). Suggested canonical tools: text_search, text_replace, research_note, gateway_send. Available tools include: assess_compatibility, capability_expand, code_intel, code_search, config_get, config_list, config_set, delegate_task, edit_file, env_info, file_find, file_list. Retry once using an exact canonical tool name from the available list and valid arguments. Do not invent tool names, use aliases, or infer a tool from argument shape; answer without a tool if no exact tool fits." - }, - { - "role": "user", - "content": "SYSTEM: Context utilization is high (25,028/32,768 estimated tokens, round 2). Prefer summaries, targeted reads, and final synthesis." - } - ], - "tools": [ - "assess_compatibility", - "capability_expand", - "click", - "code_intel", - "code_search", - "config_get", - "config_list", - "config_set", - "delegate_task", - "edit_file", - "env_info", - "file_find", - "file_list", - "file_read", - "file_write", - "gateway_connect", - "gateway_send", - "get_clipboard", - "git_query", - "git_write", - "hub_pull", - "hub_push", - "hub_search", - "hub_sync", - "lint_check", - "list_apps", - "list_windows", - "memory_add", - "memory_search", - "observe_ui", - "open_url", - "platform_action", - "platform_connect", - "plugin_disable", - "plugin_enable", - "plugin_generate", - "plugin_install", - "plugin_list", - "plugin_propose", - "plugin_reload", - "plugin_remove", - "plugin_rollback", - "plugin_status", - "plugin_unquarantine", - "plugin_versions", - "read_text", - "repo_map", - "research_note", - "right_click", - "runtime_snapshot", - "schedule_cancel", - "schedule_create", - "schedule_list", - "schedule_pause", - "schedule_reentry", - "schedule_resume", - "schedule_status", - "scm_sync", - "screenshot", - "scroll", - "select_text", - "self_describe", - "session_detail", - "session_list", - "session_search", - "set_clipboard", - "shell_run", - "shortcut", - "skill_view", - "skills_list", - "switch_app", - "terminal_close", - "terminal_list", - "terminal_open", - "terminal_read", - "terminal_send", - "test_run", - "text_replace", - "text_search", - "time_get", - "tool_describe", - "tool_search", - "type_text", - "wait", - "wait_until", - "wait_until_stable", - "web_fetch" - ] - }, - "responses": [ - { - "status": 200, - "content_type": "application/json", - "body": "{\"id\": \"chatcmpl-cassette\", \"object\": \"chat.completion\", \"model\": \"cassette-model\", \"choices\": [{\"index\": 0, \"message\": {\"role\": \"assistant\", \"content\": \"Observed the missing JSON pretty tool.\"}, \"finish_reason\": \"stop\"}], \"usage\": {\"prompt_tokens\": 64, \"completion_tokens\": 16, \"total_tokens\": 80}}" - } - ] -} diff --git a/tests/_fixtures/cassettes/r7_adaptive_plugin_loop/cassette-model-b19d1c99be3d0e2b.cassette.json b/tests/_fixtures/cassettes/r7_adaptive_plugin_loop/cassette-model-b19d1c99be3d0e2b.cassette.json new file mode 100644 index 00000000..bbeb35d7 --- /dev/null +++ b/tests/_fixtures/cassettes/r7_adaptive_plugin_loop/cassette-model-b19d1c99be3d0e2b.cassette.json @@ -0,0 +1,79 @@ +{ + "fingerprint": "b19d1c99be3d0e2b3ccfdb2cc6d96025e892a4a8e3d8c72c1c4762596d767a6e", + "note": "captured in seed mode", + "request": { + "model": "cassette-model", + "stream": false, + "messages": [ + { + "role": "system", + "content": "You are LeapFlow, an intelligent assistant that can both converse naturally and take real actions on the user's computer.\n\n## Capabilities\nThe tool index below lists **every** registered tool by name and a one-line summary — this is the complete\ncapability contract; nothing else exists. Only a subset is directly callable this turn (via native tool calling,\nnot a JSON block in your reply). If you need a tool from the index that is not yet callable, call\n`capability_expand` with its category name first — the matching tools become callable immediately after.\nWhen the user asks about LeapFlow **itself** — its identity/version/model, what tools, plugins, or capabilities\nit has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of\nself-awareness — answer from live runtime evidence via `self_describe` (facet=capabilities for \"what can you\ndo / do you support X\", identity for version/model, runtime for context/posture, or `all` for a full self-check),\nnever by reading LeapFlow's own source code, documentation, or memory. Report configuration-dependent or\nunavailable capabilities as limitations rather than inferring them.\n- **text_search**(text, pattern): Search for a regex pattern in text.\n- **text_replace**(text, old, new, count) [capability_expand category: write]: Replace occurrences of a substring in text.\n- **time_get**(): Get current date and time.\n- **env_info**(): Get system environment information (OS, Python version, cwd).\n- **skills_list**(query, category, source): List available learned skills. Use when user asks about capabilities or you need a specific skill.\n- **skill_view**(name): View the full content of a specific skill document.\n- **skill_list_files**(name) [capability_expand category: skills]: List all support files in a skill directory (configs, templates, examples). Use before skill_file_read to discover available files.\n- **skill_file_read**(name, file_path) [capability_expand category: skills]: Read an individual support file from a skill directory. Use skill_list_files first to discover available files.\n- **code_intel**(path, operation): Precise document symbols (outline) for a source file: classes, functions, and methods with line ranges. Python uses an exact AST parse; other languages use a keyword-prefix scan. Prefer over file_read mode=symbols for accurate navigation before editing. Read-only.\n- **repo_map**(path): Compact project orientation for a repository root: languages, detected test/lint commands, top-level structure, entry points, manifest, and VCS branch. Call this first when entering an unfamiliar codebase. Read-only.\n- **scm_sync**(action, cwd, remote, pull_ref, push_ref, timeout) [capability_expand category: scm]: Run a typed git SCM action. Use this instead of shell_run for git pull/push/status. For 'pull origin main then push', set action='pull_then_push', remote='origin', pull_ref='main', and omit push_ref so LeapFlow pushes the current local branch.\n- **git_query**(action, cwd, ref, path, staged, max_count, stat): Read-only structured git inspection: action=diff|log|status|branch|show. Prefer over shell_run for reading repo state — output is clipped, redacted, and log/branch are parsed into structured fields. Use scm_sync for pull/push.\n- **git_write**(action, cwd, message, stage_all, name, ref, create) [capability_expand category: scm]: Mutating git actions: action=commit (message, stage_all), branch (create+switch), checkout (switch; create=true for -b). Approval-gated. Use scm_sync for pull/push and git_query for reads.\n- **test_run**(command, cwd, timeout): Run the project's test suite and return structured results (framework, passed/failed counts, failing tests). Auto-detects the runner (pytest/npm/go/cargo) or uses a configured/explicit command; executes via the governed shell. ok=true means the runner executed — see 'success' for pass/fail.\n- **lint_check**(command, cwd, timeout): Run the project's linter and return a structured clean/issue result. Auto-detects the linter (ruff/eslint/go vet/clippy) or uses a configured/explicit command; executes via the governed shell. ok=true means the linter ran — see 'clean'.\n- **file_list**(path, pattern, depth): List files and directories at a given path. Use depth=1 or depth=2 to get a recursive tree in one call instead of listing each sub-directory separately.\n- **file_read**(path, max_lines, start_line, max_chars, mode): Read text file content with adaptive context governance. For large or unfamiliar files, prefer mode='outline' or mode='symbols' first, then use mode='raw' with start_line/max_lines for the specific range you actually need. For LeapFlow's own settings, use config_list / config_get / config_set — its config files are outside the workspace and not readable here.\n- **file_write**(path, content, mode) [capability_expand category: write]: Write content to a file (overwrite or append).\n- **code_search**(pattern, patterns, path, glob, ignore_case, multiline, max_results, context_lines): Search file CONTENTS by regex pattern across a directory tree (ripgrep-backed). Requires a regex pattern. NOT for listing or browsing directory contents — use file_list for that. Prefer this over shell_run grep: faster, skips VCS/dependency/build dirs, and returns structured path:line:column matches. Batch related lookups into ONE call via `patterns` (OR-combined, single pass) instead of issuing several separate searches. Use file_read for the surrounding context of a hit.\n- **file_find**(glob, path, max_results): Find files by a recursive glob pattern under a base path (e.g. '**/test_*.py' or '*.md'). Prefer this over shell_run find; skips VCS/dependency/build dirs.\n- **edit_file**(path, edits, dry_run, diff) [capability_expand category: write]: Apply targeted, anchored search-replace edits to an EXISTING text file (use file_write to create/overwrite). Each edit is {original_text, new_text, replace_all?}; original_text must match exactly and uniquely (or set replace_all) — a non-unique or missing anchor is rejected so files are never corrupted. Set dry_run to preview. Alternatively pass a unified 'diff' to apply its hunks as anchored edits. Far cheaper and safer than rewriting a whole file.\n- **shell_run**(command, cwd, timeout) [capability_expand category: shell]: Execute a one-shot shell command with timeout protection. Runs in the active workspace; paths resolving outside it are refused. Reach for a structured tool first when one fits — web_fetch for anything over HTTP(S), git_query/scm_sync for git, code_search/file_find/file_read for the repo, config_get/config_set for LeapFlow's own settings — because those report typed results, while a failed shell command can only be diagnosed from its exit code and stderr. Every shell run counts as an external side effect, so a failure stops the rest of the batch and is not retried automatically.\n- **terminal_open**(command, cwd, shell) [capability_expand category: terminal]: Open a PERSISTENT shell session (REPL/dev server/watch), returning a session_id for terminal_send/read/close. Disabled unless tools.terminal_session_enabled is set. For one-shot commands use shell_run instead.\n- **terminal_send**(session_id, input, wait) [capability_expand category: terminal]: Send a line of input to a persistent terminal session and return output captured shortly after.\n- **terminal_read**(session_id, wait): Drain buffered output from a persistent terminal session (optionally waiting briefly first).\n- **terminal_close**(session_id) [capability_expand category: terminal]: Terminate a persistent terminal session and release its process group.\n- **terminal_list**(): List active persistent terminal sessions.\n- **config_list**(category, limit): List LeapFlow's own writable settings (LLM model and endpoint, daemon, memory, perception, gateway, …) with current values. Use this to discover the exact key before changing anything. Optionally narrow by `category`. This is the only correct way to inspect LeapFlow configuration — never read config files from disk.\n- **config_get**(key): Read one LeapFlow setting by key (e.g. 'llm.model', 'llm.base_url', 'daemon.log_level'), returning its current value, type, scopes, and whether a change needs a daemon restart. There is no 'llm.provider' key: provider behavior is inferred from the OpenAI-compatible 'llm.base_url'. Never read LeapFlow config files from disk — use this.\n- **config_set**(key, value, scope) [capability_expand category: config]: Change one LeapFlow setting by key, e.g. switch an OpenAI-compatible LLM with key='llm.model' and key='llm.base_url'. There is no 'llm.provider' key: provider behavior is inferred from the endpoint. Values are validated and coerced; credentials are stored in the vault automatically. Call config_list or config_get first if unsure of the key. The result states whether a `leap daemon restart` is required. Never edit LeapFlow config files directly.\n- **web_fetch**(url, select, timeout, max_bytes): Read a URL over HTTP(S) and get back extracted, context-sized content: parsed JSON for API endpoints, readable text plus links for web pages. Use this for anything on the internet — prices, docs, releases, articles — instead of running curl through shell_run: it reports real HTTP status codes, retries rate limits on its own, and is a plain read so a retry is always safe. For JSON APIs pass `select` with a dotted path (e.g. 'chart.result.0.meta') to return just that part instead of the whole payload.\n- **memory_search**(query, limit): Search agent memory for relevant past experiences, observations, and facts.\n- **memory_add**(content, kind) [capability_expand category: write]: Store a new observation or insight in memory for future reference.\n- **research_note**(kind, text): Record a compact, structured note about the current task's state so it survives context compression on long / multi-step tasks. Use for durable findings, open questions still to resolve, decisions / excluded paths, and the immediate next step. One concise sentence per note.\n- **capability_expand**(category): Fetch the full callable schema for every tool in a capability category (e.g. 'hub', 'gateway', 'desktop', 'delegate', 'file', 'memory', 'skill'). The compact tool index always lists every registered tool by name and a one-line summary, but only a static low-risk subset is directly callable each turn. If you need a tool from the index that is not yet callable, call capability_expand with its category first; the matching tools become callable in this turn. Never invent a tool name — expand the category instead.\n- **delegate_task**(goal, context) [capability_expand category: delegate]: Delegate a complex sub-task to an isolated subagent. The subagent gets a fresh context and restricted tool access. Use when a task is self-contained and can be solved independently.\n- **schedule_reentry**(kind, reason, delay_seconds, event_match, max_reentries, deadline_seconds): Register a re-entry so this task can resume later from its current orientation (findings / open questions / next step). Use when work must pause and continue after a delay (kind=time) or when a matching platform event arrives (kind=event), instead of finishing now. The research-ledger state is carried over automatically.\n- **hub_push**(skill_name, visibility, version) [capability_expand category: hub]: Push a local skill to the ModelScope Hub for sharing or backup.\n- **hub_pull**(repo_id, version) [capability_expand category: hub]: Pull a skill from the ModelScope Hub to install locally.\n- **hub_search**(query) [capability_expand category: hub]: Search for skills on the Hub by keyword or description.\n- **hub_sync**(mode, dry_run) [capability_expand category: hub]: Preview or execute sync between local skills and Hub.\n- **platform_action**(platform, action, payload, backend_kind) [capability_expand category: gateway]: Execute an exact registered business action on an external platform through LeapFlow's App Connector layer. Actions must be copied from the App Connector Capability Index and are addressed as domain.operation, e.g. im.send_message or docs.create_markdown. All business fields (chat_id, text, query, etc.) MUST be placed inside `payload`, never at the top level. Example: {\"platform\":\"feishu\",\"action\":\"im.send_message\",\"payload\":{\"chat_id\":\"oc_xxx\",\"text\":\"hello\"}}. Do not invent action names, do not use management actions such as list/guide/connect/status here.\n- **platform_connect**(action, platform, credentials, options, checkpoint) [capability_expand category: gateway]: List, guide, connect, disconnect, remove, or check status for external platforms using the App Connector management namespace. Supports REST and CLI backends. Use this for management actions such as list/guide/preflight/connect/status; use platform_action only for exact registered business actions.\n- **gateway_send**(platform, chat_id, text, thread_id) [capability_expand category: gateway]: Send a message to a connected external platform (Feishu group, Telegram chat, DingTalk conversation, etc.). Requires the platform to be connected via gateway_connect first. Use gateway_connect with action='list' to see connected platforms and available chat IDs.\n- **gateway_connect**(action, platform, credentials, options) [capability_expand category: gateway]: Connect, configure, or manage external platform integrations (Feishu, DingTalk, Telegram, Slack, Discord, etc.). Conversational flow: 1) call 'guide' to get setup steps + required fields, 2) present the steps to the user and ask for ALL required credentials in a single message, 3) call 'connect' with the credentials. Goal: complete in 1–2 user turns. NEVER include credential values in your text response.\n- **plugin_list**(): List the live plugin registry across Tool/Gateway/LLM subsystems for plugin management: which plugins are loaded, their category and fiber lifecycle state. For questions about LeapFlow's own capabilities or whether it supports X, use self_describe instead.\n- **plugin_status**(plugin_id): Get detailed status of a specific plugin: its declared category, runtime dependencies, contributed tools, and fiber lifecycle state.\n- **plugin_versions**(plugin_id): List recorded source versions and active pointer for a profile-scoped plugin.\n- **plugin_propose**(requested_capability, plugin_id, proposed_tools, test_cases, risk_level, evidence): Create a side-effect-free PluginProposal from explicit capability-gap evidence. Use this before plugin_generate when a missing capability should be reviewed. Does not call an LLM, write files, or install anything.\n- **assess_compatibility**(manifest, source_path) [capability_expand category: plugin_management]: Assess whether a foreign plugin manifest or real DSH source bundle is compatible with LeapFlow. A source bundle assessment is static: runtime_ready stays false until restricted Node discovery during plugin_install. Returns component-level verdicts and limitations.\n- **plugin_generate**(plugin_id, description, proposal_id) [capability_expand category: system]: Generate a new ToolPlugin from a natural-language capability description. The LLM produces code that conforms to the ToolPlugin Protocol; it is then rigorously validated (syntax, structure, import, protocol conformance). The generated source is stored in CAS and receives explicit content approval. It DOES NOT install the plugin — installation requires a second, mutation-specific approval via plugin_install.\n- **plugin_install**(plugin_id, code, marketplace_name, source_path, proposal_id, version_label) [capability_expand category: system]: Install a plugin from exactly one source: validated Python code, the configured Python marketplace, or a real DSH source bundle. DSH bundles run restricted Node discovery before registration; only public host tools are installed and client UI limitations are reported. Writes profile-scoped state and mutates the process-global registry. REQUIRES APPROVAL.\n- **plugin_rollback**(plugin_id, version) [capability_expand category: system]: Rollback a profile-scoped plugin to a recorded source version and reload it. REQUIRES APPROVAL.\n- **plugin_reload**(plugin_id, version_label) [capability_expand category: system]: Hot-reload a plugin at runtime. Disposes the old plugin fiber, re-imports its module, and registers a fresh instance. Existing in-flight turns are unaffected (snapshot isolation). REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_disable**(plugin_id) [capability_expand category: system]: Disable a plugin by disposing its fiber, removing its tools from the runtime registry. Cannot disable self_management itself. REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_remove**(plugin_id, delete_source) [capability_expand category: system]: Terminally remove a plugin: dispose its fiber, unregister its tools, remove reload metadata, and optionally delete its profile-scoped source file. Cannot remove self_management itself. REQUIRES APPROVAL.\n- **plugin_unquarantine**(plugin_id) [capability_expand category: plugin_management]: Restore a quarantined plugin to probation status for re-evaluation. Unfreezes the trust ledger, transitions the proposal back to PROBATION, and reloads the plugin. REQUIRES APPROVAL.\n- **plugin_enable**(plugin_id) [capability_expand category: system]: Re-enable a previously disabled plugin by reloading its module and registering a fresh instance. REQUIRES APPROVAL.\n- **self_describe**(facet): Answer questions about LeapFlow ITSELF from live runtime evidence — its identity/version/model, what tools and plugins it has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of self-awareness. Prefer this over reading LeapFlow's own source code. Pick facet=capabilities for 'what can you do / do you support X', identity for version/model, runtime for context/posture, or all for a full self-check when explicitly requested.\n- **runtime_snapshot**(): Lightweight (~150 token) flat snapshot of current runtime state: model, context budget, posture, disclosure level, turn count, cache hit rate, uptime, tool count, and pending approvals.\n- **schedule_create**(trigger_expression, instruction, execution_mode, max_retries, delivery_target) [capability_expand category: scheduler]: Create a new scheduled task. Specify a trigger expression (e.g. '30m', 'every 2h', '0 9 * * *') and the instruction to execute on each trigger. Optionally provide an execution mode and a delivery target to receive notifications.\n- **schedule_list**() [capability_expand category: scheduler]: List all scheduled tasks with their current state, trigger, and next due time.\n- **schedule_status**(task_id) [capability_expand category: scheduler]: Get detailed status and recent execution history for a scheduled task.\n- **schedule_pause**(task_id) [capability_expand category: scheduler]: Pause a scheduled task so it stops firing without being cancelled.\n- **schedule_resume**(task_id) [capability_expand category: scheduler]: Resume a paused scheduled task — re-arms it and recalculates next due time.\n- **schedule_cancel**(task_id) [capability_expand category: scheduler]: Cancel a scheduled task permanently.\n- **tool_search**(query, max_results): Search all registered tools by keyword relevance. Returns a ranked list of matching tools with name, category, and summary. Use this when you need a tool but are unsure of its exact name or category.\n- **tool_describe**(tool_name): Get the full callable schema of a single tool by exact name. Returns the tool's description, parameters, and metadata. Use after tool_search to inspect a specific tool before calling it.\n- **session_search**(query, limit) [capability_expand category: unclassified]: Search past conversation sessions in the current workspace for relevant context.\n- **session_list**(limit): List recent conversation sessions in the current workspace with stable ids, titles, dates, and summaries. Use for browsing past tasks or when user asks to see history without specific search terms. No keywords needed — returns chronological list.\n- **session_detail**(session_id, limit, offset, include_inactive) [capability_expand category: unclassified]: Read a paginated persisted transcript for one past conversation session in the current workspace. Use after session_list or session_search returns a session_id.\n- **click**(element_index) [capability_expand category: desktop]: Click a UI element by its element_index (from the latest observe_ui snapshot)\n- **get_clipboard**() [capability_expand category: desktop]: Read current clipboard text content\n- **list_apps**(filter, running_only) [capability_expand category: desktop]: List available applications on this system. Use to discover correct bundle_id before switch_app.\n- **list_windows**() [capability_expand category: desktop]: List all top-level windows with pid, window_id, title, and per-window state (minimized, on-screen). Call this first to pick the pid and window_id that observe_ui and other window tools require.\n- **observe_ui**(pid, window_id, query) [capability_expand category: desktop]: Snapshot one window's actionable UI elements, each tagged with an element_index for click/right_click/read_text. Re-observe after actions — indices belong to one snapshot. Requires the window's pid and window_id from list_windows.\n- **open_url**(url, app_id) [capability_expand category: desktop]: Open a URL in the default or specified browser\n- **read_text**(element_index) [capability_expand category: desktop]: Read the text content of a specific UI element from the latest snapshot\n- **right_click**(element_index) [capability_expand category: desktop]: Right-click a UI element to open its context menu. Returns visible menu items.\n- **screenshot**(pid, window_id) [capability_expand category: desktop]: Capture a screenshot for visual verification. With pid + window_id captures that window (works across all displays); defaults to the last observed window, or the full desktop when no window has been observed.\n- **scroll**(element_index, direction, amount, pid, window_id) [capability_expand category: desktop]: Scroll a scrollable area of a window. Omit element_index to scroll the window's focused/page scroller; pass one to scroll an exact element from the latest snapshot.\n- **select_text**(element_index) [capability_expand category: desktop]: Select all text in a UI element (focus + select-all, for subsequent copy)\n- **set_clipboard**(text) [capability_expand category: desktop]: Write text to the clipboard\n- **shortcut**(keys) [capability_expand category: desktop]: Execute a keyboard shortcut\n- **switch_app**(app_id) [capability_expand category: desktop]: Switch to an app (launch if needed, activate, verify)\n- **type_text**(text) [capability_expand category: desktop]: Type text into the currently focused element\n- **wait**(seconds) [capability_expand category: desktop]: Wait for a specified duration before continuing\n- **wait_until**(condition, pid, window_id, timeout, poll_interval) [capability_expand category: desktop]: Wait until a UI condition is met (polls UI tree). Returns elements when found or on timeout.\n- **wait_until_stable**(timeout, poll_interval, pid, window_id) [capability_expand category: desktop]: Wait until the UI stops changing (element set stabilizes across polls).\n\n## App Connector Capability Index\nLeapFlow can onboard and manage external apps through `platform_connect` and execute exact registered business actions through `platform_action`.\nFor requests about connecting, setting up, configuring, enabling, or managing a supported app, use `platform_connect` first instead of generating SDK/Webhook sample code.\n`platform_connect.action` is the management namespace: list, guide, preflight, connect, disconnect, remove, status, events_start, events_stop, events_status.\n`platform_action.action` is only for exact registered platform business actions listed below, such as `im.send_message`; never use management actions like `list` or `guide` there.\nAll business fields MUST go inside `payload`; top-level keys are only `platform`, `action`, and `payload`.\nDo not invent platform IDs or platform action names. If the needed action is not listed, ask for discovery/clarification instead of guessing.\nUse `platform_connect` with `action='guide'` and the matching `platform` to start onboarding; use `action='list'` when the app is unclear.\nWhen a pending onboarding state is present, continue from that state with `platform_connect` instead of asking the user to restate the app.\nSupported apps:\n- `api_server`: API Server (OpenAI Compatible) (category=api; backend=adapter; platform_action actions=none registered)\n- `dingtalk`: 钉钉 (DingTalk) (category=im; backend=adapter; platform_action actions=none registered)\n- `feishu`: 飞书 (Feishu/Lark) (category=im; backend=cli)\n - `calendar.create_event` payload={summary*, start_time*, end_time*, attendees} [write/high]\n - `docs.create_markdown` payload={title*, markdown*, folder_token} [write/medium]\n - `drive.search` payload={query*, limit} [read/medium]\n - `im.add_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.download_resource` payload={message_id*, file_key*, type*} [read/low]\n - `im.get_messages` payload={message_ids*} [read/low]\n - `im.list_chats` payload={page_size} [read/low]\n - `im.list_messages` payload={chat_id*} [read/low]\n - `im.list_thread_messages` payload={thread*} [read/low]\n - `im.remove_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.reply_message` payload={message_id*, text*} [send/high]\n - `im.search_chats` payload={query*, page_size} [read/low]\n - `im.search_messages` payload={query*} [read/medium]\n - `im.send_message` payload={chat_id*, text*, thread_id} [send/high]\n - `im.update_card` payload={token*, card*} [write/medium]\n - `im.update_message` payload={message_id*, text*} [write/medium]\n - `mail.search_unread` payload={query, limit} [read/high]\n - `sheets.append_row` payload={spreadsheet_token*, sheet_id*, values*} [write/medium]\n - `task.create` payload={title*, description, due_time} [write/medium]\n- `telegram`: Telegram (category=social; backend=adapter; platform_action actions=none registered)\n- `webhook`: Webhook (Generic) (category=webhook; backend=adapter; platform_action actions=none registered)\n\n## Tool Usage\nTools are normally invoked through the native function-calling mechanism, not by writing JSON in your reply\ntext. Only if the provider signals that native function calling is unavailable for this turn, fall back to a\nsingle JSON code block: `{\"name\": \"tool_name\", \"arguments\": {\"key\": \"value\"}}` — use this fallback format\nonly, never both. Only call a tool whose exact name appears in the tool index above and is currently callable\n(or reachable via `capability_expand`). Never invent, rename, alias, or guess a tool name, platform ID, or\nplatform action from argument shape or wording — if the index does not list it, it does not exist.\n`platform_connect.action` (list/guide/preflight/connect/disconnect/remove/status/events_start/events_stop/\nevents_status) is the App Connector management namespace; `platform_action.action` only accepts exact\nregistered business actions such as `im.send_message` shown in the App Connector Capability Index — never mix\nthe two namespaces. If a tool call returns an unknown/unavailable result, use the returned suggestions or\navailable names for a single retry instead of trying further variations of the same guess.\n\n**Side-effect action rule** (`platform_action` with effect=send/write/execute):\n- Call each unique action+payload **exactly once**. Never include duplicates in the same turn.\n- Once the result returns `\"completed\": true`, that action is DONE for this task. Do NOT call it again\n in any subsequent turn — immediately summarize the result for the user instead.\n- The system enforces idempotency: duplicate calls are blocked and will not execute.\n- If the user explicitly requests sending/writing multiple times, use distinct payloads per call.\n\n**Resource identifier provenance rule**:\n- NEVER fabricate, guess, or infer resource identifiers (chat_id, message_id, file_key, user_id, etc.).\n Every resource ID used in a side-effect action MUST come from a successful API response in this session.\n- If a read/list action fails (e.g. authorization error), you do NOT have valid resource IDs.\n Report the failure to the user — do NOT attempt the dependent write/send action with a guessed ID.\n- When a tool result contains `\"llm_instruction\"`, follow it exactly.\n\n## Guidelines\n1. **Direct answers first**: If you already know the answer, respond directly without tools.\n2. **Avoid redundant tool calls**: Do not call the same tool with the same arguments more than once in the same user turn. When an existing tool result already answers the user's request, stop calling tools and answer directly.\n3. **Use tools proactively**: When the user asks about files, time, system state, or needs actions performed, use the appropriate tool.\n4. **Chain tools when needed**: You can call multiple tools in sequence (e.g., list files → read file → summarize).\n5. **Handle failures gracefully**: If a tool fails, explain what went wrong and suggest alternatives. If it failed because of an unknown tool/platform/action name, retry once with an exact name from the returned suggestions, then explain rather than keep guessing.\n6. **Summarize results naturally**: After tool execution, synthesize the results into a helpful answer rather than dumping raw output.\n7. **Stay conversational**: Maintain a natural, helpful tone. Acknowledge context from earlier in the conversation.\n8. **Recall past work**:\n - Broad queries (\"之前做了什么\", \"列出任务\"): answer from the \"Recent Task History\" section already in your context. If insufficient, call session_list.\n - Specific lookups (\"上次那个配置怎么改的\"): call session_search with relevant phrases (NOT single characters).\n - Do NOT call search tools repeatedly with keyword fragments. One well-phrased call is better than ten fragmented ones.\n\n## Coding & Verification\nWhen working with code, prefer the precise built-in tools over ad-hoc shell: use `repo_map` to orient in an\nunfamiliar project, `code_search`/`file_find` to locate and `code_intel` for symbols, `edit_file` (anchored\nsearch-replace, or a unified `diff`) to change files — never rewrite a whole file to change a few lines — and\n`git_query` to inspect diffs/log. After edits, check `syntax_ok` in the result and run `test_run`/`lint_check`\nbefore declaring the work done. Batch independent read-only calls (search/read/query) into a single turn — they\nrun in parallel.\n\n## Presentation Style\n1. **Polished Markdown only**: Format user-facing answers with clean Markdown headings, short paragraphs, and concise bullets. Use tables only when they improve comparison or scanning.\n2. **Terminal-friendly layout**: Keep lines readable in a TUI; avoid dense walls of text, deeply nested lists, oversized ASCII art, or heavy visual blocks.\n3. **Elegant emphasis**: Use bold text sparingly for key terms and conclusions. Avoid excessive emojis, decorative symbols, repeated separators, or visual noise.\n4. **Theme-safe colors**: Do not emit ANSI escape codes, HTML color tags, Rich markup, or hardcoded color names. Rely on the TUI theme to render Markdown professionally.\n5. **No leaked tool protocol**: Never show tool-call JSON, internal schemas, raw observations, tool result payloads, or hidden reasoning in the final answer unless the user explicitly asks for raw/debug output. Treat any prior `{\"name\": ..., \"arguments\": ...}` blocks and `Tool result (...)` messages as internal execution context only.\n6. **Professional closure**: End with a concise conclusion or next step when helpful; avoid rambling after the useful answer is complete.\n\nWhen finished with all tool calls, respond normally without a JSON block, tool-call transcript, or process log.\n\n## Task Contract\n- Task ID: turn-2\n- Original user request: Install the prepared adaptive JSON pretty plugin.\n- Workspace root: \n- Allowed roots: \n- Treat relative project paths as relative to the workspace root; never infer `.` as the project root when a workspace root is provided.\n- Workspace boundary is enforced by tools: do not read, search, edit, or run commands against paths outside the allowed roots unless the user explicitly requests an external path and the tool/approval policy permits it.\n- LeapFlow workspace config is optional at `/.leapflow/config.yaml`; runtime config is loaded from `~/.leapflow/config/user.yaml` and `~/.leapflow/profiles//config/*.yaml`.\n- Preserve this task contract across summarization, compression, tool loops, and memory retrieval.\n" + }, + { + "role": "system", + "content": "\n## Recent Session Summary\n- [assistant] Observed the missing JSON pretty tool.\n- [user] Install the prepared adaptive JSON pretty plugin.\n" + }, + { + "role": "assistant", + "content": "Observed the missing JSON pretty tool." + }, + { + "role": "user", + "content": "Install the prepared adaptive JSON pretty plugin.\nInstall the prepared adaptive JSON pretty plugin." + }, + { + "role": "assistant", + "content": "", + "tool_calls": [ + "plugin_install" + ] + }, + { + "role": "tool", + "content": "{\"ok\": true, \"action\": \"install\", \"plugin_id\": \"json_pretty_loop_e2e\", \"installed_tools\": [\"json_pretty_loop_e2e\"], \"state\": \"active\", \"shadow_validated\": true, \"behavior_tests\": [], \"version\": \"r7\", \"execution_id\": \"\", \"idempotency_key\": \"\", \"execution_policy\": \"mutating_once\", \"tool_call_id\": \"call_1\", \"execution_status\": \"completed\"}", + "tool_result": true + } + ], + "tools": [ + "capability_expand", + "code_intel", + "code_search", + "config_get", + "config_list", + "env_info", + "file_find", + "file_list", + "file_read", + "git_query", + "lint_check", + "memory_search", + "plugin_list", + "plugin_propose", + "plugin_status", + "plugin_versions", + "repo_map", + "research_note", + "runtime_snapshot", + "schedule_reentry", + "self_describe", + "session_list", + "skill_view", + "skills_list", + "terminal_list", + "terminal_read", + "test_run", + "text_search", + "time_get", + "tool_describe", + "tool_search", + "web_fetch" + ] + }, + "responses": [ + { + "status": 200, + "content_type": "application/json", + "body": "{\"id\": \"chatcmpl-cassette\", \"object\": \"chat.completion\", \"model\": \"cassette-model\", \"choices\": [{\"index\": 0, \"message\": {\"role\": \"assistant\", \"content\": \"Installed json pretty plugin.\"}, \"finish_reason\": \"stop\"}], \"usage\": {\"prompt_tokens\": 64, \"completion_tokens\": 16, \"total_tokens\": 80}}" + } + ] +} diff --git a/tests/_fixtures/cassettes/r7_adaptive_plugin_loop/cassette-model-b2f5d9ad1acae44e.cassette.json b/tests/_fixtures/cassettes/r7_adaptive_plugin_loop/cassette-model-b2f5d9ad1acae44e.cassette.json new file mode 100644 index 00000000..de8e162c --- /dev/null +++ b/tests/_fixtures/cassettes/r7_adaptive_plugin_loop/cassette-model-b2f5d9ad1acae44e.cassette.json @@ -0,0 +1,80 @@ +{ + "fingerprint": "b2f5d9ad1acae44ecf37b2530de173203a675c59497d1899193d9dc01f33e8c6", + "note": "captured in seed mode", + "request": { + "model": "cassette-model", + "stream": false, + "messages": [ + { + "role": "system", + "content": "You are LeapFlow, an intelligent assistant that can both converse naturally and take real actions on the user's computer.\n\n## Capabilities\nThe tool index below lists **every** registered tool by name and a one-line summary — this is the complete\ncapability contract; nothing else exists. Only a subset is directly callable this turn (via native tool calling,\nnot a JSON block in your reply). If you need a tool from the index that is not yet callable, call\n`capability_expand` with its category name first — the matching tools become callable immediately after.\nWhen the user asks about LeapFlow **itself** — its identity/version/model, what tools, plugins, or capabilities\nit has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of\nself-awareness — answer from live runtime evidence via `self_describe` (facet=capabilities for \"what can you\ndo / do you support X\", identity for version/model, runtime for context/posture, or `all` for a full self-check),\nnever by reading LeapFlow's own source code, documentation, or memory. Report configuration-dependent or\nunavailable capabilities as limitations rather than inferring them.\n- **text_search**(text, pattern): Search for a regex pattern in text.\n- **text_replace**(text, old, new, count) [capability_expand category: write]: Replace occurrences of a substring in text.\n- **time_get**(): Get current date and time.\n- **env_info**(): Get system environment information (OS, Python version, cwd).\n- **skills_list**(query, category, source): List available learned skills. Use when user asks about capabilities or you need a specific skill.\n- **skill_view**(name): View the full content of a specific skill document.\n- **skill_list_files**(name) [capability_expand category: skills]: List all support files in a skill directory (configs, templates, examples). Use before skill_file_read to discover available files.\n- **skill_file_read**(name, file_path) [capability_expand category: skills]: Read an individual support file from a skill directory. Use skill_list_files first to discover available files.\n- **code_intel**(path, operation): Precise document symbols (outline) for a source file: classes, functions, and methods with line ranges. Python uses an exact AST parse; other languages use a keyword-prefix scan. Prefer over file_read mode=symbols for accurate navigation before editing. Read-only.\n- **repo_map**(path): Compact project orientation for a repository root: languages, detected test/lint commands, top-level structure, entry points, manifest, and VCS branch. Call this first when entering an unfamiliar codebase. Read-only.\n- **scm_sync**(action, cwd, remote, pull_ref, push_ref, timeout) [capability_expand category: scm]: Run a typed git SCM action. Use this instead of shell_run for git pull/push/status. For 'pull origin main then push', set action='pull_then_push', remote='origin', pull_ref='main', and omit push_ref so LeapFlow pushes the current local branch.\n- **git_query**(action, cwd, ref, path, staged, max_count, stat): Read-only structured git inspection: action=diff|log|status|branch|show. Prefer over shell_run for reading repo state — output is clipped, redacted, and log/branch are parsed into structured fields. Use scm_sync for pull/push.\n- **git_write**(action, cwd, message, stage_all, name, ref, create) [capability_expand category: scm]: Mutating git actions: action=commit (message, stage_all), branch (create+switch), checkout (switch; create=true for -b). Approval-gated. Use scm_sync for pull/push and git_query for reads.\n- **test_run**(command, cwd, timeout): Run the project's test suite and return structured results (framework, passed/failed counts, failing tests). Auto-detects the runner (pytest/npm/go/cargo) or uses a configured/explicit command; executes via the governed shell. ok=true means the runner executed — see 'success' for pass/fail.\n- **lint_check**(command, cwd, timeout): Run the project's linter and return a structured clean/issue result. Auto-detects the linter (ruff/eslint/go vet/clippy) or uses a configured/explicit command; executes via the governed shell. ok=true means the linter ran — see 'clean'.\n- **file_list**(path, pattern, depth): List files and directories at a given path. Use depth=1 or depth=2 to get a recursive tree in one call instead of listing each sub-directory separately.\n- **file_read**(path, max_lines, start_line, max_chars, mode): Read text file content with adaptive context governance. For large or unfamiliar files, prefer mode='outline' or mode='symbols' first, then use mode='raw' with start_line/max_lines for the specific range you actually need. For LeapFlow's own settings, use config_list / config_get / config_set — its config files are outside the workspace and not readable here.\n- **file_write**(path, content, mode) [capability_expand category: write]: Write content to a file (overwrite or append).\n- **code_search**(pattern, patterns, path, glob, ignore_case, multiline, max_results, context_lines): Search file CONTENTS by regex pattern across a directory tree (ripgrep-backed). Requires a regex pattern. NOT for listing or browsing directory contents — use file_list for that. Prefer this over shell_run grep: faster, skips VCS/dependency/build dirs, and returns structured path:line:column matches. Batch related lookups into ONE call via `patterns` (OR-combined, single pass) instead of issuing several separate searches. Use file_read for the surrounding context of a hit.\n- **file_find**(glob, path, max_results): Find files by a recursive glob pattern under a base path (e.g. '**/test_*.py' or '*.md'). Prefer this over shell_run find; skips VCS/dependency/build dirs.\n- **edit_file**(path, edits, dry_run, diff) [capability_expand category: write]: Apply targeted, anchored search-replace edits to an EXISTING text file (use file_write to create/overwrite). Each edit is {original_text, new_text, replace_all?}; original_text must match exactly and uniquely (or set replace_all) — a non-unique or missing anchor is rejected so files are never corrupted. Set dry_run to preview. Alternatively pass a unified 'diff' to apply its hunks as anchored edits. Far cheaper and safer than rewriting a whole file.\n- **shell_run**(command, cwd, timeout) [capability_expand category: shell]: Execute a one-shot shell command with timeout protection. Runs in the active workspace; paths resolving outside it are refused. Reach for a structured tool first when one fits — web_fetch for anything over HTTP(S), git_query/scm_sync for git, code_search/file_find/file_read for the repo, config_get/config_set for LeapFlow's own settings — because those report typed results, while a failed shell command can only be diagnosed from its exit code and stderr. Every shell run counts as an external side effect, so a failure stops the rest of the batch and is not retried automatically.\n- **terminal_open**(command, cwd, shell) [capability_expand category: terminal]: Open a PERSISTENT shell session (REPL/dev server/watch), returning a session_id for terminal_send/read/close. Disabled unless tools.terminal_session_enabled is set. For one-shot commands use shell_run instead.\n- **terminal_send**(session_id, input, wait) [capability_expand category: terminal]: Send a line of input to a persistent terminal session and return output captured shortly after.\n- **terminal_read**(session_id, wait): Drain buffered output from a persistent terminal session (optionally waiting briefly first).\n- **terminal_close**(session_id) [capability_expand category: terminal]: Terminate a persistent terminal session and release its process group.\n- **terminal_list**(): List active persistent terminal sessions.\n- **config_list**(category, limit): List LeapFlow's own writable settings (LLM model and endpoint, daemon, memory, perception, gateway, …) with current values. Use this to discover the exact key before changing anything. Optionally narrow by `category`. This is the only correct way to inspect LeapFlow configuration — never read config files from disk.\n- **config_get**(key): Read one LeapFlow setting by key (e.g. 'llm.model', 'llm.base_url', 'daemon.log_level'), returning its current value, type, scopes, and whether a change needs a daemon restart. There is no 'llm.provider' key: provider behavior is inferred from the OpenAI-compatible 'llm.base_url'. Never read LeapFlow config files from disk — use this.\n- **config_set**(key, value, scope) [capability_expand category: config]: Change one LeapFlow setting by key, e.g. switch an OpenAI-compatible LLM with key='llm.model' and key='llm.base_url'. There is no 'llm.provider' key: provider behavior is inferred from the endpoint. Values are validated and coerced; credentials are stored in the vault automatically. Call config_list or config_get first if unsure of the key. The result states whether a `leap daemon restart` is required. Never edit LeapFlow config files directly.\n- **web_fetch**(url, select, timeout, max_bytes): Read a URL over HTTP(S) and get back extracted, context-sized content: parsed JSON for API endpoints, readable text plus links for web pages. Use this for anything on the internet — prices, docs, releases, articles — instead of running curl through shell_run: it reports real HTTP status codes, retries rate limits on its own, and is a plain read so a retry is always safe. For JSON APIs pass `select` with a dotted path (e.g. 'chart.result.0.meta') to return just that part instead of the whole payload.\n- **memory_search**(query, limit): Search agent memory for relevant past experiences, observations, and facts.\n- **memory_add**(content, kind) [capability_expand category: write]: Store a new observation or insight in memory for future reference.\n- **research_note**(kind, text): Record a compact, structured note about the current task's state so it survives context compression on long / multi-step tasks. Use for durable findings, open questions still to resolve, decisions / excluded paths, and the immediate next step. One concise sentence per note.\n- **capability_expand**(category): Fetch the full callable schema for every tool in a capability category (e.g. 'hub', 'gateway', 'desktop', 'delegate', 'file', 'memory', 'skill'). The compact tool index always lists every registered tool by name and a one-line summary, but only a static low-risk subset is directly callable each turn. If you need a tool from the index that is not yet callable, call capability_expand with its category first; the matching tools become callable in this turn. Never invent a tool name — expand the category instead.\n- **delegate_task**(goal, context) [capability_expand category: delegate]: Delegate a complex sub-task to an isolated subagent. The subagent gets a fresh context and restricted tool access. Use when a task is self-contained and can be solved independently.\n- **schedule_reentry**(kind, reason, delay_seconds, event_match, max_reentries, deadline_seconds): Register a re-entry so this task can resume later from its current orientation (findings / open questions / next step). Use when work must pause and continue after a delay (kind=time) or when a matching platform event arrives (kind=event), instead of finishing now. The research-ledger state is carried over automatically.\n- **hub_push**(skill_name, visibility, version) [capability_expand category: hub]: Push a local skill to the ModelScope Hub for sharing or backup.\n- **hub_pull**(repo_id, version) [capability_expand category: hub]: Pull a skill from the ModelScope Hub to install locally.\n- **hub_search**(query) [capability_expand category: hub]: Search for skills on the Hub by keyword or description.\n- **hub_sync**(mode, dry_run) [capability_expand category: hub]: Preview or execute sync between local skills and Hub.\n- **platform_action**(platform, action, payload, backend_kind) [capability_expand category: gateway]: Execute an exact registered business action on an external platform through LeapFlow's App Connector layer. Actions must be copied from the App Connector Capability Index and are addressed as domain.operation, e.g. im.send_message or docs.create_markdown. All business fields (chat_id, text, query, etc.) MUST be placed inside `payload`, never at the top level. Example: {\"platform\":\"feishu\",\"action\":\"im.send_message\",\"payload\":{\"chat_id\":\"oc_xxx\",\"text\":\"hello\"}}. Do not invent action names, do not use management actions such as list/guide/connect/status here.\n- **platform_connect**(action, platform, credentials, options, checkpoint) [capability_expand category: gateway]: List, guide, connect, disconnect, remove, or check status for external platforms using the App Connector management namespace. Supports REST and CLI backends. Use this for management actions such as list/guide/preflight/connect/status; use platform_action only for exact registered business actions.\n- **gateway_send**(platform, chat_id, text, thread_id) [capability_expand category: gateway]: Send a message to a connected external platform (Feishu group, Telegram chat, DingTalk conversation, etc.). Requires the platform to be connected via gateway_connect first. Use gateway_connect with action='list' to see connected platforms and available chat IDs.\n- **gateway_connect**(action, platform, credentials, options) [capability_expand category: gateway]: Connect, configure, or manage external platform integrations (Feishu, DingTalk, Telegram, Slack, Discord, etc.). Conversational flow: 1) call 'guide' to get setup steps + required fields, 2) present the steps to the user and ask for ALL required credentials in a single message, 3) call 'connect' with the credentials. Goal: complete in 1–2 user turns. NEVER include credential values in your text response.\n- **plugin_list**(): List the live plugin registry across Tool/Gateway/LLM subsystems for plugin management: which plugins are loaded, their category and fiber lifecycle state. For questions about LeapFlow's own capabilities or whether it supports X, use self_describe instead.\n- **plugin_status**(plugin_id): Get detailed status of a specific plugin: its declared category, runtime dependencies, contributed tools, and fiber lifecycle state.\n- **plugin_versions**(plugin_id): List recorded source versions and active pointer for a profile-scoped plugin.\n- **plugin_propose**(requested_capability, plugin_id, proposed_tools, test_cases, risk_level, evidence): Create a side-effect-free PluginProposal from explicit capability-gap evidence. Use this before plugin_generate when a missing capability should be reviewed. Does not call an LLM, write files, or install anything.\n- **assess_compatibility**(manifest, source_path) [capability_expand category: plugin_management]: Assess whether a foreign plugin manifest or real DSH source bundle is compatible with LeapFlow. A source bundle assessment is static: runtime_ready stays false until restricted Node discovery during plugin_install. Returns component-level verdicts and limitations.\n- **plugin_generate**(plugin_id, description, proposal_id) [capability_expand category: system]: Generate a new ToolPlugin from a natural-language capability description. The LLM produces code that conforms to the ToolPlugin Protocol; it is then rigorously validated (syntax, structure, import, protocol conformance). The generated source is stored in CAS and receives explicit content approval. It DOES NOT install the plugin — installation requires a second, mutation-specific approval via plugin_install.\n- **plugin_install**(plugin_id, code, marketplace_name, source_path, proposal_id, version_label) [capability_expand category: system]: Install a plugin from exactly one source: validated Python code, the configured Python marketplace, or a real DSH source bundle. DSH bundles run restricted Node discovery before registration; only public host tools are installed and client UI limitations are reported. Writes profile-scoped state and mutates the process-global registry. REQUIRES APPROVAL.\n- **plugin_rollback**(plugin_id, version) [capability_expand category: system]: Rollback a profile-scoped plugin to a recorded source version and reload it. REQUIRES APPROVAL.\n- **plugin_reload**(plugin_id, version_label) [capability_expand category: system]: Hot-reload a plugin at runtime. Disposes the old plugin fiber, re-imports its module, and registers a fresh instance. Existing in-flight turns are unaffected (snapshot isolation). REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_disable**(plugin_id) [capability_expand category: system]: Disable a plugin by disposing its fiber, removing its tools from the runtime registry. Cannot disable self_management itself. REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_remove**(plugin_id, delete_source) [capability_expand category: system]: Terminally remove a plugin: dispose its fiber, unregister its tools, remove reload metadata, and optionally delete its profile-scoped source file. Cannot remove self_management itself. REQUIRES APPROVAL.\n- **plugin_unquarantine**(plugin_id) [capability_expand category: plugin_management]: Restore a quarantined plugin to probation status for re-evaluation. Unfreezes the trust ledger, transitions the proposal back to PROBATION, and reloads the plugin. REQUIRES APPROVAL.\n- **plugin_enable**(plugin_id) [capability_expand category: system]: Re-enable a previously disabled plugin by reloading its module and registering a fresh instance. REQUIRES APPROVAL.\n- **self_describe**(facet): Answer questions about LeapFlow ITSELF from live runtime evidence — its identity/version/model, what tools and plugins it has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of self-awareness. Prefer this over reading LeapFlow's own source code. Pick facet=capabilities for 'what can you do / do you support X', identity for version/model, runtime for context/posture, or all for a full self-check when explicitly requested.\n- **runtime_snapshot**(): Lightweight (~150 token) flat snapshot of current runtime state: model, context budget, posture, disclosure level, turn count, cache hit rate, uptime, tool count, and pending approvals.\n- **schedule_create**(trigger_expression, instruction, execution_mode, max_retries, delivery_target) [capability_expand category: scheduler]: Create a new scheduled task. Specify a trigger expression (e.g. '30m', 'every 2h', '0 9 * * *') and the instruction to execute on each trigger. Optionally provide an execution mode and a delivery target to receive notifications.\n- **schedule_list**() [capability_expand category: scheduler]: List all scheduled tasks with their current state, trigger, and next due time.\n- **schedule_status**(task_id) [capability_expand category: scheduler]: Get detailed status and recent execution history for a scheduled task.\n- **schedule_pause**(task_id) [capability_expand category: scheduler]: Pause a scheduled task so it stops firing without being cancelled.\n- **schedule_resume**(task_id) [capability_expand category: scheduler]: Resume a paused scheduled task — re-arms it and recalculates next due time.\n- **schedule_cancel**(task_id) [capability_expand category: scheduler]: Cancel a scheduled task permanently.\n- **tool_search**(query, max_results): Search all registered tools by keyword relevance. Returns a ranked list of matching tools with name, category, and summary. Use this when you need a tool but are unsure of its exact name or category.\n- **tool_describe**(tool_name): Get the full callable schema of a single tool by exact name. Returns the tool's description, parameters, and metadata. Use after tool_search to inspect a specific tool before calling it.\n- **session_search**(query, limit) [capability_expand category: unclassified]: Search past conversation sessions in the current workspace for relevant context.\n- **session_list**(limit): List recent conversation sessions in the current workspace with stable ids, titles, dates, and summaries. Use for browsing past tasks or when user asks to see history without specific search terms. No keywords needed — returns chronological list.\n- **session_detail**(session_id, limit, offset, include_inactive) [capability_expand category: unclassified]: Read a paginated persisted transcript for one past conversation session in the current workspace. Use after session_list or session_search returns a session_id.\n- **json_pretty_loop_e2e**(text): Pretty-print JSON for the adaptive closed-loop journey.\n- **click**(element_index) [capability_expand category: desktop]: Click a UI element by its element_index (from the latest observe_ui snapshot)\n- **get_clipboard**() [capability_expand category: desktop]: Read current clipboard text content\n- **list_apps**(filter, running_only) [capability_expand category: desktop]: List available applications on this system. Use to discover correct bundle_id before switch_app.\n- **list_windows**() [capability_expand category: desktop]: List all top-level windows with pid, window_id, title, and per-window state (minimized, on-screen). Call this first to pick the pid and window_id that observe_ui and other window tools require.\n- **observe_ui**(pid, window_id, query) [capability_expand category: desktop]: Snapshot one window's actionable UI elements, each tagged with an element_index for click/right_click/read_text. Re-observe after actions — indices belong to one snapshot. Requires the window's pid and window_id from list_windows.\n- **open_url**(url, app_id) [capability_expand category: desktop]: Open a URL in the default or specified browser\n- **read_text**(element_index) [capability_expand category: desktop]: Read the text content of a specific UI element from the latest snapshot\n- **right_click**(element_index) [capability_expand category: desktop]: Right-click a UI element to open its context menu. Returns visible menu items.\n- **screenshot**(pid, window_id) [capability_expand category: desktop]: Capture a screenshot for visual verification. With pid + window_id captures that window (works across all displays); defaults to the last observed window, or the full desktop when no window has been observed.\n- **scroll**(element_index, direction, amount, pid, window_id) [capability_expand category: desktop]: Scroll a scrollable area of a window. Omit element_index to scroll the window's focused/page scroller; pass one to scroll an exact element from the latest snapshot.\n- **select_text**(element_index) [capability_expand category: desktop]: Select all text in a UI element (focus + select-all, for subsequent copy)\n- **set_clipboard**(text) [capability_expand category: desktop]: Write text to the clipboard\n- **shortcut**(keys) [capability_expand category: desktop]: Execute a keyboard shortcut\n- **switch_app**(app_id) [capability_expand category: desktop]: Switch to an app (launch if needed, activate, verify)\n- **type_text**(text) [capability_expand category: desktop]: Type text into the currently focused element\n- **wait**(seconds) [capability_expand category: desktop]: Wait for a specified duration before continuing\n- **wait_until**(condition, pid, window_id, timeout, poll_interval) [capability_expand category: desktop]: Wait until a UI condition is met (polls UI tree). Returns elements when found or on timeout.\n- **wait_until_stable**(timeout, poll_interval, pid, window_id) [capability_expand category: desktop]: Wait until the UI stops changing (element set stabilizes across polls).\n\n## App Connector Capability Index\nLeapFlow can onboard and manage external apps through `platform_connect` and execute exact registered business actions through `platform_action`.\nFor requests about connecting, setting up, configuring, enabling, or managing a supported app, use `platform_connect` first instead of generating SDK/Webhook sample code.\n`platform_connect.action` is the management namespace: list, guide, preflight, connect, disconnect, remove, status, events_start, events_stop, events_status.\n`platform_action.action` is only for exact registered platform business actions listed below, such as `im.send_message`; never use management actions like `list` or `guide` there.\nAll business fields MUST go inside `payload`; top-level keys are only `platform`, `action`, and `payload`.\nDo not invent platform IDs or platform action names. If the needed action is not listed, ask for discovery/clarification instead of guessing.\nUse `platform_connect` with `action='guide'` and the matching `platform` to start onboarding; use `action='list'` when the app is unclear.\nWhen a pending onboarding state is present, continue from that state with `platform_connect` instead of asking the user to restate the app.\nSupported apps:\n- `api_server`: API Server (OpenAI Compatible) (category=api; backend=adapter; platform_action actions=none registered)\n- `dingtalk`: 钉钉 (DingTalk) (category=im; backend=adapter; platform_action actions=none registered)\n- `feishu`: 飞书 (Feishu/Lark) (category=im; backend=cli)\n - `calendar.create_event` payload={summary*, start_time*, end_time*, attendees} [write/high]\n - `docs.create_markdown` payload={title*, markdown*, folder_token} [write/medium]\n - `drive.search` payload={query*, limit} [read/medium]\n - `im.add_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.download_resource` payload={message_id*, file_key*, type*} [read/low]\n - `im.get_messages` payload={message_ids*} [read/low]\n - `im.list_chats` payload={page_size} [read/low]\n - `im.list_messages` payload={chat_id*} [read/low]\n - `im.list_thread_messages` payload={thread*} [read/low]\n - `im.remove_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.reply_message` payload={message_id*, text*} [send/high]\n - `im.search_chats` payload={query*, page_size} [read/low]\n - `im.search_messages` payload={query*} [read/medium]\n - `im.send_message` payload={chat_id*, text*, thread_id} [send/high]\n - `im.update_card` payload={token*, card*} [write/medium]\n - `im.update_message` payload={message_id*, text*} [write/medium]\n - `mail.search_unread` payload={query, limit} [read/high]\n - `sheets.append_row` payload={spreadsheet_token*, sheet_id*, values*} [write/medium]\n - `task.create` payload={title*, description, due_time} [write/medium]\n- `telegram`: Telegram (category=social; backend=adapter; platform_action actions=none registered)\n- `webhook`: Webhook (Generic) (category=webhook; backend=adapter; platform_action actions=none registered)\n\n## Tool Usage\nTools are normally invoked through the native function-calling mechanism, not by writing JSON in your reply\ntext. Only if the provider signals that native function calling is unavailable for this turn, fall back to a\nsingle JSON code block: `{\"name\": \"tool_name\", \"arguments\": {\"key\": \"value\"}}` — use this fallback format\nonly, never both. Only call a tool whose exact name appears in the tool index above and is currently callable\n(or reachable via `capability_expand`). Never invent, rename, alias, or guess a tool name, platform ID, or\nplatform action from argument shape or wording — if the index does not list it, it does not exist.\n`platform_connect.action` (list/guide/preflight/connect/disconnect/remove/status/events_start/events_stop/\nevents_status) is the App Connector management namespace; `platform_action.action` only accepts exact\nregistered business actions such as `im.send_message` shown in the App Connector Capability Index — never mix\nthe two namespaces. If a tool call returns an unknown/unavailable result, use the returned suggestions or\navailable names for a single retry instead of trying further variations of the same guess.\n\n**Side-effect action rule** (`platform_action` with effect=send/write/execute):\n- Call each unique action+payload **exactly once**. Never include duplicates in the same turn.\n- Once the result returns `\"completed\": true`, that action is DONE for this task. Do NOT call it again\n in any subsequent turn — immediately summarize the result for the user instead.\n- The system enforces idempotency: duplicate calls are blocked and will not execute.\n- If the user explicitly requests sending/writing multiple times, use distinct payloads per call.\n\n**Resource identifier provenance rule**:\n- NEVER fabricate, guess, or infer resource identifiers (chat_id, message_id, file_key, user_id, etc.).\n Every resource ID used in a side-effect action MUST come from a successful API response in this session.\n- If a read/list action fails (e.g. authorization error), you do NOT have valid resource IDs.\n Report the failure to the user — do NOT attempt the dependent write/send action with a guessed ID.\n- When a tool result contains `\"llm_instruction\"`, follow it exactly.\n\n## Guidelines\n1. **Direct answers first**: If you already know the answer, respond directly without tools.\n2. **Avoid redundant tool calls**: Do not call the same tool with the same arguments more than once in the same user turn. When an existing tool result already answers the user's request, stop calling tools and answer directly.\n3. **Use tools proactively**: When the user asks about files, time, system state, or needs actions performed, use the appropriate tool.\n4. **Chain tools when needed**: You can call multiple tools in sequence (e.g., list files → read file → summarize).\n5. **Handle failures gracefully**: If a tool fails, explain what went wrong and suggest alternatives. If it failed because of an unknown tool/platform/action name, retry once with an exact name from the returned suggestions, then explain rather than keep guessing.\n6. **Summarize results naturally**: After tool execution, synthesize the results into a helpful answer rather than dumping raw output.\n7. **Stay conversational**: Maintain a natural, helpful tone. Acknowledge context from earlier in the conversation.\n8. **Recall past work**:\n - Broad queries (\"之前做了什么\", \"列出任务\"): answer from the \"Recent Task History\" section already in your context. If insufficient, call session_list.\n - Specific lookups (\"上次那个配置怎么改的\"): call session_search with relevant phrases (NOT single characters).\n - Do NOT call search tools repeatedly with keyword fragments. One well-phrased call is better than ten fragmented ones.\n\n## Coding & Verification\nWhen working with code, prefer the precise built-in tools over ad-hoc shell: use `repo_map` to orient in an\nunfamiliar project, `code_search`/`file_find` to locate and `code_intel` for symbols, `edit_file` (anchored\nsearch-replace, or a unified `diff`) to change files — never rewrite a whole file to change a few lines — and\n`git_query` to inspect diffs/log. After edits, check `syntax_ok` in the result and run `test_run`/`lint_check`\nbefore declaring the work done. Batch independent read-only calls (search/read/query) into a single turn — they\nrun in parallel.\n\n## Presentation Style\n1. **Polished Markdown only**: Format user-facing answers with clean Markdown headings, short paragraphs, and concise bullets. Use tables only when they improve comparison or scanning.\n2. **Terminal-friendly layout**: Keep lines readable in a TUI; avoid dense walls of text, deeply nested lists, oversized ASCII art, or heavy visual blocks.\n3. **Elegant emphasis**: Use bold text sparingly for key terms and conclusions. Avoid excessive emojis, decorative symbols, repeated separators, or visual noise.\n4. **Theme-safe colors**: Do not emit ANSI escape codes, HTML color tags, Rich markup, or hardcoded color names. Rely on the TUI theme to render Markdown professionally.\n5. **No leaked tool protocol**: Never show tool-call JSON, internal schemas, raw observations, tool result payloads, or hidden reasoning in the final answer unless the user explicitly asks for raw/debug output. Treat any prior `{\"name\": ..., \"arguments\": ...}` blocks and `Tool result (...)` messages as internal execution context only.\n6. **Professional closure**: End with a concise conclusion or next step when helpful; avoid rambling after the useful answer is complete.\n\nWhen finished with all tool calls, respond normally without a JSON block, tool-call transcript, or process log.\n\n## Task Contract\n- Task ID: turn-3\n- Original user request: Use the json_pretty_loop_e2e tool to format the sample JSON.\n- Workspace root: \n- Allowed roots: \n- Treat relative project paths as relative to the workspace root; never infer `.` as the project root when a workspace root is provided.\n- Workspace boundary is enforced by tools: do not read, search, edit, or run commands against paths outside the allowed roots unless the user explicitly requests an external path and the tool/approval policy permits it.\n- LeapFlow workspace config is optional at `/.leapflow/config.yaml`; runtime config is loaded from `~/.leapflow/config/user.yaml` and `~/.leapflow/profiles//config/*.yaml`.\n- Preserve this task contract across summarization, compression, tool loops, and memory retrieval.\n" + }, + { + "role": "system", + "content": "\n## Recent Session Summary\n- [assistant] Installed json pretty plugin.\n- [user] Use the json_pretty_loop_e2e tool to format the sample JSON.\n" + }, + { + "role": "assistant", + "content": "Installed json pretty plugin." + }, + { + "role": "user", + "content": "Use the json_pretty_loop_e2e tool to format the sample JSON.\nUse the json_pretty_loop_e2e tool to format the sample JSON." + }, + { + "role": "assistant", + "content": "", + "tool_calls": [ + "json_pretty_loop_e2e" + ] + }, + { + "role": "tool", + "content": "{\"ok\": true, \"content\": \"{\\n \\\"a\\\": 1,\\n \\\"b\\\": 2\\n}\", \"execution_id\": \"\", \"idempotency_key\": \"\", \"execution_policy\": \"read_only\", \"tool_call_id\": \"call_1\", \"execution_status\": \"completed\"}", + "tool_result": true + } + ], + "tools": [ + "capability_expand", + "code_intel", + "code_search", + "config_get", + "config_list", + "env_info", + "file_find", + "file_list", + "file_read", + "git_query", + "json_pretty_loop_e2e", + "lint_check", + "memory_search", + "plugin_list", + "plugin_propose", + "plugin_status", + "plugin_versions", + "repo_map", + "research_note", + "runtime_snapshot", + "schedule_reentry", + "self_describe", + "session_list", + "skill_view", + "skills_list", + "terminal_list", + "terminal_read", + "test_run", + "text_search", + "time_get", + "tool_describe", + "tool_search", + "web_fetch" + ] + }, + "responses": [ + { + "status": 200, + "content_type": "application/json", + "body": "{\"id\": \"chatcmpl-cassette\", \"object\": \"chat.completion\", \"model\": \"cassette-model\", \"choices\": [{\"index\": 0, \"message\": {\"role\": \"assistant\", \"content\": \"Formatted JSON with the new plugin.\"}, \"finish_reason\": \"stop\"}], \"usage\": {\"prompt_tokens\": 64, \"completion_tokens\": 16, \"total_tokens\": 80}}" + } + ] +} diff --git a/tests/_fixtures/cassettes/r7_adaptive_plugin_loop/cassette-model-b7be149ed0142af8.cassette.json b/tests/_fixtures/cassettes/r7_adaptive_plugin_loop/cassette-model-b7be149ed0142af8.cassette.json new file mode 100644 index 00000000..2a3a7217 --- /dev/null +++ b/tests/_fixtures/cassettes/r7_adaptive_plugin_loop/cassette-model-b7be149ed0142af8.cassette.json @@ -0,0 +1,79 @@ +{ + "fingerprint": "b7be149ed0142af8c57e372a14d9b4cccb136c34376d571033d48b19f9c760b1", + "note": "captured in seed mode", + "request": { + "model": "cassette-model", + "stream": false, + "messages": [ + { + "role": "system", + "content": "You are LeapFlow, an intelligent assistant that can both converse naturally and take real actions on the user's computer.\n\n## Capabilities\nThe tool index below lists **every** registered tool by name and a one-line summary — this is the complete\ncapability contract; nothing else exists. Only a subset is directly callable this turn (via native tool calling,\nnot a JSON block in your reply). If you need a tool from the index that is not yet callable, call\n`capability_expand` with its category name first — the matching tools become callable immediately after.\nWhen the user asks about LeapFlow **itself** — its identity/version/model, what tools, plugins, or capabilities\nit has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of\nself-awareness — answer from live runtime evidence via `self_describe` (facet=capabilities for \"what can you\ndo / do you support X\", identity for version/model, runtime for context/posture, or `all` for a full self-check),\nnever by reading LeapFlow's own source code, documentation, or memory. Report configuration-dependent or\nunavailable capabilities as limitations rather than inferring them.\n- **text_search**(text, pattern): Search for a regex pattern in text.\n- **text_replace**(text, old, new, count) [capability_expand category: write]: Replace occurrences of a substring in text.\n- **time_get**(): Get current date and time.\n- **env_info**(): Get system environment information (OS, Python version, cwd).\n- **skills_list**(query, category, source): List available learned skills. Use when user asks about capabilities or you need a specific skill.\n- **skill_view**(name): View the full content of a specific skill document.\n- **skill_list_files**(name) [capability_expand category: skills]: List all support files in a skill directory (configs, templates, examples). Use before skill_file_read to discover available files.\n- **skill_file_read**(name, file_path) [capability_expand category: skills]: Read an individual support file from a skill directory. Use skill_list_files first to discover available files.\n- **code_intel**(path, operation): Precise document symbols (outline) for a source file: classes, functions, and methods with line ranges. Python uses an exact AST parse; other languages use a keyword-prefix scan. Prefer over file_read mode=symbols for accurate navigation before editing. Read-only.\n- **repo_map**(path): Compact project orientation for a repository root: languages, detected test/lint commands, top-level structure, entry points, manifest, and VCS branch. Call this first when entering an unfamiliar codebase. Read-only.\n- **scm_sync**(action, cwd, remote, pull_ref, push_ref, timeout) [capability_expand category: scm]: Run a typed git SCM action. Use this instead of shell_run for git pull/push/status. For 'pull origin main then push', set action='pull_then_push', remote='origin', pull_ref='main', and omit push_ref so LeapFlow pushes the current local branch.\n- **git_query**(action, cwd, ref, path, staged, max_count, stat): Read-only structured git inspection: action=diff|log|status|branch|show. Prefer over shell_run for reading repo state — output is clipped, redacted, and log/branch are parsed into structured fields. Use scm_sync for pull/push.\n- **git_write**(action, cwd, message, stage_all, name, ref, create) [capability_expand category: scm]: Mutating git actions: action=commit (message, stage_all), branch (create+switch), checkout (switch; create=true for -b). Approval-gated. Use scm_sync for pull/push and git_query for reads.\n- **test_run**(command, cwd, timeout): Run the project's test suite and return structured results (framework, passed/failed counts, failing tests). Auto-detects the runner (pytest/npm/go/cargo) or uses a configured/explicit command; executes via the governed shell. ok=true means the runner executed — see 'success' for pass/fail.\n- **lint_check**(command, cwd, timeout): Run the project's linter and return a structured clean/issue result. Auto-detects the linter (ruff/eslint/go vet/clippy) or uses a configured/explicit command; executes via the governed shell. ok=true means the linter ran — see 'clean'.\n- **file_list**(path, pattern, depth): List files and directories at a given path. Use depth=1 or depth=2 to get a recursive tree in one call instead of listing each sub-directory separately.\n- **file_read**(path, max_lines, start_line, max_chars, mode): Read text file content with adaptive context governance. For large or unfamiliar files, prefer mode='outline' or mode='symbols' first, then use mode='raw' with start_line/max_lines for the specific range you actually need. For LeapFlow's own settings, use config_list / config_get / config_set — its config files are outside the workspace and not readable here.\n- **file_write**(path, content, mode) [capability_expand category: write]: Write content to a file (overwrite or append).\n- **code_search**(pattern, patterns, path, glob, ignore_case, multiline, max_results, context_lines): Search file CONTENTS by regex pattern across a directory tree (ripgrep-backed). Requires a regex pattern. NOT for listing or browsing directory contents — use file_list for that. Prefer this over shell_run grep: faster, skips VCS/dependency/build dirs, and returns structured path:line:column matches. Batch related lookups into ONE call via `patterns` (OR-combined, single pass) instead of issuing several separate searches. Use file_read for the surrounding context of a hit.\n- **file_find**(glob, path, max_results): Find files by a recursive glob pattern under a base path (e.g. '**/test_*.py' or '*.md'). Prefer this over shell_run find; skips VCS/dependency/build dirs.\n- **edit_file**(path, edits, dry_run, diff) [capability_expand category: write]: Apply targeted, anchored search-replace edits to an EXISTING text file (use file_write to create/overwrite). Each edit is {original_text, new_text, replace_all?}; original_text must match exactly and uniquely (or set replace_all) — a non-unique or missing anchor is rejected so files are never corrupted. Set dry_run to preview. Alternatively pass a unified 'diff' to apply its hunks as anchored edits. Far cheaper and safer than rewriting a whole file.\n- **shell_run**(command, cwd, timeout) [capability_expand category: shell]: Execute a one-shot shell command with timeout protection. Runs in the active workspace; paths resolving outside it are refused. Reach for a structured tool first when one fits — web_fetch for anything over HTTP(S), git_query/scm_sync for git, code_search/file_find/file_read for the repo, config_get/config_set for LeapFlow's own settings — because those report typed results, while a failed shell command can only be diagnosed from its exit code and stderr. Every shell run counts as an external side effect, so a failure stops the rest of the batch and is not retried automatically.\n- **terminal_open**(command, cwd, shell) [capability_expand category: terminal]: Open a PERSISTENT shell session (REPL/dev server/watch), returning a session_id for terminal_send/read/close. Disabled unless tools.terminal_session_enabled is set. For one-shot commands use shell_run instead.\n- **terminal_send**(session_id, input, wait) [capability_expand category: terminal]: Send a line of input to a persistent terminal session and return output captured shortly after.\n- **terminal_read**(session_id, wait): Drain buffered output from a persistent terminal session (optionally waiting briefly first).\n- **terminal_close**(session_id) [capability_expand category: terminal]: Terminate a persistent terminal session and release its process group.\n- **terminal_list**(): List active persistent terminal sessions.\n- **config_list**(category, limit): List LeapFlow's own writable settings (LLM model and endpoint, daemon, memory, perception, gateway, …) with current values. Use this to discover the exact key before changing anything. Optionally narrow by `category`. This is the only correct way to inspect LeapFlow configuration — never read config files from disk.\n- **config_get**(key): Read one LeapFlow setting by key (e.g. 'llm.model', 'llm.base_url', 'daemon.log_level'), returning its current value, type, scopes, and whether a change needs a daemon restart. There is no 'llm.provider' key: provider behavior is inferred from the OpenAI-compatible 'llm.base_url'. Never read LeapFlow config files from disk — use this.\n- **config_set**(key, value, scope) [capability_expand category: config]: Change one LeapFlow setting by key, e.g. switch an OpenAI-compatible LLM with key='llm.model' and key='llm.base_url'. There is no 'llm.provider' key: provider behavior is inferred from the endpoint. Values are validated and coerced; credentials are stored in the vault automatically. Call config_list or config_get first if unsure of the key. The result states whether a `leap daemon restart` is required. Never edit LeapFlow config files directly.\n- **web_fetch**(url, select, timeout, max_bytes): Read a URL over HTTP(S) and get back extracted, context-sized content: parsed JSON for API endpoints, readable text plus links for web pages. Use this for anything on the internet — prices, docs, releases, articles — instead of running curl through shell_run: it reports real HTTP status codes, retries rate limits on its own, and is a plain read so a retry is always safe. For JSON APIs pass `select` with a dotted path (e.g. 'chart.result.0.meta') to return just that part instead of the whole payload.\n- **memory_search**(query, limit): Search agent memory for relevant past experiences, observations, and facts.\n- **memory_add**(content, kind) [capability_expand category: write]: Store a new observation or insight in memory for future reference.\n- **research_note**(kind, text): Record a compact, structured note about the current task's state so it survives context compression on long / multi-step tasks. Use for durable findings, open questions still to resolve, decisions / excluded paths, and the immediate next step. One concise sentence per note.\n- **capability_expand**(category): Fetch the full callable schema for every tool in a capability category (e.g. 'hub', 'gateway', 'desktop', 'delegate', 'file', 'memory', 'skill'). The compact tool index always lists every registered tool by name and a one-line summary, but only a static low-risk subset is directly callable each turn. If you need a tool from the index that is not yet callable, call capability_expand with its category first; the matching tools become callable in this turn. Never invent a tool name — expand the category instead.\n- **delegate_task**(goal, context) [capability_expand category: delegate]: Delegate a complex sub-task to an isolated subagent. The subagent gets a fresh context and restricted tool access. Use when a task is self-contained and can be solved independently.\n- **schedule_reentry**(kind, reason, delay_seconds, event_match, max_reentries, deadline_seconds): Register a re-entry so this task can resume later from its current orientation (findings / open questions / next step). Use when work must pause and continue after a delay (kind=time) or when a matching platform event arrives (kind=event), instead of finishing now. The research-ledger state is carried over automatically.\n- **hub_push**(skill_name, visibility, version) [capability_expand category: hub]: Push a local skill to the ModelScope Hub for sharing or backup.\n- **hub_pull**(repo_id, version) [capability_expand category: hub]: Pull a skill from the ModelScope Hub to install locally.\n- **hub_search**(query) [capability_expand category: hub]: Search for skills on the Hub by keyword or description.\n- **hub_sync**(mode, dry_run) [capability_expand category: hub]: Preview or execute sync between local skills and Hub.\n- **platform_action**(platform, action, payload, backend_kind) [capability_expand category: gateway]: Execute an exact registered business action on an external platform through LeapFlow's App Connector layer. Actions must be copied from the App Connector Capability Index and are addressed as domain.operation, e.g. im.send_message or docs.create_markdown. All business fields (chat_id, text, query, etc.) MUST be placed inside `payload`, never at the top level. Example: {\"platform\":\"feishu\",\"action\":\"im.send_message\",\"payload\":{\"chat_id\":\"oc_xxx\",\"text\":\"hello\"}}. Do not invent action names, do not use management actions such as list/guide/connect/status here.\n- **platform_connect**(action, platform, credentials, options, checkpoint) [capability_expand category: gateway]: List, guide, connect, disconnect, remove, or check status for external platforms using the App Connector management namespace. Supports REST and CLI backends. Use this for management actions such as list/guide/preflight/connect/status; use platform_action only for exact registered business actions.\n- **gateway_send**(platform, chat_id, text, thread_id) [capability_expand category: gateway]: Send a message to a connected external platform (Feishu group, Telegram chat, DingTalk conversation, etc.). Requires the platform to be connected via gateway_connect first. Use gateway_connect with action='list' to see connected platforms and available chat IDs.\n- **gateway_connect**(action, platform, credentials, options) [capability_expand category: gateway]: Connect, configure, or manage external platform integrations (Feishu, DingTalk, Telegram, Slack, Discord, etc.). Conversational flow: 1) call 'guide' to get setup steps + required fields, 2) present the steps to the user and ask for ALL required credentials in a single message, 3) call 'connect' with the credentials. Goal: complete in 1–2 user turns. NEVER include credential values in your text response.\n- **plugin_list**(): List the live plugin registry across Tool/Gateway/LLM subsystems for plugin management: which plugins are loaded, their category and fiber lifecycle state. For questions about LeapFlow's own capabilities or whether it supports X, use self_describe instead.\n- **plugin_status**(plugin_id): Get detailed status of a specific plugin: its declared category, runtime dependencies, contributed tools, and fiber lifecycle state.\n- **plugin_versions**(plugin_id): List recorded source versions and active pointer for a profile-scoped plugin.\n- **plugin_propose**(requested_capability, plugin_id, proposed_tools, test_cases, risk_level, evidence): Create a side-effect-free PluginProposal from explicit capability-gap evidence. Use this before plugin_generate when a missing capability should be reviewed. Does not call an LLM, write files, or install anything.\n- **assess_compatibility**(manifest, source_path) [capability_expand category: plugin_management]: Assess whether a foreign plugin manifest or real DSH source bundle is compatible with LeapFlow. A source bundle assessment is static: runtime_ready stays false until restricted Node discovery during plugin_install. Returns component-level verdicts and limitations.\n- **plugin_generate**(plugin_id, description, proposal_id) [capability_expand category: system]: Generate a new ToolPlugin from a natural-language capability description. The LLM produces code that conforms to the ToolPlugin Protocol; it is then rigorously validated (syntax, structure, import, protocol conformance). The generated source is stored in CAS and receives explicit content approval. It DOES NOT install the plugin — installation requires a second, mutation-specific approval via plugin_install.\n- **plugin_install**(plugin_id, code, marketplace_name, source_path, proposal_id, version_label) [capability_expand category: system]: Install a plugin from exactly one source: validated Python code, the configured Python marketplace, or a real DSH source bundle. DSH bundles run restricted Node discovery before registration; only public host tools are installed and client UI limitations are reported. Writes profile-scoped state and mutates the process-global registry. REQUIRES APPROVAL.\n- **plugin_rollback**(plugin_id, version) [capability_expand category: system]: Rollback a profile-scoped plugin to a recorded source version and reload it. REQUIRES APPROVAL.\n- **plugin_reload**(plugin_id, version_label) [capability_expand category: system]: Hot-reload a plugin at runtime. Disposes the old plugin fiber, re-imports its module, and registers a fresh instance. Existing in-flight turns are unaffected (snapshot isolation). REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_disable**(plugin_id) [capability_expand category: system]: Disable a plugin by disposing its fiber, removing its tools from the runtime registry. Cannot disable self_management itself. REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_remove**(plugin_id, delete_source) [capability_expand category: system]: Terminally remove a plugin: dispose its fiber, unregister its tools, remove reload metadata, and optionally delete its profile-scoped source file. Cannot remove self_management itself. REQUIRES APPROVAL.\n- **plugin_unquarantine**(plugin_id) [capability_expand category: plugin_management]: Restore a quarantined plugin to probation status for re-evaluation. Unfreezes the trust ledger, transitions the proposal back to PROBATION, and reloads the plugin. REQUIRES APPROVAL.\n- **plugin_enable**(plugin_id) [capability_expand category: system]: Re-enable a previously disabled plugin by reloading its module and registering a fresh instance. REQUIRES APPROVAL.\n- **self_describe**(facet): Answer questions about LeapFlow ITSELF from live runtime evidence — its identity/version/model, what tools and plugins it has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of self-awareness. Prefer this over reading LeapFlow's own source code. Pick facet=capabilities for 'what can you do / do you support X', identity for version/model, runtime for context/posture, or all for a full self-check when explicitly requested.\n- **runtime_snapshot**(): Lightweight (~150 token) flat snapshot of current runtime state: model, context budget, posture, disclosure level, turn count, cache hit rate, uptime, tool count, and pending approvals.\n- **schedule_create**(trigger_expression, instruction, execution_mode, max_retries, delivery_target) [capability_expand category: scheduler]: Create a new scheduled task. Specify a trigger expression (e.g. '30m', 'every 2h', '0 9 * * *') and the instruction to execute on each trigger. Optionally provide an execution mode and a delivery target to receive notifications.\n- **schedule_list**() [capability_expand category: scheduler]: List all scheduled tasks with their current state, trigger, and next due time.\n- **schedule_status**(task_id) [capability_expand category: scheduler]: Get detailed status and recent execution history for a scheduled task.\n- **schedule_pause**(task_id) [capability_expand category: scheduler]: Pause a scheduled task so it stops firing without being cancelled.\n- **schedule_resume**(task_id) [capability_expand category: scheduler]: Resume a paused scheduled task — re-arms it and recalculates next due time.\n- **schedule_cancel**(task_id) [capability_expand category: scheduler]: Cancel a scheduled task permanently.\n- **tool_search**(query, max_results): Search all registered tools by keyword relevance. Returns a ranked list of matching tools with name, category, and summary. Use this when you need a tool but are unsure of its exact name or category.\n- **tool_describe**(tool_name): Get the full callable schema of a single tool by exact name. Returns the tool's description, parameters, and metadata. Use after tool_search to inspect a specific tool before calling it.\n- **session_search**(query, limit) [capability_expand category: unclassified]: Search past conversation sessions in the current workspace for relevant context.\n- **session_list**(limit): List recent conversation sessions in the current workspace with stable ids, titles, dates, and summaries. Use for browsing past tasks or when user asks to see history without specific search terms. No keywords needed — returns chronological list.\n- **session_detail**(session_id, limit, offset, include_inactive) [capability_expand category: unclassified]: Read a paginated persisted transcript for one past conversation session in the current workspace. Use after session_list or session_search returns a session_id.\n- **click**(element_index) [capability_expand category: desktop]: Click a UI element by its element_index (from the latest observe_ui snapshot)\n- **get_clipboard**() [capability_expand category: desktop]: Read current clipboard text content\n- **list_apps**(filter, running_only) [capability_expand category: desktop]: List available applications on this system. Use to discover correct bundle_id before switch_app.\n- **list_windows**() [capability_expand category: desktop]: List all top-level windows with pid, window_id, title, and per-window state (minimized, on-screen). Call this first to pick the pid and window_id that observe_ui and other window tools require.\n- **observe_ui**(pid, window_id, query) [capability_expand category: desktop]: Snapshot one window's actionable UI elements, each tagged with an element_index for click/right_click/read_text. Re-observe after actions — indices belong to one snapshot. Requires the window's pid and window_id from list_windows.\n- **open_url**(url, app_id) [capability_expand category: desktop]: Open a URL in the default or specified browser\n- **read_text**(element_index) [capability_expand category: desktop]: Read the text content of a specific UI element from the latest snapshot\n- **right_click**(element_index) [capability_expand category: desktop]: Right-click a UI element to open its context menu. Returns visible menu items.\n- **screenshot**(pid, window_id) [capability_expand category: desktop]: Capture a screenshot for visual verification. With pid + window_id captures that window (works across all displays); defaults to the last observed window, or the full desktop when no window has been observed.\n- **scroll**(element_index, direction, amount, pid, window_id) [capability_expand category: desktop]: Scroll a scrollable area of a window. Omit element_index to scroll the window's focused/page scroller; pass one to scroll an exact element from the latest snapshot.\n- **select_text**(element_index) [capability_expand category: desktop]: Select all text in a UI element (focus + select-all, for subsequent copy)\n- **set_clipboard**(text) [capability_expand category: desktop]: Write text to the clipboard\n- **shortcut**(keys) [capability_expand category: desktop]: Execute a keyboard shortcut\n- **switch_app**(app_id) [capability_expand category: desktop]: Switch to an app (launch if needed, activate, verify)\n- **type_text**(text) [capability_expand category: desktop]: Type text into the currently focused element\n- **wait**(seconds) [capability_expand category: desktop]: Wait for a specified duration before continuing\n- **wait_until**(condition, pid, window_id, timeout, poll_interval) [capability_expand category: desktop]: Wait until a UI condition is met (polls UI tree). Returns elements when found or on timeout.\n- **wait_until_stable**(timeout, poll_interval, pid, window_id) [capability_expand category: desktop]: Wait until the UI stops changing (element set stabilizes across polls).\n\n## App Connector Capability Index\nLeapFlow can onboard and manage external apps through `platform_connect` and execute exact registered business actions through `platform_action`.\nFor requests about connecting, setting up, configuring, enabling, or managing a supported app, use `platform_connect` first instead of generating SDK/Webhook sample code.\n`platform_connect.action` is the management namespace: list, guide, preflight, connect, disconnect, remove, status, events_start, events_stop, events_status.\n`platform_action.action` is only for exact registered platform business actions listed below, such as `im.send_message`; never use management actions like `list` or `guide` there.\nAll business fields MUST go inside `payload`; top-level keys are only `platform`, `action`, and `payload`.\nDo not invent platform IDs or platform action names. If the needed action is not listed, ask for discovery/clarification instead of guessing.\nUse `platform_connect` with `action='guide'` and the matching `platform` to start onboarding; use `action='list'` when the app is unclear.\nWhen a pending onboarding state is present, continue from that state with `platform_connect` instead of asking the user to restate the app.\nSupported apps:\n- `api_server`: API Server (OpenAI Compatible) (category=api; backend=adapter; platform_action actions=none registered)\n- `dingtalk`: 钉钉 (DingTalk) (category=im; backend=adapter; platform_action actions=none registered)\n- `feishu`: 飞书 (Feishu/Lark) (category=im; backend=cli)\n - `calendar.create_event` payload={summary*, start_time*, end_time*, attendees} [write/high]\n - `docs.create_markdown` payload={title*, markdown*, folder_token} [write/medium]\n - `drive.search` payload={query*, limit} [read/medium]\n - `im.add_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.download_resource` payload={message_id*, file_key*, type*} [read/low]\n - `im.get_messages` payload={message_ids*} [read/low]\n - `im.list_chats` payload={page_size} [read/low]\n - `im.list_messages` payload={chat_id*} [read/low]\n - `im.list_thread_messages` payload={thread*} [read/low]\n - `im.remove_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.reply_message` payload={message_id*, text*} [send/high]\n - `im.search_chats` payload={query*, page_size} [read/low]\n - `im.search_messages` payload={query*} [read/medium]\n - `im.send_message` payload={chat_id*, text*, thread_id} [send/high]\n - `im.update_card` payload={token*, card*} [write/medium]\n - `im.update_message` payload={message_id*, text*} [write/medium]\n - `mail.search_unread` payload={query, limit} [read/high]\n - `sheets.append_row` payload={spreadsheet_token*, sheet_id*, values*} [write/medium]\n - `task.create` payload={title*, description, due_time} [write/medium]\n- `telegram`: Telegram (category=social; backend=adapter; platform_action actions=none registered)\n- `webhook`: Webhook (Generic) (category=webhook; backend=adapter; platform_action actions=none registered)\n\n## Tool Usage\nTools are normally invoked through the native function-calling mechanism, not by writing JSON in your reply\ntext. Only if the provider signals that native function calling is unavailable for this turn, fall back to a\nsingle JSON code block: `{\"name\": \"tool_name\", \"arguments\": {\"key\": \"value\"}}` — use this fallback format\nonly, never both. Only call a tool whose exact name appears in the tool index above and is currently callable\n(or reachable via `capability_expand`). Never invent, rename, alias, or guess a tool name, platform ID, or\nplatform action from argument shape or wording — if the index does not list it, it does not exist.\n`platform_connect.action` (list/guide/preflight/connect/disconnect/remove/status/events_start/events_stop/\nevents_status) is the App Connector management namespace; `platform_action.action` only accepts exact\nregistered business actions such as `im.send_message` shown in the App Connector Capability Index — never mix\nthe two namespaces. If a tool call returns an unknown/unavailable result, use the returned suggestions or\navailable names for a single retry instead of trying further variations of the same guess.\n\n**Side-effect action rule** (`platform_action` with effect=send/write/execute):\n- Call each unique action+payload **exactly once**. Never include duplicates in the same turn.\n- Once the result returns `\"completed\": true`, that action is DONE for this task. Do NOT call it again\n in any subsequent turn — immediately summarize the result for the user instead.\n- The system enforces idempotency: duplicate calls are blocked and will not execute.\n- If the user explicitly requests sending/writing multiple times, use distinct payloads per call.\n\n**Resource identifier provenance rule**:\n- NEVER fabricate, guess, or infer resource identifiers (chat_id, message_id, file_key, user_id, etc.).\n Every resource ID used in a side-effect action MUST come from a successful API response in this session.\n- If a read/list action fails (e.g. authorization error), you do NOT have valid resource IDs.\n Report the failure to the user — do NOT attempt the dependent write/send action with a guessed ID.\n- When a tool result contains `\"llm_instruction\"`, follow it exactly.\n\n## Guidelines\n1. **Direct answers first**: If you already know the answer, respond directly without tools.\n2. **Avoid redundant tool calls**: Do not call the same tool with the same arguments more than once in the same user turn. When an existing tool result already answers the user's request, stop calling tools and answer directly.\n3. **Use tools proactively**: When the user asks about files, time, system state, or needs actions performed, use the appropriate tool.\n4. **Chain tools when needed**: You can call multiple tools in sequence (e.g., list files → read file → summarize).\n5. **Handle failures gracefully**: If a tool fails, explain what went wrong and suggest alternatives. If it failed because of an unknown tool/platform/action name, retry once with an exact name from the returned suggestions, then explain rather than keep guessing.\n6. **Summarize results naturally**: After tool execution, synthesize the results into a helpful answer rather than dumping raw output.\n7. **Stay conversational**: Maintain a natural, helpful tone. Acknowledge context from earlier in the conversation.\n8. **Recall past work**:\n - Broad queries (\"之前做了什么\", \"列出任务\"): answer from the \"Recent Task History\" section already in your context. If insufficient, call session_list.\n - Specific lookups (\"上次那个配置怎么改的\"): call session_search with relevant phrases (NOT single characters).\n - Do NOT call search tools repeatedly with keyword fragments. One well-phrased call is better than ten fragmented ones.\n\n## Coding & Verification\nWhen working with code, prefer the precise built-in tools over ad-hoc shell: use `repo_map` to orient in an\nunfamiliar project, `code_search`/`file_find` to locate and `code_intel` for symbols, `edit_file` (anchored\nsearch-replace, or a unified `diff`) to change files — never rewrite a whole file to change a few lines — and\n`git_query` to inspect diffs/log. After edits, check `syntax_ok` in the result and run `test_run`/`lint_check`\nbefore declaring the work done. Batch independent read-only calls (search/read/query) into a single turn — they\nrun in parallel.\n\n## Presentation Style\n1. **Polished Markdown only**: Format user-facing answers with clean Markdown headings, short paragraphs, and concise bullets. Use tables only when they improve comparison or scanning.\n2. **Terminal-friendly layout**: Keep lines readable in a TUI; avoid dense walls of text, deeply nested lists, oversized ASCII art, or heavy visual blocks.\n3. **Elegant emphasis**: Use bold text sparingly for key terms and conclusions. Avoid excessive emojis, decorative symbols, repeated separators, or visual noise.\n4. **Theme-safe colors**: Do not emit ANSI escape codes, HTML color tags, Rich markup, or hardcoded color names. Rely on the TUI theme to render Markdown professionally.\n5. **No leaked tool protocol**: Never show tool-call JSON, internal schemas, raw observations, tool result payloads, or hidden reasoning in the final answer unless the user explicitly asks for raw/debug output. Treat any prior `{\"name\": ..., \"arguments\": ...}` blocks and `Tool result (...)` messages as internal execution context only.\n6. **Professional closure**: End with a concise conclusion or next step when helpful; avoid rambling after the useful answer is complete.\n\nWhen finished with all tool calls, respond normally without a JSON block, tool-call transcript, or process log.\n\n## Task Contract\n- Task ID: turn-4\n- Original user request: Remove the adaptive JSON pretty plugin completely.\n- Workspace root: \n- Allowed roots: \n- Treat relative project paths as relative to the workspace root; never infer `.` as the project root when a workspace root is provided.\n- Workspace boundary is enforced by tools: do not read, search, edit, or run commands against paths outside the allowed roots unless the user explicitly requests an external path and the tool/approval policy permits it.\n- LeapFlow workspace config is optional at `/.leapflow/config.yaml`; runtime config is loaded from `~/.leapflow/config/user.yaml` and `~/.leapflow/profiles//config/*.yaml`.\n- Preserve this task contract across summarization, compression, tool loops, and memory retrieval.\n" + }, + { + "role": "system", + "content": "\n## Recent Session Summary\n- [assistant] Formatted JSON with the new plugin.\n- [user] Remove the adaptive JSON pretty plugin completely.\n" + }, + { + "role": "assistant", + "content": "Formatted JSON with the new plugin." + }, + { + "role": "user", + "content": "Remove the adaptive JSON pretty plugin completely.\nRemove the adaptive JSON pretty plugin completely." + }, + { + "role": "assistant", + "content": "", + "tool_calls": [ + "plugin_remove" + ] + }, + { + "role": "tool", + "content": "{\"ok\": true, \"action\": \"remove\", \"plugin_id\": \"json_pretty_loop_e2e\", \"state\": \"disposed\", \"source_path\": \"\", \"source_deleted\": true, \"execution_id\": \"\", \"idempotency_key\": \"\", \"execution_policy\": \"mutating_once\", \"tool_call_id\": \"call_1\", \"execution_status\": \"completed\"}", + "tool_result": true + } + ], + "tools": [ + "capability_expand", + "code_intel", + "code_search", + "config_get", + "config_list", + "env_info", + "file_find", + "file_list", + "file_read", + "git_query", + "lint_check", + "memory_search", + "plugin_list", + "plugin_propose", + "plugin_status", + "plugin_versions", + "repo_map", + "research_note", + "runtime_snapshot", + "schedule_reentry", + "self_describe", + "session_list", + "skill_view", + "skills_list", + "terminal_list", + "terminal_read", + "test_run", + "text_search", + "time_get", + "tool_describe", + "tool_search", + "web_fetch" + ] + }, + "responses": [ + { + "status": 200, + "content_type": "application/json", + "body": "{\"id\": \"chatcmpl-cassette\", \"object\": \"chat.completion\", \"model\": \"cassette-model\", \"choices\": [{\"index\": 0, \"message\": {\"role\": \"assistant\", \"content\": \"Removed json pretty plugin.\"}, \"finish_reason\": \"stop\"}], \"usage\": {\"prompt_tokens\": 64, \"completion_tokens\": 16, \"total_tokens\": 80}}" + } + ] +} diff --git a/tests/_fixtures/cassettes/r7_adaptive_plugin_loop/cassette-model-bf150d79488eaa8e.cassette.json b/tests/_fixtures/cassettes/r7_adaptive_plugin_loop/cassette-model-bf150d79488eaa8e.cassette.json deleted file mode 100644 index 01bb65a3..00000000 --- a/tests/_fixtures/cassettes/r7_adaptive_plugin_loop/cassette-model-bf150d79488eaa8e.cassette.json +++ /dev/null @@ -1,80 +0,0 @@ -{ - "fingerprint": "bf150d79488eaa8e40300ac6606e406751dc7c220101ed8c1cb99828f859bb7d", - "note": "captured in seed mode", - "request": { - "model": "cassette-model", - "stream": false, - "messages": [ - { - "role": "system", - "content": "You are LeapFlow, an intelligent assistant that can both converse naturally and take real actions on the user's computer.\n\n## Capabilities\nThe tool index below lists **every** registered tool by name and a one-line summary — this is the complete\ncapability contract; nothing else exists. Only a subset is directly callable this turn (via native tool calling,\nnot a JSON block in your reply). If you need a tool from the index that is not yet callable, call\n`capability_expand` with its category name first — the matching tools become callable immediately after.\nWhen the user asks what LeapFlow itself supports, whether it supports plugins, or which runtime capabilities\nare available, use the live capability evidence exposed by `plugin_list` before making capability claims; report\nconfiguration-dependent or unavailable capabilities as limitations instead of inferring from documentation.\n- **text_search**(text, pattern): Search for a regex pattern in text.\n- **text_replace**(text, old, new, count) [capability_expand category: write]: Replace occurrences of a substring in text.\n- **time_get**(): Get current date and time.\n- **env_info**(): Get system environment information (OS, Python version, cwd).\n- **skills_list**(query, category, source): List available learned skills. Use when user asks about capabilities or you need a specific skill.\n- **skill_view**(name): View the full content of a specific skill document.\n- **code_intel**(path, operation): Precise document symbols (outline) for a source file: classes, functions, and methods with line ranges. Python uses an exact AST parse; other languages use a keyword-prefix scan. Prefer over file_read mode=symbols for accurate navigation before editing. Read-only.\n- **repo_map**(path): Compact project orientation for a repository root: languages, detected test/lint commands, top-level structure, entry points, manifest, and VCS branch. Call this first when entering an unfamiliar codebase. Read-only.\n- **scm_sync**(action, cwd, remote, pull_ref, push_ref, timeout) [capability_expand category: scm]: Run a typed git SCM action. Use this instead of shell_run for git pull/push/status. For 'pull origin main then push', set action='pull_then_push', remote='origin', pull_ref='main', and omit push_ref so LeapFlow pushes the current local branch.\n- **git_query**(action, cwd, ref, path, staged, max_count, stat): Read-only structured git inspection: action=diff|log|status|branch|show. Prefer over shell_run for reading repo state — output is clipped, redacted, and log/branch are parsed into structured fields. Use scm_sync for pull/push.\n- **git_write**(action, cwd, message, stage_all, name, ref, create) [capability_expand category: scm]: Mutating git actions: action=commit (message, stage_all), branch (create+switch), checkout (switch; create=true for -b). Approval-gated. Use scm_sync for pull/push and git_query for reads.\n- **test_run**(command, cwd, timeout): Run the project's test suite and return structured results (framework, passed/failed counts, failing tests). Auto-detects the runner (pytest/npm/go/cargo) or uses a configured/explicit command; executes via the governed shell. ok=true means the runner executed — see 'success' for pass/fail.\n- **lint_check**(command, cwd, timeout): Run the project's linter and return a structured clean/issue result. Auto-detects the linter (ruff/eslint/go vet/clippy) or uses a configured/explicit command; executes via the governed shell. ok=true means the linter ran — see 'clean'.\n- **file_list**(path, pattern, depth): List files and directories at a given path. Use depth=1 or depth=2 to get a recursive tree in one call instead of listing each sub-directory separately.\n- **file_read**(path, max_lines, start_line, max_chars, mode): Read text file content with adaptive context governance. For large or unfamiliar files, prefer mode='outline' or mode='symbols' first, then use mode='raw' with start_line/max_lines for the specific range you actually need. For LeapFlow's own settings, use config_list / config_get / config_set — its config files are outside the workspace and not readable here.\n- **file_write**(path, content, mode) [capability_expand category: write]: Write content to a file (overwrite or append).\n- **code_search**(pattern, patterns, path, glob, ignore_case, multiline, max_results, context_lines): Search file CONTENTS by regex pattern across a directory tree (ripgrep-backed). Requires a regex pattern. NOT for listing or browsing directory contents — use file_list for that. Prefer this over shell_run grep: faster, skips VCS/dependency/build dirs, and returns structured path:line:column matches. Batch related lookups into ONE call via `patterns` (OR-combined, single pass) instead of issuing several separate searches. Use file_read for the surrounding context of a hit.\n- **file_find**(glob, path, max_results): Find files by a recursive glob pattern under a base path (e.g. '**/test_*.py' or '*.md'). Prefer this over shell_run find; skips VCS/dependency/build dirs.\n- **edit_file**(path, edits, dry_run, diff) [capability_expand category: write]: Apply targeted, anchored search-replace edits to an EXISTING text file (use file_write to create/overwrite). Each edit is {original_text, new_text, replace_all?}; original_text must match exactly and uniquely (or set replace_all) — a non-unique or missing anchor is rejected so files are never corrupted. Set dry_run to preview. Alternatively pass a unified 'diff' to apply its hunks as anchored edits. Far cheaper and safer than rewriting a whole file.\n- **shell_run**(command, cwd, timeout) [capability_expand category: shell]: Execute a one-shot shell command with timeout protection. Runs in the active workspace; paths resolving outside it are refused. Reach for a structured tool first when one fits — web_fetch for anything over HTTP(S), git_query/scm_sync for git, code_search/file_find/file_read for the repo, config_get/config_set for LeapFlow's own settings — because those report typed results, while a failed shell command can only be diagnosed from its exit code and stderr. Every shell run counts as an external side effect, so a failure stops the rest of the batch and is not retried automatically.\n- **terminal_open**(command, cwd, shell) [capability_expand category: terminal]: Open a PERSISTENT shell session (REPL/dev server/watch), returning a session_id for terminal_send/read/close. Disabled unless tools.terminal_session_enabled is set. For one-shot commands use shell_run instead.\n- **terminal_send**(session_id, input, wait) [capability_expand category: terminal]: Send a line of input to a persistent terminal session and return output captured shortly after.\n- **terminal_read**(session_id, wait): Drain buffered output from a persistent terminal session (optionally waiting briefly first).\n- **terminal_close**(session_id) [capability_expand category: terminal]: Terminate a persistent terminal session and release its process group.\n- **terminal_list**(): List active persistent terminal sessions.\n- **config_list**(category, limit): List LeapFlow's own writable settings (LLM model and endpoint, daemon, memory, perception, gateway, …) with current values. Use this to discover the exact key before changing anything. Optionally narrow by `category`. This is the only correct way to inspect LeapFlow configuration — never read config files from disk.\n- **config_get**(key): Read one LeapFlow setting by key (e.g. 'llm.model', 'llm.base_url', 'daemon.log_level'), returning its current value, type, scopes, and whether a change needs a daemon restart. There is no 'llm.provider' key: provider behavior is inferred from the OpenAI-compatible 'llm.base_url'. Never read LeapFlow config files from disk — use this.\n- **config_set**(key, value, scope) [capability_expand category: config]: Change one LeapFlow setting by key, e.g. switch an OpenAI-compatible LLM with key='llm.model' and key='llm.base_url'. There is no 'llm.provider' key: provider behavior is inferred from the endpoint. Values are validated and coerced; credentials are stored in the vault automatically. Call config_list or config_get first if unsure of the key. The result states whether a `leap daemon restart` is required. Never edit LeapFlow config files directly.\n- **web_fetch**(url, select, timeout, max_bytes): Read a URL over HTTP(S) and get back extracted, context-sized content: parsed JSON for API endpoints, readable text plus links for web pages. Use this for anything on the internet — prices, docs, releases, articles — instead of running curl through shell_run: it reports real HTTP status codes, retries rate limits on its own, and is a plain read so a retry is always safe. For JSON APIs pass `select` with a dotted path (e.g. 'chart.result.0.meta') to return just that part instead of the whole payload.\n- **memory_search**(query, limit): Search agent memory for relevant past experiences, observations, and facts.\n- **memory_add**(content, kind) [capability_expand category: write]: Store a new observation or insight in memory for future reference.\n- **research_note**(kind, text): Record a compact, structured note about the current task's state so it survives context compression on long / multi-step tasks. Use for durable findings, open questions still to resolve, decisions / excluded paths, and the immediate next step. One concise sentence per note.\n- **capability_expand**(category): Fetch the full callable schema for every tool in a capability category (e.g. 'hub', 'gateway', 'desktop', 'delegate', 'file', 'memory', 'skill'). The compact tool index always lists every registered tool by name and a one-line summary, but only a static low-risk subset is directly callable each turn. If you need a tool from the index that is not yet callable, call capability_expand with its category first; the matching tools become callable in this turn. Never invent a tool name — expand the category instead.\n- **delegate_task**(goal, context) [capability_expand category: delegate]: Delegate a complex sub-task to an isolated subagent. The subagent gets a fresh context and restricted tool access. Use when a task is self-contained and can be solved independently.\n- **schedule_reentry**(kind, reason, delay_seconds, event_match, max_reentries, deadline_seconds): Register a re-entry so this task can resume later from its current orientation (findings / open questions / next step). Use when work must pause and continue after a delay (kind=time) or when a matching platform event arrives (kind=event), instead of finishing now. The research-ledger state is carried over automatically.\n- **hub_push**(skill_name, visibility, version) [capability_expand category: hub]: Push a local skill to the ModelScope Hub for sharing or backup.\n- **hub_pull**(repo_id, version) [capability_expand category: hub]: Pull a skill from the ModelScope Hub to install locally.\n- **hub_search**(query) [capability_expand category: hub]: Search for skills on the Hub by keyword or description.\n- **hub_sync**(mode, dry_run) [capability_expand category: hub]: Preview or execute sync between local skills and Hub.\n- **platform_action**(platform, action, payload, backend_kind) [capability_expand category: gateway]: Execute an exact registered business action on an external platform through LeapFlow's App Connector layer. Actions must be copied from the App Connector Capability Index and are addressed as domain.operation, e.g. im.send_message or docs.create_markdown. All business fields (chat_id, text, query, etc.) MUST be placed inside `payload`, never at the top level. Example: {\"platform\":\"feishu\",\"action\":\"im.send_message\",\"payload\":{\"chat_id\":\"oc_xxx\",\"text\":\"hello\"}}. Do not invent action names, do not use management actions such as list/guide/connect/status here.\n- **platform_connect**(action, platform, credentials, options, checkpoint) [capability_expand category: gateway]: List, guide, connect, disconnect, remove, or check status for external platforms using the App Connector management namespace. Supports REST and CLI backends. Use this for management actions such as list/guide/preflight/connect/status; use platform_action only for exact registered business actions.\n- **gateway_send**(platform, chat_id, text, thread_id) [capability_expand category: gateway]: Send a message to a connected external platform (Feishu group, Telegram chat, DingTalk conversation, etc.). Requires the platform to be connected via gateway_connect first. Use gateway_connect with action='list' to see connected platforms and available chat IDs.\n- **gateway_connect**(action, platform, credentials, options) [capability_expand category: gateway]: Connect, configure, or manage external platform integrations (Feishu, DingTalk, Telegram, Slack, Discord, etc.). Conversational flow: 1) call 'guide' to get setup steps + required fields, 2) present the steps to the user and ask for ALL required credentials in a single message, 3) call 'connect' with the credentials. Goal: complete in 1–2 user turns. NEVER include credential values in your text response.\n- **plugin_list**(): List the live plugin registry and cross-subsystem capability evidence. Use this before answering questions about whether LeapFlow supports plugins, self-evolution, plugin installation, hot reload, versioning, or other runtime capabilities.\n- **plugin_status**(plugin_id): Get detailed status of a specific plugin: its declared category, runtime dependencies, contributed tools, and fiber lifecycle state.\n- **plugin_versions**(plugin_id): List recorded source versions and active pointer for a profile-scoped plugin.\n- **plugin_propose**(requested_capability, plugin_id, proposed_tools, test_cases, risk_level, evidence): Create a side-effect-free PluginProposal from explicit capability-gap evidence. Use this before plugin_generate when a missing capability should be reviewed. Does not call an LLM, write files, or install anything.\n- **assess_compatibility**(manifest, source_path) [capability_expand category: plugin_management]: Assess whether a foreign plugin manifest or real DSH source bundle is compatible with LeapFlow. A source bundle assessment is static: runtime_ready stays false until restricted Node discovery during plugin_install. Returns component-level verdicts and limitations.\n- **plugin_generate**(plugin_id, description, proposal_id) [capability_expand category: system]: Generate a new ToolPlugin from a natural-language capability description. The LLM produces code that conforms to the ToolPlugin Protocol; it is then rigorously validated (syntax, structure, import, protocol conformance). The generated source is stored in CAS and receives explicit content approval. It DOES NOT install the plugin — installation requires a second, mutation-specific approval via plugin_install.\n- **plugin_install**(plugin_id, code, marketplace_name, source_path, proposal_id, version_label) [capability_expand category: system]: Install a plugin from exactly one source: validated Python code, the configured Python marketplace, or a real DSH source bundle. DSH bundles run restricted Node discovery before registration; only public host tools are installed and client UI limitations are reported. Writes profile-scoped state and mutates the process-global registry. REQUIRES APPROVAL.\n- **plugin_rollback**(plugin_id, version) [capability_expand category: system]: Rollback a profile-scoped plugin to a recorded source version and reload it. REQUIRES APPROVAL.\n- **plugin_reload**(plugin_id, version_label) [capability_expand category: system]: Hot-reload a plugin at runtime. Disposes the old plugin fiber, re-imports its module, and registers a fresh instance. Existing in-flight turns are unaffected (snapshot isolation). REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_disable**(plugin_id) [capability_expand category: system]: Disable a plugin by disposing its fiber, removing its tools from the runtime registry. Cannot disable self_management itself. REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_remove**(plugin_id, delete_source) [capability_expand category: system]: Terminally remove a plugin: dispose its fiber, unregister its tools, remove reload metadata, and optionally delete its profile-scoped source file. Cannot remove self_management itself. REQUIRES APPROVAL.\n- **plugin_unquarantine**(plugin_id) [capability_expand category: plugin_management]: Restore a quarantined plugin to probation status for re-evaluation. Unfreezes the trust ledger, transitions the proposal back to PROBATION, and reloads the plugin. REQUIRES APPROVAL.\n- **plugin_enable**(plugin_id) [capability_expand category: system]: Re-enable a previously disabled plugin by reloading its module and registering a fresh instance. REQUIRES APPROVAL.\n- **self_describe**(facet): Introspect LeapFlow's own identity, capabilities, runtime state, evolution metrics, or platform connections. Use facet='all' only when a comprehensive self-check is explicitly requested.\n- **runtime_snapshot**(): Lightweight (~150 token) flat snapshot of current runtime state: model, context budget, posture, disclosure level, turn count, cache hit rate, uptime, tool count, and pending approvals.\n- **schedule_create**(trigger_expression, instruction, execution_mode, max_retries, delivery_target) [capability_expand category: scheduler]: Create a new scheduled task. Specify a trigger expression (e.g. '30m', 'every 2h', '0 9 * * *') and the instruction to execute on each trigger. Optionally provide an execution mode and a delivery target to receive notifications.\n- **schedule_list**() [capability_expand category: scheduler]: List all scheduled tasks with their current state, trigger, and next due time.\n- **schedule_status**(task_id) [capability_expand category: scheduler]: Get detailed status and recent execution history for a scheduled task.\n- **schedule_pause**(task_id) [capability_expand category: scheduler]: Pause a scheduled task so it stops firing without being cancelled.\n- **schedule_resume**(task_id) [capability_expand category: scheduler]: Resume a paused scheduled task — re-arms it and recalculates next due time.\n- **schedule_cancel**(task_id) [capability_expand category: scheduler]: Cancel a scheduled task permanently.\n- **tool_search**(query, max_results): Search all registered tools by keyword relevance. Returns a ranked list of matching tools with name, category, and summary. Use this when you need a tool but are unsure of its exact name or category.\n- **tool_describe**(tool_name): Get the full callable schema of a single tool by exact name. Returns the tool's description, parameters, and metadata. Use after tool_search to inspect a specific tool before calling it.\n- **session_search**(query, limit) [capability_expand category: unclassified]: Search past conversation sessions in the current workspace for relevant context.\n- **session_list**(limit): List recent conversation sessions in the current workspace with stable ids, titles, dates, and summaries. Use for browsing past tasks or when user asks to see history without specific search terms. No keywords needed — returns chronological list.\n- **session_detail**(session_id, limit, offset, include_inactive) [capability_expand category: unclassified]: Read a paginated persisted transcript for one past conversation session in the current workspace. Use after session_list or session_search returns a session_id.\n- **json_pretty_loop_e2e**(text): Pretty-print JSON for the adaptive closed-loop journey.\n- **click**(element_index) [capability_expand category: desktop]: Click a UI element by its element_index (from the latest observe_ui snapshot)\n- **get_clipboard**() [capability_expand category: desktop]: Read current clipboard text content\n- **list_apps**(filter, running_only) [capability_expand category: desktop]: List available applications on this system. Use to discover correct bundle_id before switch_app.\n- **list_windows**() [capability_expand category: desktop]: List all top-level windows with pid, window_id, title, and per-window state (minimized, on-screen). Call this first to pick the pid and window_id that observe_ui and other window tools require.\n- **observe_ui**(pid, window_id, query) [capability_expand category: desktop]: Snapshot one window's actionable UI elements, each tagged with an element_index for click/right_click/read_text. Re-observe after actions — indices belong to one snapshot. Requires the window's pid and window_id from list_windows.\n- **open_url**(url, app_id) [capability_expand category: desktop]: Open a URL in the default or specified browser\n- **read_text**(element_index) [capability_expand category: desktop]: Read the text content of a specific UI element from the latest snapshot\n- **right_click**(element_index) [capability_expand category: desktop]: Right-click a UI element to open its context menu. Returns visible menu items.\n- **screenshot**(pid, window_id) [capability_expand category: desktop]: Capture a screenshot for visual verification. With pid + window_id captures that window (works across all displays); defaults to the last observed window, or the full desktop when no window has been observed.\n- **scroll**(element_index, direction, amount, pid, window_id) [capability_expand category: desktop]: Scroll a scrollable area of a window. Omit element_index to scroll the window's focused/page scroller; pass one to scroll an exact element from the latest snapshot.\n- **select_text**(element_index) [capability_expand category: desktop]: Select all text in a UI element (focus + select-all, for subsequent copy)\n- **set_clipboard**(text) [capability_expand category: desktop]: Write text to the clipboard\n- **shortcut**(keys) [capability_expand category: desktop]: Execute a keyboard shortcut\n- **switch_app**(app_id) [capability_expand category: desktop]: Switch to an app (launch if needed, activate, verify)\n- **type_text**(text) [capability_expand category: desktop]: Type text into the currently focused element\n- **wait**(seconds) [capability_expand category: desktop]: Wait for a specified duration before continuing\n- **wait_until**(condition, pid, window_id, timeout, poll_interval) [capability_expand category: desktop]: Wait until a UI condition is met (polls UI tree). Returns elements when found or on timeout.\n- **wait_until_stable**(timeout, poll_interval, pid, window_id) [capability_expand category: desktop]: Wait until the UI stops changing (element set stabilizes across polls).\n\n## App Connector Capability Index\nLeapFlow can onboard and manage external apps through `platform_connect` and execute exact registered business actions through `platform_action`.\nFor requests about connecting, setting up, configuring, enabling, or managing a supported app, use `platform_connect` first instead of generating SDK/Webhook sample code.\n`platform_connect.action` is the management namespace: list, guide, preflight, connect, disconnect, remove, status, events_start, events_stop, events_status.\n`platform_action.action` is only for exact registered platform business actions listed below, such as `im.send_message`; never use management actions like `list` or `guide` there.\nAll business fields MUST go inside `payload`; top-level keys are only `platform`, `action`, and `payload`.\nDo not invent platform IDs or platform action names. If the needed action is not listed, ask for discovery/clarification instead of guessing.\nUse `platform_connect` with `action='guide'` and the matching `platform` to start onboarding; use `action='list'` when the app is unclear.\nWhen a pending onboarding state is present, continue from that state with `platform_connect` instead of asking the user to restate the app.\nSupported apps:\n- `api_server`: API Server (OpenAI Compatible) (category=api; backend=adapter; platform_action actions=none registered)\n- `dingtalk`: 钉钉 (DingTalk) (category=im; backend=adapter; platform_action actions=none registered)\n- `feishu`: 飞书 (Feishu/Lark) (category=im; backend=cli)\n - `calendar.create_event` payload={summary*, start_time*, end_time*, attendees} [write/high]\n - `docs.create_markdown` payload={title*, markdown*, folder_token} [write/medium]\n - `drive.search` payload={query*, limit} [read/medium]\n - `im.add_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.download_resource` payload={message_id*, file_key*, type*} [read/low]\n - `im.get_messages` payload={message_ids*} [read/low]\n - `im.list_chats` payload={page_size} [read/low]\n - `im.list_messages` payload={chat_id*} [read/low]\n - `im.list_thread_messages` payload={thread*} [read/low]\n - `im.remove_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.reply_message` payload={message_id*, text*} [send/high]\n - `im.search_chats` payload={query*, page_size} [read/low]\n - `im.search_messages` payload={query*} [read/medium]\n - `im.send_message` payload={chat_id*, text*, thread_id} [send/high]\n - `im.update_card` payload={token*, card*} [write/medium]\n - `im.update_message` payload={message_id*, text*} [write/medium]\n - `mail.search_unread` payload={query, limit} [read/high]\n - `sheets.append_row` payload={spreadsheet_token*, sheet_id*, values*} [write/medium]\n - `task.create` payload={title*, description, due_time} [write/medium]\n- `telegram`: Telegram (category=social; backend=adapter; platform_action actions=none registered)\n- `webhook`: Webhook (Generic) (category=webhook; backend=adapter; platform_action actions=none registered)\n\n## Tool Usage\nTools are normally invoked through the native function-calling mechanism, not by writing JSON in your reply\ntext. Only if the provider signals that native function calling is unavailable for this turn, fall back to a\nsingle JSON code block: `{\"name\": \"tool_name\", \"arguments\": {\"key\": \"value\"}}` — use this fallback format\nonly, never both. Only call a tool whose exact name appears in the tool index above and is currently callable\n(or reachable via `capability_expand`). Never invent, rename, alias, or guess a tool name, platform ID, or\nplatform action from argument shape or wording — if the index does not list it, it does not exist.\n`platform_connect.action` (list/guide/preflight/connect/disconnect/remove/status/events_start/events_stop/\nevents_status) is the App Connector management namespace; `platform_action.action` only accepts exact\nregistered business actions such as `im.send_message` shown in the App Connector Capability Index — never mix\nthe two namespaces. If a tool call returns an unknown/unavailable result, use the returned suggestions or\navailable names for a single retry instead of trying further variations of the same guess.\n\n**Side-effect action rule** (`platform_action` with effect=send/write/execute):\n- Call each unique action+payload **exactly once**. Never include duplicates in the same turn.\n- Once the result returns `\"completed\": true`, that action is DONE for this task. Do NOT call it again\n in any subsequent turn — immediately summarize the result for the user instead.\n- The system enforces idempotency: duplicate calls are blocked and will not execute.\n- If the user explicitly requests sending/writing multiple times, use distinct payloads per call.\n\n**Resource identifier provenance rule**:\n- NEVER fabricate, guess, or infer resource identifiers (chat_id, message_id, file_key, user_id, etc.).\n Every resource ID used in a side-effect action MUST come from a successful API response in this session.\n- If a read/list action fails (e.g. authorization error), you do NOT have valid resource IDs.\n Report the failure to the user — do NOT attempt the dependent write/send action with a guessed ID.\n- When a tool result contains `\"llm_instruction\"`, follow it exactly.\n\n## Guidelines\n1. **Direct answers first**: If you already know the answer, respond directly without tools.\n2. **Avoid redundant tool calls**: Do not call the same tool with the same arguments more than once in the same user turn. When an existing tool result already answers the user's request, stop calling tools and answer directly.\n3. **Use tools proactively**: When the user asks about files, time, system state, or needs actions performed, use the appropriate tool.\n4. **Chain tools when needed**: You can call multiple tools in sequence (e.g., list files → read file → summarize).\n5. **Handle failures gracefully**: If a tool fails, explain what went wrong and suggest alternatives. If it failed because of an unknown tool/platform/action name, retry once with an exact name from the returned suggestions, then explain rather than keep guessing.\n6. **Summarize results naturally**: After tool execution, synthesize the results into a helpful answer rather than dumping raw output.\n7. **Stay conversational**: Maintain a natural, helpful tone. Acknowledge context from earlier in the conversation.\n8. **Recall past work**:\n - Broad queries (\"之前做了什么\", \"列出任务\"): answer from the \"Recent Task History\" section already in your context. If insufficient, call session_list.\n - Specific lookups (\"上次那个配置怎么改的\"): call session_search with relevant phrases (NOT single characters).\n - Do NOT call search tools repeatedly with keyword fragments. One well-phrased call is better than ten fragmented ones.\n\n## Coding & Verification\nWhen working with code, prefer the precise built-in tools over ad-hoc shell: use `repo_map` to orient in an\nunfamiliar project, `code_search`/`file_find` to locate and `code_intel` for symbols, `edit_file` (anchored\nsearch-replace, or a unified `diff`) to change files — never rewrite a whole file to change a few lines — and\n`git_query` to inspect diffs/log. After edits, check `syntax_ok` in the result and run `test_run`/`lint_check`\nbefore declaring the work done. Batch independent read-only calls (search/read/query) into a single turn — they\nrun in parallel.\n\n## Presentation Style\n1. **Polished Markdown only**: Format user-facing answers with clean Markdown headings, short paragraphs, and concise bullets. Use tables only when they improve comparison or scanning.\n2. **Terminal-friendly layout**: Keep lines readable in a TUI; avoid dense walls of text, deeply nested lists, oversized ASCII art, or heavy visual blocks.\n3. **Elegant emphasis**: Use bold text sparingly for key terms and conclusions. Avoid excessive emojis, decorative symbols, repeated separators, or visual noise.\n4. **Theme-safe colors**: Do not emit ANSI escape codes, HTML color tags, Rich markup, or hardcoded color names. Rely on the TUI theme to render Markdown professionally.\n5. **No leaked tool protocol**: Never show tool-call JSON, internal schemas, raw observations, tool result payloads, or hidden reasoning in the final answer unless the user explicitly asks for raw/debug output. Treat any prior `{\"name\": ..., \"arguments\": ...}` blocks and `Tool result (...)` messages as internal execution context only.\n6. **Professional closure**: End with a concise conclusion or next step when helpful; avoid rambling after the useful answer is complete.\n\nWhen finished with all tool calls, respond normally without a JSON block, tool-call transcript, or process log.\n\n## Task Contract\n- Task ID: turn-3\n- Original user request: Use the json_pretty_loop_e2e tool to format the sample JSON.\n- Workspace root: \n- Allowed roots: \n- Treat relative project paths as relative to the workspace root; never infer `.` as the project root when a workspace root is provided.\n- Workspace boundary is enforced by tools: do not read, search, edit, or run commands against paths outside the allowed roots unless the user explicitly requests an external path and the tool/approval policy permits it.\n- LeapFlow workspace config is optional at `/.leapflow/config.yaml`; runtime config is loaded from `~/.leapflow/config/user.yaml` and `~/.leapflow/profiles//config/*.yaml`.\n- Preserve this task contract across summarization, compression, tool loops, and memory retrieval.\n" - }, - { - "role": "system", - "content": "\n## Recent Session Summary\n- [assistant] Installed json pretty plugin.\n- [user] Use the json_pretty_loop_e2e tool to format the sample JSON.\n" - }, - { - "role": "assistant", - "content": "Installed json pretty plugin." - }, - { - "role": "user", - "content": "Use the json_pretty_loop_e2e tool to format the sample JSON.\nUse the json_pretty_loop_e2e tool to format the sample JSON." - }, - { - "role": "assistant", - "content": "", - "tool_calls": [ - "json_pretty_loop_e2e" - ] - }, - { - "role": "tool", - "content": "{\"ok\": true, \"content\": \"{\\n \\\"a\\\": 1,\\n \\\"b\\\": 2\\n}\", \"execution_id\": \"\", \"idempotency_key\": \"\", \"execution_policy\": \"read_only\", \"tool_call_id\": \"call_1\", \"execution_status\": \"completed\"}", - "tool_result": true - } - ], - "tools": [ - "capability_expand", - "code_intel", - "code_search", - "config_get", - "config_list", - "env_info", - "file_find", - "file_list", - "file_read", - "git_query", - "json_pretty_loop_e2e", - "lint_check", - "memory_search", - "plugin_list", - "plugin_propose", - "plugin_status", - "plugin_versions", - "repo_map", - "research_note", - "runtime_snapshot", - "schedule_reentry", - "self_describe", - "session_list", - "skill_view", - "skills_list", - "terminal_list", - "terminal_read", - "test_run", - "text_search", - "time_get", - "tool_describe", - "tool_search", - "web_fetch" - ] - }, - "responses": [ - { - "status": 200, - "content_type": "application/json", - "body": "{\"id\": \"chatcmpl-cassette\", \"object\": \"chat.completion\", \"model\": \"cassette-model\", \"choices\": [{\"index\": 0, \"message\": {\"role\": \"assistant\", \"content\": \"Formatted JSON with the new plugin.\"}, \"finish_reason\": \"stop\"}], \"usage\": {\"prompt_tokens\": 64, \"completion_tokens\": 16, \"total_tokens\": 80}}" - } - ] -} diff --git a/tests/_fixtures/cassettes/r8_hardware/cassette-model-00600853bb84f29e.cassette.json b/tests/_fixtures/cassettes/r8_hardware/cassette-model-00600853bb84f29e.cassette.json new file mode 100644 index 00000000..2dfd852c --- /dev/null +++ b/tests/_fixtures/cassettes/r8_hardware/cassette-model-00600853bb84f29e.cassette.json @@ -0,0 +1,79 @@ +{ + "fingerprint": "00600853bb84f29e8682da73f77c49e441adb75d1d8df4657e2339dcf9cb1750", + "note": "captured in seed mode", + "request": { + "model": "cassette-model", + "stream": false, + "messages": [ + { + "role": "system", + "content": "You are LeapFlow, an intelligent assistant that can both converse naturally and take real actions on the user's computer.\n\n## Capabilities\nThe tool index below lists **every** registered tool by name and a one-line summary — this is the complete\ncapability contract; nothing else exists. Only a subset is directly callable this turn (via native tool calling,\nnot a JSON block in your reply). If you need a tool from the index that is not yet callable, call\n`capability_expand` with its category name first — the matching tools become callable immediately after.\nWhen the user asks about LeapFlow **itself** — its identity/version/model, what tools, plugins, or capabilities\nit has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of\nself-awareness — answer from live runtime evidence via `self_describe` (facet=capabilities for \"what can you\ndo / do you support X\", identity for version/model, runtime for context/posture, or `all` for a full self-check),\nnever by reading LeapFlow's own source code, documentation, or memory. Report configuration-dependent or\nunavailable capabilities as limitations rather than inferring them.\n- **text_search**(text, pattern): Search for a regex pattern in text.\n- **text_replace**(text, old, new, count) [capability_expand category: write]: Replace occurrences of a substring in text.\n- **time_get**(): Get current date and time.\n- **env_info**(): Get system environment information (OS, Python version, cwd).\n- **skills_list**(query, category, source): List available learned skills. Use when user asks about capabilities or you need a specific skill.\n- **skill_view**(name): View the full content of a specific skill document.\n- **skill_list_files**(name) [capability_expand category: skills]: List all support files in a skill directory (configs, templates, examples). Use before skill_file_read to discover available files.\n- **skill_file_read**(name, file_path) [capability_expand category: skills]: Read an individual support file from a skill directory. Use skill_list_files first to discover available files.\n- **code_intel**(path, operation): Precise document symbols (outline) for a source file: classes, functions, and methods with line ranges. Python uses an exact AST parse; other languages use a keyword-prefix scan. Prefer over file_read mode=symbols for accurate navigation before editing. Read-only.\n- **repo_map**(path): Compact project orientation for a repository root: languages, detected test/lint commands, top-level structure, entry points, manifest, and VCS branch. Call this first when entering an unfamiliar codebase. Read-only.\n- **scm_sync**(action, cwd, remote, pull_ref, push_ref, timeout) [capability_expand category: scm]: Run a typed git SCM action. Use this instead of shell_run for git pull/push/status. For 'pull origin main then push', set action='pull_then_push', remote='origin', pull_ref='main', and omit push_ref so LeapFlow pushes the current local branch.\n- **git_query**(action, cwd, ref, path, staged, max_count, stat): Read-only structured git inspection: action=diff|log|status|branch|show. Prefer over shell_run for reading repo state — output is clipped, redacted, and log/branch are parsed into structured fields. Use scm_sync for pull/push.\n- **git_write**(action, cwd, message, stage_all, name, ref, create) [capability_expand category: scm]: Mutating git actions: action=commit (message, stage_all), branch (create+switch), checkout (switch; create=true for -b). Approval-gated. Use scm_sync for pull/push and git_query for reads.\n- **test_run**(command, cwd, timeout): Run the project's test suite and return structured results (framework, passed/failed counts, failing tests). Auto-detects the runner (pytest/npm/go/cargo) or uses a configured/explicit command; executes via the governed shell. ok=true means the runner executed — see 'success' for pass/fail.\n- **lint_check**(command, cwd, timeout): Run the project's linter and return a structured clean/issue result. Auto-detects the linter (ruff/eslint/go vet/clippy) or uses a configured/explicit command; executes via the governed shell. ok=true means the linter ran — see 'clean'.\n- **file_list**(path, pattern, depth): List files and directories at a given path. Use depth=1 or depth=2 to get a recursive tree in one call instead of listing each sub-directory separately.\n- **file_read**(path, max_lines, start_line, max_chars, mode): Read text file content with adaptive context governance. For large or unfamiliar files, prefer mode='outline' or mode='symbols' first, then use mode='raw' with start_line/max_lines for the specific range you actually need. For LeapFlow's own settings, use config_list / config_get / config_set — its config files are outside the workspace and not readable here.\n- **file_write**(path, content, mode) [capability_expand category: write]: Write content to a file (overwrite or append).\n- **code_search**(pattern, patterns, path, glob, ignore_case, multiline, max_results, context_lines): Search file CONTENTS by regex pattern across a directory tree (ripgrep-backed). Requires a regex pattern. NOT for listing or browsing directory contents — use file_list for that. Prefer this over shell_run grep: faster, skips VCS/dependency/build dirs, and returns structured path:line:column matches. Batch related lookups into ONE call via `patterns` (OR-combined, single pass) instead of issuing several separate searches. Use file_read for the surrounding context of a hit.\n- **file_find**(glob, path, max_results): Find files by a recursive glob pattern under a base path (e.g. '**/test_*.py' or '*.md'). Prefer this over shell_run find; skips VCS/dependency/build dirs.\n- **edit_file**(path, edits, dry_run, diff) [capability_expand category: write]: Apply targeted, anchored search-replace edits to an EXISTING text file (use file_write to create/overwrite). Each edit is {original_text, new_text, replace_all?}; original_text must match exactly and uniquely (or set replace_all) — a non-unique or missing anchor is rejected so files are never corrupted. Set dry_run to preview. Alternatively pass a unified 'diff' to apply its hunks as anchored edits. Far cheaper and safer than rewriting a whole file.\n- **shell_run**(command, cwd, timeout) [capability_expand category: shell]: Execute a one-shot shell command with timeout protection. Runs in the active workspace; paths resolving outside it are refused. Reach for a structured tool first when one fits — web_fetch for anything over HTTP(S), git_query/scm_sync for git, code_search/file_find/file_read for the repo, config_get/config_set for LeapFlow's own settings — because those report typed results, while a failed shell command can only be diagnosed from its exit code and stderr. Every shell run counts as an external side effect, so a failure stops the rest of the batch and is not retried automatically.\n- **terminal_open**(command, cwd, shell) [capability_expand category: terminal]: Open a PERSISTENT shell session (REPL/dev server/watch), returning a session_id for terminal_send/read/close. Disabled unless tools.terminal_session_enabled is set. For one-shot commands use shell_run instead.\n- **terminal_send**(session_id, input, wait) [capability_expand category: terminal]: Send a line of input to a persistent terminal session and return output captured shortly after.\n- **terminal_read**(session_id, wait): Drain buffered output from a persistent terminal session (optionally waiting briefly first).\n- **terminal_close**(session_id) [capability_expand category: terminal]: Terminate a persistent terminal session and release its process group.\n- **terminal_list**(): List active persistent terminal sessions.\n- **config_list**(category, limit): List LeapFlow's own writable settings (LLM model and endpoint, daemon, memory, perception, gateway, …) with current values. Use this to discover the exact key before changing anything. Optionally narrow by `category`. This is the only correct way to inspect LeapFlow configuration — never read config files from disk.\n- **config_get**(key): Read one LeapFlow setting by key (e.g. 'llm.model', 'llm.base_url', 'daemon.log_level'), returning its current value, type, scopes, and whether a change needs a daemon restart. There is no 'llm.provider' key: provider behavior is inferred from the OpenAI-compatible 'llm.base_url'. Never read LeapFlow config files from disk — use this.\n- **config_set**(key, value, scope) [capability_expand category: config]: Change one LeapFlow setting by key, e.g. switch an OpenAI-compatible LLM with key='llm.model' and key='llm.base_url'. There is no 'llm.provider' key: provider behavior is inferred from the endpoint. Values are validated and coerced; credentials are stored in the vault automatically. Call config_list or config_get first if unsure of the key. The result states whether a `leap daemon restart` is required. Never edit LeapFlow config files directly.\n- **web_fetch**(url, select, timeout, max_bytes): Read a URL over HTTP(S) and get back extracted, context-sized content: parsed JSON for API endpoints, readable text plus links for web pages. Use this for anything on the internet — prices, docs, releases, articles — instead of running curl through shell_run: it reports real HTTP status codes, retries rate limits on its own, and is a plain read so a retry is always safe. For JSON APIs pass `select` with a dotted path (e.g. 'chart.result.0.meta') to return just that part instead of the whole payload.\n- **memory_search**(query, limit): Search agent memory for relevant past experiences, observations, and facts.\n- **memory_add**(content, kind) [capability_expand category: write]: Store a new observation or insight in memory for future reference.\n- **research_note**(kind, text): Record a compact, structured note about the current task's state so it survives context compression on long / multi-step tasks. Use for durable findings, open questions still to resolve, decisions / excluded paths, and the immediate next step. One concise sentence per note.\n- **capability_expand**(category): Fetch the full callable schema for every tool in a capability category (e.g. 'hub', 'gateway', 'desktop', 'delegate', 'file', 'memory', 'skill'). The compact tool index always lists every registered tool by name and a one-line summary, but only a static low-risk subset is directly callable each turn. If you need a tool from the index that is not yet callable, call capability_expand with its category first; the matching tools become callable in this turn. Never invent a tool name — expand the category instead.\n- **delegate_task**(goal, context) [capability_expand category: delegate]: Delegate a complex sub-task to an isolated subagent. The subagent gets a fresh context and restricted tool access. Use when a task is self-contained and can be solved independently.\n- **schedule_reentry**(kind, reason, delay_seconds, event_match, max_reentries, deadline_seconds): Register a re-entry so this task can resume later from its current orientation (findings / open questions / next step). Use when work must pause and continue after a delay (kind=time) or when a matching platform event arrives (kind=event), instead of finishing now. The research-ledger state is carried over automatically.\n- **hub_push**(skill_name, visibility, version) [capability_expand category: hub]: Push a local skill to the ModelScope Hub for sharing or backup.\n- **hub_pull**(repo_id, version) [capability_expand category: hub]: Pull a skill from the ModelScope Hub to install locally.\n- **hub_search**(query) [capability_expand category: hub]: Search for skills on the Hub by keyword or description.\n- **hub_sync**(mode, dry_run) [capability_expand category: hub]: Preview or execute sync between local skills and Hub.\n- **platform_action**(platform, action, payload, backend_kind) [capability_expand category: gateway]: Execute an exact registered business action on an external platform through LeapFlow's App Connector layer. Actions must be copied from the App Connector Capability Index and are addressed as domain.operation, e.g. im.send_message or docs.create_markdown. All business fields (chat_id, text, query, etc.) MUST be placed inside `payload`, never at the top level. Example: {\"platform\":\"feishu\",\"action\":\"im.send_message\",\"payload\":{\"chat_id\":\"oc_xxx\",\"text\":\"hello\"}}. Do not invent action names, do not use management actions such as list/guide/connect/status here.\n- **platform_connect**(action, platform, credentials, options, checkpoint) [capability_expand category: gateway]: List, guide, connect, disconnect, remove, or check status for external platforms using the App Connector management namespace. Supports REST and CLI backends. Use this for management actions such as list/guide/preflight/connect/status; use platform_action only for exact registered business actions.\n- **gateway_send**(platform, chat_id, text, thread_id) [capability_expand category: gateway]: Send a message to a connected external platform (Feishu group, Telegram chat, DingTalk conversation, etc.). Requires the platform to be connected via gateway_connect first. Use gateway_connect with action='list' to see connected platforms and available chat IDs.\n- **gateway_connect**(action, platform, credentials, options) [capability_expand category: gateway]: Connect, configure, or manage external platform integrations (Feishu, DingTalk, Telegram, Slack, Discord, etc.). Conversational flow: 1) call 'guide' to get setup steps + required fields, 2) present the steps to the user and ask for ALL required credentials in a single message, 3) call 'connect' with the credentials. Goal: complete in 1–2 user turns. NEVER include credential values in your text response.\n- **plugin_list**(): List the live plugin registry across Tool/Gateway/LLM subsystems for plugin management: which plugins are loaded, their category and fiber lifecycle state. For questions about LeapFlow's own capabilities or whether it supports X, use self_describe instead.\n- **plugin_status**(plugin_id): Get detailed status of a specific plugin: its declared category, runtime dependencies, contributed tools, and fiber lifecycle state.\n- **plugin_versions**(plugin_id): List recorded source versions and active pointer for a profile-scoped plugin.\n- **plugin_propose**(requested_capability, plugin_id, proposed_tools, test_cases, risk_level, evidence): Create a side-effect-free PluginProposal from explicit capability-gap evidence. Use this before plugin_generate when a missing capability should be reviewed. Does not call an LLM, write files, or install anything.\n- **assess_compatibility**(manifest, source_path) [capability_expand category: plugin_management]: Assess whether a foreign plugin manifest or real DSH source bundle is compatible with LeapFlow. A source bundle assessment is static: runtime_ready stays false until restricted Node discovery during plugin_install. Returns component-level verdicts and limitations.\n- **plugin_generate**(plugin_id, description, proposal_id) [capability_expand category: system]: Generate a new ToolPlugin from a natural-language capability description. The LLM produces code that conforms to the ToolPlugin Protocol; it is then rigorously validated (syntax, structure, import, protocol conformance). The generated source is stored in CAS and receives explicit content approval. It DOES NOT install the plugin — installation requires a second, mutation-specific approval via plugin_install.\n- **plugin_install**(plugin_id, code, marketplace_name, source_path, proposal_id, version_label) [capability_expand category: system]: Install a plugin from exactly one source: validated Python code, the configured Python marketplace, or a real DSH source bundle. DSH bundles run restricted Node discovery before registration; only public host tools are installed and client UI limitations are reported. Writes profile-scoped state and mutates the process-global registry. REQUIRES APPROVAL.\n- **plugin_rollback**(plugin_id, version) [capability_expand category: system]: Rollback a profile-scoped plugin to a recorded source version and reload it. REQUIRES APPROVAL.\n- **plugin_reload**(plugin_id, version_label) [capability_expand category: system]: Hot-reload a plugin at runtime. Disposes the old plugin fiber, re-imports its module, and registers a fresh instance. Existing in-flight turns are unaffected (snapshot isolation). REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_disable**(plugin_id) [capability_expand category: system]: Disable a plugin by disposing its fiber, removing its tools from the runtime registry. Cannot disable self_management itself. REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_remove**(plugin_id, delete_source) [capability_expand category: system]: Terminally remove a plugin: dispose its fiber, unregister its tools, remove reload metadata, and optionally delete its profile-scoped source file. Cannot remove self_management itself. REQUIRES APPROVAL.\n- **plugin_unquarantine**(plugin_id) [capability_expand category: plugin_management]: Restore a quarantined plugin to probation status for re-evaluation. Unfreezes the trust ledger, transitions the proposal back to PROBATION, and reloads the plugin. REQUIRES APPROVAL.\n- **plugin_enable**(plugin_id) [capability_expand category: system]: Re-enable a previously disabled plugin by reloading its module and registering a fresh instance. REQUIRES APPROVAL.\n- **self_describe**(facet): Answer questions about LeapFlow ITSELF from live runtime evidence — its identity/version/model, what tools and plugins it has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of self-awareness. Prefer this over reading LeapFlow's own source code. Pick facet=capabilities for 'what can you do / do you support X', identity for version/model, runtime for context/posture, or all for a full self-check when explicitly requested.\n- **runtime_snapshot**(): Lightweight (~150 token) flat snapshot of current runtime state: model, context budget, posture, disclosure level, turn count, cache hit rate, uptime, tool count, and pending approvals.\n- **schedule_create**(trigger_expression, instruction, execution_mode, max_retries, delivery_target) [capability_expand category: scheduler]: Create a new scheduled task. Specify a trigger expression (e.g. '30m', 'every 2h', '0 9 * * *') and the instruction to execute on each trigger. Optionally provide an execution mode and a delivery target to receive notifications.\n- **schedule_list**() [capability_expand category: scheduler]: List all scheduled tasks with their current state, trigger, and next due time.\n- **schedule_status**(task_id) [capability_expand category: scheduler]: Get detailed status and recent execution history for a scheduled task.\n- **schedule_pause**(task_id) [capability_expand category: scheduler]: Pause a scheduled task so it stops firing without being cancelled.\n- **schedule_resume**(task_id) [capability_expand category: scheduler]: Resume a paused scheduled task — re-arms it and recalculates next due time.\n- **schedule_cancel**(task_id) [capability_expand category: scheduler]: Cancel a scheduled task permanently.\n- **tool_search**(query, max_results): Search all registered tools by keyword relevance. Returns a ranked list of matching tools with name, category, and summary. Use this when you need a tool but are unsure of its exact name or category.\n- **tool_describe**(tool_name): Get the full callable schema of a single tool by exact name. Returns the tool's description, parameters, and metadata. Use after tool_search to inspect a specific tool before calling it.\n- **hw_list**(): List connected hardware devices with their channel counts and measured quantities. Start here; it does not include operating limits.\n- **hw_describe**(device_id): Return the full reference for one device: every channel, its unit, its operating envelope, rate limit, reversibility, and required interlocks, plus any prior outcomes recorded for its writable channels. Required before commanding a device.\n- **hw_read**(device_id, channel_id): Read the current value of one device channel. Has no physical effect.\n- **hw_status**(device_id): Report connection health, halt capability, and recent notable events (threshold excursions, lost samples, stalled channels) for one device.\n- **hw_configure**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Set a configuration value or setpoint on a channel declaring effect=configure. Setpoints often have inertia: the value may need time to stabilise.\n- **hw_actuate**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Command motion or output on a channel declaring effect=actuate. This moves physical hardware; a repeat from an unknown state is not a safe retry.\n- **hw_dispense**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Consume an irreversible resource on a channel declaring effect=dispense. Running this twice dispenses twice; never repeat it after a failure without first verifying what already happened.\n- **hw_estop**(device_id) [capability_expand category: hardware]: Stop all motion and output on a device immediately. Never requires approval; use it whenever device behaviour is unexpected.\n- **hw_batch_actuate**(device_id, commands, dry_run, verify) [capability_expand category: hardware]: Write multiple actuator channels in one atomic transaction. Used for coordinated multi-joint control where all positions must be commanded simultaneously (e.g. robot arm joint-space control).\n- **session_search**(query, limit) [capability_expand category: unclassified]: Search past conversation sessions in the current workspace for relevant context.\n- **session_list**(limit): List recent conversation sessions in the current workspace with stable ids, titles, dates, and summaries. Use for browsing past tasks or when user asks to see history without specific search terms. No keywords needed — returns chronological list.\n- **session_detail**(session_id, limit, offset, include_inactive) [capability_expand category: unclassified]: Read a paginated persisted transcript for one past conversation session in the current workspace. Use after session_list or session_search returns a session_id.\n- **click**(element_index) [capability_expand category: desktop]: Click a UI element by its element_index (from the latest observe_ui snapshot)\n- **get_clipboard**() [capability_expand category: desktop]: Read current clipboard text content\n- **list_apps**(filter, running_only) [capability_expand category: desktop]: List available applications on this system. Use to discover correct bundle_id before switch_app.\n- **list_windows**() [capability_expand category: desktop]: List all top-level windows with pid, window_id, title, and per-window state (minimized, on-screen). Call this first to pick the pid and window_id that observe_ui and other window tools require.\n- **observe_ui**(pid, window_id, query) [capability_expand category: desktop]: Snapshot one window's actionable UI elements, each tagged with an element_index for click/right_click/read_text. Re-observe after actions — indices belong to one snapshot. Requires the window's pid and window_id from list_windows.\n- **open_url**(url, app_id) [capability_expand category: desktop]: Open a URL in the default or specified browser\n- **read_text**(element_index) [capability_expand category: desktop]: Read the text content of a specific UI element from the latest snapshot\n- **right_click**(element_index) [capability_expand category: desktop]: Right-click a UI element to open its context menu. Returns visible menu items.\n- **screenshot**(pid, window_id) [capability_expand category: desktop]: Capture a screenshot for visual verification. With pid + window_id captures that window (works across all displays); defaults to the last observed window, or the full desktop when no window has been observed.\n- **scroll**(element_index, direction, amount, pid, window_id) [capability_expand category: desktop]: Scroll a scrollable area of a window. Omit element_index to scroll the window's focused/page scroller; pass one to scroll an exact element from the latest snapshot.\n- **select_text**(element_index) [capability_expand category: desktop]: Select all text in a UI element (focus + select-all, for subsequent copy)\n- **set_clipboard**(text) [capability_expand category: desktop]: Write text to the clipboard\n- **shortcut**(keys) [capability_expand category: desktop]: Execute a keyboard shortcut\n- **switch_app**(app_id) [capability_expand category: desktop]: Switch to an app (launch if needed, activate, verify)\n- **type_text**(text) [capability_expand category: desktop]: Type text into the currently focused element\n- **wait**(seconds) [capability_expand category: desktop]: Wait for a specified duration before continuing\n- **wait_until**(condition, pid, window_id, timeout, poll_interval) [capability_expand category: desktop]: Wait until a UI condition is met (polls UI tree). Returns elements when found or on timeout.\n- **wait_until_stable**(timeout, poll_interval, pid, window_id) [capability_expand category: desktop]: Wait until the UI stops changing (element set stabilizes across polls).\n\n## App Connector Capability Index\nLeapFlow can onboard and manage external apps through `platform_connect` and execute exact registered business actions through `platform_action`.\nFor requests about connecting, setting up, configuring, enabling, or managing a supported app, use `platform_connect` first instead of generating SDK/Webhook sample code.\n`platform_connect.action` is the management namespace: list, guide, preflight, connect, disconnect, remove, status, events_start, events_stop, events_status.\n`platform_action.action` is only for exact registered platform business actions listed below, such as `im.send_message`; never use management actions like `list` or `guide` there.\nAll business fields MUST go inside `payload`; top-level keys are only `platform`, `action`, and `payload`.\nDo not invent platform IDs or platform action names. If the needed action is not listed, ask for discovery/clarification instead of guessing.\nUse `platform_connect` with `action='guide'` and the matching `platform` to start onboarding; use `action='list'` when the app is unclear.\nWhen a pending onboarding state is present, continue from that state with `platform_connect` instead of asking the user to restate the app.\nSupported apps:\n- `api_server`: API Server (OpenAI Compatible) (category=api; backend=adapter; platform_action actions=none registered)\n- `dingtalk`: 钉钉 (DingTalk) (category=im; backend=adapter; platform_action actions=none registered)\n- `feishu`: 飞书 (Feishu/Lark) (category=im; backend=cli)\n - `calendar.create_event` payload={summary*, start_time*, end_time*, attendees} [write/high]\n - `docs.create_markdown` payload={title*, markdown*, folder_token} [write/medium]\n - `drive.search` payload={query*, limit} [read/medium]\n - `im.add_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.download_resource` payload={message_id*, file_key*, type*} [read/low]\n - `im.get_messages` payload={message_ids*} [read/low]\n - `im.list_chats` payload={page_size} [read/low]\n - `im.list_messages` payload={chat_id*} [read/low]\n - `im.list_thread_messages` payload={thread*} [read/low]\n - `im.remove_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.reply_message` payload={message_id*, text*} [send/high]\n - `im.search_chats` payload={query*, page_size} [read/low]\n - `im.search_messages` payload={query*} [read/medium]\n - `im.send_message` payload={chat_id*, text*, thread_id} [send/high]\n - `im.update_card` payload={token*, card*} [write/medium]\n - `im.update_message` payload={message_id*, text*} [write/medium]\n - `mail.search_unread` payload={query, limit} [read/high]\n - `sheets.append_row` payload={spreadsheet_token*, sheet_id*, values*} [write/medium]\n - `task.create` payload={title*, description, due_time} [write/medium]\n- `telegram`: Telegram (category=social; backend=adapter; platform_action actions=none registered)\n- `webhook`: Webhook (Generic) (category=webhook; backend=adapter; platform_action actions=none registered)\n\n## Tool Usage\nTools are normally invoked through the native function-calling mechanism, not by writing JSON in your reply\ntext. Only if the provider signals that native function calling is unavailable for this turn, fall back to a\nsingle JSON code block: `{\"name\": \"tool_name\", \"arguments\": {\"key\": \"value\"}}` — use this fallback format\nonly, never both. Only call a tool whose exact name appears in the tool index above and is currently callable\n(or reachable via `capability_expand`). Never invent, rename, alias, or guess a tool name, platform ID, or\nplatform action from argument shape or wording — if the index does not list it, it does not exist.\n`platform_connect.action` (list/guide/preflight/connect/disconnect/remove/status/events_start/events_stop/\nevents_status) is the App Connector management namespace; `platform_action.action` only accepts exact\nregistered business actions such as `im.send_message` shown in the App Connector Capability Index — never mix\nthe two namespaces. If a tool call returns an unknown/unavailable result, use the returned suggestions or\navailable names for a single retry instead of trying further variations of the same guess.\n\n**Side-effect action rule** (`platform_action` with effect=send/write/execute):\n- Call each unique action+payload **exactly once**. Never include duplicates in the same turn.\n- Once the result returns `\"completed\": true`, that action is DONE for this task. Do NOT call it again\n in any subsequent turn — immediately summarize the result for the user instead.\n- The system enforces idempotency: duplicate calls are blocked and will not execute.\n- If the user explicitly requests sending/writing multiple times, use distinct payloads per call.\n\n**Resource identifier provenance rule**:\n- NEVER fabricate, guess, or infer resource identifiers (chat_id, message_id, file_key, user_id, etc.).\n Every resource ID used in a side-effect action MUST come from a successful API response in this session.\n- If a read/list action fails (e.g. authorization error), you do NOT have valid resource IDs.\n Report the failure to the user — do NOT attempt the dependent write/send action with a guessed ID.\n- When a tool result contains `\"llm_instruction\"`, follow it exactly.\n\n## Guidelines\n1. **Direct answers first**: If you already know the answer, respond directly without tools.\n2. **Avoid redundant tool calls**: Do not call the same tool with the same arguments more than once in the same user turn. When an existing tool result already answers the user's request, stop calling tools and answer directly.\n3. **Use tools proactively**: When the user asks about files, time, system state, or needs actions performed, use the appropriate tool.\n4. **Chain tools when needed**: You can call multiple tools in sequence (e.g., list files → read file → summarize).\n5. **Handle failures gracefully**: If a tool fails, explain what went wrong and suggest alternatives. If it failed because of an unknown tool/platform/action name, retry once with an exact name from the returned suggestions, then explain rather than keep guessing.\n6. **Summarize results naturally**: After tool execution, synthesize the results into a helpful answer rather than dumping raw output.\n7. **Stay conversational**: Maintain a natural, helpful tone. Acknowledge context from earlier in the conversation.\n8. **Recall past work**:\n - Broad queries (\"之前做了什么\", \"列出任务\"): answer from the \"Recent Task History\" section already in your context. If insufficient, call session_list.\n - Specific lookups (\"上次那个配置怎么改的\"): call session_search with relevant phrases (NOT single characters).\n - Do NOT call search tools repeatedly with keyword fragments. One well-phrased call is better than ten fragmented ones.\n\n## Coding & Verification\nWhen working with code, prefer the precise built-in tools over ad-hoc shell: use `repo_map` to orient in an\nunfamiliar project, `code_search`/`file_find` to locate and `code_intel` for symbols, `edit_file` (anchored\nsearch-replace, or a unified `diff`) to change files — never rewrite a whole file to change a few lines — and\n`git_query` to inspect diffs/log. After edits, check `syntax_ok` in the result and run `test_run`/`lint_check`\nbefore declaring the work done. Batch independent read-only calls (search/read/query) into a single turn — they\nrun in parallel.\n\n## Presentation Style\n1. **Polished Markdown only**: Format user-facing answers with clean Markdown headings, short paragraphs, and concise bullets. Use tables only when they improve comparison or scanning.\n2. **Terminal-friendly layout**: Keep lines readable in a TUI; avoid dense walls of text, deeply nested lists, oversized ASCII art, or heavy visual blocks.\n3. **Elegant emphasis**: Use bold text sparingly for key terms and conclusions. Avoid excessive emojis, decorative symbols, repeated separators, or visual noise.\n4. **Theme-safe colors**: Do not emit ANSI escape codes, HTML color tags, Rich markup, or hardcoded color names. Rely on the TUI theme to render Markdown professionally.\n5. **No leaked tool protocol**: Never show tool-call JSON, internal schemas, raw observations, tool result payloads, or hidden reasoning in the final answer unless the user explicitly asks for raw/debug output. Treat any prior `{\"name\": ..., \"arguments\": ...}` blocks and `Tool result (...)` messages as internal execution context only.\n6. **Professional closure**: End with a concise conclusion or next step when helpful; avoid rambling after the useful answer is complete.\n\nWhen finished with all tool calls, respond normally without a JSON block, tool-call transcript, or process log.\n\n## Task Contract\n- Task ID: turn-1\n- Original user request: List the connected hardware devices, then describe bench_r8 in full.\n- Workspace root: \n- Allowed roots: \n- Treat relative project paths as relative to the workspace root; never infer `.` as the project root when a workspace root is provided.\n- Workspace boundary is enforced by tools: do not read, search, edit, or run commands against paths outside the allowed roots unless the user explicitly requests an external path and the tool/approval policy permits it.\n- LeapFlow workspace config is optional at `/.leapflow/config.yaml`; runtime config is loaded from `~/.leapflow/config/user.yaml` and `~/.leapflow/profiles//config/*.yaml`.\n- Preserve this task contract across summarization, compression, tool loops, and memory retrieval.\n" + }, + { + "role": "system", + "content": "\n## Recent Session Summary\n- [user] List the connected hardware devices, then describe bench_r8 in full.\n" + }, + { + "role": "user", + "content": "List the connected hardware devices, then describe bench_r8 in full.\nList the connected hardware devices, then describe bench_r8 in full." + }, + { + "role": "assistant", + "content": "", + "tool_calls": [ + "hw_list" + ] + }, + { + "role": "tool", + "content": "{\"ok\": true, \"devices\": [{\"device_id\": \"bench_r8\", \"display_name\": \"R8 Simulated Bench\", \"location\": \"journey-lab\", \"channels\": 3, \"writable\": 2, \"streaming\": 1, \"quantities\": [\"state.homed\", \"temperature\"], \"verified\": true, \"halt_supported\": true}], \"count\": 1, \"hint\": \"Call hw_describe(device_id) for channel limits before commanding a device.\", \"execution_id\": \"\", \"idempotency_key\": \"\", \"execution_policy\": \"read_only\", \"tool_call_id\": \"call_1\", \"execution_status\": \"completed\"}", + "tool_result": true + } + ], + "tools": [ + "capability_expand", + "code_intel", + "code_search", + "config_get", + "config_list", + "env_info", + "file_find", + "file_list", + "file_read", + "git_query", + "hw_describe", + "hw_list", + "hw_read", + "hw_status", + "lint_check", + "memory_search", + "plugin_list", + "plugin_propose", + "plugin_status", + "plugin_versions", + "repo_map", + "research_note", + "runtime_snapshot", + "schedule_reentry", + "self_describe", + "session_list", + "skill_view", + "skills_list", + "terminal_list", + "terminal_read", + "test_run", + "text_search", + "time_get", + "tool_describe", + "tool_search", + "web_fetch" + ] + }, + "responses": [ + { + "status": 200, + "content_type": "application/json", + "body": "{\"id\": \"chatcmpl-cassette\", \"object\": \"chat.completion\", \"model\": \"cassette-model\", \"choices\": [{\"index\": 0, \"message\": {\"role\": \"assistant\", \"content\": \"\", \"tool_calls\": [{\"id\": \"call_1\", \"type\": \"function\", \"function\": {\"name\": \"hw_describe\", \"arguments\": \"{\\\"device_id\\\": \\\"bench_r8\\\"}\"}}]}, \"finish_reason\": \"tool_calls\"}], \"usage\": {\"prompt_tokens\": 64, \"completion_tokens\": 16, \"total_tokens\": 80}}" + } + ] +} diff --git a/tests/_fixtures/cassettes/r8_hardware/cassette-model-0c0469d68ba791cb.cassette.json b/tests/_fixtures/cassettes/r8_hardware/cassette-model-0c0469d68ba791cb.cassette.json deleted file mode 100644 index 6874845a..00000000 --- a/tests/_fixtures/cassettes/r8_hardware/cassette-model-0c0469d68ba791cb.cassette.json +++ /dev/null @@ -1,87 +0,0 @@ -{ - "fingerprint": "0c0469d68ba791cbad8f2b7ba5c12abf519af745b4f5256f162c735a6d245ce6", - "note": "captured in seed mode", - "request": { - "model": "cassette-model", - "stream": false, - "messages": [ - { - "role": "system", - "content": "You are LeapFlow, an intelligent assistant that can both converse naturally and take real actions on the user's computer.\n\n## Capabilities\nThe tool index below lists **every** registered tool by name and a one-line summary — this is the complete\ncapability contract; nothing else exists. Only a subset is directly callable this turn (via native tool calling,\nnot a JSON block in your reply). If you need a tool from the index that is not yet callable, call\n`capability_expand` with its category name first — the matching tools become callable immediately after.\nWhen the user asks what LeapFlow itself supports, whether it supports plugins, or which runtime capabilities\nare available, use the live capability evidence exposed by `plugin_list` before making capability claims; report\nconfiguration-dependent or unavailable capabilities as limitations instead of inferring from documentation.\n- **text_search**(text, pattern): Search for a regex pattern in text.\n- **text_replace**(text, old, new, count) [capability_expand category: write]: Replace occurrences of a substring in text.\n- **time_get**(): Get current date and time.\n- **env_info**(): Get system environment information (OS, Python version, cwd).\n- **skills_list**(query, category, source): List available learned skills. Use when user asks about capabilities or you need a specific skill.\n- **skill_view**(name): View the full content of a specific skill document.\n- **code_intel**(path, operation): Precise document symbols (outline) for a source file: classes, functions, and methods with line ranges. Python uses an exact AST parse; other languages use a keyword-prefix scan. Prefer over file_read mode=symbols for accurate navigation before editing. Read-only.\n- **repo_map**(path): Compact project orientation for a repository root: languages, detected test/lint commands, top-level structure, entry points, manifest, and VCS branch. Call this first when entering an unfamiliar codebase. Read-only.\n- **scm_sync**(action, cwd, remote, pull_ref, push_ref, timeout) [capability_expand category: scm]: Run a typed git SCM action. Use this instead of shell_run for git pull/push/status. For 'pull origin main then push', set action='pull_then_push', remote='origin', pull_ref='main', and omit push_ref so LeapFlow pushes the current local branch.\n- **git_query**(action, cwd, ref, path, staged, max_count, stat): Read-only structured git inspection: action=diff|log|status|branch|show. Prefer over shell_run for reading repo state — output is clipped, redacted, and log/branch are parsed into structured fields. Use scm_sync for pull/push.\n- **git_write**(action, cwd, message, stage_all, name, ref, create) [capability_expand category: scm]: Mutating git actions: action=commit (message, stage_all), branch (create+switch), checkout (switch; create=true for -b). Approval-gated. Use scm_sync for pull/push and git_query for reads.\n- **test_run**(command, cwd, timeout): Run the project's test suite and return structured results (framework, passed/failed counts, failing tests). Auto-detects the runner (pytest/npm/go/cargo) or uses a configured/explicit command; executes via the governed shell. ok=true means the runner executed — see 'success' for pass/fail.\n- **lint_check**(command, cwd, timeout): Run the project's linter and return a structured clean/issue result. Auto-detects the linter (ruff/eslint/go vet/clippy) or uses a configured/explicit command; executes via the governed shell. ok=true means the linter ran — see 'clean'.\n- **file_list**(path, pattern, depth): List files and directories at a given path. Use depth=1 or depth=2 to get a recursive tree in one call instead of listing each sub-directory separately.\n- **file_read**(path, max_lines, start_line, max_chars, mode): Read text file content with adaptive context governance. For large or unfamiliar files, prefer mode='outline' or mode='symbols' first, then use mode='raw' with start_line/max_lines for the specific range you actually need. For LeapFlow's own settings, use config_list / config_get / config_set — its config files are outside the workspace and not readable here.\n- **file_write**(path, content, mode) [capability_expand category: write]: Write content to a file (overwrite or append).\n- **code_search**(pattern, patterns, path, glob, ignore_case, multiline, max_results, context_lines): Search file CONTENTS by regex pattern across a directory tree (ripgrep-backed). Requires a regex pattern. NOT for listing or browsing directory contents — use file_list for that. Prefer this over shell_run grep: faster, skips VCS/dependency/build dirs, and returns structured path:line:column matches. Batch related lookups into ONE call via `patterns` (OR-combined, single pass) instead of issuing several separate searches. Use file_read for the surrounding context of a hit.\n- **file_find**(glob, path, max_results): Find files by a recursive glob pattern under a base path (e.g. '**/test_*.py' or '*.md'). Prefer this over shell_run find; skips VCS/dependency/build dirs.\n- **edit_file**(path, edits, dry_run, diff) [capability_expand category: write]: Apply targeted, anchored search-replace edits to an EXISTING text file (use file_write to create/overwrite). Each edit is {original_text, new_text, replace_all?}; original_text must match exactly and uniquely (or set replace_all) — a non-unique or missing anchor is rejected so files are never corrupted. Set dry_run to preview. Alternatively pass a unified 'diff' to apply its hunks as anchored edits. Far cheaper and safer than rewriting a whole file.\n- **shell_run**(command, cwd, timeout) [capability_expand category: shell]: Execute a one-shot shell command with timeout protection. Runs in the active workspace; paths resolving outside it are refused. Reach for a structured tool first when one fits — web_fetch for anything over HTTP(S), git_query/scm_sync for git, code_search/file_find/file_read for the repo, config_get/config_set for LeapFlow's own settings — because those report typed results, while a failed shell command can only be diagnosed from its exit code and stderr. Every shell run counts as an external side effect, so a failure stops the rest of the batch and is not retried automatically.\n- **terminal_open**(command, cwd, shell) [capability_expand category: terminal]: Open a PERSISTENT shell session (REPL/dev server/watch), returning a session_id for terminal_send/read/close. Disabled unless tools.terminal_session_enabled is set. For one-shot commands use shell_run instead.\n- **terminal_send**(session_id, input, wait) [capability_expand category: terminal]: Send a line of input to a persistent terminal session and return output captured shortly after.\n- **terminal_read**(session_id, wait): Drain buffered output from a persistent terminal session (optionally waiting briefly first).\n- **terminal_close**(session_id) [capability_expand category: terminal]: Terminate a persistent terminal session and release its process group.\n- **terminal_list**(): List active persistent terminal sessions.\n- **config_list**(category, limit): List LeapFlow's own writable settings (LLM model and endpoint, daemon, memory, perception, gateway, …) with current values. Use this to discover the exact key before changing anything. Optionally narrow by `category`. This is the only correct way to inspect LeapFlow configuration — never read config files from disk.\n- **config_get**(key): Read one LeapFlow setting by key (e.g. 'llm.model', 'llm.base_url', 'daemon.log_level'), returning its current value, type, scopes, and whether a change needs a daemon restart. There is no 'llm.provider' key: provider behavior is inferred from the OpenAI-compatible 'llm.base_url'. Never read LeapFlow config files from disk — use this.\n- **config_set**(key, value, scope) [capability_expand category: config]: Change one LeapFlow setting by key, e.g. switch an OpenAI-compatible LLM with key='llm.model' and key='llm.base_url'. There is no 'llm.provider' key: provider behavior is inferred from the endpoint. Values are validated and coerced; credentials are stored in the vault automatically. Call config_list or config_get first if unsure of the key. The result states whether a `leap daemon restart` is required. Never edit LeapFlow config files directly.\n- **web_fetch**(url, select, timeout, max_bytes): Read a URL over HTTP(S) and get back extracted, context-sized content: parsed JSON for API endpoints, readable text plus links for web pages. Use this for anything on the internet — prices, docs, releases, articles — instead of running curl through shell_run: it reports real HTTP status codes, retries rate limits on its own, and is a plain read so a retry is always safe. For JSON APIs pass `select` with a dotted path (e.g. 'chart.result.0.meta') to return just that part instead of the whole payload.\n- **memory_search**(query, limit): Search agent memory for relevant past experiences, observations, and facts.\n- **memory_add**(content, kind) [capability_expand category: write]: Store a new observation or insight in memory for future reference.\n- **research_note**(kind, text): Record a compact, structured note about the current task's state so it survives context compression on long / multi-step tasks. Use for durable findings, open questions still to resolve, decisions / excluded paths, and the immediate next step. One concise sentence per note.\n- **capability_expand**(category): Fetch the full callable schema for every tool in a capability category (e.g. 'hub', 'gateway', 'desktop', 'delegate', 'file', 'memory', 'skill'). The compact tool index always lists every registered tool by name and a one-line summary, but only a static low-risk subset is directly callable each turn. If you need a tool from the index that is not yet callable, call capability_expand with its category first; the matching tools become callable in this turn. Never invent a tool name — expand the category instead.\n- **delegate_task**(goal, context) [capability_expand category: delegate]: Delegate a complex sub-task to an isolated subagent. The subagent gets a fresh context and restricted tool access. Use when a task is self-contained and can be solved independently.\n- **schedule_reentry**(kind, reason, delay_seconds, event_match, max_reentries, deadline_seconds): Register a re-entry so this task can resume later from its current orientation (findings / open questions / next step). Use when work must pause and continue after a delay (kind=time) or when a matching platform event arrives (kind=event), instead of finishing now. The research-ledger state is carried over automatically.\n- **hub_push**(skill_name, visibility, version) [capability_expand category: hub]: Push a local skill to the ModelScope Hub for sharing or backup.\n- **hub_pull**(repo_id, version) [capability_expand category: hub]: Pull a skill from the ModelScope Hub to install locally.\n- **hub_search**(query) [capability_expand category: hub]: Search for skills on the Hub by keyword or description.\n- **hub_sync**(mode, dry_run) [capability_expand category: hub]: Preview or execute sync between local skills and Hub.\n- **platform_action**(platform, action, payload, backend_kind) [capability_expand category: gateway]: Execute an exact registered business action on an external platform through LeapFlow's App Connector layer. Actions must be copied from the App Connector Capability Index and are addressed as domain.operation, e.g. im.send_message or docs.create_markdown. All business fields (chat_id, text, query, etc.) MUST be placed inside `payload`, never at the top level. Example: {\"platform\":\"feishu\",\"action\":\"im.send_message\",\"payload\":{\"chat_id\":\"oc_xxx\",\"text\":\"hello\"}}. Do not invent action names, do not use management actions such as list/guide/connect/status here.\n- **platform_connect**(action, platform, credentials, options, checkpoint) [capability_expand category: gateway]: List, guide, connect, disconnect, remove, or check status for external platforms using the App Connector management namespace. Supports REST and CLI backends. Use this for management actions such as list/guide/preflight/connect/status; use platform_action only for exact registered business actions.\n- **gateway_send**(platform, chat_id, text, thread_id) [capability_expand category: gateway]: Send a message to a connected external platform (Feishu group, Telegram chat, DingTalk conversation, etc.). Requires the platform to be connected via gateway_connect first. Use gateway_connect with action='list' to see connected platforms and available chat IDs.\n- **gateway_connect**(action, platform, credentials, options) [capability_expand category: gateway]: Connect, configure, or manage external platform integrations (Feishu, DingTalk, Telegram, Slack, Discord, etc.). Conversational flow: 1) call 'guide' to get setup steps + required fields, 2) present the steps to the user and ask for ALL required credentials in a single message, 3) call 'connect' with the credentials. Goal: complete in 1–2 user turns. NEVER include credential values in your text response.\n- **plugin_list**(): List the live plugin registry and cross-subsystem capability evidence. Use this before answering questions about whether LeapFlow supports plugins, self-evolution, plugin installation, hot reload, versioning, or other runtime capabilities.\n- **plugin_status**(plugin_id): Get detailed status of a specific plugin: its declared category, runtime dependencies, contributed tools, and fiber lifecycle state.\n- **plugin_versions**(plugin_id): List recorded source versions and active pointer for a profile-scoped plugin.\n- **plugin_propose**(requested_capability, plugin_id, proposed_tools, test_cases, risk_level, evidence): Create a side-effect-free PluginProposal from explicit capability-gap evidence. Use this before plugin_generate when a missing capability should be reviewed. Does not call an LLM, write files, or install anything.\n- **assess_compatibility**(manifest, source_path) [capability_expand category: plugin_management]: Assess whether a foreign plugin manifest or real DSH source bundle is compatible with LeapFlow. A source bundle assessment is static: runtime_ready stays false until restricted Node discovery during plugin_install. Returns component-level verdicts and limitations.\n- **plugin_generate**(plugin_id, description, proposal_id) [capability_expand category: system]: Generate a new ToolPlugin from a natural-language capability description. The LLM produces code that conforms to the ToolPlugin Protocol; it is then rigorously validated (syntax, structure, import, protocol conformance). The generated source is stored in CAS and receives explicit content approval. It DOES NOT install the plugin — installation requires a second, mutation-specific approval via plugin_install.\n- **plugin_install**(plugin_id, code, marketplace_name, source_path, proposal_id, version_label) [capability_expand category: system]: Install a plugin from exactly one source: validated Python code, the configured Python marketplace, or a real DSH source bundle. DSH bundles run restricted Node discovery before registration; only public host tools are installed and client UI limitations are reported. Writes profile-scoped state and mutates the process-global registry. REQUIRES APPROVAL.\n- **plugin_rollback**(plugin_id, version) [capability_expand category: system]: Rollback a profile-scoped plugin to a recorded source version and reload it. REQUIRES APPROVAL.\n- **plugin_reload**(plugin_id, version_label) [capability_expand category: system]: Hot-reload a plugin at runtime. Disposes the old plugin fiber, re-imports its module, and registers a fresh instance. Existing in-flight turns are unaffected (snapshot isolation). REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_disable**(plugin_id) [capability_expand category: system]: Disable a plugin by disposing its fiber, removing its tools from the runtime registry. Cannot disable self_management itself. REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_remove**(plugin_id, delete_source) [capability_expand category: system]: Terminally remove a plugin: dispose its fiber, unregister its tools, remove reload metadata, and optionally delete its profile-scoped source file. Cannot remove self_management itself. REQUIRES APPROVAL.\n- **plugin_unquarantine**(plugin_id) [capability_expand category: plugin_management]: Restore a quarantined plugin to probation status for re-evaluation. Unfreezes the trust ledger, transitions the proposal back to PROBATION, and reloads the plugin. REQUIRES APPROVAL.\n- **plugin_enable**(plugin_id) [capability_expand category: system]: Re-enable a previously disabled plugin by reloading its module and registering a fresh instance. REQUIRES APPROVAL.\n- **self_describe**(facet): Introspect LeapFlow's own identity, capabilities, runtime state, evolution metrics, or platform connections. Use facet='all' only when a comprehensive self-check is explicitly requested.\n- **runtime_snapshot**(): Lightweight (~150 token) flat snapshot of current runtime state: model, context budget, posture, disclosure level, turn count, cache hit rate, uptime, tool count, and pending approvals.\n- **schedule_create**(trigger_expression, instruction, execution_mode, max_retries, delivery_target) [capability_expand category: scheduler]: Create a new scheduled task. Specify a trigger expression (e.g. '30m', 'every 2h', '0 9 * * *') and the instruction to execute on each trigger. Optionally provide an execution mode and a delivery target to receive notifications.\n- **schedule_list**() [capability_expand category: scheduler]: List all scheduled tasks with their current state, trigger, and next due time.\n- **schedule_status**(task_id) [capability_expand category: scheduler]: Get detailed status and recent execution history for a scheduled task.\n- **schedule_pause**(task_id) [capability_expand category: scheduler]: Pause a scheduled task so it stops firing without being cancelled.\n- **schedule_resume**(task_id) [capability_expand category: scheduler]: Resume a paused scheduled task — re-arms it and recalculates next due time.\n- **schedule_cancel**(task_id) [capability_expand category: scheduler]: Cancel a scheduled task permanently.\n- **tool_search**(query, max_results): Search all registered tools by keyword relevance. Returns a ranked list of matching tools with name, category, and summary. Use this when you need a tool but are unsure of its exact name or category.\n- **tool_describe**(tool_name): Get the full callable schema of a single tool by exact name. Returns the tool's description, parameters, and metadata. Use after tool_search to inspect a specific tool before calling it.\n- **hw_list**(): List connected hardware devices with their channel counts and measured quantities. Start here; it does not include operating limits.\n- **hw_describe**(device_id): Return the full reference for one device: every channel, its unit, its operating envelope, rate limit, reversibility, and required interlocks, plus any prior outcomes recorded for its writable channels. Required before commanding a device.\n- **hw_read**(device_id, channel_id): Read the current value of one device channel. Has no physical effect.\n- **hw_status**(device_id): Report connection health, halt capability, and recent notable events (threshold excursions, lost samples, stalled channels) for one device.\n- **hw_configure**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Set a configuration value or setpoint on a channel declaring effect=configure. Setpoints often have inertia: the value may need time to stabilise.\n- **hw_actuate**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Command motion or output on a channel declaring effect=actuate. This moves physical hardware; a repeat from an unknown state is not a safe retry.\n- **hw_dispense**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Consume an irreversible resource on a channel declaring effect=dispense. Running this twice dispenses twice; never repeat it after a failure without first verifying what already happened.\n- **hw_estop**(device_id) [capability_expand category: hardware]: Stop all motion and output on a device immediately. Never requires approval; use it whenever device behaviour is unexpected.\n- **session_search**(query, limit) [capability_expand category: unclassified]: Search past conversation sessions in the current workspace for relevant context.\n- **session_list**(limit): List recent conversation sessions in the current workspace with stable ids, titles, dates, and summaries. Use for browsing past tasks or when user asks to see history without specific search terms. No keywords needed — returns chronological list.\n- **session_detail**(session_id, limit, offset, include_inactive) [capability_expand category: unclassified]: Read a paginated persisted transcript for one past conversation session in the current workspace. Use after session_list or session_search returns a session_id.\n- **click**(element_index) [capability_expand category: desktop]: Click a UI element by its element_index (from the latest observe_ui snapshot)\n- **get_clipboard**() [capability_expand category: desktop]: Read current clipboard text content\n- **list_apps**(filter, running_only) [capability_expand category: desktop]: List available applications on this system. Use to discover correct bundle_id before switch_app.\n- **list_windows**() [capability_expand category: desktop]: List all top-level windows with pid, window_id, title, and per-window state (minimized, on-screen). Call this first to pick the pid and window_id that observe_ui and other window tools require.\n- **observe_ui**(pid, window_id, query) [capability_expand category: desktop]: Snapshot one window's actionable UI elements, each tagged with an element_index for click/right_click/read_text. Re-observe after actions — indices belong to one snapshot. Requires the window's pid and window_id from list_windows.\n- **open_url**(url, app_id) [capability_expand category: desktop]: Open a URL in the default or specified browser\n- **read_text**(element_index) [capability_expand category: desktop]: Read the text content of a specific UI element from the latest snapshot\n- **right_click**(element_index) [capability_expand category: desktop]: Right-click a UI element to open its context menu. Returns visible menu items.\n- **screenshot**(pid, window_id) [capability_expand category: desktop]: Capture a screenshot for visual verification. With pid + window_id captures that window (works across all displays); defaults to the last observed window, or the full desktop when no window has been observed.\n- **scroll**(element_index, direction, amount, pid, window_id) [capability_expand category: desktop]: Scroll a scrollable area of a window. Omit element_index to scroll the window's focused/page scroller; pass one to scroll an exact element from the latest snapshot.\n- **select_text**(element_index) [capability_expand category: desktop]: Select all text in a UI element (focus + select-all, for subsequent copy)\n- **set_clipboard**(text) [capability_expand category: desktop]: Write text to the clipboard\n- **shortcut**(keys) [capability_expand category: desktop]: Execute a keyboard shortcut\n- **switch_app**(app_id) [capability_expand category: desktop]: Switch to an app (launch if needed, activate, verify)\n- **type_text**(text) [capability_expand category: desktop]: Type text into the currently focused element\n- **wait**(seconds) [capability_expand category: desktop]: Wait for a specified duration before continuing\n- **wait_until**(condition, pid, window_id, timeout, poll_interval) [capability_expand category: desktop]: Wait until a UI condition is met (polls UI tree). Returns elements when found or on timeout.\n- **wait_until_stable**(timeout, poll_interval, pid, window_id) [capability_expand category: desktop]: Wait until the UI stops changing (element set stabilizes across polls).\n\n## App Connector Capability Index\nLeapFlow can onboard and manage external apps through `platform_connect` and execute exact registered business actions through `platform_action`.\nFor requests about connecting, setting up, configuring, enabling, or managing a supported app, use `platform_connect` first instead of generating SDK/Webhook sample code.\n`platform_connect.action` is the management namespace: list, guide, preflight, connect, disconnect, remove, status, events_start, events_stop, events_status.\n`platform_action.action` is only for exact registered platform business actions listed below, such as `im.send_message`; never use management actions like `list` or `guide` there.\nAll business fields MUST go inside `payload`; top-level keys are only `platform`, `action`, and `payload`.\nDo not invent platform IDs or platform action names. If the needed action is not listed, ask for discovery/clarification instead of guessing.\nUse `platform_connect` with `action='guide'` and the matching `platform` to start onboarding; use `action='list'` when the app is unclear.\nWhen a pending onboarding state is present, continue from that state with `platform_connect` instead of asking the user to restate the app.\nSupported apps:\n- `api_server`: API Server (OpenAI Compatible) (category=api; backend=adapter; platform_action actions=none registered)\n- `dingtalk`: 钉钉 (DingTalk) (category=im; backend=adapter; platform_action actions=none registered)\n- `feishu`: 飞书 (Feishu/Lark) (category=im; backend=cli)\n - `calendar.create_event` payload={summary*, start_time*, end_time*, attendees} [write/high]\n - `docs.create_markdown` payload={title*, markdown*, folder_token} [write/medium]\n - `drive.search` payload={query*, limit} [read/medium]\n - `im.add_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.download_resource` payload={message_id*, file_key*, type*} [read/low]\n - `im.get_messages` payload={message_ids*} [read/low]\n - `im.list_chats` payload={page_size} [read/low]\n - `im.list_messages` payload={chat_id*} [read/low]\n - `im.list_thread_messages` payload={thread*} [read/low]\n - `im.remove_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.reply_message` payload={message_id*, text*} [send/high]\n - `im.search_chats` payload={query*, page_size} [read/low]\n - `im.search_messages` payload={query*} [read/medium]\n - `im.send_message` payload={chat_id*, text*, thread_id} [send/high]\n - `im.update_card` payload={token*, card*} [write/medium]\n - `im.update_message` payload={message_id*, text*} [write/medium]\n - `mail.search_unread` payload={query, limit} [read/high]\n - `sheets.append_row` payload={spreadsheet_token*, sheet_id*, values*} [write/medium]\n - `task.create` payload={title*, description, due_time} [write/medium]\n- `telegram`: Telegram (category=social; backend=adapter; platform_action actions=none registered)\n- `webhook`: Webhook (Generic) (category=webhook; backend=adapter; platform_action actions=none registered)\n\n## Tool Usage\nTools are normally invoked through the native function-calling mechanism, not by writing JSON in your reply\ntext. Only if the provider signals that native function calling is unavailable for this turn, fall back to a\nsingle JSON code block: `{\"name\": \"tool_name\", \"arguments\": {\"key\": \"value\"}}` — use this fallback format\nonly, never both. Only call a tool whose exact name appears in the tool index above and is currently callable\n(or reachable via `capability_expand`). Never invent, rename, alias, or guess a tool name, platform ID, or\nplatform action from argument shape or wording — if the index does not list it, it does not exist.\n`platform_connect.action` (list/guide/preflight/connect/disconnect/remove/status/events_start/events_stop/\nevents_status) is the App Connector management namespace; `platform_action.action` only accepts exact\nregistered business actions such as `im.send_message` shown in the App Connector Capability Index — never mix\nthe two namespaces. If a tool call returns an unknown/unavailable result, use the returned suggestions or\navailable names for a single retry instead of trying further variations of the same guess.\n\n**Side-effect action rule** (`platform_action` with effect=send/write/execute):\n- Call each unique action+payload **exactly once**. Never include duplicates in the same turn.\n- Once the result returns `\"completed\": true`, that action is DONE for this task. Do NOT call it again\n in any subsequent turn — immediately summarize the result for the user instead.\n- The system enforces idempotency: duplicate calls are blocked and will not execute.\n- If the user explicitly requests sending/writing multiple times, use distinct payloads per call.\n\n**Resource identifier provenance rule**:\n- NEVER fabricate, guess, or infer resource identifiers (chat_id, message_id, file_key, user_id, etc.).\n Every resource ID used in a side-effect action MUST come from a successful API response in this session.\n- If a read/list action fails (e.g. authorization error), you do NOT have valid resource IDs.\n Report the failure to the user — do NOT attempt the dependent write/send action with a guessed ID.\n- When a tool result contains `\"llm_instruction\"`, follow it exactly.\n\n## Guidelines\n1. **Direct answers first**: If you already know the answer, respond directly without tools.\n2. **Avoid redundant tool calls**: Do not call the same tool with the same arguments more than once in the same user turn. When an existing tool result already answers the user's request, stop calling tools and answer directly.\n3. **Use tools proactively**: When the user asks about files, time, system state, or needs actions performed, use the appropriate tool.\n4. **Chain tools when needed**: You can call multiple tools in sequence (e.g., list files → read file → summarize).\n5. **Handle failures gracefully**: If a tool fails, explain what went wrong and suggest alternatives. If it failed because of an unknown tool/platform/action name, retry once with an exact name from the returned suggestions, then explain rather than keep guessing.\n6. **Summarize results naturally**: After tool execution, synthesize the results into a helpful answer rather than dumping raw output.\n7. **Stay conversational**: Maintain a natural, helpful tone. Acknowledge context from earlier in the conversation.\n8. **Recall past work**:\n - Broad queries (\"之前做了什么\", \"列出任务\"): answer from the \"Recent Task History\" section already in your context. If insufficient, call session_list.\n - Specific lookups (\"上次那个配置怎么改的\"): call session_search with relevant phrases (NOT single characters).\n - Do NOT call search tools repeatedly with keyword fragments. One well-phrased call is better than ten fragmented ones.\n\n## Coding & Verification\nWhen working with code, prefer the precise built-in tools over ad-hoc shell: use `repo_map` to orient in an\nunfamiliar project, `code_search`/`file_find` to locate and `code_intel` for symbols, `edit_file` (anchored\nsearch-replace, or a unified `diff`) to change files — never rewrite a whole file to change a few lines — and\n`git_query` to inspect diffs/log. After edits, check `syntax_ok` in the result and run `test_run`/`lint_check`\nbefore declaring the work done. Batch independent read-only calls (search/read/query) into a single turn — they\nrun in parallel.\n\n## Presentation Style\n1. **Polished Markdown only**: Format user-facing answers with clean Markdown headings, short paragraphs, and concise bullets. Use tables only when they improve comparison or scanning.\n2. **Terminal-friendly layout**: Keep lines readable in a TUI; avoid dense walls of text, deeply nested lists, oversized ASCII art, or heavy visual blocks.\n3. **Elegant emphasis**: Use bold text sparingly for key terms and conclusions. Avoid excessive emojis, decorative symbols, repeated separators, or visual noise.\n4. **Theme-safe colors**: Do not emit ANSI escape codes, HTML color tags, Rich markup, or hardcoded color names. Rely on the TUI theme to render Markdown professionally.\n5. **No leaked tool protocol**: Never show tool-call JSON, internal schemas, raw observations, tool result payloads, or hidden reasoning in the final answer unless the user explicitly asks for raw/debug output. Treat any prior `{\"name\": ..., \"arguments\": ...}` blocks and `Tool result (...)` messages as internal execution context only.\n6. **Professional closure**: End with a concise conclusion or next step when helpful; avoid rambling after the useful answer is complete.\n\nWhen finished with all tool calls, respond normally without a JSON block, tool-call transcript, or process log.\n\n## Task Contract\n- Task ID: turn-2\n- Original user request: Read the current value of the setpoint channel on bench_r8.\n- Workspace root: \n- Allowed roots: \n- Treat relative project paths as relative to the workspace root; never infer `.` as the project root when a workspace root is provided.\n- Workspace boundary is enforced by tools: do not read, search, edit, or run commands against paths outside the allowed roots unless the user explicitly requests an external path and the tool/approval policy permits it.\n- LeapFlow workspace config is optional at `/.leapflow/config.yaml`; runtime config is loaded from `~/.leapflow/config/user.yaml` and `~/.leapflow/profiles//config/*.yaml`.\n- Preserve this task contract across summarization, compression, tool loops, and memory retrieval.\n" - }, - { - "role": "user", - "content": "List the connected hardware devices, then describe bench_r8 in full." - }, - { - "role": "assistant", - "content": "[Called: hw_list]\n[Called: hw_describe]\nDiscovered the simulated bench and read its channel limits." - }, - { - "role": "user", - "content": "Read the current value of the setpoint channel on bench_r8.\nRead the current value of the setpoint channel on bench_r8." - }, - { - "role": "assistant", - "content": "", - "tool_calls": [ - "hw_read" - ] - }, - { - "role": "tool", - "content": "{\"ok\": true, \"reading\": {\"device_id\": \"bench_r8\", \"channel_id\": \"setpoint\", \"value\": 50.0, \"quantity\": \"temperature\", \"unit\": \"C\", \"observed_at\": , \"sequence\": 1, \"quality\": \"ok\"}, \"execution_id\": \"\", \"idempotency_key\": \"\", \"execution_policy\": \"read_only\", \"tool_call_id\": \"call_1\", \"execution_status\": \"completed\"}", - "tool_result": true - } - ], - "tools": [ - "capability_expand", - "code_intel", - "code_search", - "config_get", - "config_list", - "env_info", - "file_find", - "file_list", - "file_read", - "git_query", - "hw_actuate", - "hw_configure", - "hw_describe", - "hw_dispense", - "hw_estop", - "hw_list", - "hw_read", - "hw_status", - "lint_check", - "memory_search", - "plugin_list", - "plugin_propose", - "plugin_status", - "plugin_versions", - "repo_map", - "research_note", - "runtime_snapshot", - "schedule_reentry", - "self_describe", - "session_list", - "skill_view", - "skills_list", - "terminal_list", - "terminal_read", - "test_run", - "text_search", - "time_get", - "tool_describe", - "tool_search", - "web_fetch" - ] - }, - "responses": [ - { - "status": 200, - "content_type": "application/json", - "body": "{\"id\": \"chatcmpl-cassette\", \"object\": \"chat.completion\", \"model\": \"cassette-model\", \"choices\": [{\"index\": 0, \"message\": {\"role\": \"assistant\", \"content\": \"Read the setpoint channel.\"}, \"finish_reason\": \"stop\"}], \"usage\": {\"prompt_tokens\": 64, \"completion_tokens\": 16, \"total_tokens\": 80}}" - } - ] -} diff --git a/tests/_fixtures/cassettes/r8_hardware/cassette-model-2c150449ffad6f0c.cassette.json b/tests/_fixtures/cassettes/r8_hardware/cassette-model-2c150449ffad6f0c.cassette.json new file mode 100644 index 00000000..89c70f14 --- /dev/null +++ b/tests/_fixtures/cassettes/r8_hardware/cassette-model-2c150449ffad6f0c.cassette.json @@ -0,0 +1,76 @@ +{ + "fingerprint": "2c150449ffad6f0cd0deaab1cd48e0151101fac732f3e39e084eed74d704b2cb", + "note": "captured in seed mode", + "request": { + "model": "cassette-model", + "stream": false, + "messages": [ + { + "role": "system", + "content": "You are LeapFlow, an intelligent assistant that can both converse naturally and take real actions on the user's computer.\n\n## Capabilities\nThe tool index below lists **every** registered tool by name and a one-line summary — this is the complete\ncapability contract; nothing else exists. Only a subset is directly callable this turn (via native tool calling,\nnot a JSON block in your reply). If you need a tool from the index that is not yet callable, call\n`capability_expand` with its category name first — the matching tools become callable immediately after.\nWhen the user asks about LeapFlow **itself** — its identity/version/model, what tools, plugins, or capabilities\nit has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of\nself-awareness — answer from live runtime evidence via `self_describe` (facet=capabilities for \"what can you\ndo / do you support X\", identity for version/model, runtime for context/posture, or `all` for a full self-check),\nnever by reading LeapFlow's own source code, documentation, or memory. Report configuration-dependent or\nunavailable capabilities as limitations rather than inferring them.\n- **text_search**(text, pattern): Search for a regex pattern in text.\n- **text_replace**(text, old, new, count) [capability_expand category: write]: Replace occurrences of a substring in text.\n- **time_get**(): Get current date and time.\n- **env_info**(): Get system environment information (OS, Python version, cwd).\n- **skills_list**(query, category, source): List available learned skills. Use when user asks about capabilities or you need a specific skill.\n- **skill_view**(name): View the full content of a specific skill document.\n- **skill_list_files**(name) [capability_expand category: skills]: List all support files in a skill directory (configs, templates, examples). Use before skill_file_read to discover available files.\n- **skill_file_read**(name, file_path) [capability_expand category: skills]: Read an individual support file from a skill directory. Use skill_list_files first to discover available files.\n- **code_intel**(path, operation): Precise document symbols (outline) for a source file: classes, functions, and methods with line ranges. Python uses an exact AST parse; other languages use a keyword-prefix scan. Prefer over file_read mode=symbols for accurate navigation before editing. Read-only.\n- **repo_map**(path): Compact project orientation for a repository root: languages, detected test/lint commands, top-level structure, entry points, manifest, and VCS branch. Call this first when entering an unfamiliar codebase. Read-only.\n- **scm_sync**(action, cwd, remote, pull_ref, push_ref, timeout) [capability_expand category: scm]: Run a typed git SCM action. Use this instead of shell_run for git pull/push/status. For 'pull origin main then push', set action='pull_then_push', remote='origin', pull_ref='main', and omit push_ref so LeapFlow pushes the current local branch.\n- **git_query**(action, cwd, ref, path, staged, max_count, stat): Read-only structured git inspection: action=diff|log|status|branch|show. Prefer over shell_run for reading repo state — output is clipped, redacted, and log/branch are parsed into structured fields. Use scm_sync for pull/push.\n- **git_write**(action, cwd, message, stage_all, name, ref, create) [capability_expand category: scm]: Mutating git actions: action=commit (message, stage_all), branch (create+switch), checkout (switch; create=true for -b). Approval-gated. Use scm_sync for pull/push and git_query for reads.\n- **test_run**(command, cwd, timeout): Run the project's test suite and return structured results (framework, passed/failed counts, failing tests). Auto-detects the runner (pytest/npm/go/cargo) or uses a configured/explicit command; executes via the governed shell. ok=true means the runner executed — see 'success' for pass/fail.\n- **lint_check**(command, cwd, timeout): Run the project's linter and return a structured clean/issue result. Auto-detects the linter (ruff/eslint/go vet/clippy) or uses a configured/explicit command; executes via the governed shell. ok=true means the linter ran — see 'clean'.\n- **file_list**(path, pattern, depth): List files and directories at a given path. Use depth=1 or depth=2 to get a recursive tree in one call instead of listing each sub-directory separately.\n- **file_read**(path, max_lines, start_line, max_chars, mode): Read text file content with adaptive context governance. For large or unfamiliar files, prefer mode='outline' or mode='symbols' first, then use mode='raw' with start_line/max_lines for the specific range you actually need. For LeapFlow's own settings, use config_list / config_get / config_set — its config files are outside the workspace and not readable here.\n- **file_write**(path, content, mode) [capability_expand category: write]: Write content to a file (overwrite or append).\n- **code_search**(pattern, patterns, path, glob, ignore_case, multiline, max_results, context_lines): Search file CONTENTS by regex pattern across a directory tree (ripgrep-backed). Requires a regex pattern. NOT for listing or browsing directory contents — use file_list for that. Prefer this over shell_run grep: faster, skips VCS/dependency/build dirs, and returns structured path:line:column matches. Batch related lookups into ONE call via `patterns` (OR-combined, single pass) instead of issuing several separate searches. Use file_read for the surrounding context of a hit.\n- **file_find**(glob, path, max_results): Find files by a recursive glob pattern under a base path (e.g. '**/test_*.py' or '*.md'). Prefer this over shell_run find; skips VCS/dependency/build dirs.\n- **edit_file**(path, edits, dry_run, diff) [capability_expand category: write]: Apply targeted, anchored search-replace edits to an EXISTING text file (use file_write to create/overwrite). Each edit is {original_text, new_text, replace_all?}; original_text must match exactly and uniquely (or set replace_all) — a non-unique or missing anchor is rejected so files are never corrupted. Set dry_run to preview. Alternatively pass a unified 'diff' to apply its hunks as anchored edits. Far cheaper and safer than rewriting a whole file.\n- **shell_run**(command, cwd, timeout) [capability_expand category: shell]: Execute a one-shot shell command with timeout protection. Runs in the active workspace; paths resolving outside it are refused. Reach for a structured tool first when one fits — web_fetch for anything over HTTP(S), git_query/scm_sync for git, code_search/file_find/file_read for the repo, config_get/config_set for LeapFlow's own settings — because those report typed results, while a failed shell command can only be diagnosed from its exit code and stderr. Every shell run counts as an external side effect, so a failure stops the rest of the batch and is not retried automatically.\n- **terminal_open**(command, cwd, shell) [capability_expand category: terminal]: Open a PERSISTENT shell session (REPL/dev server/watch), returning a session_id for terminal_send/read/close. Disabled unless tools.terminal_session_enabled is set. For one-shot commands use shell_run instead.\n- **terminal_send**(session_id, input, wait) [capability_expand category: terminal]: Send a line of input to a persistent terminal session and return output captured shortly after.\n- **terminal_read**(session_id, wait): Drain buffered output from a persistent terminal session (optionally waiting briefly first).\n- **terminal_close**(session_id) [capability_expand category: terminal]: Terminate a persistent terminal session and release its process group.\n- **terminal_list**(): List active persistent terminal sessions.\n- **config_list**(category, limit): List LeapFlow's own writable settings (LLM model and endpoint, daemon, memory, perception, gateway, …) with current values. Use this to discover the exact key before changing anything. Optionally narrow by `category`. This is the only correct way to inspect LeapFlow configuration — never read config files from disk.\n- **config_get**(key): Read one LeapFlow setting by key (e.g. 'llm.model', 'llm.base_url', 'daemon.log_level'), returning its current value, type, scopes, and whether a change needs a daemon restart. There is no 'llm.provider' key: provider behavior is inferred from the OpenAI-compatible 'llm.base_url'. Never read LeapFlow config files from disk — use this.\n- **config_set**(key, value, scope) [capability_expand category: config]: Change one LeapFlow setting by key, e.g. switch an OpenAI-compatible LLM with key='llm.model' and key='llm.base_url'. There is no 'llm.provider' key: provider behavior is inferred from the endpoint. Values are validated and coerced; credentials are stored in the vault automatically. Call config_list or config_get first if unsure of the key. The result states whether a `leap daemon restart` is required. Never edit LeapFlow config files directly.\n- **web_fetch**(url, select, timeout, max_bytes): Read a URL over HTTP(S) and get back extracted, context-sized content: parsed JSON for API endpoints, readable text plus links for web pages. Use this for anything on the internet — prices, docs, releases, articles — instead of running curl through shell_run: it reports real HTTP status codes, retries rate limits on its own, and is a plain read so a retry is always safe. For JSON APIs pass `select` with a dotted path (e.g. 'chart.result.0.meta') to return just that part instead of the whole payload.\n- **memory_search**(query, limit): Search agent memory for relevant past experiences, observations, and facts.\n- **memory_add**(content, kind) [capability_expand category: write]: Store a new observation or insight in memory for future reference.\n- **research_note**(kind, text): Record a compact, structured note about the current task's state so it survives context compression on long / multi-step tasks. Use for durable findings, open questions still to resolve, decisions / excluded paths, and the immediate next step. One concise sentence per note.\n- **capability_expand**(category): Fetch the full callable schema for every tool in a capability category (e.g. 'hub', 'gateway', 'desktop', 'delegate', 'file', 'memory', 'skill'). The compact tool index always lists every registered tool by name and a one-line summary, but only a static low-risk subset is directly callable each turn. If you need a tool from the index that is not yet callable, call capability_expand with its category first; the matching tools become callable in this turn. Never invent a tool name — expand the category instead.\n- **delegate_task**(goal, context) [capability_expand category: delegate]: Delegate a complex sub-task to an isolated subagent. The subagent gets a fresh context and restricted tool access. Use when a task is self-contained and can be solved independently.\n- **schedule_reentry**(kind, reason, delay_seconds, event_match, max_reentries, deadline_seconds): Register a re-entry so this task can resume later from its current orientation (findings / open questions / next step). Use when work must pause and continue after a delay (kind=time) or when a matching platform event arrives (kind=event), instead of finishing now. The research-ledger state is carried over automatically.\n- **hub_push**(skill_name, visibility, version) [capability_expand category: hub]: Push a local skill to the ModelScope Hub for sharing or backup.\n- **hub_pull**(repo_id, version) [capability_expand category: hub]: Pull a skill from the ModelScope Hub to install locally.\n- **hub_search**(query) [capability_expand category: hub]: Search for skills on the Hub by keyword or description.\n- **hub_sync**(mode, dry_run) [capability_expand category: hub]: Preview or execute sync between local skills and Hub.\n- **platform_action**(platform, action, payload, backend_kind) [capability_expand category: gateway]: Execute an exact registered business action on an external platform through LeapFlow's App Connector layer. Actions must be copied from the App Connector Capability Index and are addressed as domain.operation, e.g. im.send_message or docs.create_markdown. All business fields (chat_id, text, query, etc.) MUST be placed inside `payload`, never at the top level. Example: {\"platform\":\"feishu\",\"action\":\"im.send_message\",\"payload\":{\"chat_id\":\"oc_xxx\",\"text\":\"hello\"}}. Do not invent action names, do not use management actions such as list/guide/connect/status here.\n- **platform_connect**(action, platform, credentials, options, checkpoint) [capability_expand category: gateway]: List, guide, connect, disconnect, remove, or check status for external platforms using the App Connector management namespace. Supports REST and CLI backends. Use this for management actions such as list/guide/preflight/connect/status; use platform_action only for exact registered business actions.\n- **gateway_send**(platform, chat_id, text, thread_id) [capability_expand category: gateway]: Send a message to a connected external platform (Feishu group, Telegram chat, DingTalk conversation, etc.). Requires the platform to be connected via gateway_connect first. Use gateway_connect with action='list' to see connected platforms and available chat IDs.\n- **gateway_connect**(action, platform, credentials, options) [capability_expand category: gateway]: Connect, configure, or manage external platform integrations (Feishu, DingTalk, Telegram, Slack, Discord, etc.). Conversational flow: 1) call 'guide' to get setup steps + required fields, 2) present the steps to the user and ask for ALL required credentials in a single message, 3) call 'connect' with the credentials. Goal: complete in 1–2 user turns. NEVER include credential values in your text response.\n- **plugin_list**(): List the live plugin registry across Tool/Gateway/LLM subsystems for plugin management: which plugins are loaded, their category and fiber lifecycle state. For questions about LeapFlow's own capabilities or whether it supports X, use self_describe instead.\n- **plugin_status**(plugin_id): Get detailed status of a specific plugin: its declared category, runtime dependencies, contributed tools, and fiber lifecycle state.\n- **plugin_versions**(plugin_id): List recorded source versions and active pointer for a profile-scoped plugin.\n- **plugin_propose**(requested_capability, plugin_id, proposed_tools, test_cases, risk_level, evidence): Create a side-effect-free PluginProposal from explicit capability-gap evidence. Use this before plugin_generate when a missing capability should be reviewed. Does not call an LLM, write files, or install anything.\n- **assess_compatibility**(manifest, source_path) [capability_expand category: plugin_management]: Assess whether a foreign plugin manifest or real DSH source bundle is compatible with LeapFlow. A source bundle assessment is static: runtime_ready stays false until restricted Node discovery during plugin_install. Returns component-level verdicts and limitations.\n- **plugin_generate**(plugin_id, description, proposal_id) [capability_expand category: system]: Generate a new ToolPlugin from a natural-language capability description. The LLM produces code that conforms to the ToolPlugin Protocol; it is then rigorously validated (syntax, structure, import, protocol conformance). The generated source is stored in CAS and receives explicit content approval. It DOES NOT install the plugin — installation requires a second, mutation-specific approval via plugin_install.\n- **plugin_install**(plugin_id, code, marketplace_name, source_path, proposal_id, version_label) [capability_expand category: system]: Install a plugin from exactly one source: validated Python code, the configured Python marketplace, or a real DSH source bundle. DSH bundles run restricted Node discovery before registration; only public host tools are installed and client UI limitations are reported. Writes profile-scoped state and mutates the process-global registry. REQUIRES APPROVAL.\n- **plugin_rollback**(plugin_id, version) [capability_expand category: system]: Rollback a profile-scoped plugin to a recorded source version and reload it. REQUIRES APPROVAL.\n- **plugin_reload**(plugin_id, version_label) [capability_expand category: system]: Hot-reload a plugin at runtime. Disposes the old plugin fiber, re-imports its module, and registers a fresh instance. Existing in-flight turns are unaffected (snapshot isolation). REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_disable**(plugin_id) [capability_expand category: system]: Disable a plugin by disposing its fiber, removing its tools from the runtime registry. Cannot disable self_management itself. REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_remove**(plugin_id, delete_source) [capability_expand category: system]: Terminally remove a plugin: dispose its fiber, unregister its tools, remove reload metadata, and optionally delete its profile-scoped source file. Cannot remove self_management itself. REQUIRES APPROVAL.\n- **plugin_unquarantine**(plugin_id) [capability_expand category: plugin_management]: Restore a quarantined plugin to probation status for re-evaluation. Unfreezes the trust ledger, transitions the proposal back to PROBATION, and reloads the plugin. REQUIRES APPROVAL.\n- **plugin_enable**(plugin_id) [capability_expand category: system]: Re-enable a previously disabled plugin by reloading its module and registering a fresh instance. REQUIRES APPROVAL.\n- **self_describe**(facet): Answer questions about LeapFlow ITSELF from live runtime evidence — its identity/version/model, what tools and plugins it has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of self-awareness. Prefer this over reading LeapFlow's own source code. Pick facet=capabilities for 'what can you do / do you support X', identity for version/model, runtime for context/posture, or all for a full self-check when explicitly requested.\n- **runtime_snapshot**(): Lightweight (~150 token) flat snapshot of current runtime state: model, context budget, posture, disclosure level, turn count, cache hit rate, uptime, tool count, and pending approvals.\n- **schedule_create**(trigger_expression, instruction, execution_mode, max_retries, delivery_target) [capability_expand category: scheduler]: Create a new scheduled task. Specify a trigger expression (e.g. '30m', 'every 2h', '0 9 * * *') and the instruction to execute on each trigger. Optionally provide an execution mode and a delivery target to receive notifications.\n- **schedule_list**() [capability_expand category: scheduler]: List all scheduled tasks with their current state, trigger, and next due time.\n- **schedule_status**(task_id) [capability_expand category: scheduler]: Get detailed status and recent execution history for a scheduled task.\n- **schedule_pause**(task_id) [capability_expand category: scheduler]: Pause a scheduled task so it stops firing without being cancelled.\n- **schedule_resume**(task_id) [capability_expand category: scheduler]: Resume a paused scheduled task — re-arms it and recalculates next due time.\n- **schedule_cancel**(task_id) [capability_expand category: scheduler]: Cancel a scheduled task permanently.\n- **tool_search**(query, max_results): Search all registered tools by keyword relevance. Returns a ranked list of matching tools with name, category, and summary. Use this when you need a tool but are unsure of its exact name or category.\n- **tool_describe**(tool_name): Get the full callable schema of a single tool by exact name. Returns the tool's description, parameters, and metadata. Use after tool_search to inspect a specific tool before calling it.\n- **hw_list**(): List connected hardware devices with their channel counts and measured quantities. Start here; it does not include operating limits.\n- **hw_describe**(device_id): Return the full reference for one device: every channel, its unit, its operating envelope, rate limit, reversibility, and required interlocks, plus any prior outcomes recorded for its writable channels. Required before commanding a device.\n- **hw_read**(device_id, channel_id): Read the current value of one device channel. Has no physical effect.\n- **hw_status**(device_id): Report connection health, halt capability, and recent notable events (threshold excursions, lost samples, stalled channels) for one device.\n- **hw_configure**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Set a configuration value or setpoint on a channel declaring effect=configure. Setpoints often have inertia: the value may need time to stabilise.\n- **hw_actuate**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Command motion or output on a channel declaring effect=actuate. This moves physical hardware; a repeat from an unknown state is not a safe retry.\n- **hw_dispense**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Consume an irreversible resource on a channel declaring effect=dispense. Running this twice dispenses twice; never repeat it after a failure without first verifying what already happened.\n- **hw_estop**(device_id) [capability_expand category: hardware]: Stop all motion and output on a device immediately. Never requires approval; use it whenever device behaviour is unexpected.\n- **hw_batch_actuate**(device_id, commands, dry_run, verify) [capability_expand category: hardware]: Write multiple actuator channels in one atomic transaction. Used for coordinated multi-joint control where all positions must be commanded simultaneously (e.g. robot arm joint-space control).\n- **session_search**(query, limit) [capability_expand category: unclassified]: Search past conversation sessions in the current workspace for relevant context.\n- **session_list**(limit): List recent conversation sessions in the current workspace with stable ids, titles, dates, and summaries. Use for browsing past tasks or when user asks to see history without specific search terms. No keywords needed — returns chronological list.\n- **session_detail**(session_id, limit, offset, include_inactive) [capability_expand category: unclassified]: Read a paginated persisted transcript for one past conversation session in the current workspace. Use after session_list or session_search returns a session_id.\n- **click**(element_index) [capability_expand category: desktop]: Click a UI element by its element_index (from the latest observe_ui snapshot)\n- **get_clipboard**() [capability_expand category: desktop]: Read current clipboard text content\n- **list_apps**(filter, running_only) [capability_expand category: desktop]: List available applications on this system. Use to discover correct bundle_id before switch_app.\n- **list_windows**() [capability_expand category: desktop]: List all top-level windows with pid, window_id, title, and per-window state (minimized, on-screen). Call this first to pick the pid and window_id that observe_ui and other window tools require.\n- **observe_ui**(pid, window_id, query) [capability_expand category: desktop]: Snapshot one window's actionable UI elements, each tagged with an element_index for click/right_click/read_text. Re-observe after actions — indices belong to one snapshot. Requires the window's pid and window_id from list_windows.\n- **open_url**(url, app_id) [capability_expand category: desktop]: Open a URL in the default or specified browser\n- **read_text**(element_index) [capability_expand category: desktop]: Read the text content of a specific UI element from the latest snapshot\n- **right_click**(element_index) [capability_expand category: desktop]: Right-click a UI element to open its context menu. Returns visible menu items.\n- **screenshot**(pid, window_id) [capability_expand category: desktop]: Capture a screenshot for visual verification. With pid + window_id captures that window (works across all displays); defaults to the last observed window, or the full desktop when no window has been observed.\n- **scroll**(element_index, direction, amount, pid, window_id) [capability_expand category: desktop]: Scroll a scrollable area of a window. Omit element_index to scroll the window's focused/page scroller; pass one to scroll an exact element from the latest snapshot.\n- **select_text**(element_index) [capability_expand category: desktop]: Select all text in a UI element (focus + select-all, for subsequent copy)\n- **set_clipboard**(text) [capability_expand category: desktop]: Write text to the clipboard\n- **shortcut**(keys) [capability_expand category: desktop]: Execute a keyboard shortcut\n- **switch_app**(app_id) [capability_expand category: desktop]: Switch to an app (launch if needed, activate, verify)\n- **type_text**(text) [capability_expand category: desktop]: Type text into the currently focused element\n- **wait**(seconds) [capability_expand category: desktop]: Wait for a specified duration before continuing\n- **wait_until**(condition, pid, window_id, timeout, poll_interval) [capability_expand category: desktop]: Wait until a UI condition is met (polls UI tree). Returns elements when found or on timeout.\n- **wait_until_stable**(timeout, poll_interval, pid, window_id) [capability_expand category: desktop]: Wait until the UI stops changing (element set stabilizes across polls).\n\n## App Connector Capability Index\nLeapFlow can onboard and manage external apps through `platform_connect` and execute exact registered business actions through `platform_action`.\nFor requests about connecting, setting up, configuring, enabling, or managing a supported app, use `platform_connect` first instead of generating SDK/Webhook sample code.\n`platform_connect.action` is the management namespace: list, guide, preflight, connect, disconnect, remove, status, events_start, events_stop, events_status.\n`platform_action.action` is only for exact registered platform business actions listed below, such as `im.send_message`; never use management actions like `list` or `guide` there.\nAll business fields MUST go inside `payload`; top-level keys are only `platform`, `action`, and `payload`.\nDo not invent platform IDs or platform action names. If the needed action is not listed, ask for discovery/clarification instead of guessing.\nUse `platform_connect` with `action='guide'` and the matching `platform` to start onboarding; use `action='list'` when the app is unclear.\nWhen a pending onboarding state is present, continue from that state with `platform_connect` instead of asking the user to restate the app.\nSupported apps:\n- `api_server`: API Server (OpenAI Compatible) (category=api; backend=adapter; platform_action actions=none registered)\n- `dingtalk`: 钉钉 (DingTalk) (category=im; backend=adapter; platform_action actions=none registered)\n- `feishu`: 飞书 (Feishu/Lark) (category=im; backend=cli)\n - `calendar.create_event` payload={summary*, start_time*, end_time*, attendees} [write/high]\n - `docs.create_markdown` payload={title*, markdown*, folder_token} [write/medium]\n - `drive.search` payload={query*, limit} [read/medium]\n - `im.add_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.download_resource` payload={message_id*, file_key*, type*} [read/low]\n - `im.get_messages` payload={message_ids*} [read/low]\n - `im.list_chats` payload={page_size} [read/low]\n - `im.list_messages` payload={chat_id*} [read/low]\n - `im.list_thread_messages` payload={thread*} [read/low]\n - `im.remove_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.reply_message` payload={message_id*, text*} [send/high]\n - `im.search_chats` payload={query*, page_size} [read/low]\n - `im.search_messages` payload={query*} [read/medium]\n - `im.send_message` payload={chat_id*, text*, thread_id} [send/high]\n - `im.update_card` payload={token*, card*} [write/medium]\n - `im.update_message` payload={message_id*, text*} [write/medium]\n - `mail.search_unread` payload={query, limit} [read/high]\n - `sheets.append_row` payload={spreadsheet_token*, sheet_id*, values*} [write/medium]\n - `task.create` payload={title*, description, due_time} [write/medium]\n- `telegram`: Telegram (category=social; backend=adapter; platform_action actions=none registered)\n- `webhook`: Webhook (Generic) (category=webhook; backend=adapter; platform_action actions=none registered)\n\n## Learned Skills\nYou have access to the following learned skills. Use `skills_list` to browse or `skill_view` to read details:\n- api_designer: REST and GraphQL API design with OpenAPI spec generation and best practices [API, REST, GraphQL]\n- code_review: Structured code review for security, performance, and style [code, review, security]\n- data_analysis: CSV/JSON data analysis with statistical summaries, quality checks, and visualiza [data, analysis, statistics]\n- devops_helper: CI/CD pipeline configuration, Docker orchestration, and deployment strategy [devops, CI/CD, Docker]\n- document_writer: Structured document generation: technical docs, API references, reports, and REA [documentation, technical-writing, README]\n- git_workflow: Git operation orchestration: branching, commits, conflicts, and PR workflow [git, version-control, branching]\n- i18n_helper: Internationalization workflow: string extraction, translation, locale management [i18n, internationalization, localization]\n- project_analysis: Project structure analysis, dependency audit, and tech stack identification [project, analysis, dependencies]\n- security_audit: Security vulnerability scanning strategy, threat modeling, and fix recommendatio [security, vulnerability, audit]\n- shell_automation: Natural language to shell commands with safety review and step-by-step execution [shell, terminal, automation]\n- test_writer: Generate unit and integration tests from source code with edge-case coverage [testing, unit-test, integration-test]\n- web_research: Multi-step web search with cross-verification and structured synthesis [web, search, research]\n\n## Tool Usage\nTools are normally invoked through the native function-calling mechanism, not by writing JSON in your reply\ntext. Only if the provider signals that native function calling is unavailable for this turn, fall back to a\nsingle JSON code block: `{\"name\": \"tool_name\", \"arguments\": {\"key\": \"value\"}}` — use this fallback format\nonly, never both. Only call a tool whose exact name appears in the tool index above and is currently callable\n(or reachable via `capability_expand`). Never invent, rename, alias, or guess a tool name, platform ID, or\nplatform action from argument shape or wording — if the index does not list it, it does not exist.\n`platform_connect.action` (list/guide/preflight/connect/disconnect/remove/status/events_start/events_stop/\nevents_status) is the App Connector management namespace; `platform_action.action` only accepts exact\nregistered business actions such as `im.send_message` shown in the App Connector Capability Index — never mix\nthe two namespaces. If a tool call returns an unknown/unavailable result, use the returned suggestions or\navailable names for a single retry instead of trying further variations of the same guess.\n\n**Side-effect action rule** (`platform_action` with effect=send/write/execute):\n- Call each unique action+payload **exactly once**. Never include duplicates in the same turn.\n- Once the result returns `\"completed\": true`, that action is DONE for this task. Do NOT call it again\n in any subsequent turn — immediately summarize the result for the user instead.\n- The system enforces idempotency: duplicate calls are blocked and will not execute.\n- If the user explicitly requests sending/writing multiple times, use distinct payloads per call.\n\n**Resource identifier provenance rule**:\n- NEVER fabricate, guess, or infer resource identifiers (chat_id, message_id, file_key, user_id, etc.).\n Every resource ID used in a side-effect action MUST come from a successful API response in this session.\n- If a read/list action fails (e.g. authorization error), you do NOT have valid resource IDs.\n Report the failure to the user — do NOT attempt the dependent write/send action with a guessed ID.\n- When a tool result contains `\"llm_instruction\"`, follow it exactly.\n\n## Guidelines\n1. **Direct answers first**: If you already know the answer, respond directly without tools.\n2. **Avoid redundant tool calls**: Do not call the same tool with the same arguments more than once in the same user turn. When an existing tool result already answers the user's request, stop calling tools and answer directly.\n3. **Use tools proactively**: When the user asks about files, time, system state, or needs actions performed, use the appropriate tool.\n4. **Chain tools when needed**: You can call multiple tools in sequence (e.g., list files → read file → summarize).\n5. **Handle failures gracefully**: If a tool fails, explain what went wrong and suggest alternatives. If it failed because of an unknown tool/platform/action name, retry once with an exact name from the returned suggestions, then explain rather than keep guessing.\n6. **Summarize results naturally**: After tool execution, synthesize the results into a helpful answer rather than dumping raw output.\n7. **Stay conversational**: Maintain a natural, helpful tone. Acknowledge context from earlier in the conversation.\n8. **Recall past work**:\n - Broad queries (\"之前做了什么\", \"列出任务\"): answer from the \"Recent Task History\" section already in your context. If insufficient, call session_list.\n - Specific lookups (\"上次那个配置怎么改的\"): call session_search with relevant phrases (NOT single characters).\n - Do NOT call search tools repeatedly with keyword fragments. One well-phrased call is better than ten fragmented ones.\n\n## Coding & Verification\nWhen working with code, prefer the precise built-in tools over ad-hoc shell: use `repo_map` to orient in an\nunfamiliar project, `code_search`/`file_find` to locate and `code_intel` for symbols, `edit_file` (anchored\nsearch-replace, or a unified `diff`) to change files — never rewrite a whole file to change a few lines — and\n`git_query` to inspect diffs/log. After edits, check `syntax_ok` in the result and run `test_run`/`lint_check`\nbefore declaring the work done. Batch independent read-only calls (search/read/query) into a single turn — they\nrun in parallel.\n\n## Presentation Style\n1. **Polished Markdown only**: Format user-facing answers with clean Markdown headings, short paragraphs, and concise bullets. Use tables only when they improve comparison or scanning.\n2. **Terminal-friendly layout**: Keep lines readable in a TUI; avoid dense walls of text, deeply nested lists, oversized ASCII art, or heavy visual blocks.\n3. **Elegant emphasis**: Use bold text sparingly for key terms and conclusions. Avoid excessive emojis, decorative symbols, repeated separators, or visual noise.\n4. **Theme-safe colors**: Do not emit ANSI escape codes, HTML color tags, Rich markup, or hardcoded color names. Rely on the TUI theme to render Markdown professionally.\n5. **No leaked tool protocol**: Never show tool-call JSON, internal schemas, raw observations, tool result payloads, or hidden reasoning in the final answer unless the user explicitly asks for raw/debug output. Treat any prior `{\"name\": ..., \"arguments\": ...}` blocks and `Tool result (...)` messages as internal execution context only.\n6. **Professional closure**: End with a concise conclusion or next step when helpful; avoid rambling after the useful answer is complete.\n\nWhen finished with all tool calls, respond normally without a JSON block, tool-call transcript, or process log.\n\n## Task Contract\n- Task ID: turn-2\n- Original user request: Read the current value of the setpoint channel on bench_r8.\n- Workspace root: \n- Allowed roots: \n- Treat relative project paths as relative to the workspace root; never infer `.` as the project root when a workspace root is provided.\n- Workspace boundary is enforced by tools: do not read, search, edit, or run commands against paths outside the allowed roots unless the user explicitly requests an external path and the tool/approval policy permits it.\n- LeapFlow workspace config is optional at `/.leapflow/config.yaml`; runtime config is loaded from `~/.leapflow/config/user.yaml` and `~/.leapflow/profiles//config/*.yaml`.\n- Preserve this task contract across summarization, compression, tool loops, and memory retrieval.\n" + }, + { + "role": "user", + "content": "List the connected hardware devices, then describe bench_r8 in full." + }, + { + "role": "assistant", + "content": "[Called: hw_list]\n[Called: hw_describe]\nDiscovered the simulated bench and read its channel limits." + }, + { + "role": "user", + "content": "Read the current value of the setpoint channel on bench_r8.\nRead the current value of the setpoint channel on bench_r8." + } + ], + "tools": [ + "capability_expand", + "code_intel", + "code_search", + "config_get", + "config_list", + "env_info", + "file_find", + "file_list", + "file_read", + "git_query", + "hw_actuate", + "hw_batch_actuate", + "hw_configure", + "hw_describe", + "hw_dispense", + "hw_estop", + "hw_list", + "hw_read", + "hw_status", + "lint_check", + "memory_search", + "plugin_list", + "plugin_propose", + "plugin_status", + "plugin_versions", + "repo_map", + "research_note", + "runtime_snapshot", + "schedule_reentry", + "self_describe", + "session_list", + "skill_view", + "skills_list", + "terminal_list", + "terminal_read", + "test_run", + "text_search", + "time_get", + "tool_describe", + "tool_search", + "web_fetch" + ] + }, + "responses": [ + { + "status": 200, + "content_type": "application/json", + "body": "{\"id\": \"chatcmpl-cassette\", \"object\": \"chat.completion\", \"model\": \"cassette-model\", \"choices\": [{\"index\": 0, \"message\": {\"role\": \"assistant\", \"content\": \"\", \"tool_calls\": [{\"id\": \"call_1\", \"type\": \"function\", \"function\": {\"name\": \"hw_read\", \"arguments\": \"{\\\"device_id\\\": \\\"bench_r8\\\", \\\"channel_id\\\": \\\"setpoint\\\"}\"}}]}, \"finish_reason\": \"tool_calls\"}], \"usage\": {\"prompt_tokens\": 64, \"completion_tokens\": 16, \"total_tokens\": 80}}" + } + ] +} diff --git a/tests/_fixtures/cassettes/r8_hardware/cassette-model-34622e77d4a39a56.cassette.json b/tests/_fixtures/cassettes/r8_hardware/cassette-model-34622e77d4a39a56.cassette.json new file mode 100644 index 00000000..a872bab1 --- /dev/null +++ b/tests/_fixtures/cassettes/r8_hardware/cassette-model-34622e77d4a39a56.cassette.json @@ -0,0 +1,92 @@ +{ + "fingerprint": "34622e77d4a39a5683be23e6c5b30987e38cb2e4b1a5f99c88dcbf904946d00c", + "note": "captured in seed mode", + "request": { + "model": "cassette-model", + "stream": false, + "messages": [ + { + "role": "system", + "content": "You are LeapFlow, an intelligent assistant that can both converse naturally and take real actions on the user's computer.\n\n## Capabilities\nThe tool index below lists **every** registered tool by name and a one-line summary — this is the complete\ncapability contract; nothing else exists. Only a subset is directly callable this turn (via native tool calling,\nnot a JSON block in your reply). If you need a tool from the index that is not yet callable, call\n`capability_expand` with its category name first — the matching tools become callable immediately after.\nWhen the user asks about LeapFlow **itself** — its identity/version/model, what tools, plugins, or capabilities\nit has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of\nself-awareness — answer from live runtime evidence via `self_describe` (facet=capabilities for \"what can you\ndo / do you support X\", identity for version/model, runtime for context/posture, or `all` for a full self-check),\nnever by reading LeapFlow's own source code, documentation, or memory. Report configuration-dependent or\nunavailable capabilities as limitations rather than inferring them.\n- **text_search**(text, pattern): Search for a regex pattern in text.\n- **text_replace**(text, old, new, count) [capability_expand category: write]: Replace occurrences of a substring in text.\n- **time_get**(): Get current date and time.\n- **env_info**(): Get system environment information (OS, Python version, cwd).\n- **skills_list**(query, category, source): List available learned skills. Use when user asks about capabilities or you need a specific skill.\n- **skill_view**(name): View the full content of a specific skill document.\n- **skill_list_files**(name) [capability_expand category: skills]: List all support files in a skill directory (configs, templates, examples). Use before skill_file_read to discover available files.\n- **skill_file_read**(name, file_path) [capability_expand category: skills]: Read an individual support file from a skill directory. Use skill_list_files first to discover available files.\n- **code_intel**(path, operation): Precise document symbols (outline) for a source file: classes, functions, and methods with line ranges. Python uses an exact AST parse; other languages use a keyword-prefix scan. Prefer over file_read mode=symbols for accurate navigation before editing. Read-only.\n- **repo_map**(path): Compact project orientation for a repository root: languages, detected test/lint commands, top-level structure, entry points, manifest, and VCS branch. Call this first when entering an unfamiliar codebase. Read-only.\n- **scm_sync**(action, cwd, remote, pull_ref, push_ref, timeout) [capability_expand category: scm]: Run a typed git SCM action. Use this instead of shell_run for git pull/push/status. For 'pull origin main then push', set action='pull_then_push', remote='origin', pull_ref='main', and omit push_ref so LeapFlow pushes the current local branch.\n- **git_query**(action, cwd, ref, path, staged, max_count, stat): Read-only structured git inspection: action=diff|log|status|branch|show. Prefer over shell_run for reading repo state — output is clipped, redacted, and log/branch are parsed into structured fields. Use scm_sync for pull/push.\n- **git_write**(action, cwd, message, stage_all, name, ref, create) [capability_expand category: scm]: Mutating git actions: action=commit (message, stage_all), branch (create+switch), checkout (switch; create=true for -b). Approval-gated. Use scm_sync for pull/push and git_query for reads.\n- **test_run**(command, cwd, timeout): Run the project's test suite and return structured results (framework, passed/failed counts, failing tests). Auto-detects the runner (pytest/npm/go/cargo) or uses a configured/explicit command; executes via the governed shell. ok=true means the runner executed — see 'success' for pass/fail.\n- **lint_check**(command, cwd, timeout): Run the project's linter and return a structured clean/issue result. Auto-detects the linter (ruff/eslint/go vet/clippy) or uses a configured/explicit command; executes via the governed shell. ok=true means the linter ran — see 'clean'.\n- **file_list**(path, pattern, depth): List files and directories at a given path. Use depth=1 or depth=2 to get a recursive tree in one call instead of listing each sub-directory separately.\n- **file_read**(path, max_lines, start_line, max_chars, mode): Read text file content with adaptive context governance. For large or unfamiliar files, prefer mode='outline' or mode='symbols' first, then use mode='raw' with start_line/max_lines for the specific range you actually need. For LeapFlow's own settings, use config_list / config_get / config_set — its config files are outside the workspace and not readable here.\n- **file_write**(path, content, mode) [capability_expand category: write]: Write content to a file (overwrite or append).\n- **code_search**(pattern, patterns, path, glob, ignore_case, multiline, max_results, context_lines): Search file CONTENTS by regex pattern across a directory tree (ripgrep-backed). Requires a regex pattern. NOT for listing or browsing directory contents — use file_list for that. Prefer this over shell_run grep: faster, skips VCS/dependency/build dirs, and returns structured path:line:column matches. Batch related lookups into ONE call via `patterns` (OR-combined, single pass) instead of issuing several separate searches. Use file_read for the surrounding context of a hit.\n- **file_find**(glob, path, max_results): Find files by a recursive glob pattern under a base path (e.g. '**/test_*.py' or '*.md'). Prefer this over shell_run find; skips VCS/dependency/build dirs.\n- **edit_file**(path, edits, dry_run, diff) [capability_expand category: write]: Apply targeted, anchored search-replace edits to an EXISTING text file (use file_write to create/overwrite). Each edit is {original_text, new_text, replace_all?}; original_text must match exactly and uniquely (or set replace_all) — a non-unique or missing anchor is rejected so files are never corrupted. Set dry_run to preview. Alternatively pass a unified 'diff' to apply its hunks as anchored edits. Far cheaper and safer than rewriting a whole file.\n- **shell_run**(command, cwd, timeout) [capability_expand category: shell]: Execute a one-shot shell command with timeout protection. Runs in the active workspace; paths resolving outside it are refused. Reach for a structured tool first when one fits — web_fetch for anything over HTTP(S), git_query/scm_sync for git, code_search/file_find/file_read for the repo, config_get/config_set for LeapFlow's own settings — because those report typed results, while a failed shell command can only be diagnosed from its exit code and stderr. Every shell run counts as an external side effect, so a failure stops the rest of the batch and is not retried automatically.\n- **terminal_open**(command, cwd, shell) [capability_expand category: terminal]: Open a PERSISTENT shell session (REPL/dev server/watch), returning a session_id for terminal_send/read/close. Disabled unless tools.terminal_session_enabled is set. For one-shot commands use shell_run instead.\n- **terminal_send**(session_id, input, wait) [capability_expand category: terminal]: Send a line of input to a persistent terminal session and return output captured shortly after.\n- **terminal_read**(session_id, wait): Drain buffered output from a persistent terminal session (optionally waiting briefly first).\n- **terminal_close**(session_id) [capability_expand category: terminal]: Terminate a persistent terminal session and release its process group.\n- **terminal_list**(): List active persistent terminal sessions.\n- **config_list**(category, limit): List LeapFlow's own writable settings (LLM model and endpoint, daemon, memory, perception, gateway, …) with current values. Use this to discover the exact key before changing anything. Optionally narrow by `category`. This is the only correct way to inspect LeapFlow configuration — never read config files from disk.\n- **config_get**(key): Read one LeapFlow setting by key (e.g. 'llm.model', 'llm.base_url', 'daemon.log_level'), returning its current value, type, scopes, and whether a change needs a daemon restart. There is no 'llm.provider' key: provider behavior is inferred from the OpenAI-compatible 'llm.base_url'. Never read LeapFlow config files from disk — use this.\n- **config_set**(key, value, scope) [capability_expand category: config]: Change one LeapFlow setting by key, e.g. switch an OpenAI-compatible LLM with key='llm.model' and key='llm.base_url'. There is no 'llm.provider' key: provider behavior is inferred from the endpoint. Values are validated and coerced; credentials are stored in the vault automatically. Call config_list or config_get first if unsure of the key. The result states whether a `leap daemon restart` is required. Never edit LeapFlow config files directly.\n- **web_fetch**(url, select, timeout, max_bytes): Read a URL over HTTP(S) and get back extracted, context-sized content: parsed JSON for API endpoints, readable text plus links for web pages. Use this for anything on the internet — prices, docs, releases, articles — instead of running curl through shell_run: it reports real HTTP status codes, retries rate limits on its own, and is a plain read so a retry is always safe. For JSON APIs pass `select` with a dotted path (e.g. 'chart.result.0.meta') to return just that part instead of the whole payload.\n- **memory_search**(query, limit): Search agent memory for relevant past experiences, observations, and facts.\n- **memory_add**(content, kind) [capability_expand category: write]: Store a new observation or insight in memory for future reference.\n- **research_note**(kind, text): Record a compact, structured note about the current task's state so it survives context compression on long / multi-step tasks. Use for durable findings, open questions still to resolve, decisions / excluded paths, and the immediate next step. One concise sentence per note.\n- **capability_expand**(category): Fetch the full callable schema for every tool in a capability category (e.g. 'hub', 'gateway', 'desktop', 'delegate', 'file', 'memory', 'skill'). The compact tool index always lists every registered tool by name and a one-line summary, but only a static low-risk subset is directly callable each turn. If you need a tool from the index that is not yet callable, call capability_expand with its category first; the matching tools become callable in this turn. Never invent a tool name — expand the category instead.\n- **delegate_task**(goal, context) [capability_expand category: delegate]: Delegate a complex sub-task to an isolated subagent. The subagent gets a fresh context and restricted tool access. Use when a task is self-contained and can be solved independently.\n- **schedule_reentry**(kind, reason, delay_seconds, event_match, max_reentries, deadline_seconds): Register a re-entry so this task can resume later from its current orientation (findings / open questions / next step). Use when work must pause and continue after a delay (kind=time) or when a matching platform event arrives (kind=event), instead of finishing now. The research-ledger state is carried over automatically.\n- **hub_push**(skill_name, visibility, version) [capability_expand category: hub]: Push a local skill to the ModelScope Hub for sharing or backup.\n- **hub_pull**(repo_id, version) [capability_expand category: hub]: Pull a skill from the ModelScope Hub to install locally.\n- **hub_search**(query) [capability_expand category: hub]: Search for skills on the Hub by keyword or description.\n- **hub_sync**(mode, dry_run) [capability_expand category: hub]: Preview or execute sync between local skills and Hub.\n- **platform_action**(platform, action, payload, backend_kind) [capability_expand category: gateway]: Execute an exact registered business action on an external platform through LeapFlow's App Connector layer. Actions must be copied from the App Connector Capability Index and are addressed as domain.operation, e.g. im.send_message or docs.create_markdown. All business fields (chat_id, text, query, etc.) MUST be placed inside `payload`, never at the top level. Example: {\"platform\":\"feishu\",\"action\":\"im.send_message\",\"payload\":{\"chat_id\":\"oc_xxx\",\"text\":\"hello\"}}. Do not invent action names, do not use management actions such as list/guide/connect/status here.\n- **platform_connect**(action, platform, credentials, options, checkpoint) [capability_expand category: gateway]: List, guide, connect, disconnect, remove, or check status for external platforms using the App Connector management namespace. Supports REST and CLI backends. Use this for management actions such as list/guide/preflight/connect/status; use platform_action only for exact registered business actions.\n- **gateway_send**(platform, chat_id, text, thread_id) [capability_expand category: gateway]: Send a message to a connected external platform (Feishu group, Telegram chat, DingTalk conversation, etc.). Requires the platform to be connected via gateway_connect first. Use gateway_connect with action='list' to see connected platforms and available chat IDs.\n- **gateway_connect**(action, platform, credentials, options) [capability_expand category: gateway]: Connect, configure, or manage external platform integrations (Feishu, DingTalk, Telegram, Slack, Discord, etc.). Conversational flow: 1) call 'guide' to get setup steps + required fields, 2) present the steps to the user and ask for ALL required credentials in a single message, 3) call 'connect' with the credentials. Goal: complete in 1–2 user turns. NEVER include credential values in your text response.\n- **plugin_list**(): List the live plugin registry across Tool/Gateway/LLM subsystems for plugin management: which plugins are loaded, their category and fiber lifecycle state. For questions about LeapFlow's own capabilities or whether it supports X, use self_describe instead.\n- **plugin_status**(plugin_id): Get detailed status of a specific plugin: its declared category, runtime dependencies, contributed tools, and fiber lifecycle state.\n- **plugin_versions**(plugin_id): List recorded source versions and active pointer for a profile-scoped plugin.\n- **plugin_propose**(requested_capability, plugin_id, proposed_tools, test_cases, risk_level, evidence): Create a side-effect-free PluginProposal from explicit capability-gap evidence. Use this before plugin_generate when a missing capability should be reviewed. Does not call an LLM, write files, or install anything.\n- **assess_compatibility**(manifest, source_path) [capability_expand category: plugin_management]: Assess whether a foreign plugin manifest or real DSH source bundle is compatible with LeapFlow. A source bundle assessment is static: runtime_ready stays false until restricted Node discovery during plugin_install. Returns component-level verdicts and limitations.\n- **plugin_generate**(plugin_id, description, proposal_id) [capability_expand category: system]: Generate a new ToolPlugin from a natural-language capability description. The LLM produces code that conforms to the ToolPlugin Protocol; it is then rigorously validated (syntax, structure, import, protocol conformance). The generated source is stored in CAS and receives explicit content approval. It DOES NOT install the plugin — installation requires a second, mutation-specific approval via plugin_install.\n- **plugin_install**(plugin_id, code, marketplace_name, source_path, proposal_id, version_label) [capability_expand category: system]: Install a plugin from exactly one source: validated Python code, the configured Python marketplace, or a real DSH source bundle. DSH bundles run restricted Node discovery before registration; only public host tools are installed and client UI limitations are reported. Writes profile-scoped state and mutates the process-global registry. REQUIRES APPROVAL.\n- **plugin_rollback**(plugin_id, version) [capability_expand category: system]: Rollback a profile-scoped plugin to a recorded source version and reload it. REQUIRES APPROVAL.\n- **plugin_reload**(plugin_id, version_label) [capability_expand category: system]: Hot-reload a plugin at runtime. Disposes the old plugin fiber, re-imports its module, and registers a fresh instance. Existing in-flight turns are unaffected (snapshot isolation). REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_disable**(plugin_id) [capability_expand category: system]: Disable a plugin by disposing its fiber, removing its tools from the runtime registry. Cannot disable self_management itself. REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_remove**(plugin_id, delete_source) [capability_expand category: system]: Terminally remove a plugin: dispose its fiber, unregister its tools, remove reload metadata, and optionally delete its profile-scoped source file. Cannot remove self_management itself. REQUIRES APPROVAL.\n- **plugin_unquarantine**(plugin_id) [capability_expand category: plugin_management]: Restore a quarantined plugin to probation status for re-evaluation. Unfreezes the trust ledger, transitions the proposal back to PROBATION, and reloads the plugin. REQUIRES APPROVAL.\n- **plugin_enable**(plugin_id) [capability_expand category: system]: Re-enable a previously disabled plugin by reloading its module and registering a fresh instance. REQUIRES APPROVAL.\n- **self_describe**(facet): Answer questions about LeapFlow ITSELF from live runtime evidence — its identity/version/model, what tools and plugins it has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of self-awareness. Prefer this over reading LeapFlow's own source code. Pick facet=capabilities for 'what can you do / do you support X', identity for version/model, runtime for context/posture, or all for a full self-check when explicitly requested.\n- **runtime_snapshot**(): Lightweight (~150 token) flat snapshot of current runtime state: model, context budget, posture, disclosure level, turn count, cache hit rate, uptime, tool count, and pending approvals.\n- **schedule_create**(trigger_expression, instruction, execution_mode, max_retries, delivery_target) [capability_expand category: scheduler]: Create a new scheduled task. Specify a trigger expression (e.g. '30m', 'every 2h', '0 9 * * *') and the instruction to execute on each trigger. Optionally provide an execution mode and a delivery target to receive notifications.\n- **schedule_list**() [capability_expand category: scheduler]: List all scheduled tasks with their current state, trigger, and next due time.\n- **schedule_status**(task_id) [capability_expand category: scheduler]: Get detailed status and recent execution history for a scheduled task.\n- **schedule_pause**(task_id) [capability_expand category: scheduler]: Pause a scheduled task so it stops firing without being cancelled.\n- **schedule_resume**(task_id) [capability_expand category: scheduler]: Resume a paused scheduled task — re-arms it and recalculates next due time.\n- **schedule_cancel**(task_id) [capability_expand category: scheduler]: Cancel a scheduled task permanently.\n- **tool_search**(query, max_results): Search all registered tools by keyword relevance. Returns a ranked list of matching tools with name, category, and summary. Use this when you need a tool but are unsure of its exact name or category.\n- **tool_describe**(tool_name): Get the full callable schema of a single tool by exact name. Returns the tool's description, parameters, and metadata. Use after tool_search to inspect a specific tool before calling it.\n- **hw_list**(): List connected hardware devices with their channel counts and measured quantities. Start here; it does not include operating limits.\n- **hw_describe**(device_id): Return the full reference for one device: every channel, its unit, its operating envelope, rate limit, reversibility, and required interlocks, plus any prior outcomes recorded for its writable channels. Required before commanding a device.\n- **hw_read**(device_id, channel_id): Read the current value of one device channel. Has no physical effect.\n- **hw_status**(device_id): Report connection health, halt capability, and recent notable events (threshold excursions, lost samples, stalled channels) for one device.\n- **hw_configure**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Set a configuration value or setpoint on a channel declaring effect=configure. Setpoints often have inertia: the value may need time to stabilise.\n- **hw_actuate**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Command motion or output on a channel declaring effect=actuate. This moves physical hardware; a repeat from an unknown state is not a safe retry.\n- **hw_dispense**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Consume an irreversible resource on a channel declaring effect=dispense. Running this twice dispenses twice; never repeat it after a failure without first verifying what already happened.\n- **hw_estop**(device_id) [capability_expand category: hardware]: Stop all motion and output on a device immediately. Never requires approval; use it whenever device behaviour is unexpected.\n- **hw_batch_actuate**(device_id, commands, dry_run, verify) [capability_expand category: hardware]: Write multiple actuator channels in one atomic transaction. Used for coordinated multi-joint control where all positions must be commanded simultaneously (e.g. robot arm joint-space control).\n- **session_search**(query, limit) [capability_expand category: unclassified]: Search past conversation sessions in the current workspace for relevant context.\n- **session_list**(limit): List recent conversation sessions in the current workspace with stable ids, titles, dates, and summaries. Use for browsing past tasks or when user asks to see history without specific search terms. No keywords needed — returns chronological list.\n- **session_detail**(session_id, limit, offset, include_inactive) [capability_expand category: unclassified]: Read a paginated persisted transcript for one past conversation session in the current workspace. Use after session_list or session_search returns a session_id.\n- **click**(element_index) [capability_expand category: desktop]: Click a UI element by its element_index (from the latest observe_ui snapshot)\n- **get_clipboard**() [capability_expand category: desktop]: Read current clipboard text content\n- **list_apps**(filter, running_only) [capability_expand category: desktop]: List available applications on this system. Use to discover correct bundle_id before switch_app.\n- **list_windows**() [capability_expand category: desktop]: List all top-level windows with pid, window_id, title, and per-window state (minimized, on-screen). Call this first to pick the pid and window_id that observe_ui and other window tools require.\n- **observe_ui**(pid, window_id, query) [capability_expand category: desktop]: Snapshot one window's actionable UI elements, each tagged with an element_index for click/right_click/read_text. Re-observe after actions — indices belong to one snapshot. Requires the window's pid and window_id from list_windows.\n- **open_url**(url, app_id) [capability_expand category: desktop]: Open a URL in the default or specified browser\n- **read_text**(element_index) [capability_expand category: desktop]: Read the text content of a specific UI element from the latest snapshot\n- **right_click**(element_index) [capability_expand category: desktop]: Right-click a UI element to open its context menu. Returns visible menu items.\n- **screenshot**(pid, window_id) [capability_expand category: desktop]: Capture a screenshot for visual verification. With pid + window_id captures that window (works across all displays); defaults to the last observed window, or the full desktop when no window has been observed.\n- **scroll**(element_index, direction, amount, pid, window_id) [capability_expand category: desktop]: Scroll a scrollable area of a window. Omit element_index to scroll the window's focused/page scroller; pass one to scroll an exact element from the latest snapshot.\n- **select_text**(element_index) [capability_expand category: desktop]: Select all text in a UI element (focus + select-all, for subsequent copy)\n- **set_clipboard**(text) [capability_expand category: desktop]: Write text to the clipboard\n- **shortcut**(keys) [capability_expand category: desktop]: Execute a keyboard shortcut\n- **switch_app**(app_id) [capability_expand category: desktop]: Switch to an app (launch if needed, activate, verify)\n- **type_text**(text) [capability_expand category: desktop]: Type text into the currently focused element\n- **wait**(seconds) [capability_expand category: desktop]: Wait for a specified duration before continuing\n- **wait_until**(condition, pid, window_id, timeout, poll_interval) [capability_expand category: desktop]: Wait until a UI condition is met (polls UI tree). Returns elements when found or on timeout.\n- **wait_until_stable**(timeout, poll_interval, pid, window_id) [capability_expand category: desktop]: Wait until the UI stops changing (element set stabilizes across polls).\n\n## App Connector Capability Index\nLeapFlow can onboard and manage external apps through `platform_connect` and execute exact registered business actions through `platform_action`.\nFor requests about connecting, setting up, configuring, enabling, or managing a supported app, use `platform_connect` first instead of generating SDK/Webhook sample code.\n`platform_connect.action` is the management namespace: list, guide, preflight, connect, disconnect, remove, status, events_start, events_stop, events_status.\n`platform_action.action` is only for exact registered platform business actions listed below, such as `im.send_message`; never use management actions like `list` or `guide` there.\nAll business fields MUST go inside `payload`; top-level keys are only `platform`, `action`, and `payload`.\nDo not invent platform IDs or platform action names. If the needed action is not listed, ask for discovery/clarification instead of guessing.\nUse `platform_connect` with `action='guide'` and the matching `platform` to start onboarding; use `action='list'` when the app is unclear.\nWhen a pending onboarding state is present, continue from that state with `platform_connect` instead of asking the user to restate the app.\nSupported apps:\n- `api_server`: API Server (OpenAI Compatible) (category=api; backend=adapter; platform_action actions=none registered)\n- `dingtalk`: 钉钉 (DingTalk) (category=im; backend=adapter; platform_action actions=none registered)\n- `feishu`: 飞书 (Feishu/Lark) (category=im; backend=cli)\n - `calendar.create_event` payload={summary*, start_time*, end_time*, attendees} [write/high]\n - `docs.create_markdown` payload={title*, markdown*, folder_token} [write/medium]\n - `drive.search` payload={query*, limit} [read/medium]\n - `im.add_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.download_resource` payload={message_id*, file_key*, type*} [read/low]\n - `im.get_messages` payload={message_ids*} [read/low]\n - `im.list_chats` payload={page_size} [read/low]\n - `im.list_messages` payload={chat_id*} [read/low]\n - `im.list_thread_messages` payload={thread*} [read/low]\n - `im.remove_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.reply_message` payload={message_id*, text*} [send/high]\n - `im.search_chats` payload={query*, page_size} [read/low]\n - `im.search_messages` payload={query*} [read/medium]\n - `im.send_message` payload={chat_id*, text*, thread_id} [send/high]\n - `im.update_card` payload={token*, card*} [write/medium]\n - `im.update_message` payload={message_id*, text*} [write/medium]\n - `mail.search_unread` payload={query, limit} [read/high]\n - `sheets.append_row` payload={spreadsheet_token*, sheet_id*, values*} [write/medium]\n - `task.create` payload={title*, description, due_time} [write/medium]\n- `telegram`: Telegram (category=social; backend=adapter; platform_action actions=none registered)\n- `webhook`: Webhook (Generic) (category=webhook; backend=adapter; platform_action actions=none registered)\n\n## Learned Skills\nYou have access to the following learned skills. Use `skills_list` to browse or `skill_view` to read details:\n- api_designer: REST and GraphQL API design with OpenAPI spec generation and best practices [API, REST, GraphQL]\n- code_review: Structured code review for security, performance, and style [code, review, security]\n- data_analysis: CSV/JSON data analysis with statistical summaries, quality checks, and visualiza [data, analysis, statistics]\n- devops_helper: CI/CD pipeline configuration, Docker orchestration, and deployment strategy [devops, CI/CD, Docker]\n- document_writer: Structured document generation: technical docs, API references, reports, and REA [documentation, technical-writing, README]\n- git_workflow: Git operation orchestration: branching, commits, conflicts, and PR workflow [git, version-control, branching]\n- i18n_helper: Internationalization workflow: string extraction, translation, locale management [i18n, internationalization, localization]\n- project_analysis: Project structure analysis, dependency audit, and tech stack identification [project, analysis, dependencies]\n- security_audit: Security vulnerability scanning strategy, threat modeling, and fix recommendatio [security, vulnerability, audit]\n- shell_automation: Natural language to shell commands with safety review and step-by-step execution [shell, terminal, automation]\n- test_writer: Generate unit and integration tests from source code with edge-case coverage [testing, unit-test, integration-test]\n- web_research: Multi-step web search with cross-verification and structured synthesis [web, search, research]\n\n## Tool Usage\nTools are normally invoked through the native function-calling mechanism, not by writing JSON in your reply\ntext. Only if the provider signals that native function calling is unavailable for this turn, fall back to a\nsingle JSON code block: `{\"name\": \"tool_name\", \"arguments\": {\"key\": \"value\"}}` — use this fallback format\nonly, never both. Only call a tool whose exact name appears in the tool index above and is currently callable\n(or reachable via `capability_expand`). Never invent, rename, alias, or guess a tool name, platform ID, or\nplatform action from argument shape or wording — if the index does not list it, it does not exist.\n`platform_connect.action` (list/guide/preflight/connect/disconnect/remove/status/events_start/events_stop/\nevents_status) is the App Connector management namespace; `platform_action.action` only accepts exact\nregistered business actions such as `im.send_message` shown in the App Connector Capability Index — never mix\nthe two namespaces. If a tool call returns an unknown/unavailable result, use the returned suggestions or\navailable names for a single retry instead of trying further variations of the same guess.\n\n**Side-effect action rule** (`platform_action` with effect=send/write/execute):\n- Call each unique action+payload **exactly once**. Never include duplicates in the same turn.\n- Once the result returns `\"completed\": true`, that action is DONE for this task. Do NOT call it again\n in any subsequent turn — immediately summarize the result for the user instead.\n- The system enforces idempotency: duplicate calls are blocked and will not execute.\n- If the user explicitly requests sending/writing multiple times, use distinct payloads per call.\n\n**Resource identifier provenance rule**:\n- NEVER fabricate, guess, or infer resource identifiers (chat_id, message_id, file_key, user_id, etc.).\n Every resource ID used in a side-effect action MUST come from a successful API response in this session.\n- If a read/list action fails (e.g. authorization error), you do NOT have valid resource IDs.\n Report the failure to the user — do NOT attempt the dependent write/send action with a guessed ID.\n- When a tool result contains `\"llm_instruction\"`, follow it exactly.\n\n## Guidelines\n1. **Direct answers first**: If you already know the answer, respond directly without tools.\n2. **Avoid redundant tool calls**: Do not call the same tool with the same arguments more than once in the same user turn. When an existing tool result already answers the user's request, stop calling tools and answer directly.\n3. **Use tools proactively**: When the user asks about files, time, system state, or needs actions performed, use the appropriate tool.\n4. **Chain tools when needed**: You can call multiple tools in sequence (e.g., list files → read file → summarize).\n5. **Handle failures gracefully**: If a tool fails, explain what went wrong and suggest alternatives. If it failed because of an unknown tool/platform/action name, retry once with an exact name from the returned suggestions, then explain rather than keep guessing.\n6. **Summarize results naturally**: After tool execution, synthesize the results into a helpful answer rather than dumping raw output.\n7. **Stay conversational**: Maintain a natural, helpful tone. Acknowledge context from earlier in the conversation.\n8. **Recall past work**:\n - Broad queries (\"之前做了什么\", \"列出任务\"): answer from the \"Recent Task History\" section already in your context. If insufficient, call session_list.\n - Specific lookups (\"上次那个配置怎么改的\"): call session_search with relevant phrases (NOT single characters).\n - Do NOT call search tools repeatedly with keyword fragments. One well-phrased call is better than ten fragmented ones.\n\n## Coding & Verification\nWhen working with code, prefer the precise built-in tools over ad-hoc shell: use `repo_map` to orient in an\nunfamiliar project, `code_search`/`file_find` to locate and `code_intel` for symbols, `edit_file` (anchored\nsearch-replace, or a unified `diff`) to change files — never rewrite a whole file to change a few lines — and\n`git_query` to inspect diffs/log. After edits, check `syntax_ok` in the result and run `test_run`/`lint_check`\nbefore declaring the work done. Batch independent read-only calls (search/read/query) into a single turn — they\nrun in parallel.\n\n## Presentation Style\n1. **Polished Markdown only**: Format user-facing answers with clean Markdown headings, short paragraphs, and concise bullets. Use tables only when they improve comparison or scanning.\n2. **Terminal-friendly layout**: Keep lines readable in a TUI; avoid dense walls of text, deeply nested lists, oversized ASCII art, or heavy visual blocks.\n3. **Elegant emphasis**: Use bold text sparingly for key terms and conclusions. Avoid excessive emojis, decorative symbols, repeated separators, or visual noise.\n4. **Theme-safe colors**: Do not emit ANSI escape codes, HTML color tags, Rich markup, or hardcoded color names. Rely on the TUI theme to render Markdown professionally.\n5. **No leaked tool protocol**: Never show tool-call JSON, internal schemas, raw observations, tool result payloads, or hidden reasoning in the final answer unless the user explicitly asks for raw/debug output. Treat any prior `{\"name\": ..., \"arguments\": ...}` blocks and `Tool result (...)` messages as internal execution context only.\n6. **Professional closure**: End with a concise conclusion or next step when helpful; avoid rambling after the useful answer is complete.\n\nWhen finished with all tool calls, respond normally without a JSON block, tool-call transcript, or process log.\n\n## Task Contract\n- Task ID: turn-4\n- Original user request: Configure the bench_r8 homed channel to true to complete homing.\n- Workspace root: \n- Allowed roots: \n- Treat relative project paths as relative to the workspace root; never infer `.` as the project root when a workspace root is provided.\n- Workspace boundary is enforced by tools: do not read, search, edit, or run commands against paths outside the allowed roots unless the user explicitly requests an external path and the tool/approval policy permits it.\n- LeapFlow workspace config is optional at `/.leapflow/config.yaml`; runtime config is loaded from `~/.leapflow/config/user.yaml` and `~/.leapflow/profiles//config/*.yaml`.\n- Preserve this task contract across summarization, compression, tool loops, and memory retrieval.\n" + }, + { + "role": "assistant", + "content": "Discovered the simulated bench and read its channel limits." + }, + { + "role": "user", + "content": "Read the current value of the setpoint channel on bench_r8." + }, + { + "role": "assistant", + "content": "[Called: hw_read]\nRead the setpoint channel." + }, + { + "role": "user", + "content": "Actuate the bench_r8 setpoint to 60.\nConfigure the bench_r8 homed channel to true to complete homing.\nConfigure the bench_r8 homed channel to true to complete homing." + }, + { + "role": "assistant", + "content": "", + "tool_calls": [ + "hw_configure" + ] + }, + { + "role": "tool", + "content": "{\"ok\": true, \"device_id\": \"bench_r8\", \"channel_id\": \"homed\", \"side_effect_state\": \"committed\", \"settled\": true, \"execution_id\": \"\", \"idempotency_key\": \"\", \"execution_policy\": \"external_side_effect\", \"tool_call_id\": \"call_1\", \"execution_status\": \"completed\"}", + "tool_result": true + } + ], + "tools": [ + "capability_expand", + "code_intel", + "code_search", + "config_get", + "config_list", + "env_info", + "file_find", + "file_list", + "file_read", + "git_query", + "hw_actuate", + "hw_batch_actuate", + "hw_configure", + "hw_describe", + "hw_dispense", + "hw_estop", + "hw_list", + "hw_read", + "hw_status", + "lint_check", + "memory_search", + "plugin_list", + "plugin_propose", + "plugin_status", + "plugin_versions", + "repo_map", + "research_note", + "runtime_snapshot", + "schedule_reentry", + "self_describe", + "session_list", + "skill_view", + "skills_list", + "terminal_list", + "terminal_read", + "test_run", + "text_search", + "time_get", + "tool_describe", + "tool_search", + "web_fetch" + ] + }, + "responses": [ + { + "status": 200, + "content_type": "application/json", + "body": "{\"id\": \"chatcmpl-cassette\", \"object\": \"chat.completion\", \"model\": \"cassette-model\", \"choices\": [{\"index\": 0, \"message\": {\"role\": \"assistant\", \"content\": \"Homed the device; the readiness gate is now satisfied.\"}, \"finish_reason\": \"stop\"}], \"usage\": {\"prompt_tokens\": 64, \"completion_tokens\": 16, \"total_tokens\": 80}}" + } + ] +} diff --git a/tests/_fixtures/cassettes/r8_hardware/cassette-model-352db33d2794588e.cassette.json b/tests/_fixtures/cassettes/r8_hardware/cassette-model-352db33d2794588e.cassette.json new file mode 100644 index 00000000..7ab16c1f --- /dev/null +++ b/tests/_fixtures/cassettes/r8_hardware/cassette-model-352db33d2794588e.cassette.json @@ -0,0 +1,80 @@ +{ + "fingerprint": "352db33d2794588e5aae455a4bb9a9c7fd97a39afce16426feb39da9a6925ff8", + "note": "captured in seed mode", + "request": { + "model": "cassette-model", + "stream": false, + "messages": [ + { + "role": "system", + "content": "You are LeapFlow, an intelligent assistant that can both converse naturally and take real actions on the user's computer.\n\n## Capabilities\nThe tool index below lists **every** registered tool by name and a one-line summary — this is the complete\ncapability contract; nothing else exists. Only a subset is directly callable this turn (via native tool calling,\nnot a JSON block in your reply). If you need a tool from the index that is not yet callable, call\n`capability_expand` with its category name first — the matching tools become callable immediately after.\nWhen the user asks about LeapFlow **itself** — its identity/version/model, what tools, plugins, or capabilities\nit has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of\nself-awareness — answer from live runtime evidence via `self_describe` (facet=capabilities for \"what can you\ndo / do you support X\", identity for version/model, runtime for context/posture, or `all` for a full self-check),\nnever by reading LeapFlow's own source code, documentation, or memory. Report configuration-dependent or\nunavailable capabilities as limitations rather than inferring them.\n- **text_search**(text, pattern): Search for a regex pattern in text.\n- **text_replace**(text, old, new, count) [capability_expand category: write]: Replace occurrences of a substring in text.\n- **time_get**(): Get current date and time.\n- **env_info**(): Get system environment information (OS, Python version, cwd).\n- **skills_list**(query, category, source): List available learned skills. Use when user asks about capabilities or you need a specific skill.\n- **skill_view**(name): View the full content of a specific skill document.\n- **skill_list_files**(name) [capability_expand category: skills]: List all support files in a skill directory (configs, templates, examples). Use before skill_file_read to discover available files.\n- **skill_file_read**(name, file_path) [capability_expand category: skills]: Read an individual support file from a skill directory. Use skill_list_files first to discover available files.\n- **code_intel**(path, operation): Precise document symbols (outline) for a source file: classes, functions, and methods with line ranges. Python uses an exact AST parse; other languages use a keyword-prefix scan. Prefer over file_read mode=symbols for accurate navigation before editing. Read-only.\n- **repo_map**(path): Compact project orientation for a repository root: languages, detected test/lint commands, top-level structure, entry points, manifest, and VCS branch. Call this first when entering an unfamiliar codebase. Read-only.\n- **scm_sync**(action, cwd, remote, pull_ref, push_ref, timeout) [capability_expand category: scm]: Run a typed git SCM action. Use this instead of shell_run for git pull/push/status. For 'pull origin main then push', set action='pull_then_push', remote='origin', pull_ref='main', and omit push_ref so LeapFlow pushes the current local branch.\n- **git_query**(action, cwd, ref, path, staged, max_count, stat): Read-only structured git inspection: action=diff|log|status|branch|show. Prefer over shell_run for reading repo state — output is clipped, redacted, and log/branch are parsed into structured fields. Use scm_sync for pull/push.\n- **git_write**(action, cwd, message, stage_all, name, ref, create) [capability_expand category: scm]: Mutating git actions: action=commit (message, stage_all), branch (create+switch), checkout (switch; create=true for -b). Approval-gated. Use scm_sync for pull/push and git_query for reads.\n- **test_run**(command, cwd, timeout): Run the project's test suite and return structured results (framework, passed/failed counts, failing tests). Auto-detects the runner (pytest/npm/go/cargo) or uses a configured/explicit command; executes via the governed shell. ok=true means the runner executed — see 'success' for pass/fail.\n- **lint_check**(command, cwd, timeout): Run the project's linter and return a structured clean/issue result. Auto-detects the linter (ruff/eslint/go vet/clippy) or uses a configured/explicit command; executes via the governed shell. ok=true means the linter ran — see 'clean'.\n- **file_list**(path, pattern, depth): List files and directories at a given path. Use depth=1 or depth=2 to get a recursive tree in one call instead of listing each sub-directory separately.\n- **file_read**(path, max_lines, start_line, max_chars, mode): Read text file content with adaptive context governance. For large or unfamiliar files, prefer mode='outline' or mode='symbols' first, then use mode='raw' with start_line/max_lines for the specific range you actually need. For LeapFlow's own settings, use config_list / config_get / config_set — its config files are outside the workspace and not readable here.\n- **file_write**(path, content, mode) [capability_expand category: write]: Write content to a file (overwrite or append).\n- **code_search**(pattern, patterns, path, glob, ignore_case, multiline, max_results, context_lines): Search file CONTENTS by regex pattern across a directory tree (ripgrep-backed). Requires a regex pattern. NOT for listing or browsing directory contents — use file_list for that. Prefer this over shell_run grep: faster, skips VCS/dependency/build dirs, and returns structured path:line:column matches. Batch related lookups into ONE call via `patterns` (OR-combined, single pass) instead of issuing several separate searches. Use file_read for the surrounding context of a hit.\n- **file_find**(glob, path, max_results): Find files by a recursive glob pattern under a base path (e.g. '**/test_*.py' or '*.md'). Prefer this over shell_run find; skips VCS/dependency/build dirs.\n- **edit_file**(path, edits, dry_run, diff) [capability_expand category: write]: Apply targeted, anchored search-replace edits to an EXISTING text file (use file_write to create/overwrite). Each edit is {original_text, new_text, replace_all?}; original_text must match exactly and uniquely (or set replace_all) — a non-unique or missing anchor is rejected so files are never corrupted. Set dry_run to preview. Alternatively pass a unified 'diff' to apply its hunks as anchored edits. Far cheaper and safer than rewriting a whole file.\n- **shell_run**(command, cwd, timeout) [capability_expand category: shell]: Execute a one-shot shell command with timeout protection. Runs in the active workspace; paths resolving outside it are refused. Reach for a structured tool first when one fits — web_fetch for anything over HTTP(S), git_query/scm_sync for git, code_search/file_find/file_read for the repo, config_get/config_set for LeapFlow's own settings — because those report typed results, while a failed shell command can only be diagnosed from its exit code and stderr. Every shell run counts as an external side effect, so a failure stops the rest of the batch and is not retried automatically.\n- **terminal_open**(command, cwd, shell) [capability_expand category: terminal]: Open a PERSISTENT shell session (REPL/dev server/watch), returning a session_id for terminal_send/read/close. Disabled unless tools.terminal_session_enabled is set. For one-shot commands use shell_run instead.\n- **terminal_send**(session_id, input, wait) [capability_expand category: terminal]: Send a line of input to a persistent terminal session and return output captured shortly after.\n- **terminal_read**(session_id, wait): Drain buffered output from a persistent terminal session (optionally waiting briefly first).\n- **terminal_close**(session_id) [capability_expand category: terminal]: Terminate a persistent terminal session and release its process group.\n- **terminal_list**(): List active persistent terminal sessions.\n- **config_list**(category, limit): List LeapFlow's own writable settings (LLM model and endpoint, daemon, memory, perception, gateway, …) with current values. Use this to discover the exact key before changing anything. Optionally narrow by `category`. This is the only correct way to inspect LeapFlow configuration — never read config files from disk.\n- **config_get**(key): Read one LeapFlow setting by key (e.g. 'llm.model', 'llm.base_url', 'daemon.log_level'), returning its current value, type, scopes, and whether a change needs a daemon restart. There is no 'llm.provider' key: provider behavior is inferred from the OpenAI-compatible 'llm.base_url'. Never read LeapFlow config files from disk — use this.\n- **config_set**(key, value, scope) [capability_expand category: config]: Change one LeapFlow setting by key, e.g. switch an OpenAI-compatible LLM with key='llm.model' and key='llm.base_url'. There is no 'llm.provider' key: provider behavior is inferred from the endpoint. Values are validated and coerced; credentials are stored in the vault automatically. Call config_list or config_get first if unsure of the key. The result states whether a `leap daemon restart` is required. Never edit LeapFlow config files directly.\n- **web_fetch**(url, select, timeout, max_bytes): Read a URL over HTTP(S) and get back extracted, context-sized content: parsed JSON for API endpoints, readable text plus links for web pages. Use this for anything on the internet — prices, docs, releases, articles — instead of running curl through shell_run: it reports real HTTP status codes, retries rate limits on its own, and is a plain read so a retry is always safe. For JSON APIs pass `select` with a dotted path (e.g. 'chart.result.0.meta') to return just that part instead of the whole payload.\n- **memory_search**(query, limit): Search agent memory for relevant past experiences, observations, and facts.\n- **memory_add**(content, kind) [capability_expand category: write]: Store a new observation or insight in memory for future reference.\n- **research_note**(kind, text): Record a compact, structured note about the current task's state so it survives context compression on long / multi-step tasks. Use for durable findings, open questions still to resolve, decisions / excluded paths, and the immediate next step. One concise sentence per note.\n- **capability_expand**(category): Fetch the full callable schema for every tool in a capability category (e.g. 'hub', 'gateway', 'desktop', 'delegate', 'file', 'memory', 'skill'). The compact tool index always lists every registered tool by name and a one-line summary, but only a static low-risk subset is directly callable each turn. If you need a tool from the index that is not yet callable, call capability_expand with its category first; the matching tools become callable in this turn. Never invent a tool name — expand the category instead.\n- **delegate_task**(goal, context) [capability_expand category: delegate]: Delegate a complex sub-task to an isolated subagent. The subagent gets a fresh context and restricted tool access. Use when a task is self-contained and can be solved independently.\n- **schedule_reentry**(kind, reason, delay_seconds, event_match, max_reentries, deadline_seconds): Register a re-entry so this task can resume later from its current orientation (findings / open questions / next step). Use when work must pause and continue after a delay (kind=time) or when a matching platform event arrives (kind=event), instead of finishing now. The research-ledger state is carried over automatically.\n- **hub_push**(skill_name, visibility, version) [capability_expand category: hub]: Push a local skill to the ModelScope Hub for sharing or backup.\n- **hub_pull**(repo_id, version) [capability_expand category: hub]: Pull a skill from the ModelScope Hub to install locally.\n- **hub_search**(query) [capability_expand category: hub]: Search for skills on the Hub by keyword or description.\n- **hub_sync**(mode, dry_run) [capability_expand category: hub]: Preview or execute sync between local skills and Hub.\n- **platform_action**(platform, action, payload, backend_kind) [capability_expand category: gateway]: Execute an exact registered business action on an external platform through LeapFlow's App Connector layer. Actions must be copied from the App Connector Capability Index and are addressed as domain.operation, e.g. im.send_message or docs.create_markdown. All business fields (chat_id, text, query, etc.) MUST be placed inside `payload`, never at the top level. Example: {\"platform\":\"feishu\",\"action\":\"im.send_message\",\"payload\":{\"chat_id\":\"oc_xxx\",\"text\":\"hello\"}}. Do not invent action names, do not use management actions such as list/guide/connect/status here.\n- **platform_connect**(action, platform, credentials, options, checkpoint) [capability_expand category: gateway]: List, guide, connect, disconnect, remove, or check status for external platforms using the App Connector management namespace. Supports REST and CLI backends. Use this for management actions such as list/guide/preflight/connect/status; use platform_action only for exact registered business actions.\n- **gateway_send**(platform, chat_id, text, thread_id) [capability_expand category: gateway]: Send a message to a connected external platform (Feishu group, Telegram chat, DingTalk conversation, etc.). Requires the platform to be connected via gateway_connect first. Use gateway_connect with action='list' to see connected platforms and available chat IDs.\n- **gateway_connect**(action, platform, credentials, options) [capability_expand category: gateway]: Connect, configure, or manage external platform integrations (Feishu, DingTalk, Telegram, Slack, Discord, etc.). Conversational flow: 1) call 'guide' to get setup steps + required fields, 2) present the steps to the user and ask for ALL required credentials in a single message, 3) call 'connect' with the credentials. Goal: complete in 1–2 user turns. NEVER include credential values in your text response.\n- **plugin_list**(): List the live plugin registry across Tool/Gateway/LLM subsystems for plugin management: which plugins are loaded, their category and fiber lifecycle state. For questions about LeapFlow's own capabilities or whether it supports X, use self_describe instead.\n- **plugin_status**(plugin_id): Get detailed status of a specific plugin: its declared category, runtime dependencies, contributed tools, and fiber lifecycle state.\n- **plugin_versions**(plugin_id): List recorded source versions and active pointer for a profile-scoped plugin.\n- **plugin_propose**(requested_capability, plugin_id, proposed_tools, test_cases, risk_level, evidence): Create a side-effect-free PluginProposal from explicit capability-gap evidence. Use this before plugin_generate when a missing capability should be reviewed. Does not call an LLM, write files, or install anything.\n- **assess_compatibility**(manifest, source_path) [capability_expand category: plugin_management]: Assess whether a foreign plugin manifest or real DSH source bundle is compatible with LeapFlow. A source bundle assessment is static: runtime_ready stays false until restricted Node discovery during plugin_install. Returns component-level verdicts and limitations.\n- **plugin_generate**(plugin_id, description, proposal_id) [capability_expand category: system]: Generate a new ToolPlugin from a natural-language capability description. The LLM produces code that conforms to the ToolPlugin Protocol; it is then rigorously validated (syntax, structure, import, protocol conformance). The generated source is stored in CAS and receives explicit content approval. It DOES NOT install the plugin — installation requires a second, mutation-specific approval via plugin_install.\n- **plugin_install**(plugin_id, code, marketplace_name, source_path, proposal_id, version_label) [capability_expand category: system]: Install a plugin from exactly one source: validated Python code, the configured Python marketplace, or a real DSH source bundle. DSH bundles run restricted Node discovery before registration; only public host tools are installed and client UI limitations are reported. Writes profile-scoped state and mutates the process-global registry. REQUIRES APPROVAL.\n- **plugin_rollback**(plugin_id, version) [capability_expand category: system]: Rollback a profile-scoped plugin to a recorded source version and reload it. REQUIRES APPROVAL.\n- **plugin_reload**(plugin_id, version_label) [capability_expand category: system]: Hot-reload a plugin at runtime. Disposes the old plugin fiber, re-imports its module, and registers a fresh instance. Existing in-flight turns are unaffected (snapshot isolation). REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_disable**(plugin_id) [capability_expand category: system]: Disable a plugin by disposing its fiber, removing its tools from the runtime registry. Cannot disable self_management itself. REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_remove**(plugin_id, delete_source) [capability_expand category: system]: Terminally remove a plugin: dispose its fiber, unregister its tools, remove reload metadata, and optionally delete its profile-scoped source file. Cannot remove self_management itself. REQUIRES APPROVAL.\n- **plugin_unquarantine**(plugin_id) [capability_expand category: plugin_management]: Restore a quarantined plugin to probation status for re-evaluation. Unfreezes the trust ledger, transitions the proposal back to PROBATION, and reloads the plugin. REQUIRES APPROVAL.\n- **plugin_enable**(plugin_id) [capability_expand category: system]: Re-enable a previously disabled plugin by reloading its module and registering a fresh instance. REQUIRES APPROVAL.\n- **self_describe**(facet): Answer questions about LeapFlow ITSELF from live runtime evidence — its identity/version/model, what tools and plugins it has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of self-awareness. Prefer this over reading LeapFlow's own source code. Pick facet=capabilities for 'what can you do / do you support X', identity for version/model, runtime for context/posture, or all for a full self-check when explicitly requested.\n- **runtime_snapshot**(): Lightweight (~150 token) flat snapshot of current runtime state: model, context budget, posture, disclosure level, turn count, cache hit rate, uptime, tool count, and pending approvals.\n- **schedule_create**(trigger_expression, instruction, execution_mode, max_retries, delivery_target) [capability_expand category: scheduler]: Create a new scheduled task. Specify a trigger expression (e.g. '30m', 'every 2h', '0 9 * * *') and the instruction to execute on each trigger. Optionally provide an execution mode and a delivery target to receive notifications.\n- **schedule_list**() [capability_expand category: scheduler]: List all scheduled tasks with their current state, trigger, and next due time.\n- **schedule_status**(task_id) [capability_expand category: scheduler]: Get detailed status and recent execution history for a scheduled task.\n- **schedule_pause**(task_id) [capability_expand category: scheduler]: Pause a scheduled task so it stops firing without being cancelled.\n- **schedule_resume**(task_id) [capability_expand category: scheduler]: Resume a paused scheduled task — re-arms it and recalculates next due time.\n- **schedule_cancel**(task_id) [capability_expand category: scheduler]: Cancel a scheduled task permanently.\n- **tool_search**(query, max_results): Search all registered tools by keyword relevance. Returns a ranked list of matching tools with name, category, and summary. Use this when you need a tool but are unsure of its exact name or category.\n- **tool_describe**(tool_name): Get the full callable schema of a single tool by exact name. Returns the tool's description, parameters, and metadata. Use after tool_search to inspect a specific tool before calling it.\n- **hw_list**(): List connected hardware devices with their channel counts and measured quantities. Start here; it does not include operating limits.\n- **hw_describe**(device_id): Return the full reference for one device: every channel, its unit, its operating envelope, rate limit, reversibility, and required interlocks, plus any prior outcomes recorded for its writable channels. Required before commanding a device.\n- **hw_read**(device_id, channel_id): Read the current value of one device channel. Has no physical effect.\n- **hw_status**(device_id): Report connection health, halt capability, and recent notable events (threshold excursions, lost samples, stalled channels) for one device.\n- **hw_configure**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Set a configuration value or setpoint on a channel declaring effect=configure. Setpoints often have inertia: the value may need time to stabilise.\n- **hw_actuate**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Command motion or output on a channel declaring effect=actuate. This moves physical hardware; a repeat from an unknown state is not a safe retry.\n- **hw_dispense**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Consume an irreversible resource on a channel declaring effect=dispense. Running this twice dispenses twice; never repeat it after a failure without first verifying what already happened.\n- **hw_estop**(device_id) [capability_expand category: hardware]: Stop all motion and output on a device immediately. Never requires approval; use it whenever device behaviour is unexpected.\n- **hw_batch_actuate**(device_id, commands, dry_run, verify) [capability_expand category: hardware]: Write multiple actuator channels in one atomic transaction. Used for coordinated multi-joint control where all positions must be commanded simultaneously (e.g. robot arm joint-space control).\n- **session_search**(query, limit) [capability_expand category: unclassified]: Search past conversation sessions in the current workspace for relevant context.\n- **session_list**(limit): List recent conversation sessions in the current workspace with stable ids, titles, dates, and summaries. Use for browsing past tasks or when user asks to see history without specific search terms. No keywords needed — returns chronological list.\n- **session_detail**(session_id, limit, offset, include_inactive) [capability_expand category: unclassified]: Read a paginated persisted transcript for one past conversation session in the current workspace. Use after session_list or session_search returns a session_id.\n- **click**(element_index) [capability_expand category: desktop]: Click a UI element by its element_index (from the latest observe_ui snapshot)\n- **get_clipboard**() [capability_expand category: desktop]: Read current clipboard text content\n- **list_apps**(filter, running_only) [capability_expand category: desktop]: List available applications on this system. Use to discover correct bundle_id before switch_app.\n- **list_windows**() [capability_expand category: desktop]: List all top-level windows with pid, window_id, title, and per-window state (minimized, on-screen). Call this first to pick the pid and window_id that observe_ui and other window tools require.\n- **observe_ui**(pid, window_id, query) [capability_expand category: desktop]: Snapshot one window's actionable UI elements, each tagged with an element_index for click/right_click/read_text. Re-observe after actions — indices belong to one snapshot. Requires the window's pid and window_id from list_windows.\n- **open_url**(url, app_id) [capability_expand category: desktop]: Open a URL in the default or specified browser\n- **read_text**(element_index) [capability_expand category: desktop]: Read the text content of a specific UI element from the latest snapshot\n- **right_click**(element_index) [capability_expand category: desktop]: Right-click a UI element to open its context menu. Returns visible menu items.\n- **screenshot**(pid, window_id) [capability_expand category: desktop]: Capture a screenshot for visual verification. With pid + window_id captures that window (works across all displays); defaults to the last observed window, or the full desktop when no window has been observed.\n- **scroll**(element_index, direction, amount, pid, window_id) [capability_expand category: desktop]: Scroll a scrollable area of a window. Omit element_index to scroll the window's focused/page scroller; pass one to scroll an exact element from the latest snapshot.\n- **select_text**(element_index) [capability_expand category: desktop]: Select all text in a UI element (focus + select-all, for subsequent copy)\n- **set_clipboard**(text) [capability_expand category: desktop]: Write text to the clipboard\n- **shortcut**(keys) [capability_expand category: desktop]: Execute a keyboard shortcut\n- **switch_app**(app_id) [capability_expand category: desktop]: Switch to an app (launch if needed, activate, verify)\n- **type_text**(text) [capability_expand category: desktop]: Type text into the currently focused element\n- **wait**(seconds) [capability_expand category: desktop]: Wait for a specified duration before continuing\n- **wait_until**(condition, pid, window_id, timeout, poll_interval) [capability_expand category: desktop]: Wait until a UI condition is met (polls UI tree). Returns elements when found or on timeout.\n- **wait_until_stable**(timeout, poll_interval, pid, window_id) [capability_expand category: desktop]: Wait until the UI stops changing (element set stabilizes across polls).\n\n## App Connector Capability Index\nLeapFlow can onboard and manage external apps through `platform_connect` and execute exact registered business actions through `platform_action`.\nFor requests about connecting, setting up, configuring, enabling, or managing a supported app, use `platform_connect` first instead of generating SDK/Webhook sample code.\n`platform_connect.action` is the management namespace: list, guide, preflight, connect, disconnect, remove, status, events_start, events_stop, events_status.\n`platform_action.action` is only for exact registered platform business actions listed below, such as `im.send_message`; never use management actions like `list` or `guide` there.\nAll business fields MUST go inside `payload`; top-level keys are only `platform`, `action`, and `payload`.\nDo not invent platform IDs or platform action names. If the needed action is not listed, ask for discovery/clarification instead of guessing.\nUse `platform_connect` with `action='guide'` and the matching `platform` to start onboarding; use `action='list'` when the app is unclear.\nWhen a pending onboarding state is present, continue from that state with `platform_connect` instead of asking the user to restate the app.\nSupported apps:\n- `api_server`: API Server (OpenAI Compatible) (category=api; backend=adapter; platform_action actions=none registered)\n- `dingtalk`: 钉钉 (DingTalk) (category=im; backend=adapter; platform_action actions=none registered)\n- `feishu`: 飞书 (Feishu/Lark) (category=im; backend=cli)\n - `calendar.create_event` payload={summary*, start_time*, end_time*, attendees} [write/high]\n - `docs.create_markdown` payload={title*, markdown*, folder_token} [write/medium]\n - `drive.search` payload={query*, limit} [read/medium]\n - `im.add_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.download_resource` payload={message_id*, file_key*, type*} [read/low]\n - `im.get_messages` payload={message_ids*} [read/low]\n - `im.list_chats` payload={page_size} [read/low]\n - `im.list_messages` payload={chat_id*} [read/low]\n - `im.list_thread_messages` payload={thread*} [read/low]\n - `im.remove_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.reply_message` payload={message_id*, text*} [send/high]\n - `im.search_chats` payload={query*, page_size} [read/low]\n - `im.search_messages` payload={query*} [read/medium]\n - `im.send_message` payload={chat_id*, text*, thread_id} [send/high]\n - `im.update_card` payload={token*, card*} [write/medium]\n - `im.update_message` payload={message_id*, text*} [write/medium]\n - `mail.search_unread` payload={query, limit} [read/high]\n - `sheets.append_row` payload={spreadsheet_token*, sheet_id*, values*} [write/medium]\n - `task.create` payload={title*, description, due_time} [write/medium]\n- `telegram`: Telegram (category=social; backend=adapter; platform_action actions=none registered)\n- `webhook`: Webhook (Generic) (category=webhook; backend=adapter; platform_action actions=none registered)\n\n## Learned Skills\nYou have access to the following learned skills. Use `skills_list` to browse or `skill_view` to read details:\n- api_designer: REST and GraphQL API design with OpenAPI spec generation and best practices [API, REST, GraphQL]\n- code_review: Structured code review for security, performance, and style [code, review, security]\n- data_analysis: CSV/JSON data analysis with statistical summaries, quality checks, and visualiza [data, analysis, statistics]\n- devops_helper: CI/CD pipeline configuration, Docker orchestration, and deployment strategy [devops, CI/CD, Docker]\n- document_writer: Structured document generation: technical docs, API references, reports, and REA [documentation, technical-writing, README]\n- git_workflow: Git operation orchestration: branching, commits, conflicts, and PR workflow [git, version-control, branching]\n- i18n_helper: Internationalization workflow: string extraction, translation, locale management [i18n, internationalization, localization]\n- project_analysis: Project structure analysis, dependency audit, and tech stack identification [project, analysis, dependencies]\n- security_audit: Security vulnerability scanning strategy, threat modeling, and fix recommendatio [security, vulnerability, audit]\n- shell_automation: Natural language to shell commands with safety review and step-by-step execution [shell, terminal, automation]\n- test_writer: Generate unit and integration tests from source code with edge-case coverage [testing, unit-test, integration-test]\n- web_research: Multi-step web search with cross-verification and structured synthesis [web, search, research]\n\n## Tool Usage\nTools are normally invoked through the native function-calling mechanism, not by writing JSON in your reply\ntext. Only if the provider signals that native function calling is unavailable for this turn, fall back to a\nsingle JSON code block: `{\"name\": \"tool_name\", \"arguments\": {\"key\": \"value\"}}` — use this fallback format\nonly, never both. Only call a tool whose exact name appears in the tool index above and is currently callable\n(or reachable via `capability_expand`). Never invent, rename, alias, or guess a tool name, platform ID, or\nplatform action from argument shape or wording — if the index does not list it, it does not exist.\n`platform_connect.action` (list/guide/preflight/connect/disconnect/remove/status/events_start/events_stop/\nevents_status) is the App Connector management namespace; `platform_action.action` only accepts exact\nregistered business actions such as `im.send_message` shown in the App Connector Capability Index — never mix\nthe two namespaces. If a tool call returns an unknown/unavailable result, use the returned suggestions or\navailable names for a single retry instead of trying further variations of the same guess.\n\n**Side-effect action rule** (`platform_action` with effect=send/write/execute):\n- Call each unique action+payload **exactly once**. Never include duplicates in the same turn.\n- Once the result returns `\"completed\": true`, that action is DONE for this task. Do NOT call it again\n in any subsequent turn — immediately summarize the result for the user instead.\n- The system enforces idempotency: duplicate calls are blocked and will not execute.\n- If the user explicitly requests sending/writing multiple times, use distinct payloads per call.\n\n**Resource identifier provenance rule**:\n- NEVER fabricate, guess, or infer resource identifiers (chat_id, message_id, file_key, user_id, etc.).\n Every resource ID used in a side-effect action MUST come from a successful API response in this session.\n- If a read/list action fails (e.g. authorization error), you do NOT have valid resource IDs.\n Report the failure to the user — do NOT attempt the dependent write/send action with a guessed ID.\n- When a tool result contains `\"llm_instruction\"`, follow it exactly.\n\n## Guidelines\n1. **Direct answers first**: If you already know the answer, respond directly without tools.\n2. **Avoid redundant tool calls**: Do not call the same tool with the same arguments more than once in the same user turn. When an existing tool result already answers the user's request, stop calling tools and answer directly.\n3. **Use tools proactively**: When the user asks about files, time, system state, or needs actions performed, use the appropriate tool.\n4. **Chain tools when needed**: You can call multiple tools in sequence (e.g., list files → read file → summarize).\n5. **Handle failures gracefully**: If a tool fails, explain what went wrong and suggest alternatives. If it failed because of an unknown tool/platform/action name, retry once with an exact name from the returned suggestions, then explain rather than keep guessing.\n6. **Summarize results naturally**: After tool execution, synthesize the results into a helpful answer rather than dumping raw output.\n7. **Stay conversational**: Maintain a natural, helpful tone. Acknowledge context from earlier in the conversation.\n8. **Recall past work**:\n - Broad queries (\"之前做了什么\", \"列出任务\"): answer from the \"Recent Task History\" section already in your context. If insufficient, call session_list.\n - Specific lookups (\"上次那个配置怎么改的\"): call session_search with relevant phrases (NOT single characters).\n - Do NOT call search tools repeatedly with keyword fragments. One well-phrased call is better than ten fragmented ones.\n\n## Coding & Verification\nWhen working with code, prefer the precise built-in tools over ad-hoc shell: use `repo_map` to orient in an\nunfamiliar project, `code_search`/`file_find` to locate and `code_intel` for symbols, `edit_file` (anchored\nsearch-replace, or a unified `diff`) to change files — never rewrite a whole file to change a few lines — and\n`git_query` to inspect diffs/log. After edits, check `syntax_ok` in the result and run `test_run`/`lint_check`\nbefore declaring the work done. Batch independent read-only calls (search/read/query) into a single turn — they\nrun in parallel.\n\n## Presentation Style\n1. **Polished Markdown only**: Format user-facing answers with clean Markdown headings, short paragraphs, and concise bullets. Use tables only when they improve comparison or scanning.\n2. **Terminal-friendly layout**: Keep lines readable in a TUI; avoid dense walls of text, deeply nested lists, oversized ASCII art, or heavy visual blocks.\n3. **Elegant emphasis**: Use bold text sparingly for key terms and conclusions. Avoid excessive emojis, decorative symbols, repeated separators, or visual noise.\n4. **Theme-safe colors**: Do not emit ANSI escape codes, HTML color tags, Rich markup, or hardcoded color names. Rely on the TUI theme to render Markdown professionally.\n5. **No leaked tool protocol**: Never show tool-call JSON, internal schemas, raw observations, tool result payloads, or hidden reasoning in the final answer unless the user explicitly asks for raw/debug output. Treat any prior `{\"name\": ..., \"arguments\": ...}` blocks and `Tool result (...)` messages as internal execution context only.\n6. **Professional closure**: End with a concise conclusion or next step when helpful; avoid rambling after the useful answer is complete.\n\nWhen finished with all tool calls, respond normally without a JSON block, tool-call transcript, or process log.\n\n## Task Contract\n- Task ID: turn-6\n- Original user request: Actuate the bench_r8 setpoint to 65.\n- Workspace root: \n- Allowed roots: \n- Treat relative project paths as relative to the workspace root; never infer `.` as the project root when a workspace root is provided.\n- Workspace boundary is enforced by tools: do not read, search, edit, or run commands against paths outside the allowed roots unless the user explicitly requests an external path and the tool/approval policy permits it.\n- LeapFlow workspace config is optional at `/.leapflow/config.yaml`; runtime config is loaded from `~/.leapflow/config/user.yaml` and `~/.leapflow/profiles//config/*.yaml`.\n- Preserve this task contract across summarization, compression, tool loops, and memory retrieval.\n" + }, + { + "role": "assistant", + "content": "[Called: hw_configure]\nHomed the device; the readiness gate is now satisfied." + }, + { + "role": "user", + "content": "Preview an actuate of bench_r8 setpoint to 55, dry run only." + }, + { + "role": "assistant", + "content": "[Called: hw_actuate]\nDry run confirmed the setpoint command is feasible after homing." + }, + { + "role": "user", + "content": "Actuate the bench_r8 setpoint to 65.\nActuate the bench_r8 setpoint to 65." + } + ], + "tools": [ + "capability_expand", + "code_intel", + "code_search", + "config_get", + "config_list", + "env_info", + "file_find", + "file_list", + "file_read", + "git_query", + "hw_actuate", + "hw_batch_actuate", + "hw_configure", + "hw_describe", + "hw_dispense", + "hw_estop", + "hw_list", + "hw_read", + "hw_status", + "lint_check", + "memory_search", + "plugin_list", + "plugin_propose", + "plugin_status", + "plugin_versions", + "repo_map", + "research_note", + "runtime_snapshot", + "schedule_reentry", + "self_describe", + "session_list", + "skill_view", + "skills_list", + "terminal_list", + "terminal_read", + "test_run", + "text_search", + "time_get", + "tool_describe", + "tool_search", + "web_fetch" + ] + }, + "responses": [ + { + "status": 200, + "content_type": "application/json", + "body": "{\"id\": \"chatcmpl-cassette\", \"object\": \"chat.completion\", \"model\": \"cassette-model\", \"choices\": [{\"index\": 0, \"message\": {\"role\": \"assistant\", \"content\": \"\", \"tool_calls\": [{\"id\": \"call_1\", \"type\": \"function\", \"function\": {\"name\": \"hw_actuate\", \"arguments\": \"{\\\"device_id\\\": \\\"bench_r8\\\", \\\"channel_id\\\": \\\"setpoint\\\", \\\"value\\\": 65.0}\"}}]}, \"finish_reason\": \"tool_calls\"}], \"usage\": {\"prompt_tokens\": 64, \"completion_tokens\": 16, \"total_tokens\": 80}}" + } + ] +} diff --git a/tests/_fixtures/cassettes/r8_hardware/cassette-model-383884acaf1063e3.cassette.json b/tests/_fixtures/cassettes/r8_hardware/cassette-model-383884acaf1063e3.cassette.json new file mode 100644 index 00000000..c8bdcfc8 --- /dev/null +++ b/tests/_fixtures/cassettes/r8_hardware/cassette-model-383884acaf1063e3.cassette.json @@ -0,0 +1,80 @@ +{ + "fingerprint": "383884acaf1063e35a4e006eb879c45ea5299abf5803989e6ae6ac5714f0aef4", + "note": "captured in seed mode", + "request": { + "model": "cassette-model", + "stream": false, + "messages": [ + { + "role": "system", + "content": "You are LeapFlow, an intelligent assistant that can both converse naturally and take real actions on the user's computer.\n\n## Capabilities\nThe tool index below lists **every** registered tool by name and a one-line summary — this is the complete\ncapability contract; nothing else exists. Only a subset is directly callable this turn (via native tool calling,\nnot a JSON block in your reply). If you need a tool from the index that is not yet callable, call\n`capability_expand` with its category name first — the matching tools become callable immediately after.\nWhen the user asks about LeapFlow **itself** — its identity/version/model, what tools, plugins, or capabilities\nit has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of\nself-awareness — answer from live runtime evidence via `self_describe` (facet=capabilities for \"what can you\ndo / do you support X\", identity for version/model, runtime for context/posture, or `all` for a full self-check),\nnever by reading LeapFlow's own source code, documentation, or memory. Report configuration-dependent or\nunavailable capabilities as limitations rather than inferring them.\n- **text_search**(text, pattern): Search for a regex pattern in text.\n- **text_replace**(text, old, new, count) [capability_expand category: write]: Replace occurrences of a substring in text.\n- **time_get**(): Get current date and time.\n- **env_info**(): Get system environment information (OS, Python version, cwd).\n- **skills_list**(query, category, source): List available learned skills. Use when user asks about capabilities or you need a specific skill.\n- **skill_view**(name): View the full content of a specific skill document.\n- **skill_list_files**(name) [capability_expand category: skills]: List all support files in a skill directory (configs, templates, examples). Use before skill_file_read to discover available files.\n- **skill_file_read**(name, file_path) [capability_expand category: skills]: Read an individual support file from a skill directory. Use skill_list_files first to discover available files.\n- **code_intel**(path, operation): Precise document symbols (outline) for a source file: classes, functions, and methods with line ranges. Python uses an exact AST parse; other languages use a keyword-prefix scan. Prefer over file_read mode=symbols for accurate navigation before editing. Read-only.\n- **repo_map**(path): Compact project orientation for a repository root: languages, detected test/lint commands, top-level structure, entry points, manifest, and VCS branch. Call this first when entering an unfamiliar codebase. Read-only.\n- **scm_sync**(action, cwd, remote, pull_ref, push_ref, timeout) [capability_expand category: scm]: Run a typed git SCM action. Use this instead of shell_run for git pull/push/status. For 'pull origin main then push', set action='pull_then_push', remote='origin', pull_ref='main', and omit push_ref so LeapFlow pushes the current local branch.\n- **git_query**(action, cwd, ref, path, staged, max_count, stat): Read-only structured git inspection: action=diff|log|status|branch|show. Prefer over shell_run for reading repo state — output is clipped, redacted, and log/branch are parsed into structured fields. Use scm_sync for pull/push.\n- **git_write**(action, cwd, message, stage_all, name, ref, create) [capability_expand category: scm]: Mutating git actions: action=commit (message, stage_all), branch (create+switch), checkout (switch; create=true for -b). Approval-gated. Use scm_sync for pull/push and git_query for reads.\n- **test_run**(command, cwd, timeout): Run the project's test suite and return structured results (framework, passed/failed counts, failing tests). Auto-detects the runner (pytest/npm/go/cargo) or uses a configured/explicit command; executes via the governed shell. ok=true means the runner executed — see 'success' for pass/fail.\n- **lint_check**(command, cwd, timeout): Run the project's linter and return a structured clean/issue result. Auto-detects the linter (ruff/eslint/go vet/clippy) or uses a configured/explicit command; executes via the governed shell. ok=true means the linter ran — see 'clean'.\n- **file_list**(path, pattern, depth): List files and directories at a given path. Use depth=1 or depth=2 to get a recursive tree in one call instead of listing each sub-directory separately.\n- **file_read**(path, max_lines, start_line, max_chars, mode): Read text file content with adaptive context governance. For large or unfamiliar files, prefer mode='outline' or mode='symbols' first, then use mode='raw' with start_line/max_lines for the specific range you actually need. For LeapFlow's own settings, use config_list / config_get / config_set — its config files are outside the workspace and not readable here.\n- **file_write**(path, content, mode) [capability_expand category: write]: Write content to a file (overwrite or append).\n- **code_search**(pattern, patterns, path, glob, ignore_case, multiline, max_results, context_lines): Search file CONTENTS by regex pattern across a directory tree (ripgrep-backed). Requires a regex pattern. NOT for listing or browsing directory contents — use file_list for that. Prefer this over shell_run grep: faster, skips VCS/dependency/build dirs, and returns structured path:line:column matches. Batch related lookups into ONE call via `patterns` (OR-combined, single pass) instead of issuing several separate searches. Use file_read for the surrounding context of a hit.\n- **file_find**(glob, path, max_results): Find files by a recursive glob pattern under a base path (e.g. '**/test_*.py' or '*.md'). Prefer this over shell_run find; skips VCS/dependency/build dirs.\n- **edit_file**(path, edits, dry_run, diff) [capability_expand category: write]: Apply targeted, anchored search-replace edits to an EXISTING text file (use file_write to create/overwrite). Each edit is {original_text, new_text, replace_all?}; original_text must match exactly and uniquely (or set replace_all) — a non-unique or missing anchor is rejected so files are never corrupted. Set dry_run to preview. Alternatively pass a unified 'diff' to apply its hunks as anchored edits. Far cheaper and safer than rewriting a whole file.\n- **shell_run**(command, cwd, timeout) [capability_expand category: shell]: Execute a one-shot shell command with timeout protection. Runs in the active workspace; paths resolving outside it are refused. Reach for a structured tool first when one fits — web_fetch for anything over HTTP(S), git_query/scm_sync for git, code_search/file_find/file_read for the repo, config_get/config_set for LeapFlow's own settings — because those report typed results, while a failed shell command can only be diagnosed from its exit code and stderr. Every shell run counts as an external side effect, so a failure stops the rest of the batch and is not retried automatically.\n- **terminal_open**(command, cwd, shell) [capability_expand category: terminal]: Open a PERSISTENT shell session (REPL/dev server/watch), returning a session_id for terminal_send/read/close. Disabled unless tools.terminal_session_enabled is set. For one-shot commands use shell_run instead.\n- **terminal_send**(session_id, input, wait) [capability_expand category: terminal]: Send a line of input to a persistent terminal session and return output captured shortly after.\n- **terminal_read**(session_id, wait): Drain buffered output from a persistent terminal session (optionally waiting briefly first).\n- **terminal_close**(session_id) [capability_expand category: terminal]: Terminate a persistent terminal session and release its process group.\n- **terminal_list**(): List active persistent terminal sessions.\n- **config_list**(category, limit): List LeapFlow's own writable settings (LLM model and endpoint, daemon, memory, perception, gateway, …) with current values. Use this to discover the exact key before changing anything. Optionally narrow by `category`. This is the only correct way to inspect LeapFlow configuration — never read config files from disk.\n- **config_get**(key): Read one LeapFlow setting by key (e.g. 'llm.model', 'llm.base_url', 'daemon.log_level'), returning its current value, type, scopes, and whether a change needs a daemon restart. There is no 'llm.provider' key: provider behavior is inferred from the OpenAI-compatible 'llm.base_url'. Never read LeapFlow config files from disk — use this.\n- **config_set**(key, value, scope) [capability_expand category: config]: Change one LeapFlow setting by key, e.g. switch an OpenAI-compatible LLM with key='llm.model' and key='llm.base_url'. There is no 'llm.provider' key: provider behavior is inferred from the endpoint. Values are validated and coerced; credentials are stored in the vault automatically. Call config_list or config_get first if unsure of the key. The result states whether a `leap daemon restart` is required. Never edit LeapFlow config files directly.\n- **web_fetch**(url, select, timeout, max_bytes): Read a URL over HTTP(S) and get back extracted, context-sized content: parsed JSON for API endpoints, readable text plus links for web pages. Use this for anything on the internet — prices, docs, releases, articles — instead of running curl through shell_run: it reports real HTTP status codes, retries rate limits on its own, and is a plain read so a retry is always safe. For JSON APIs pass `select` with a dotted path (e.g. 'chart.result.0.meta') to return just that part instead of the whole payload.\n- **memory_search**(query, limit): Search agent memory for relevant past experiences, observations, and facts.\n- **memory_add**(content, kind) [capability_expand category: write]: Store a new observation or insight in memory for future reference.\n- **research_note**(kind, text): Record a compact, structured note about the current task's state so it survives context compression on long / multi-step tasks. Use for durable findings, open questions still to resolve, decisions / excluded paths, and the immediate next step. One concise sentence per note.\n- **capability_expand**(category): Fetch the full callable schema for every tool in a capability category (e.g. 'hub', 'gateway', 'desktop', 'delegate', 'file', 'memory', 'skill'). The compact tool index always lists every registered tool by name and a one-line summary, but only a static low-risk subset is directly callable each turn. If you need a tool from the index that is not yet callable, call capability_expand with its category first; the matching tools become callable in this turn. Never invent a tool name — expand the category instead.\n- **delegate_task**(goal, context) [capability_expand category: delegate]: Delegate a complex sub-task to an isolated subagent. The subagent gets a fresh context and restricted tool access. Use when a task is self-contained and can be solved independently.\n- **schedule_reentry**(kind, reason, delay_seconds, event_match, max_reentries, deadline_seconds): Register a re-entry so this task can resume later from its current orientation (findings / open questions / next step). Use when work must pause and continue after a delay (kind=time) or when a matching platform event arrives (kind=event), instead of finishing now. The research-ledger state is carried over automatically.\n- **hub_push**(skill_name, visibility, version) [capability_expand category: hub]: Push a local skill to the ModelScope Hub for sharing or backup.\n- **hub_pull**(repo_id, version) [capability_expand category: hub]: Pull a skill from the ModelScope Hub to install locally.\n- **hub_search**(query) [capability_expand category: hub]: Search for skills on the Hub by keyword or description.\n- **hub_sync**(mode, dry_run) [capability_expand category: hub]: Preview or execute sync between local skills and Hub.\n- **platform_action**(platform, action, payload, backend_kind) [capability_expand category: gateway]: Execute an exact registered business action on an external platform through LeapFlow's App Connector layer. Actions must be copied from the App Connector Capability Index and are addressed as domain.operation, e.g. im.send_message or docs.create_markdown. All business fields (chat_id, text, query, etc.) MUST be placed inside `payload`, never at the top level. Example: {\"platform\":\"feishu\",\"action\":\"im.send_message\",\"payload\":{\"chat_id\":\"oc_xxx\",\"text\":\"hello\"}}. Do not invent action names, do not use management actions such as list/guide/connect/status here.\n- **platform_connect**(action, platform, credentials, options, checkpoint) [capability_expand category: gateway]: List, guide, connect, disconnect, remove, or check status for external platforms using the App Connector management namespace. Supports REST and CLI backends. Use this for management actions such as list/guide/preflight/connect/status; use platform_action only for exact registered business actions.\n- **gateway_send**(platform, chat_id, text, thread_id) [capability_expand category: gateway]: Send a message to a connected external platform (Feishu group, Telegram chat, DingTalk conversation, etc.). Requires the platform to be connected via gateway_connect first. Use gateway_connect with action='list' to see connected platforms and available chat IDs.\n- **gateway_connect**(action, platform, credentials, options) [capability_expand category: gateway]: Connect, configure, or manage external platform integrations (Feishu, DingTalk, Telegram, Slack, Discord, etc.). Conversational flow: 1) call 'guide' to get setup steps + required fields, 2) present the steps to the user and ask for ALL required credentials in a single message, 3) call 'connect' with the credentials. Goal: complete in 1–2 user turns. NEVER include credential values in your text response.\n- **plugin_list**(): List the live plugin registry across Tool/Gateway/LLM subsystems for plugin management: which plugins are loaded, their category and fiber lifecycle state. For questions about LeapFlow's own capabilities or whether it supports X, use self_describe instead.\n- **plugin_status**(plugin_id): Get detailed status of a specific plugin: its declared category, runtime dependencies, contributed tools, and fiber lifecycle state.\n- **plugin_versions**(plugin_id): List recorded source versions and active pointer for a profile-scoped plugin.\n- **plugin_propose**(requested_capability, plugin_id, proposed_tools, test_cases, risk_level, evidence): Create a side-effect-free PluginProposal from explicit capability-gap evidence. Use this before plugin_generate when a missing capability should be reviewed. Does not call an LLM, write files, or install anything.\n- **assess_compatibility**(manifest, source_path) [capability_expand category: plugin_management]: Assess whether a foreign plugin manifest or real DSH source bundle is compatible with LeapFlow. A source bundle assessment is static: runtime_ready stays false until restricted Node discovery during plugin_install. Returns component-level verdicts and limitations.\n- **plugin_generate**(plugin_id, description, proposal_id) [capability_expand category: system]: Generate a new ToolPlugin from a natural-language capability description. The LLM produces code that conforms to the ToolPlugin Protocol; it is then rigorously validated (syntax, structure, import, protocol conformance). The generated source is stored in CAS and receives explicit content approval. It DOES NOT install the plugin — installation requires a second, mutation-specific approval via plugin_install.\n- **plugin_install**(plugin_id, code, marketplace_name, source_path, proposal_id, version_label) [capability_expand category: system]: Install a plugin from exactly one source: validated Python code, the configured Python marketplace, or a real DSH source bundle. DSH bundles run restricted Node discovery before registration; only public host tools are installed and client UI limitations are reported. Writes profile-scoped state and mutates the process-global registry. REQUIRES APPROVAL.\n- **plugin_rollback**(plugin_id, version) [capability_expand category: system]: Rollback a profile-scoped plugin to a recorded source version and reload it. REQUIRES APPROVAL.\n- **plugin_reload**(plugin_id, version_label) [capability_expand category: system]: Hot-reload a plugin at runtime. Disposes the old plugin fiber, re-imports its module, and registers a fresh instance. Existing in-flight turns are unaffected (snapshot isolation). REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_disable**(plugin_id) [capability_expand category: system]: Disable a plugin by disposing its fiber, removing its tools from the runtime registry. Cannot disable self_management itself. REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_remove**(plugin_id, delete_source) [capability_expand category: system]: Terminally remove a plugin: dispose its fiber, unregister its tools, remove reload metadata, and optionally delete its profile-scoped source file. Cannot remove self_management itself. REQUIRES APPROVAL.\n- **plugin_unquarantine**(plugin_id) [capability_expand category: plugin_management]: Restore a quarantined plugin to probation status for re-evaluation. Unfreezes the trust ledger, transitions the proposal back to PROBATION, and reloads the plugin. REQUIRES APPROVAL.\n- **plugin_enable**(plugin_id) [capability_expand category: system]: Re-enable a previously disabled plugin by reloading its module and registering a fresh instance. REQUIRES APPROVAL.\n- **self_describe**(facet): Answer questions about LeapFlow ITSELF from live runtime evidence — its identity/version/model, what tools and plugins it has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of self-awareness. Prefer this over reading LeapFlow's own source code. Pick facet=capabilities for 'what can you do / do you support X', identity for version/model, runtime for context/posture, or all for a full self-check when explicitly requested.\n- **runtime_snapshot**(): Lightweight (~150 token) flat snapshot of current runtime state: model, context budget, posture, disclosure level, turn count, cache hit rate, uptime, tool count, and pending approvals.\n- **schedule_create**(trigger_expression, instruction, execution_mode, max_retries, delivery_target) [capability_expand category: scheduler]: Create a new scheduled task. Specify a trigger expression (e.g. '30m', 'every 2h', '0 9 * * *') and the instruction to execute on each trigger. Optionally provide an execution mode and a delivery target to receive notifications.\n- **schedule_list**() [capability_expand category: scheduler]: List all scheduled tasks with their current state, trigger, and next due time.\n- **schedule_status**(task_id) [capability_expand category: scheduler]: Get detailed status and recent execution history for a scheduled task.\n- **schedule_pause**(task_id) [capability_expand category: scheduler]: Pause a scheduled task so it stops firing without being cancelled.\n- **schedule_resume**(task_id) [capability_expand category: scheduler]: Resume a paused scheduled task — re-arms it and recalculates next due time.\n- **schedule_cancel**(task_id) [capability_expand category: scheduler]: Cancel a scheduled task permanently.\n- **tool_search**(query, max_results): Search all registered tools by keyword relevance. Returns a ranked list of matching tools with name, category, and summary. Use this when you need a tool but are unsure of its exact name or category.\n- **tool_describe**(tool_name): Get the full callable schema of a single tool by exact name. Returns the tool's description, parameters, and metadata. Use after tool_search to inspect a specific tool before calling it.\n- **hw_list**(): List connected hardware devices with their channel counts and measured quantities. Start here; it does not include operating limits.\n- **hw_describe**(device_id): Return the full reference for one device: every channel, its unit, its operating envelope, rate limit, reversibility, and required interlocks, plus any prior outcomes recorded for its writable channels. Required before commanding a device.\n- **hw_read**(device_id, channel_id): Read the current value of one device channel. Has no physical effect.\n- **hw_status**(device_id): Report connection health, halt capability, and recent notable events (threshold excursions, lost samples, stalled channels) for one device.\n- **hw_configure**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Set a configuration value or setpoint on a channel declaring effect=configure. Setpoints often have inertia: the value may need time to stabilise.\n- **hw_actuate**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Command motion or output on a channel declaring effect=actuate. This moves physical hardware; a repeat from an unknown state is not a safe retry.\n- **hw_dispense**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Consume an irreversible resource on a channel declaring effect=dispense. Running this twice dispenses twice; never repeat it after a failure without first verifying what already happened.\n- **hw_estop**(device_id) [capability_expand category: hardware]: Stop all motion and output on a device immediately. Never requires approval; use it whenever device behaviour is unexpected.\n- **hw_batch_actuate**(device_id, commands, dry_run, verify) [capability_expand category: hardware]: Write multiple actuator channels in one atomic transaction. Used for coordinated multi-joint control where all positions must be commanded simultaneously (e.g. robot arm joint-space control).\n- **session_search**(query, limit) [capability_expand category: unclassified]: Search past conversation sessions in the current workspace for relevant context.\n- **session_list**(limit): List recent conversation sessions in the current workspace with stable ids, titles, dates, and summaries. Use for browsing past tasks or when user asks to see history without specific search terms. No keywords needed — returns chronological list.\n- **session_detail**(session_id, limit, offset, include_inactive) [capability_expand category: unclassified]: Read a paginated persisted transcript for one past conversation session in the current workspace. Use after session_list or session_search returns a session_id.\n- **click**(element_index) [capability_expand category: desktop]: Click a UI element by its element_index (from the latest observe_ui snapshot)\n- **get_clipboard**() [capability_expand category: desktop]: Read current clipboard text content\n- **list_apps**(filter, running_only) [capability_expand category: desktop]: List available applications on this system. Use to discover correct bundle_id before switch_app.\n- **list_windows**() [capability_expand category: desktop]: List all top-level windows with pid, window_id, title, and per-window state (minimized, on-screen). Call this first to pick the pid and window_id that observe_ui and other window tools require.\n- **observe_ui**(pid, window_id, query) [capability_expand category: desktop]: Snapshot one window's actionable UI elements, each tagged with an element_index for click/right_click/read_text. Re-observe after actions — indices belong to one snapshot. Requires the window's pid and window_id from list_windows.\n- **open_url**(url, app_id) [capability_expand category: desktop]: Open a URL in the default or specified browser\n- **read_text**(element_index) [capability_expand category: desktop]: Read the text content of a specific UI element from the latest snapshot\n- **right_click**(element_index) [capability_expand category: desktop]: Right-click a UI element to open its context menu. Returns visible menu items.\n- **screenshot**(pid, window_id) [capability_expand category: desktop]: Capture a screenshot for visual verification. With pid + window_id captures that window (works across all displays); defaults to the last observed window, or the full desktop when no window has been observed.\n- **scroll**(element_index, direction, amount, pid, window_id) [capability_expand category: desktop]: Scroll a scrollable area of a window. Omit element_index to scroll the window's focused/page scroller; pass one to scroll an exact element from the latest snapshot.\n- **select_text**(element_index) [capability_expand category: desktop]: Select all text in a UI element (focus + select-all, for subsequent copy)\n- **set_clipboard**(text) [capability_expand category: desktop]: Write text to the clipboard\n- **shortcut**(keys) [capability_expand category: desktop]: Execute a keyboard shortcut\n- **switch_app**(app_id) [capability_expand category: desktop]: Switch to an app (launch if needed, activate, verify)\n- **type_text**(text) [capability_expand category: desktop]: Type text into the currently focused element\n- **wait**(seconds) [capability_expand category: desktop]: Wait for a specified duration before continuing\n- **wait_until**(condition, pid, window_id, timeout, poll_interval) [capability_expand category: desktop]: Wait until a UI condition is met (polls UI tree). Returns elements when found or on timeout.\n- **wait_until_stable**(timeout, poll_interval, pid, window_id) [capability_expand category: desktop]: Wait until the UI stops changing (element set stabilizes across polls).\n\n## App Connector Capability Index\nLeapFlow can onboard and manage external apps through `platform_connect` and execute exact registered business actions through `platform_action`.\nFor requests about connecting, setting up, configuring, enabling, or managing a supported app, use `platform_connect` first instead of generating SDK/Webhook sample code.\n`platform_connect.action` is the management namespace: list, guide, preflight, connect, disconnect, remove, status, events_start, events_stop, events_status.\n`platform_action.action` is only for exact registered platform business actions listed below, such as `im.send_message`; never use management actions like `list` or `guide` there.\nAll business fields MUST go inside `payload`; top-level keys are only `platform`, `action`, and `payload`.\nDo not invent platform IDs or platform action names. If the needed action is not listed, ask for discovery/clarification instead of guessing.\nUse `platform_connect` with `action='guide'` and the matching `platform` to start onboarding; use `action='list'` when the app is unclear.\nWhen a pending onboarding state is present, continue from that state with `platform_connect` instead of asking the user to restate the app.\nSupported apps:\n- `api_server`: API Server (OpenAI Compatible) (category=api; backend=adapter; platform_action actions=none registered)\n- `dingtalk`: 钉钉 (DingTalk) (category=im; backend=adapter; platform_action actions=none registered)\n- `feishu`: 飞书 (Feishu/Lark) (category=im; backend=cli)\n - `calendar.create_event` payload={summary*, start_time*, end_time*, attendees} [write/high]\n - `docs.create_markdown` payload={title*, markdown*, folder_token} [write/medium]\n - `drive.search` payload={query*, limit} [read/medium]\n - `im.add_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.download_resource` payload={message_id*, file_key*, type*} [read/low]\n - `im.get_messages` payload={message_ids*} [read/low]\n - `im.list_chats` payload={page_size} [read/low]\n - `im.list_messages` payload={chat_id*} [read/low]\n - `im.list_thread_messages` payload={thread*} [read/low]\n - `im.remove_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.reply_message` payload={message_id*, text*} [send/high]\n - `im.search_chats` payload={query*, page_size} [read/low]\n - `im.search_messages` payload={query*} [read/medium]\n - `im.send_message` payload={chat_id*, text*, thread_id} [send/high]\n - `im.update_card` payload={token*, card*} [write/medium]\n - `im.update_message` payload={message_id*, text*} [write/medium]\n - `mail.search_unread` payload={query, limit} [read/high]\n - `sheets.append_row` payload={spreadsheet_token*, sheet_id*, values*} [write/medium]\n - `task.create` payload={title*, description, due_time} [write/medium]\n- `telegram`: Telegram (category=social; backend=adapter; platform_action actions=none registered)\n- `webhook`: Webhook (Generic) (category=webhook; backend=adapter; platform_action actions=none registered)\n\n## Learned Skills\nYou have access to the following learned skills. Use `skills_list` to browse or `skill_view` to read details:\n- api_designer: REST and GraphQL API design with OpenAPI spec generation and best practices [API, REST, GraphQL]\n- code_review: Structured code review for security, performance, and style [code, review, security]\n- data_analysis: CSV/JSON data analysis with statistical summaries, quality checks, and visualiza [data, analysis, statistics]\n- devops_helper: CI/CD pipeline configuration, Docker orchestration, and deployment strategy [devops, CI/CD, Docker]\n- document_writer: Structured document generation: technical docs, API references, reports, and REA [documentation, technical-writing, README]\n- git_workflow: Git operation orchestration: branching, commits, conflicts, and PR workflow [git, version-control, branching]\n- i18n_helper: Internationalization workflow: string extraction, translation, locale management [i18n, internationalization, localization]\n- project_analysis: Project structure analysis, dependency audit, and tech stack identification [project, analysis, dependencies]\n- security_audit: Security vulnerability scanning strategy, threat modeling, and fix recommendatio [security, vulnerability, audit]\n- shell_automation: Natural language to shell commands with safety review and step-by-step execution [shell, terminal, automation]\n- test_writer: Generate unit and integration tests from source code with edge-case coverage [testing, unit-test, integration-test]\n- web_research: Multi-step web search with cross-verification and structured synthesis [web, search, research]\n\n## Tool Usage\nTools are normally invoked through the native function-calling mechanism, not by writing JSON in your reply\ntext. Only if the provider signals that native function calling is unavailable for this turn, fall back to a\nsingle JSON code block: `{\"name\": \"tool_name\", \"arguments\": {\"key\": \"value\"}}` — use this fallback format\nonly, never both. Only call a tool whose exact name appears in the tool index above and is currently callable\n(or reachable via `capability_expand`). Never invent, rename, alias, or guess a tool name, platform ID, or\nplatform action from argument shape or wording — if the index does not list it, it does not exist.\n`platform_connect.action` (list/guide/preflight/connect/disconnect/remove/status/events_start/events_stop/\nevents_status) is the App Connector management namespace; `platform_action.action` only accepts exact\nregistered business actions such as `im.send_message` shown in the App Connector Capability Index — never mix\nthe two namespaces. If a tool call returns an unknown/unavailable result, use the returned suggestions or\navailable names for a single retry instead of trying further variations of the same guess.\n\n**Side-effect action rule** (`platform_action` with effect=send/write/execute):\n- Call each unique action+payload **exactly once**. Never include duplicates in the same turn.\n- Once the result returns `\"completed\": true`, that action is DONE for this task. Do NOT call it again\n in any subsequent turn — immediately summarize the result for the user instead.\n- The system enforces idempotency: duplicate calls are blocked and will not execute.\n- If the user explicitly requests sending/writing multiple times, use distinct payloads per call.\n\n**Resource identifier provenance rule**:\n- NEVER fabricate, guess, or infer resource identifiers (chat_id, message_id, file_key, user_id, etc.).\n Every resource ID used in a side-effect action MUST come from a successful API response in this session.\n- If a read/list action fails (e.g. authorization error), you do NOT have valid resource IDs.\n Report the failure to the user — do NOT attempt the dependent write/send action with a guessed ID.\n- When a tool result contains `\"llm_instruction\"`, follow it exactly.\n\n## Guidelines\n1. **Direct answers first**: If you already know the answer, respond directly without tools.\n2. **Avoid redundant tool calls**: Do not call the same tool with the same arguments more than once in the same user turn. When an existing tool result already answers the user's request, stop calling tools and answer directly.\n3. **Use tools proactively**: When the user asks about files, time, system state, or needs actions performed, use the appropriate tool.\n4. **Chain tools when needed**: You can call multiple tools in sequence (e.g., list files → read file → summarize).\n5. **Handle failures gracefully**: If a tool fails, explain what went wrong and suggest alternatives. If it failed because of an unknown tool/platform/action name, retry once with an exact name from the returned suggestions, then explain rather than keep guessing.\n6. **Summarize results naturally**: After tool execution, synthesize the results into a helpful answer rather than dumping raw output.\n7. **Stay conversational**: Maintain a natural, helpful tone. Acknowledge context from earlier in the conversation.\n8. **Recall past work**:\n - Broad queries (\"之前做了什么\", \"列出任务\"): answer from the \"Recent Task History\" section already in your context. If insufficient, call session_list.\n - Specific lookups (\"上次那个配置怎么改的\"): call session_search with relevant phrases (NOT single characters).\n - Do NOT call search tools repeatedly with keyword fragments. One well-phrased call is better than ten fragmented ones.\n\n## Coding & Verification\nWhen working with code, prefer the precise built-in tools over ad-hoc shell: use `repo_map` to orient in an\nunfamiliar project, `code_search`/`file_find` to locate and `code_intel` for symbols, `edit_file` (anchored\nsearch-replace, or a unified `diff`) to change files — never rewrite a whole file to change a few lines — and\n`git_query` to inspect diffs/log. After edits, check `syntax_ok` in the result and run `test_run`/`lint_check`\nbefore declaring the work done. Batch independent read-only calls (search/read/query) into a single turn — they\nrun in parallel.\n\n## Presentation Style\n1. **Polished Markdown only**: Format user-facing answers with clean Markdown headings, short paragraphs, and concise bullets. Use tables only when they improve comparison or scanning.\n2. **Terminal-friendly layout**: Keep lines readable in a TUI; avoid dense walls of text, deeply nested lists, oversized ASCII art, or heavy visual blocks.\n3. **Elegant emphasis**: Use bold text sparingly for key terms and conclusions. Avoid excessive emojis, decorative symbols, repeated separators, or visual noise.\n4. **Theme-safe colors**: Do not emit ANSI escape codes, HTML color tags, Rich markup, or hardcoded color names. Rely on the TUI theme to render Markdown professionally.\n5. **No leaked tool protocol**: Never show tool-call JSON, internal schemas, raw observations, tool result payloads, or hidden reasoning in the final answer unless the user explicitly asks for raw/debug output. Treat any prior `{\"name\": ..., \"arguments\": ...}` blocks and `Tool result (...)` messages as internal execution context only.\n6. **Professional closure**: End with a concise conclusion or next step when helpful; avoid rambling after the useful answer is complete.\n\nWhen finished with all tool calls, respond normally without a JSON block, tool-call transcript, or process log.\n\n## Task Contract\n- Task ID: turn-3\n- Original user request: Actuate the bench_r8 setpoint to 60.\n- Workspace root: \n- Allowed roots: \n- Treat relative project paths as relative to the workspace root; never infer `.` as the project root when a workspace root is provided.\n- Workspace boundary is enforced by tools: do not read, search, edit, or run commands against paths outside the allowed roots unless the user explicitly requests an external path and the tool/approval policy permits it.\n- LeapFlow workspace config is optional at `/.leapflow/config.yaml`; runtime config is loaded from `~/.leapflow/config/user.yaml` and `~/.leapflow/profiles//config/*.yaml`.\n- Preserve this task contract across summarization, compression, tool loops, and memory retrieval.\n" + }, + { + "role": "assistant", + "content": "[Called: hw_describe]\nDiscovered the simulated bench and read its channel limits." + }, + { + "role": "user", + "content": "Read the current value of the setpoint channel on bench_r8." + }, + { + "role": "assistant", + "content": "[Called: hw_read]\nRead the setpoint channel." + }, + { + "role": "user", + "content": "Actuate the bench_r8 setpoint to 60.\nActuate the bench_r8 setpoint to 60." + } + ], + "tools": [ + "capability_expand", + "code_intel", + "code_search", + "config_get", + "config_list", + "env_info", + "file_find", + "file_list", + "file_read", + "git_query", + "hw_actuate", + "hw_batch_actuate", + "hw_configure", + "hw_describe", + "hw_dispense", + "hw_estop", + "hw_list", + "hw_read", + "hw_status", + "lint_check", + "memory_search", + "plugin_list", + "plugin_propose", + "plugin_status", + "plugin_versions", + "repo_map", + "research_note", + "runtime_snapshot", + "schedule_reentry", + "self_describe", + "session_list", + "skill_view", + "skills_list", + "terminal_list", + "terminal_read", + "test_run", + "text_search", + "time_get", + "tool_describe", + "tool_search", + "web_fetch" + ] + }, + "responses": [ + { + "status": 200, + "content_type": "application/json", + "body": "{\"id\": \"chatcmpl-cassette\", \"object\": \"chat.completion\", \"model\": \"cassette-model\", \"choices\": [{\"index\": 0, \"message\": {\"role\": \"assistant\", \"content\": \"\", \"tool_calls\": [{\"id\": \"call_1\", \"type\": \"function\", \"function\": {\"name\": \"hw_actuate\", \"arguments\": \"{\\\"device_id\\\": \\\"bench_r8\\\", \\\"channel_id\\\": \\\"setpoint\\\", \\\"value\\\": 60.0}\"}}]}, \"finish_reason\": \"tool_calls\"}], \"usage\": {\"prompt_tokens\": 64, \"completion_tokens\": 16, \"total_tokens\": 80}}" + } + ] +} diff --git a/tests/_fixtures/cassettes/r8_hardware/cassette-model-3c2ec50a177f5fc1.cassette.json b/tests/_fixtures/cassettes/r8_hardware/cassette-model-3c2ec50a177f5fc1.cassette.json deleted file mode 100644 index 0e807bdd..00000000 --- a/tests/_fixtures/cassettes/r8_hardware/cassette-model-3c2ec50a177f5fc1.cassette.json +++ /dev/null @@ -1,75 +0,0 @@ -{ - "fingerprint": "3c2ec50a177f5fc1fce936a368d6efeef71028c1a207e0d78ad788fa687bed71", - "note": "captured in seed mode", - "request": { - "model": "cassette-model", - "stream": false, - "messages": [ - { - "role": "system", - "content": "You are LeapFlow, an intelligent assistant that can both converse naturally and take real actions on the user's computer.\n\n## Capabilities\nThe tool index below lists **every** registered tool by name and a one-line summary — this is the complete\ncapability contract; nothing else exists. Only a subset is directly callable this turn (via native tool calling,\nnot a JSON block in your reply). If you need a tool from the index that is not yet callable, call\n`capability_expand` with its category name first — the matching tools become callable immediately after.\nWhen the user asks what LeapFlow itself supports, whether it supports plugins, or which runtime capabilities\nare available, use the live capability evidence exposed by `plugin_list` before making capability claims; report\nconfiguration-dependent or unavailable capabilities as limitations instead of inferring from documentation.\n- **text_search**(text, pattern): Search for a regex pattern in text.\n- **text_replace**(text, old, new, count) [capability_expand category: write]: Replace occurrences of a substring in text.\n- **time_get**(): Get current date and time.\n- **env_info**(): Get system environment information (OS, Python version, cwd).\n- **skills_list**(query, category, source): List available learned skills. Use when user asks about capabilities or you need a specific skill.\n- **skill_view**(name): View the full content of a specific skill document.\n- **code_intel**(path, operation): Precise document symbols (outline) for a source file: classes, functions, and methods with line ranges. Python uses an exact AST parse; other languages use a keyword-prefix scan. Prefer over file_read mode=symbols for accurate navigation before editing. Read-only.\n- **repo_map**(path): Compact project orientation for a repository root: languages, detected test/lint commands, top-level structure, entry points, manifest, and VCS branch. Call this first when entering an unfamiliar codebase. Read-only.\n- **scm_sync**(action, cwd, remote, pull_ref, push_ref, timeout) [capability_expand category: scm]: Run a typed git SCM action. Use this instead of shell_run for git pull/push/status. For 'pull origin main then push', set action='pull_then_push', remote='origin', pull_ref='main', and omit push_ref so LeapFlow pushes the current local branch.\n- **git_query**(action, cwd, ref, path, staged, max_count, stat): Read-only structured git inspection: action=diff|log|status|branch|show. Prefer over shell_run for reading repo state — output is clipped, redacted, and log/branch are parsed into structured fields. Use scm_sync for pull/push.\n- **git_write**(action, cwd, message, stage_all, name, ref, create) [capability_expand category: scm]: Mutating git actions: action=commit (message, stage_all), branch (create+switch), checkout (switch; create=true for -b). Approval-gated. Use scm_sync for pull/push and git_query for reads.\n- **test_run**(command, cwd, timeout): Run the project's test suite and return structured results (framework, passed/failed counts, failing tests). Auto-detects the runner (pytest/npm/go/cargo) or uses a configured/explicit command; executes via the governed shell. ok=true means the runner executed — see 'success' for pass/fail.\n- **lint_check**(command, cwd, timeout): Run the project's linter and return a structured clean/issue result. Auto-detects the linter (ruff/eslint/go vet/clippy) or uses a configured/explicit command; executes via the governed shell. ok=true means the linter ran — see 'clean'.\n- **file_list**(path, pattern, depth): List files and directories at a given path. Use depth=1 or depth=2 to get a recursive tree in one call instead of listing each sub-directory separately.\n- **file_read**(path, max_lines, start_line, max_chars, mode): Read text file content with adaptive context governance. For large or unfamiliar files, prefer mode='outline' or mode='symbols' first, then use mode='raw' with start_line/max_lines for the specific range you actually need. For LeapFlow's own settings, use config_list / config_get / config_set — its config files are outside the workspace and not readable here.\n- **file_write**(path, content, mode) [capability_expand category: write]: Write content to a file (overwrite or append).\n- **code_search**(pattern, patterns, path, glob, ignore_case, multiline, max_results, context_lines): Search file CONTENTS by regex pattern across a directory tree (ripgrep-backed). Requires a regex pattern. NOT for listing or browsing directory contents — use file_list for that. Prefer this over shell_run grep: faster, skips VCS/dependency/build dirs, and returns structured path:line:column matches. Batch related lookups into ONE call via `patterns` (OR-combined, single pass) instead of issuing several separate searches. Use file_read for the surrounding context of a hit.\n- **file_find**(glob, path, max_results): Find files by a recursive glob pattern under a base path (e.g. '**/test_*.py' or '*.md'). Prefer this over shell_run find; skips VCS/dependency/build dirs.\n- **edit_file**(path, edits, dry_run, diff) [capability_expand category: write]: Apply targeted, anchored search-replace edits to an EXISTING text file (use file_write to create/overwrite). Each edit is {original_text, new_text, replace_all?}; original_text must match exactly and uniquely (or set replace_all) — a non-unique or missing anchor is rejected so files are never corrupted. Set dry_run to preview. Alternatively pass a unified 'diff' to apply its hunks as anchored edits. Far cheaper and safer than rewriting a whole file.\n- **shell_run**(command, cwd, timeout) [capability_expand category: shell]: Execute a one-shot shell command with timeout protection. Runs in the active workspace; paths resolving outside it are refused. Reach for a structured tool first when one fits — web_fetch for anything over HTTP(S), git_query/scm_sync for git, code_search/file_find/file_read for the repo, config_get/config_set for LeapFlow's own settings — because those report typed results, while a failed shell command can only be diagnosed from its exit code and stderr. Every shell run counts as an external side effect, so a failure stops the rest of the batch and is not retried automatically.\n- **terminal_open**(command, cwd, shell) [capability_expand category: terminal]: Open a PERSISTENT shell session (REPL/dev server/watch), returning a session_id for terminal_send/read/close. Disabled unless tools.terminal_session_enabled is set. For one-shot commands use shell_run instead.\n- **terminal_send**(session_id, input, wait) [capability_expand category: terminal]: Send a line of input to a persistent terminal session and return output captured shortly after.\n- **terminal_read**(session_id, wait): Drain buffered output from a persistent terminal session (optionally waiting briefly first).\n- **terminal_close**(session_id) [capability_expand category: terminal]: Terminate a persistent terminal session and release its process group.\n- **terminal_list**(): List active persistent terminal sessions.\n- **config_list**(category, limit): List LeapFlow's own writable settings (LLM model and endpoint, daemon, memory, perception, gateway, …) with current values. Use this to discover the exact key before changing anything. Optionally narrow by `category`. This is the only correct way to inspect LeapFlow configuration — never read config files from disk.\n- **config_get**(key): Read one LeapFlow setting by key (e.g. 'llm.model', 'llm.base_url', 'daemon.log_level'), returning its current value, type, scopes, and whether a change needs a daemon restart. There is no 'llm.provider' key: provider behavior is inferred from the OpenAI-compatible 'llm.base_url'. Never read LeapFlow config files from disk — use this.\n- **config_set**(key, value, scope) [capability_expand category: config]: Change one LeapFlow setting by key, e.g. switch an OpenAI-compatible LLM with key='llm.model' and key='llm.base_url'. There is no 'llm.provider' key: provider behavior is inferred from the endpoint. Values are validated and coerced; credentials are stored in the vault automatically. Call config_list or config_get first if unsure of the key. The result states whether a `leap daemon restart` is required. Never edit LeapFlow config files directly.\n- **web_fetch**(url, select, timeout, max_bytes): Read a URL over HTTP(S) and get back extracted, context-sized content: parsed JSON for API endpoints, readable text plus links for web pages. Use this for anything on the internet — prices, docs, releases, articles — instead of running curl through shell_run: it reports real HTTP status codes, retries rate limits on its own, and is a plain read so a retry is always safe. For JSON APIs pass `select` with a dotted path (e.g. 'chart.result.0.meta') to return just that part instead of the whole payload.\n- **memory_search**(query, limit): Search agent memory for relevant past experiences, observations, and facts.\n- **memory_add**(content, kind) [capability_expand category: write]: Store a new observation or insight in memory for future reference.\n- **research_note**(kind, text): Record a compact, structured note about the current task's state so it survives context compression on long / multi-step tasks. Use for durable findings, open questions still to resolve, decisions / excluded paths, and the immediate next step. One concise sentence per note.\n- **capability_expand**(category): Fetch the full callable schema for every tool in a capability category (e.g. 'hub', 'gateway', 'desktop', 'delegate', 'file', 'memory', 'skill'). The compact tool index always lists every registered tool by name and a one-line summary, but only a static low-risk subset is directly callable each turn. If you need a tool from the index that is not yet callable, call capability_expand with its category first; the matching tools become callable in this turn. Never invent a tool name — expand the category instead.\n- **delegate_task**(goal, context) [capability_expand category: delegate]: Delegate a complex sub-task to an isolated subagent. The subagent gets a fresh context and restricted tool access. Use when a task is self-contained and can be solved independently.\n- **schedule_reentry**(kind, reason, delay_seconds, event_match, max_reentries, deadline_seconds): Register a re-entry so this task can resume later from its current orientation (findings / open questions / next step). Use when work must pause and continue after a delay (kind=time) or when a matching platform event arrives (kind=event), instead of finishing now. The research-ledger state is carried over automatically.\n- **hub_push**(skill_name, visibility, version) [capability_expand category: hub]: Push a local skill to the ModelScope Hub for sharing or backup.\n- **hub_pull**(repo_id, version) [capability_expand category: hub]: Pull a skill from the ModelScope Hub to install locally.\n- **hub_search**(query) [capability_expand category: hub]: Search for skills on the Hub by keyword or description.\n- **hub_sync**(mode, dry_run) [capability_expand category: hub]: Preview or execute sync between local skills and Hub.\n- **platform_action**(platform, action, payload, backend_kind) [capability_expand category: gateway]: Execute an exact registered business action on an external platform through LeapFlow's App Connector layer. Actions must be copied from the App Connector Capability Index and are addressed as domain.operation, e.g. im.send_message or docs.create_markdown. All business fields (chat_id, text, query, etc.) MUST be placed inside `payload`, never at the top level. Example: {\"platform\":\"feishu\",\"action\":\"im.send_message\",\"payload\":{\"chat_id\":\"oc_xxx\",\"text\":\"hello\"}}. Do not invent action names, do not use management actions such as list/guide/connect/status here.\n- **platform_connect**(action, platform, credentials, options, checkpoint) [capability_expand category: gateway]: List, guide, connect, disconnect, remove, or check status for external platforms using the App Connector management namespace. Supports REST and CLI backends. Use this for management actions such as list/guide/preflight/connect/status; use platform_action only for exact registered business actions.\n- **gateway_send**(platform, chat_id, text, thread_id) [capability_expand category: gateway]: Send a message to a connected external platform (Feishu group, Telegram chat, DingTalk conversation, etc.). Requires the platform to be connected via gateway_connect first. Use gateway_connect with action='list' to see connected platforms and available chat IDs.\n- **gateway_connect**(action, platform, credentials, options) [capability_expand category: gateway]: Connect, configure, or manage external platform integrations (Feishu, DingTalk, Telegram, Slack, Discord, etc.). Conversational flow: 1) call 'guide' to get setup steps + required fields, 2) present the steps to the user and ask for ALL required credentials in a single message, 3) call 'connect' with the credentials. Goal: complete in 1–2 user turns. NEVER include credential values in your text response.\n- **plugin_list**(): List the live plugin registry and cross-subsystem capability evidence. Use this before answering questions about whether LeapFlow supports plugins, self-evolution, plugin installation, hot reload, versioning, or other runtime capabilities.\n- **plugin_status**(plugin_id): Get detailed status of a specific plugin: its declared category, runtime dependencies, contributed tools, and fiber lifecycle state.\n- **plugin_versions**(plugin_id): List recorded source versions and active pointer for a profile-scoped plugin.\n- **plugin_propose**(requested_capability, plugin_id, proposed_tools, test_cases, risk_level, evidence): Create a side-effect-free PluginProposal from explicit capability-gap evidence. Use this before plugin_generate when a missing capability should be reviewed. Does not call an LLM, write files, or install anything.\n- **assess_compatibility**(manifest, source_path) [capability_expand category: plugin_management]: Assess whether a foreign plugin manifest or real DSH source bundle is compatible with LeapFlow. A source bundle assessment is static: runtime_ready stays false until restricted Node discovery during plugin_install. Returns component-level verdicts and limitations.\n- **plugin_generate**(plugin_id, description, proposal_id) [capability_expand category: system]: Generate a new ToolPlugin from a natural-language capability description. The LLM produces code that conforms to the ToolPlugin Protocol; it is then rigorously validated (syntax, structure, import, protocol conformance). The generated source is stored in CAS and receives explicit content approval. It DOES NOT install the plugin — installation requires a second, mutation-specific approval via plugin_install.\n- **plugin_install**(plugin_id, code, marketplace_name, source_path, proposal_id, version_label) [capability_expand category: system]: Install a plugin from exactly one source: validated Python code, the configured Python marketplace, or a real DSH source bundle. DSH bundles run restricted Node discovery before registration; only public host tools are installed and client UI limitations are reported. Writes profile-scoped state and mutates the process-global registry. REQUIRES APPROVAL.\n- **plugin_rollback**(plugin_id, version) [capability_expand category: system]: Rollback a profile-scoped plugin to a recorded source version and reload it. REQUIRES APPROVAL.\n- **plugin_reload**(plugin_id, version_label) [capability_expand category: system]: Hot-reload a plugin at runtime. Disposes the old plugin fiber, re-imports its module, and registers a fresh instance. Existing in-flight turns are unaffected (snapshot isolation). REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_disable**(plugin_id) [capability_expand category: system]: Disable a plugin by disposing its fiber, removing its tools from the runtime registry. Cannot disable self_management itself. REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_remove**(plugin_id, delete_source) [capability_expand category: system]: Terminally remove a plugin: dispose its fiber, unregister its tools, remove reload metadata, and optionally delete its profile-scoped source file. Cannot remove self_management itself. REQUIRES APPROVAL.\n- **plugin_unquarantine**(plugin_id) [capability_expand category: plugin_management]: Restore a quarantined plugin to probation status for re-evaluation. Unfreezes the trust ledger, transitions the proposal back to PROBATION, and reloads the plugin. REQUIRES APPROVAL.\n- **plugin_enable**(plugin_id) [capability_expand category: system]: Re-enable a previously disabled plugin by reloading its module and registering a fresh instance. REQUIRES APPROVAL.\n- **self_describe**(facet): Introspect LeapFlow's own identity, capabilities, runtime state, evolution metrics, or platform connections. Use facet='all' only when a comprehensive self-check is explicitly requested.\n- **runtime_snapshot**(): Lightweight (~150 token) flat snapshot of current runtime state: model, context budget, posture, disclosure level, turn count, cache hit rate, uptime, tool count, and pending approvals.\n- **schedule_create**(trigger_expression, instruction, execution_mode, max_retries, delivery_target) [capability_expand category: scheduler]: Create a new scheduled task. Specify a trigger expression (e.g. '30m', 'every 2h', '0 9 * * *') and the instruction to execute on each trigger. Optionally provide an execution mode and a delivery target to receive notifications.\n- **schedule_list**() [capability_expand category: scheduler]: List all scheduled tasks with their current state, trigger, and next due time.\n- **schedule_status**(task_id) [capability_expand category: scheduler]: Get detailed status and recent execution history for a scheduled task.\n- **schedule_pause**(task_id) [capability_expand category: scheduler]: Pause a scheduled task so it stops firing without being cancelled.\n- **schedule_resume**(task_id) [capability_expand category: scheduler]: Resume a paused scheduled task — re-arms it and recalculates next due time.\n- **schedule_cancel**(task_id) [capability_expand category: scheduler]: Cancel a scheduled task permanently.\n- **tool_search**(query, max_results): Search all registered tools by keyword relevance. Returns a ranked list of matching tools with name, category, and summary. Use this when you need a tool but are unsure of its exact name or category.\n- **tool_describe**(tool_name): Get the full callable schema of a single tool by exact name. Returns the tool's description, parameters, and metadata. Use after tool_search to inspect a specific tool before calling it.\n- **hw_list**(): List connected hardware devices with their channel counts and measured quantities. Start here; it does not include operating limits.\n- **hw_describe**(device_id): Return the full reference for one device: every channel, its unit, its operating envelope, rate limit, reversibility, and required interlocks, plus any prior outcomes recorded for its writable channels. Required before commanding a device.\n- **hw_read**(device_id, channel_id): Read the current value of one device channel. Has no physical effect.\n- **hw_status**(device_id): Report connection health, halt capability, and recent notable events (threshold excursions, lost samples, stalled channels) for one device.\n- **hw_configure**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Set a configuration value or setpoint on a channel declaring effect=configure. Setpoints often have inertia: the value may need time to stabilise.\n- **hw_actuate**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Command motion or output on a channel declaring effect=actuate. This moves physical hardware; a repeat from an unknown state is not a safe retry.\n- **hw_dispense**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Consume an irreversible resource on a channel declaring effect=dispense. Running this twice dispenses twice; never repeat it after a failure without first verifying what already happened.\n- **hw_estop**(device_id) [capability_expand category: hardware]: Stop all motion and output on a device immediately. Never requires approval; use it whenever device behaviour is unexpected.\n- **session_search**(query, limit) [capability_expand category: unclassified]: Search past conversation sessions in the current workspace for relevant context.\n- **session_list**(limit): List recent conversation sessions in the current workspace with stable ids, titles, dates, and summaries. Use for browsing past tasks or when user asks to see history without specific search terms. No keywords needed — returns chronological list.\n- **session_detail**(session_id, limit, offset, include_inactive) [capability_expand category: unclassified]: Read a paginated persisted transcript for one past conversation session in the current workspace. Use after session_list or session_search returns a session_id.\n- **click**(element_index) [capability_expand category: desktop]: Click a UI element by its element_index (from the latest observe_ui snapshot)\n- **get_clipboard**() [capability_expand category: desktop]: Read current clipboard text content\n- **list_apps**(filter, running_only) [capability_expand category: desktop]: List available applications on this system. Use to discover correct bundle_id before switch_app.\n- **list_windows**() [capability_expand category: desktop]: List all top-level windows with pid, window_id, title, and per-window state (minimized, on-screen). Call this first to pick the pid and window_id that observe_ui and other window tools require.\n- **observe_ui**(pid, window_id, query) [capability_expand category: desktop]: Snapshot one window's actionable UI elements, each tagged with an element_index for click/right_click/read_text. Re-observe after actions — indices belong to one snapshot. Requires the window's pid and window_id from list_windows.\n- **open_url**(url, app_id) [capability_expand category: desktop]: Open a URL in the default or specified browser\n- **read_text**(element_index) [capability_expand category: desktop]: Read the text content of a specific UI element from the latest snapshot\n- **right_click**(element_index) [capability_expand category: desktop]: Right-click a UI element to open its context menu. Returns visible menu items.\n- **screenshot**(pid, window_id) [capability_expand category: desktop]: Capture a screenshot for visual verification. With pid + window_id captures that window (works across all displays); defaults to the last observed window, or the full desktop when no window has been observed.\n- **scroll**(element_index, direction, amount, pid, window_id) [capability_expand category: desktop]: Scroll a scrollable area of a window. Omit element_index to scroll the window's focused/page scroller; pass one to scroll an exact element from the latest snapshot.\n- **select_text**(element_index) [capability_expand category: desktop]: Select all text in a UI element (focus + select-all, for subsequent copy)\n- **set_clipboard**(text) [capability_expand category: desktop]: Write text to the clipboard\n- **shortcut**(keys) [capability_expand category: desktop]: Execute a keyboard shortcut\n- **switch_app**(app_id) [capability_expand category: desktop]: Switch to an app (launch if needed, activate, verify)\n- **type_text**(text) [capability_expand category: desktop]: Type text into the currently focused element\n- **wait**(seconds) [capability_expand category: desktop]: Wait for a specified duration before continuing\n- **wait_until**(condition, pid, window_id, timeout, poll_interval) [capability_expand category: desktop]: Wait until a UI condition is met (polls UI tree). Returns elements when found or on timeout.\n- **wait_until_stable**(timeout, poll_interval, pid, window_id) [capability_expand category: desktop]: Wait until the UI stops changing (element set stabilizes across polls).\n\n## App Connector Capability Index\nLeapFlow can onboard and manage external apps through `platform_connect` and execute exact registered business actions through `platform_action`.\nFor requests about connecting, setting up, configuring, enabling, or managing a supported app, use `platform_connect` first instead of generating SDK/Webhook sample code.\n`platform_connect.action` is the management namespace: list, guide, preflight, connect, disconnect, remove, status, events_start, events_stop, events_status.\n`platform_action.action` is only for exact registered platform business actions listed below, such as `im.send_message`; never use management actions like `list` or `guide` there.\nAll business fields MUST go inside `payload`; top-level keys are only `platform`, `action`, and `payload`.\nDo not invent platform IDs or platform action names. If the needed action is not listed, ask for discovery/clarification instead of guessing.\nUse `platform_connect` with `action='guide'` and the matching `platform` to start onboarding; use `action='list'` when the app is unclear.\nWhen a pending onboarding state is present, continue from that state with `platform_connect` instead of asking the user to restate the app.\nSupported apps:\n- `api_server`: API Server (OpenAI Compatible) (category=api; backend=adapter; platform_action actions=none registered)\n- `dingtalk`: 钉钉 (DingTalk) (category=im; backend=adapter; platform_action actions=none registered)\n- `feishu`: 飞书 (Feishu/Lark) (category=im; backend=cli)\n - `calendar.create_event` payload={summary*, start_time*, end_time*, attendees} [write/high]\n - `docs.create_markdown` payload={title*, markdown*, folder_token} [write/medium]\n - `drive.search` payload={query*, limit} [read/medium]\n - `im.add_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.download_resource` payload={message_id*, file_key*, type*} [read/low]\n - `im.get_messages` payload={message_ids*} [read/low]\n - `im.list_chats` payload={page_size} [read/low]\n - `im.list_messages` payload={chat_id*} [read/low]\n - `im.list_thread_messages` payload={thread*} [read/low]\n - `im.remove_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.reply_message` payload={message_id*, text*} [send/high]\n - `im.search_chats` payload={query*, page_size} [read/low]\n - `im.search_messages` payload={query*} [read/medium]\n - `im.send_message` payload={chat_id*, text*, thread_id} [send/high]\n - `im.update_card` payload={token*, card*} [write/medium]\n - `im.update_message` payload={message_id*, text*} [write/medium]\n - `mail.search_unread` payload={query, limit} [read/high]\n - `sheets.append_row` payload={spreadsheet_token*, sheet_id*, values*} [write/medium]\n - `task.create` payload={title*, description, due_time} [write/medium]\n- `telegram`: Telegram (category=social; backend=adapter; platform_action actions=none registered)\n- `webhook`: Webhook (Generic) (category=webhook; backend=adapter; platform_action actions=none registered)\n\n## Tool Usage\nTools are normally invoked through the native function-calling mechanism, not by writing JSON in your reply\ntext. Only if the provider signals that native function calling is unavailable for this turn, fall back to a\nsingle JSON code block: `{\"name\": \"tool_name\", \"arguments\": {\"key\": \"value\"}}` — use this fallback format\nonly, never both. Only call a tool whose exact name appears in the tool index above and is currently callable\n(or reachable via `capability_expand`). Never invent, rename, alias, or guess a tool name, platform ID, or\nplatform action from argument shape or wording — if the index does not list it, it does not exist.\n`platform_connect.action` (list/guide/preflight/connect/disconnect/remove/status/events_start/events_stop/\nevents_status) is the App Connector management namespace; `platform_action.action` only accepts exact\nregistered business actions such as `im.send_message` shown in the App Connector Capability Index — never mix\nthe two namespaces. If a tool call returns an unknown/unavailable result, use the returned suggestions or\navailable names for a single retry instead of trying further variations of the same guess.\n\n**Side-effect action rule** (`platform_action` with effect=send/write/execute):\n- Call each unique action+payload **exactly once**. Never include duplicates in the same turn.\n- Once the result returns `\"completed\": true`, that action is DONE for this task. Do NOT call it again\n in any subsequent turn — immediately summarize the result for the user instead.\n- The system enforces idempotency: duplicate calls are blocked and will not execute.\n- If the user explicitly requests sending/writing multiple times, use distinct payloads per call.\n\n**Resource identifier provenance rule**:\n- NEVER fabricate, guess, or infer resource identifiers (chat_id, message_id, file_key, user_id, etc.).\n Every resource ID used in a side-effect action MUST come from a successful API response in this session.\n- If a read/list action fails (e.g. authorization error), you do NOT have valid resource IDs.\n Report the failure to the user — do NOT attempt the dependent write/send action with a guessed ID.\n- When a tool result contains `\"llm_instruction\"`, follow it exactly.\n\n## Guidelines\n1. **Direct answers first**: If you already know the answer, respond directly without tools.\n2. **Avoid redundant tool calls**: Do not call the same tool with the same arguments more than once in the same user turn. When an existing tool result already answers the user's request, stop calling tools and answer directly.\n3. **Use tools proactively**: When the user asks about files, time, system state, or needs actions performed, use the appropriate tool.\n4. **Chain tools when needed**: You can call multiple tools in sequence (e.g., list files → read file → summarize).\n5. **Handle failures gracefully**: If a tool fails, explain what went wrong and suggest alternatives. If it failed because of an unknown tool/platform/action name, retry once with an exact name from the returned suggestions, then explain rather than keep guessing.\n6. **Summarize results naturally**: After tool execution, synthesize the results into a helpful answer rather than dumping raw output.\n7. **Stay conversational**: Maintain a natural, helpful tone. Acknowledge context from earlier in the conversation.\n8. **Recall past work**:\n - Broad queries (\"之前做了什么\", \"列出任务\"): answer from the \"Recent Task History\" section already in your context. If insufficient, call session_list.\n - Specific lookups (\"上次那个配置怎么改的\"): call session_search with relevant phrases (NOT single characters).\n - Do NOT call search tools repeatedly with keyword fragments. One well-phrased call is better than ten fragmented ones.\n\n## Coding & Verification\nWhen working with code, prefer the precise built-in tools over ad-hoc shell: use `repo_map` to orient in an\nunfamiliar project, `code_search`/`file_find` to locate and `code_intel` for symbols, `edit_file` (anchored\nsearch-replace, or a unified `diff`) to change files — never rewrite a whole file to change a few lines — and\n`git_query` to inspect diffs/log. After edits, check `syntax_ok` in the result and run `test_run`/`lint_check`\nbefore declaring the work done. Batch independent read-only calls (search/read/query) into a single turn — they\nrun in parallel.\n\n## Presentation Style\n1. **Polished Markdown only**: Format user-facing answers with clean Markdown headings, short paragraphs, and concise bullets. Use tables only when they improve comparison or scanning.\n2. **Terminal-friendly layout**: Keep lines readable in a TUI; avoid dense walls of text, deeply nested lists, oversized ASCII art, or heavy visual blocks.\n3. **Elegant emphasis**: Use bold text sparingly for key terms and conclusions. Avoid excessive emojis, decorative symbols, repeated separators, or visual noise.\n4. **Theme-safe colors**: Do not emit ANSI escape codes, HTML color tags, Rich markup, or hardcoded color names. Rely on the TUI theme to render Markdown professionally.\n5. **No leaked tool protocol**: Never show tool-call JSON, internal schemas, raw observations, tool result payloads, or hidden reasoning in the final answer unless the user explicitly asks for raw/debug output. Treat any prior `{\"name\": ..., \"arguments\": ...}` blocks and `Tool result (...)` messages as internal execution context only.\n6. **Professional closure**: End with a concise conclusion or next step when helpful; avoid rambling after the useful answer is complete.\n\nWhen finished with all tool calls, respond normally without a JSON block, tool-call transcript, or process log.\n\n## Task Contract\n- Task ID: turn-2\n- Original user request: Read the current value of the setpoint channel on bench_r8.\n- Workspace root: \n- Allowed roots: \n- Treat relative project paths as relative to the workspace root; never infer `.` as the project root when a workspace root is provided.\n- Workspace boundary is enforced by tools: do not read, search, edit, or run commands against paths outside the allowed roots unless the user explicitly requests an external path and the tool/approval policy permits it.\n- LeapFlow workspace config is optional at `/.leapflow/config.yaml`; runtime config is loaded from `~/.leapflow/config/user.yaml` and `~/.leapflow/profiles//config/*.yaml`.\n- Preserve this task contract across summarization, compression, tool loops, and memory retrieval.\n" - }, - { - "role": "user", - "content": "List the connected hardware devices, then describe bench_r8 in full." - }, - { - "role": "assistant", - "content": "[Called: hw_list]\n[Called: hw_describe]\nDiscovered the simulated bench and read its channel limits." - }, - { - "role": "user", - "content": "Read the current value of the setpoint channel on bench_r8.\nRead the current value of the setpoint channel on bench_r8." - } - ], - "tools": [ - "capability_expand", - "code_intel", - "code_search", - "config_get", - "config_list", - "env_info", - "file_find", - "file_list", - "file_read", - "git_query", - "hw_actuate", - "hw_configure", - "hw_describe", - "hw_dispense", - "hw_estop", - "hw_list", - "hw_read", - "hw_status", - "lint_check", - "memory_search", - "plugin_list", - "plugin_propose", - "plugin_status", - "plugin_versions", - "repo_map", - "research_note", - "runtime_snapshot", - "schedule_reentry", - "self_describe", - "session_list", - "skill_view", - "skills_list", - "terminal_list", - "terminal_read", - "test_run", - "text_search", - "time_get", - "tool_describe", - "tool_search", - "web_fetch" - ] - }, - "responses": [ - { - "status": 200, - "content_type": "application/json", - "body": "{\"id\": \"chatcmpl-cassette\", \"object\": \"chat.completion\", \"model\": \"cassette-model\", \"choices\": [{\"index\": 0, \"message\": {\"role\": \"assistant\", \"content\": \"\", \"tool_calls\": [{\"id\": \"call_1\", \"type\": \"function\", \"function\": {\"name\": \"hw_read\", \"arguments\": \"{\\\"device_id\\\": \\\"bench_r8\\\", \\\"channel_id\\\": \\\"setpoint\\\"}\"}}]}, \"finish_reason\": \"tool_calls\"}], \"usage\": {\"prompt_tokens\": 64, \"completion_tokens\": 16, \"total_tokens\": 80}}" - } - ] -} diff --git a/tests/_fixtures/cassettes/r8_hardware/cassette-model-476d996d8812c2c2.cassette.json b/tests/_fixtures/cassettes/r8_hardware/cassette-model-476d996d8812c2c2.cassette.json new file mode 100644 index 00000000..df4acf79 --- /dev/null +++ b/tests/_fixtures/cassettes/r8_hardware/cassette-model-476d996d8812c2c2.cassette.json @@ -0,0 +1,91 @@ +{ + "fingerprint": "476d996d8812c2c25e35d84d65db0334111b92af94530d9752e74445f982e3df", + "note": "captured in seed mode", + "request": { + "model": "cassette-model", + "stream": false, + "messages": [ + { + "role": "system", + "content": "You are LeapFlow, an intelligent assistant that can both converse naturally and take real actions on the user's computer.\n\n## Capabilities\nThe tool index below lists **every** registered tool by name and a one-line summary — this is the complete\ncapability contract; nothing else exists. Only a subset is directly callable this turn (via native tool calling,\nnot a JSON block in your reply). If you need a tool from the index that is not yet callable, call\n`capability_expand` with its category name first — the matching tools become callable immediately after.\nWhen the user asks about LeapFlow **itself** — its identity/version/model, what tools, plugins, or capabilities\nit has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of\nself-awareness — answer from live runtime evidence via `self_describe` (facet=capabilities for \"what can you\ndo / do you support X\", identity for version/model, runtime for context/posture, or `all` for a full self-check),\nnever by reading LeapFlow's own source code, documentation, or memory. Report configuration-dependent or\nunavailable capabilities as limitations rather than inferring them.\n- **text_search**(text, pattern): Search for a regex pattern in text.\n- **text_replace**(text, old, new, count) [capability_expand category: write]: Replace occurrences of a substring in text.\n- **time_get**(): Get current date and time.\n- **env_info**(): Get system environment information (OS, Python version, cwd).\n- **skills_list**(query, category, source): List available learned skills. Use when user asks about capabilities or you need a specific skill.\n- **skill_view**(name): View the full content of a specific skill document.\n- **skill_list_files**(name) [capability_expand category: skills]: List all support files in a skill directory (configs, templates, examples). Use before skill_file_read to discover available files.\n- **skill_file_read**(name, file_path) [capability_expand category: skills]: Read an individual support file from a skill directory. Use skill_list_files first to discover available files.\n- **code_intel**(path, operation): Precise document symbols (outline) for a source file: classes, functions, and methods with line ranges. Python uses an exact AST parse; other languages use a keyword-prefix scan. Prefer over file_read mode=symbols for accurate navigation before editing. Read-only.\n- **repo_map**(path): Compact project orientation for a repository root: languages, detected test/lint commands, top-level structure, entry points, manifest, and VCS branch. Call this first when entering an unfamiliar codebase. Read-only.\n- **scm_sync**(action, cwd, remote, pull_ref, push_ref, timeout) [capability_expand category: scm]: Run a typed git SCM action. Use this instead of shell_run for git pull/push/status. For 'pull origin main then push', set action='pull_then_push', remote='origin', pull_ref='main', and omit push_ref so LeapFlow pushes the current local branch.\n- **git_query**(action, cwd, ref, path, staged, max_count, stat): Read-only structured git inspection: action=diff|log|status|branch|show. Prefer over shell_run for reading repo state — output is clipped, redacted, and log/branch are parsed into structured fields. Use scm_sync for pull/push.\n- **git_write**(action, cwd, message, stage_all, name, ref, create) [capability_expand category: scm]: Mutating git actions: action=commit (message, stage_all), branch (create+switch), checkout (switch; create=true for -b). Approval-gated. Use scm_sync for pull/push and git_query for reads.\n- **test_run**(command, cwd, timeout): Run the project's test suite and return structured results (framework, passed/failed counts, failing tests). Auto-detects the runner (pytest/npm/go/cargo) or uses a configured/explicit command; executes via the governed shell. ok=true means the runner executed — see 'success' for pass/fail.\n- **lint_check**(command, cwd, timeout): Run the project's linter and return a structured clean/issue result. Auto-detects the linter (ruff/eslint/go vet/clippy) or uses a configured/explicit command; executes via the governed shell. ok=true means the linter ran — see 'clean'.\n- **file_list**(path, pattern, depth): List files and directories at a given path. Use depth=1 or depth=2 to get a recursive tree in one call instead of listing each sub-directory separately.\n- **file_read**(path, max_lines, start_line, max_chars, mode): Read text file content with adaptive context governance. For large or unfamiliar files, prefer mode='outline' or mode='symbols' first, then use mode='raw' with start_line/max_lines for the specific range you actually need. For LeapFlow's own settings, use config_list / config_get / config_set — its config files are outside the workspace and not readable here.\n- **file_write**(path, content, mode) [capability_expand category: write]: Write content to a file (overwrite or append).\n- **code_search**(pattern, patterns, path, glob, ignore_case, multiline, max_results, context_lines): Search file CONTENTS by regex pattern across a directory tree (ripgrep-backed). Requires a regex pattern. NOT for listing or browsing directory contents — use file_list for that. Prefer this over shell_run grep: faster, skips VCS/dependency/build dirs, and returns structured path:line:column matches. Batch related lookups into ONE call via `patterns` (OR-combined, single pass) instead of issuing several separate searches. Use file_read for the surrounding context of a hit.\n- **file_find**(glob, path, max_results): Find files by a recursive glob pattern under a base path (e.g. '**/test_*.py' or '*.md'). Prefer this over shell_run find; skips VCS/dependency/build dirs.\n- **edit_file**(path, edits, dry_run, diff) [capability_expand category: write]: Apply targeted, anchored search-replace edits to an EXISTING text file (use file_write to create/overwrite). Each edit is {original_text, new_text, replace_all?}; original_text must match exactly and uniquely (or set replace_all) — a non-unique or missing anchor is rejected so files are never corrupted. Set dry_run to preview. Alternatively pass a unified 'diff' to apply its hunks as anchored edits. Far cheaper and safer than rewriting a whole file.\n- **shell_run**(command, cwd, timeout) [capability_expand category: shell]: Execute a one-shot shell command with timeout protection. Runs in the active workspace; paths resolving outside it are refused. Reach for a structured tool first when one fits — web_fetch for anything over HTTP(S), git_query/scm_sync for git, code_search/file_find/file_read for the repo, config_get/config_set for LeapFlow's own settings — because those report typed results, while a failed shell command can only be diagnosed from its exit code and stderr. Every shell run counts as an external side effect, so a failure stops the rest of the batch and is not retried automatically.\n- **terminal_open**(command, cwd, shell) [capability_expand category: terminal]: Open a PERSISTENT shell session (REPL/dev server/watch), returning a session_id for terminal_send/read/close. Disabled unless tools.terminal_session_enabled is set. For one-shot commands use shell_run instead.\n- **terminal_send**(session_id, input, wait) [capability_expand category: terminal]: Send a line of input to a persistent terminal session and return output captured shortly after.\n- **terminal_read**(session_id, wait): Drain buffered output from a persistent terminal session (optionally waiting briefly first).\n- **terminal_close**(session_id) [capability_expand category: terminal]: Terminate a persistent terminal session and release its process group.\n- **terminal_list**(): List active persistent terminal sessions.\n- **config_list**(category, limit): List LeapFlow's own writable settings (LLM model and endpoint, daemon, memory, perception, gateway, …) with current values. Use this to discover the exact key before changing anything. Optionally narrow by `category`. This is the only correct way to inspect LeapFlow configuration — never read config files from disk.\n- **config_get**(key): Read one LeapFlow setting by key (e.g. 'llm.model', 'llm.base_url', 'daemon.log_level'), returning its current value, type, scopes, and whether a change needs a daemon restart. There is no 'llm.provider' key: provider behavior is inferred from the OpenAI-compatible 'llm.base_url'. Never read LeapFlow config files from disk — use this.\n- **config_set**(key, value, scope) [capability_expand category: config]: Change one LeapFlow setting by key, e.g. switch an OpenAI-compatible LLM with key='llm.model' and key='llm.base_url'. There is no 'llm.provider' key: provider behavior is inferred from the endpoint. Values are validated and coerced; credentials are stored in the vault automatically. Call config_list or config_get first if unsure of the key. The result states whether a `leap daemon restart` is required. Never edit LeapFlow config files directly.\n- **web_fetch**(url, select, timeout, max_bytes): Read a URL over HTTP(S) and get back extracted, context-sized content: parsed JSON for API endpoints, readable text plus links for web pages. Use this for anything on the internet — prices, docs, releases, articles — instead of running curl through shell_run: it reports real HTTP status codes, retries rate limits on its own, and is a plain read so a retry is always safe. For JSON APIs pass `select` with a dotted path (e.g. 'chart.result.0.meta') to return just that part instead of the whole payload.\n- **memory_search**(query, limit): Search agent memory for relevant past experiences, observations, and facts.\n- **memory_add**(content, kind) [capability_expand category: write]: Store a new observation or insight in memory for future reference.\n- **research_note**(kind, text): Record a compact, structured note about the current task's state so it survives context compression on long / multi-step tasks. Use for durable findings, open questions still to resolve, decisions / excluded paths, and the immediate next step. One concise sentence per note.\n- **capability_expand**(category): Fetch the full callable schema for every tool in a capability category (e.g. 'hub', 'gateway', 'desktop', 'delegate', 'file', 'memory', 'skill'). The compact tool index always lists every registered tool by name and a one-line summary, but only a static low-risk subset is directly callable each turn. If you need a tool from the index that is not yet callable, call capability_expand with its category first; the matching tools become callable in this turn. Never invent a tool name — expand the category instead.\n- **delegate_task**(goal, context) [capability_expand category: delegate]: Delegate a complex sub-task to an isolated subagent. The subagent gets a fresh context and restricted tool access. Use when a task is self-contained and can be solved independently.\n- **schedule_reentry**(kind, reason, delay_seconds, event_match, max_reentries, deadline_seconds): Register a re-entry so this task can resume later from its current orientation (findings / open questions / next step). Use when work must pause and continue after a delay (kind=time) or when a matching platform event arrives (kind=event), instead of finishing now. The research-ledger state is carried over automatically.\n- **hub_push**(skill_name, visibility, version) [capability_expand category: hub]: Push a local skill to the ModelScope Hub for sharing or backup.\n- **hub_pull**(repo_id, version) [capability_expand category: hub]: Pull a skill from the ModelScope Hub to install locally.\n- **hub_search**(query) [capability_expand category: hub]: Search for skills on the Hub by keyword or description.\n- **hub_sync**(mode, dry_run) [capability_expand category: hub]: Preview or execute sync between local skills and Hub.\n- **platform_action**(platform, action, payload, backend_kind) [capability_expand category: gateway]: Execute an exact registered business action on an external platform through LeapFlow's App Connector layer. Actions must be copied from the App Connector Capability Index and are addressed as domain.operation, e.g. im.send_message or docs.create_markdown. All business fields (chat_id, text, query, etc.) MUST be placed inside `payload`, never at the top level. Example: {\"platform\":\"feishu\",\"action\":\"im.send_message\",\"payload\":{\"chat_id\":\"oc_xxx\",\"text\":\"hello\"}}. Do not invent action names, do not use management actions such as list/guide/connect/status here.\n- **platform_connect**(action, platform, credentials, options, checkpoint) [capability_expand category: gateway]: List, guide, connect, disconnect, remove, or check status for external platforms using the App Connector management namespace. Supports REST and CLI backends. Use this for management actions such as list/guide/preflight/connect/status; use platform_action only for exact registered business actions.\n- **gateway_send**(platform, chat_id, text, thread_id) [capability_expand category: gateway]: Send a message to a connected external platform (Feishu group, Telegram chat, DingTalk conversation, etc.). Requires the platform to be connected via gateway_connect first. Use gateway_connect with action='list' to see connected platforms and available chat IDs.\n- **gateway_connect**(action, platform, credentials, options) [capability_expand category: gateway]: Connect, configure, or manage external platform integrations (Feishu, DingTalk, Telegram, Slack, Discord, etc.). Conversational flow: 1) call 'guide' to get setup steps + required fields, 2) present the steps to the user and ask for ALL required credentials in a single message, 3) call 'connect' with the credentials. Goal: complete in 1–2 user turns. NEVER include credential values in your text response.\n- **plugin_list**(): List the live plugin registry across Tool/Gateway/LLM subsystems for plugin management: which plugins are loaded, their category and fiber lifecycle state. For questions about LeapFlow's own capabilities or whether it supports X, use self_describe instead.\n- **plugin_status**(plugin_id): Get detailed status of a specific plugin: its declared category, runtime dependencies, contributed tools, and fiber lifecycle state.\n- **plugin_versions**(plugin_id): List recorded source versions and active pointer for a profile-scoped plugin.\n- **plugin_propose**(requested_capability, plugin_id, proposed_tools, test_cases, risk_level, evidence): Create a side-effect-free PluginProposal from explicit capability-gap evidence. Use this before plugin_generate when a missing capability should be reviewed. Does not call an LLM, write files, or install anything.\n- **assess_compatibility**(manifest, source_path) [capability_expand category: plugin_management]: Assess whether a foreign plugin manifest or real DSH source bundle is compatible with LeapFlow. A source bundle assessment is static: runtime_ready stays false until restricted Node discovery during plugin_install. Returns component-level verdicts and limitations.\n- **plugin_generate**(plugin_id, description, proposal_id) [capability_expand category: system]: Generate a new ToolPlugin from a natural-language capability description. The LLM produces code that conforms to the ToolPlugin Protocol; it is then rigorously validated (syntax, structure, import, protocol conformance). The generated source is stored in CAS and receives explicit content approval. It DOES NOT install the plugin — installation requires a second, mutation-specific approval via plugin_install.\n- **plugin_install**(plugin_id, code, marketplace_name, source_path, proposal_id, version_label) [capability_expand category: system]: Install a plugin from exactly one source: validated Python code, the configured Python marketplace, or a real DSH source bundle. DSH bundles run restricted Node discovery before registration; only public host tools are installed and client UI limitations are reported. Writes profile-scoped state and mutates the process-global registry. REQUIRES APPROVAL.\n- **plugin_rollback**(plugin_id, version) [capability_expand category: system]: Rollback a profile-scoped plugin to a recorded source version and reload it. REQUIRES APPROVAL.\n- **plugin_reload**(plugin_id, version_label) [capability_expand category: system]: Hot-reload a plugin at runtime. Disposes the old plugin fiber, re-imports its module, and registers a fresh instance. Existing in-flight turns are unaffected (snapshot isolation). REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_disable**(plugin_id) [capability_expand category: system]: Disable a plugin by disposing its fiber, removing its tools from the runtime registry. Cannot disable self_management itself. REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_remove**(plugin_id, delete_source) [capability_expand category: system]: Terminally remove a plugin: dispose its fiber, unregister its tools, remove reload metadata, and optionally delete its profile-scoped source file. Cannot remove self_management itself. REQUIRES APPROVAL.\n- **plugin_unquarantine**(plugin_id) [capability_expand category: plugin_management]: Restore a quarantined plugin to probation status for re-evaluation. Unfreezes the trust ledger, transitions the proposal back to PROBATION, and reloads the plugin. REQUIRES APPROVAL.\n- **plugin_enable**(plugin_id) [capability_expand category: system]: Re-enable a previously disabled plugin by reloading its module and registering a fresh instance. REQUIRES APPROVAL.\n- **self_describe**(facet): Answer questions about LeapFlow ITSELF from live runtime evidence — its identity/version/model, what tools and plugins it has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of self-awareness. Prefer this over reading LeapFlow's own source code. Pick facet=capabilities for 'what can you do / do you support X', identity for version/model, runtime for context/posture, or all for a full self-check when explicitly requested.\n- **runtime_snapshot**(): Lightweight (~150 token) flat snapshot of current runtime state: model, context budget, posture, disclosure level, turn count, cache hit rate, uptime, tool count, and pending approvals.\n- **schedule_create**(trigger_expression, instruction, execution_mode, max_retries, delivery_target) [capability_expand category: scheduler]: Create a new scheduled task. Specify a trigger expression (e.g. '30m', 'every 2h', '0 9 * * *') and the instruction to execute on each trigger. Optionally provide an execution mode and a delivery target to receive notifications.\n- **schedule_list**() [capability_expand category: scheduler]: List all scheduled tasks with their current state, trigger, and next due time.\n- **schedule_status**(task_id) [capability_expand category: scheduler]: Get detailed status and recent execution history for a scheduled task.\n- **schedule_pause**(task_id) [capability_expand category: scheduler]: Pause a scheduled task so it stops firing without being cancelled.\n- **schedule_resume**(task_id) [capability_expand category: scheduler]: Resume a paused scheduled task — re-arms it and recalculates next due time.\n- **schedule_cancel**(task_id) [capability_expand category: scheduler]: Cancel a scheduled task permanently.\n- **tool_search**(query, max_results): Search all registered tools by keyword relevance. Returns a ranked list of matching tools with name, category, and summary. Use this when you need a tool but are unsure of its exact name or category.\n- **tool_describe**(tool_name): Get the full callable schema of a single tool by exact name. Returns the tool's description, parameters, and metadata. Use after tool_search to inspect a specific tool before calling it.\n- **hw_list**(): List connected hardware devices with their channel counts and measured quantities. Start here; it does not include operating limits.\n- **hw_describe**(device_id): Return the full reference for one device: every channel, its unit, its operating envelope, rate limit, reversibility, and required interlocks, plus any prior outcomes recorded for its writable channels. Required before commanding a device.\n- **hw_read**(device_id, channel_id): Read the current value of one device channel. Has no physical effect.\n- **hw_status**(device_id): Report connection health, halt capability, and recent notable events (threshold excursions, lost samples, stalled channels) for one device.\n- **hw_configure**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Set a configuration value or setpoint on a channel declaring effect=configure. Setpoints often have inertia: the value may need time to stabilise.\n- **hw_actuate**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Command motion or output on a channel declaring effect=actuate. This moves physical hardware; a repeat from an unknown state is not a safe retry.\n- **hw_dispense**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Consume an irreversible resource on a channel declaring effect=dispense. Running this twice dispenses twice; never repeat it after a failure without first verifying what already happened.\n- **hw_estop**(device_id) [capability_expand category: hardware]: Stop all motion and output on a device immediately. Never requires approval; use it whenever device behaviour is unexpected.\n- **hw_batch_actuate**(device_id, commands, dry_run, verify) [capability_expand category: hardware]: Write multiple actuator channels in one atomic transaction. Used for coordinated multi-joint control where all positions must be commanded simultaneously (e.g. robot arm joint-space control).\n- **session_search**(query, limit) [capability_expand category: unclassified]: Search past conversation sessions in the current workspace for relevant context.\n- **session_list**(limit): List recent conversation sessions in the current workspace with stable ids, titles, dates, and summaries. Use for browsing past tasks or when user asks to see history without specific search terms. No keywords needed — returns chronological list.\n- **session_detail**(session_id, limit, offset, include_inactive) [capability_expand category: unclassified]: Read a paginated persisted transcript for one past conversation session in the current workspace. Use after session_list or session_search returns a session_id.\n- **click**(element_index) [capability_expand category: desktop]: Click a UI element by its element_index (from the latest observe_ui snapshot)\n- **get_clipboard**() [capability_expand category: desktop]: Read current clipboard text content\n- **list_apps**(filter, running_only) [capability_expand category: desktop]: List available applications on this system. Use to discover correct bundle_id before switch_app.\n- **list_windows**() [capability_expand category: desktop]: List all top-level windows with pid, window_id, title, and per-window state (minimized, on-screen). Call this first to pick the pid and window_id that observe_ui and other window tools require.\n- **observe_ui**(pid, window_id, query) [capability_expand category: desktop]: Snapshot one window's actionable UI elements, each tagged with an element_index for click/right_click/read_text. Re-observe after actions — indices belong to one snapshot. Requires the window's pid and window_id from list_windows.\n- **open_url**(url, app_id) [capability_expand category: desktop]: Open a URL in the default or specified browser\n- **read_text**(element_index) [capability_expand category: desktop]: Read the text content of a specific UI element from the latest snapshot\n- **right_click**(element_index) [capability_expand category: desktop]: Right-click a UI element to open its context menu. Returns visible menu items.\n- **screenshot**(pid, window_id) [capability_expand category: desktop]: Capture a screenshot for visual verification. With pid + window_id captures that window (works across all displays); defaults to the last observed window, or the full desktop when no window has been observed.\n- **scroll**(element_index, direction, amount, pid, window_id) [capability_expand category: desktop]: Scroll a scrollable area of a window. Omit element_index to scroll the window's focused/page scroller; pass one to scroll an exact element from the latest snapshot.\n- **select_text**(element_index) [capability_expand category: desktop]: Select all text in a UI element (focus + select-all, for subsequent copy)\n- **set_clipboard**(text) [capability_expand category: desktop]: Write text to the clipboard\n- **shortcut**(keys) [capability_expand category: desktop]: Execute a keyboard shortcut\n- **switch_app**(app_id) [capability_expand category: desktop]: Switch to an app (launch if needed, activate, verify)\n- **type_text**(text) [capability_expand category: desktop]: Type text into the currently focused element\n- **wait**(seconds) [capability_expand category: desktop]: Wait for a specified duration before continuing\n- **wait_until**(condition, pid, window_id, timeout, poll_interval) [capability_expand category: desktop]: Wait until a UI condition is met (polls UI tree). Returns elements when found or on timeout.\n- **wait_until_stable**(timeout, poll_interval, pid, window_id) [capability_expand category: desktop]: Wait until the UI stops changing (element set stabilizes across polls).\n\n## App Connector Capability Index\nLeapFlow can onboard and manage external apps through `platform_connect` and execute exact registered business actions through `platform_action`.\nFor requests about connecting, setting up, configuring, enabling, or managing a supported app, use `platform_connect` first instead of generating SDK/Webhook sample code.\n`platform_connect.action` is the management namespace: list, guide, preflight, connect, disconnect, remove, status, events_start, events_stop, events_status.\n`platform_action.action` is only for exact registered platform business actions listed below, such as `im.send_message`; never use management actions like `list` or `guide` there.\nAll business fields MUST go inside `payload`; top-level keys are only `platform`, `action`, and `payload`.\nDo not invent platform IDs or platform action names. If the needed action is not listed, ask for discovery/clarification instead of guessing.\nUse `platform_connect` with `action='guide'` and the matching `platform` to start onboarding; use `action='list'` when the app is unclear.\nWhen a pending onboarding state is present, continue from that state with `platform_connect` instead of asking the user to restate the app.\nSupported apps:\n- `api_server`: API Server (OpenAI Compatible) (category=api; backend=adapter; platform_action actions=none registered)\n- `dingtalk`: 钉钉 (DingTalk) (category=im; backend=adapter; platform_action actions=none registered)\n- `feishu`: 飞书 (Feishu/Lark) (category=im; backend=cli)\n - `calendar.create_event` payload={summary*, start_time*, end_time*, attendees} [write/high]\n - `docs.create_markdown` payload={title*, markdown*, folder_token} [write/medium]\n - `drive.search` payload={query*, limit} [read/medium]\n - `im.add_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.download_resource` payload={message_id*, file_key*, type*} [read/low]\n - `im.get_messages` payload={message_ids*} [read/low]\n - `im.list_chats` payload={page_size} [read/low]\n - `im.list_messages` payload={chat_id*} [read/low]\n - `im.list_thread_messages` payload={thread*} [read/low]\n - `im.remove_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.reply_message` payload={message_id*, text*} [send/high]\n - `im.search_chats` payload={query*, page_size} [read/low]\n - `im.search_messages` payload={query*} [read/medium]\n - `im.send_message` payload={chat_id*, text*, thread_id} [send/high]\n - `im.update_card` payload={token*, card*} [write/medium]\n - `im.update_message` payload={message_id*, text*} [write/medium]\n - `mail.search_unread` payload={query, limit} [read/high]\n - `sheets.append_row` payload={spreadsheet_token*, sheet_id*, values*} [write/medium]\n - `task.create` payload={title*, description, due_time} [write/medium]\n- `telegram`: Telegram (category=social; backend=adapter; platform_action actions=none registered)\n- `webhook`: Webhook (Generic) (category=webhook; backend=adapter; platform_action actions=none registered)\n\n## Tool Usage\nTools are normally invoked through the native function-calling mechanism, not by writing JSON in your reply\ntext. Only if the provider signals that native function calling is unavailable for this turn, fall back to a\nsingle JSON code block: `{\"name\": \"tool_name\", \"arguments\": {\"key\": \"value\"}}` — use this fallback format\nonly, never both. Only call a tool whose exact name appears in the tool index above and is currently callable\n(or reachable via `capability_expand`). Never invent, rename, alias, or guess a tool name, platform ID, or\nplatform action from argument shape or wording — if the index does not list it, it does not exist.\n`platform_connect.action` (list/guide/preflight/connect/disconnect/remove/status/events_start/events_stop/\nevents_status) is the App Connector management namespace; `platform_action.action` only accepts exact\nregistered business actions such as `im.send_message` shown in the App Connector Capability Index — never mix\nthe two namespaces. If a tool call returns an unknown/unavailable result, use the returned suggestions or\navailable names for a single retry instead of trying further variations of the same guess.\n\n**Side-effect action rule** (`platform_action` with effect=send/write/execute):\n- Call each unique action+payload **exactly once**. Never include duplicates in the same turn.\n- Once the result returns `\"completed\": true`, that action is DONE for this task. Do NOT call it again\n in any subsequent turn — immediately summarize the result for the user instead.\n- The system enforces idempotency: duplicate calls are blocked and will not execute.\n- If the user explicitly requests sending/writing multiple times, use distinct payloads per call.\n\n**Resource identifier provenance rule**:\n- NEVER fabricate, guess, or infer resource identifiers (chat_id, message_id, file_key, user_id, etc.).\n Every resource ID used in a side-effect action MUST come from a successful API response in this session.\n- If a read/list action fails (e.g. authorization error), you do NOT have valid resource IDs.\n Report the failure to the user — do NOT attempt the dependent write/send action with a guessed ID.\n- When a tool result contains `\"llm_instruction\"`, follow it exactly.\n\n## Guidelines\n1. **Direct answers first**: If you already know the answer, respond directly without tools.\n2. **Avoid redundant tool calls**: Do not call the same tool with the same arguments more than once in the same user turn. When an existing tool result already answers the user's request, stop calling tools and answer directly.\n3. **Use tools proactively**: When the user asks about files, time, system state, or needs actions performed, use the appropriate tool.\n4. **Chain tools when needed**: You can call multiple tools in sequence (e.g., list files → read file → summarize).\n5. **Handle failures gracefully**: If a tool fails, explain what went wrong and suggest alternatives. If it failed because of an unknown tool/platform/action name, retry once with an exact name from the returned suggestions, then explain rather than keep guessing.\n6. **Summarize results naturally**: After tool execution, synthesize the results into a helpful answer rather than dumping raw output.\n7. **Stay conversational**: Maintain a natural, helpful tone. Acknowledge context from earlier in the conversation.\n8. **Recall past work**:\n - Broad queries (\"之前做了什么\", \"列出任务\"): answer from the \"Recent Task History\" section already in your context. If insufficient, call session_list.\n - Specific lookups (\"上次那个配置怎么改的\"): call session_search with relevant phrases (NOT single characters).\n - Do NOT call search tools repeatedly with keyword fragments. One well-phrased call is better than ten fragmented ones.\n\n## Coding & Verification\nWhen working with code, prefer the precise built-in tools over ad-hoc shell: use `repo_map` to orient in an\nunfamiliar project, `code_search`/`file_find` to locate and `code_intel` for symbols, `edit_file` (anchored\nsearch-replace, or a unified `diff`) to change files — never rewrite a whole file to change a few lines — and\n`git_query` to inspect diffs/log. After edits, check `syntax_ok` in the result and run `test_run`/`lint_check`\nbefore declaring the work done. Batch independent read-only calls (search/read/query) into a single turn — they\nrun in parallel.\n\n## Presentation Style\n1. **Polished Markdown only**: Format user-facing answers with clean Markdown headings, short paragraphs, and concise bullets. Use tables only when they improve comparison or scanning.\n2. **Terminal-friendly layout**: Keep lines readable in a TUI; avoid dense walls of text, deeply nested lists, oversized ASCII art, or heavy visual blocks.\n3. **Elegant emphasis**: Use bold text sparingly for key terms and conclusions. Avoid excessive emojis, decorative symbols, repeated separators, or visual noise.\n4. **Theme-safe colors**: Do not emit ANSI escape codes, HTML color tags, Rich markup, or hardcoded color names. Rely on the TUI theme to render Markdown professionally.\n5. **No leaked tool protocol**: Never show tool-call JSON, internal schemas, raw observations, tool result payloads, or hidden reasoning in the final answer unless the user explicitly asks for raw/debug output. Treat any prior `{\"name\": ..., \"arguments\": ...}` blocks and `Tool result (...)` messages as internal execution context only.\n6. **Professional closure**: End with a concise conclusion or next step when helpful; avoid rambling after the useful answer is complete.\n\nWhen finished with all tool calls, respond normally without a JSON block, tool-call transcript, or process log.\n\n## Task Contract\n- Task ID: turn-1\n- Original user request: List the connected hardware devices, then describe bench_r8 in full.\n- Workspace root: \n- Allowed roots: \n- Treat relative project paths as relative to the workspace root; never infer `.` as the project root when a workspace root is provided.\n- Workspace boundary is enforced by tools: do not read, search, edit, or run commands against paths outside the allowed roots unless the user explicitly requests an external path and the tool/approval policy permits it.\n- LeapFlow workspace config is optional at `/.leapflow/config.yaml`; runtime config is loaded from `~/.leapflow/config/user.yaml` and `~/.leapflow/profiles//config/*.yaml`.\n- Preserve this task contract across summarization, compression, tool loops, and memory retrieval.\n" + }, + { + "role": "system", + "content": "\n## Recent Session Summary\n- [user] List the connected hardware devices, then describe bench_r8 in full.\n" + }, + { + "role": "user", + "content": "List the connected hardware devices, then describe bench_r8 in full.\nList the connected hardware devices, then describe bench_r8 in full." + }, + { + "role": "assistant", + "content": "", + "tool_calls": [ + "hw_list" + ] + }, + { + "role": "tool", + "content": "{\"ok\": true, \"devices\": [{\"device_id\": \"bench_r8\", \"display_name\": \"R8 Simulated Bench\", \"location\": \"journey-lab\", \"channels\": 3, \"writable\": 2, \"streaming\": 1, \"quantities\": [\"state.homed\", \"temperature\"], \"verified\": true, \"halt_supported\": true}], \"count\": 1, \"hint\": \"Call hw_describe(device_id) for channel limits before commanding a device.\", \"execution_id\": \"\", \"idempotency_key\": \"\", \"execution_policy\": \"read_only\", \"tool_call_id\": \"call_1\", \"execution_status\": \"completed\"}", + "tool_result": true + }, + { + "role": "assistant", + "content": "", + "tool_calls": [ + "hw_describe" + ] + }, + { + "role": "tool", + "content": "{\"ok\": true, \"kind\": \"\", \"truncated\": true, \"original_chars\": 846, \"budget_chars\": 819}", + "tool_result": true + } + ], + "tools": [ + "capability_expand", + "code_intel", + "code_search", + "config_get", + "config_list", + "env_info", + "file_find", + "file_list", + "file_read", + "git_query", + "hw_describe", + "hw_list", + "hw_read", + "hw_status", + "lint_check", + "memory_search", + "plugin_list", + "plugin_propose", + "plugin_status", + "plugin_versions", + "repo_map", + "research_note", + "runtime_snapshot", + "schedule_reentry", + "self_describe", + "session_list", + "skill_view", + "skills_list", + "terminal_list", + "terminal_read", + "test_run", + "text_search", + "time_get", + "tool_describe", + "tool_search", + "web_fetch" + ] + }, + "responses": [ + { + "status": 200, + "content_type": "application/json", + "body": "{\"id\": \"chatcmpl-cassette\", \"object\": \"chat.completion\", \"model\": \"cassette-model\", \"choices\": [{\"index\": 0, \"message\": {\"role\": \"assistant\", \"content\": \"Discovered the simulated bench and read its channel limits.\"}, \"finish_reason\": \"stop\"}], \"usage\": {\"prompt_tokens\": 64, \"completion_tokens\": 16, \"total_tokens\": 80}}" + } + ] +} diff --git a/tests/_fixtures/cassettes/r8_hardware/cassette-model-4948dea531cf18fc.cassette.json b/tests/_fixtures/cassettes/r8_hardware/cassette-model-4948dea531cf18fc.cassette.json deleted file mode 100644 index bb402760..00000000 --- a/tests/_fixtures/cassettes/r8_hardware/cassette-model-4948dea531cf18fc.cassette.json +++ /dev/null @@ -1,91 +0,0 @@ -{ - "fingerprint": "4948dea531cf18fcd7adc5d305638a089687bf60360ee0d7f6d7fb7d065bed10", - "note": "captured in seed mode", - "request": { - "model": "cassette-model", - "stream": false, - "messages": [ - { - "role": "system", - "content": "You are LeapFlow, an intelligent assistant that can both converse naturally and take real actions on the user's computer.\n\n## Capabilities\nThe tool index below lists **every** registered tool by name and a one-line summary — this is the complete\ncapability contract; nothing else exists. Only a subset is directly callable this turn (via native tool calling,\nnot a JSON block in your reply). If you need a tool from the index that is not yet callable, call\n`capability_expand` with its category name first — the matching tools become callable immediately after.\nWhen the user asks what LeapFlow itself supports, whether it supports plugins, or which runtime capabilities\nare available, use the live capability evidence exposed by `plugin_list` before making capability claims; report\nconfiguration-dependent or unavailable capabilities as limitations instead of inferring from documentation.\n- **text_search**(text, pattern): Search for a regex pattern in text.\n- **text_replace**(text, old, new, count) [capability_expand category: write]: Replace occurrences of a substring in text.\n- **time_get**(): Get current date and time.\n- **env_info**(): Get system environment information (OS, Python version, cwd).\n- **skills_list**(query, category, source): List available learned skills. Use when user asks about capabilities or you need a specific skill.\n- **skill_view**(name): View the full content of a specific skill document.\n- **code_intel**(path, operation): Precise document symbols (outline) for a source file: classes, functions, and methods with line ranges. Python uses an exact AST parse; other languages use a keyword-prefix scan. Prefer over file_read mode=symbols for accurate navigation before editing. Read-only.\n- **repo_map**(path): Compact project orientation for a repository root: languages, detected test/lint commands, top-level structure, entry points, manifest, and VCS branch. Call this first when entering an unfamiliar codebase. Read-only.\n- **scm_sync**(action, cwd, remote, pull_ref, push_ref, timeout) [capability_expand category: scm]: Run a typed git SCM action. Use this instead of shell_run for git pull/push/status. For 'pull origin main then push', set action='pull_then_push', remote='origin', pull_ref='main', and omit push_ref so LeapFlow pushes the current local branch.\n- **git_query**(action, cwd, ref, path, staged, max_count, stat): Read-only structured git inspection: action=diff|log|status|branch|show. Prefer over shell_run for reading repo state — output is clipped, redacted, and log/branch are parsed into structured fields. Use scm_sync for pull/push.\n- **git_write**(action, cwd, message, stage_all, name, ref, create) [capability_expand category: scm]: Mutating git actions: action=commit (message, stage_all), branch (create+switch), checkout (switch; create=true for -b). Approval-gated. Use scm_sync for pull/push and git_query for reads.\n- **test_run**(command, cwd, timeout): Run the project's test suite and return structured results (framework, passed/failed counts, failing tests). Auto-detects the runner (pytest/npm/go/cargo) or uses a configured/explicit command; executes via the governed shell. ok=true means the runner executed — see 'success' for pass/fail.\n- **lint_check**(command, cwd, timeout): Run the project's linter and return a structured clean/issue result. Auto-detects the linter (ruff/eslint/go vet/clippy) or uses a configured/explicit command; executes via the governed shell. ok=true means the linter ran — see 'clean'.\n- **file_list**(path, pattern, depth): List files and directories at a given path. Use depth=1 or depth=2 to get a recursive tree in one call instead of listing each sub-directory separately.\n- **file_read**(path, max_lines, start_line, max_chars, mode): Read text file content with adaptive context governance. For large or unfamiliar files, prefer mode='outline' or mode='symbols' first, then use mode='raw' with start_line/max_lines for the specific range you actually need. For LeapFlow's own settings, use config_list / config_get / config_set — its config files are outside the workspace and not readable here.\n- **file_write**(path, content, mode) [capability_expand category: write]: Write content to a file (overwrite or append).\n- **code_search**(pattern, patterns, path, glob, ignore_case, multiline, max_results, context_lines): Search file CONTENTS by regex pattern across a directory tree (ripgrep-backed). Requires a regex pattern. NOT for listing or browsing directory contents — use file_list for that. Prefer this over shell_run grep: faster, skips VCS/dependency/build dirs, and returns structured path:line:column matches. Batch related lookups into ONE call via `patterns` (OR-combined, single pass) instead of issuing several separate searches. Use file_read for the surrounding context of a hit.\n- **file_find**(glob, path, max_results): Find files by a recursive glob pattern under a base path (e.g. '**/test_*.py' or '*.md'). Prefer this over shell_run find; skips VCS/dependency/build dirs.\n- **edit_file**(path, edits, dry_run, diff) [capability_expand category: write]: Apply targeted, anchored search-replace edits to an EXISTING text file (use file_write to create/overwrite). Each edit is {original_text, new_text, replace_all?}; original_text must match exactly and uniquely (or set replace_all) — a non-unique or missing anchor is rejected so files are never corrupted. Set dry_run to preview. Alternatively pass a unified 'diff' to apply its hunks as anchored edits. Far cheaper and safer than rewriting a whole file.\n- **shell_run**(command, cwd, timeout) [capability_expand category: shell]: Execute a one-shot shell command with timeout protection. Runs in the active workspace; paths resolving outside it are refused. Reach for a structured tool first when one fits — web_fetch for anything over HTTP(S), git_query/scm_sync for git, code_search/file_find/file_read for the repo, config_get/config_set for LeapFlow's own settings — because those report typed results, while a failed shell command can only be diagnosed from its exit code and stderr. Every shell run counts as an external side effect, so a failure stops the rest of the batch and is not retried automatically.\n- **terminal_open**(command, cwd, shell) [capability_expand category: terminal]: Open a PERSISTENT shell session (REPL/dev server/watch), returning a session_id for terminal_send/read/close. Disabled unless tools.terminal_session_enabled is set. For one-shot commands use shell_run instead.\n- **terminal_send**(session_id, input, wait) [capability_expand category: terminal]: Send a line of input to a persistent terminal session and return output captured shortly after.\n- **terminal_read**(session_id, wait): Drain buffered output from a persistent terminal session (optionally waiting briefly first).\n- **terminal_close**(session_id) [capability_expand category: terminal]: Terminate a persistent terminal session and release its process group.\n- **terminal_list**(): List active persistent terminal sessions.\n- **config_list**(category, limit): List LeapFlow's own writable settings (LLM model and endpoint, daemon, memory, perception, gateway, …) with current values. Use this to discover the exact key before changing anything. Optionally narrow by `category`. This is the only correct way to inspect LeapFlow configuration — never read config files from disk.\n- **config_get**(key): Read one LeapFlow setting by key (e.g. 'llm.model', 'llm.base_url', 'daemon.log_level'), returning its current value, type, scopes, and whether a change needs a daemon restart. There is no 'llm.provider' key: provider behavior is inferred from the OpenAI-compatible 'llm.base_url'. Never read LeapFlow config files from disk — use this.\n- **config_set**(key, value, scope) [capability_expand category: config]: Change one LeapFlow setting by key, e.g. switch an OpenAI-compatible LLM with key='llm.model' and key='llm.base_url'. There is no 'llm.provider' key: provider behavior is inferred from the endpoint. Values are validated and coerced; credentials are stored in the vault automatically. Call config_list or config_get first if unsure of the key. The result states whether a `leap daemon restart` is required. Never edit LeapFlow config files directly.\n- **web_fetch**(url, select, timeout, max_bytes): Read a URL over HTTP(S) and get back extracted, context-sized content: parsed JSON for API endpoints, readable text plus links for web pages. Use this for anything on the internet — prices, docs, releases, articles — instead of running curl through shell_run: it reports real HTTP status codes, retries rate limits on its own, and is a plain read so a retry is always safe. For JSON APIs pass `select` with a dotted path (e.g. 'chart.result.0.meta') to return just that part instead of the whole payload.\n- **memory_search**(query, limit): Search agent memory for relevant past experiences, observations, and facts.\n- **memory_add**(content, kind) [capability_expand category: write]: Store a new observation or insight in memory for future reference.\n- **research_note**(kind, text): Record a compact, structured note about the current task's state so it survives context compression on long / multi-step tasks. Use for durable findings, open questions still to resolve, decisions / excluded paths, and the immediate next step. One concise sentence per note.\n- **capability_expand**(category): Fetch the full callable schema for every tool in a capability category (e.g. 'hub', 'gateway', 'desktop', 'delegate', 'file', 'memory', 'skill'). The compact tool index always lists every registered tool by name and a one-line summary, but only a static low-risk subset is directly callable each turn. If you need a tool from the index that is not yet callable, call capability_expand with its category first; the matching tools become callable in this turn. Never invent a tool name — expand the category instead.\n- **delegate_task**(goal, context) [capability_expand category: delegate]: Delegate a complex sub-task to an isolated subagent. The subagent gets a fresh context and restricted tool access. Use when a task is self-contained and can be solved independently.\n- **schedule_reentry**(kind, reason, delay_seconds, event_match, max_reentries, deadline_seconds): Register a re-entry so this task can resume later from its current orientation (findings / open questions / next step). Use when work must pause and continue after a delay (kind=time) or when a matching platform event arrives (kind=event), instead of finishing now. The research-ledger state is carried over automatically.\n- **hub_push**(skill_name, visibility, version) [capability_expand category: hub]: Push a local skill to the ModelScope Hub for sharing or backup.\n- **hub_pull**(repo_id, version) [capability_expand category: hub]: Pull a skill from the ModelScope Hub to install locally.\n- **hub_search**(query) [capability_expand category: hub]: Search for skills on the Hub by keyword or description.\n- **hub_sync**(mode, dry_run) [capability_expand category: hub]: Preview or execute sync between local skills and Hub.\n- **platform_action**(platform, action, payload, backend_kind) [capability_expand category: gateway]: Execute an exact registered business action on an external platform through LeapFlow's App Connector layer. Actions must be copied from the App Connector Capability Index and are addressed as domain.operation, e.g. im.send_message or docs.create_markdown. All business fields (chat_id, text, query, etc.) MUST be placed inside `payload`, never at the top level. Example: {\"platform\":\"feishu\",\"action\":\"im.send_message\",\"payload\":{\"chat_id\":\"oc_xxx\",\"text\":\"hello\"}}. Do not invent action names, do not use management actions such as list/guide/connect/status here.\n- **platform_connect**(action, platform, credentials, options, checkpoint) [capability_expand category: gateway]: List, guide, connect, disconnect, remove, or check status for external platforms using the App Connector management namespace. Supports REST and CLI backends. Use this for management actions such as list/guide/preflight/connect/status; use platform_action only for exact registered business actions.\n- **gateway_send**(platform, chat_id, text, thread_id) [capability_expand category: gateway]: Send a message to a connected external platform (Feishu group, Telegram chat, DingTalk conversation, etc.). Requires the platform to be connected via gateway_connect first. Use gateway_connect with action='list' to see connected platforms and available chat IDs.\n- **gateway_connect**(action, platform, credentials, options) [capability_expand category: gateway]: Connect, configure, or manage external platform integrations (Feishu, DingTalk, Telegram, Slack, Discord, etc.). Conversational flow: 1) call 'guide' to get setup steps + required fields, 2) present the steps to the user and ask for ALL required credentials in a single message, 3) call 'connect' with the credentials. Goal: complete in 1–2 user turns. NEVER include credential values in your text response.\n- **plugin_list**(): List the live plugin registry and cross-subsystem capability evidence. Use this before answering questions about whether LeapFlow supports plugins, self-evolution, plugin installation, hot reload, versioning, or other runtime capabilities.\n- **plugin_status**(plugin_id): Get detailed status of a specific plugin: its declared category, runtime dependencies, contributed tools, and fiber lifecycle state.\n- **plugin_versions**(plugin_id): List recorded source versions and active pointer for a profile-scoped plugin.\n- **plugin_propose**(requested_capability, plugin_id, proposed_tools, test_cases, risk_level, evidence): Create a side-effect-free PluginProposal from explicit capability-gap evidence. Use this before plugin_generate when a missing capability should be reviewed. Does not call an LLM, write files, or install anything.\n- **assess_compatibility**(manifest, source_path) [capability_expand category: plugin_management]: Assess whether a foreign plugin manifest or real DSH source bundle is compatible with LeapFlow. A source bundle assessment is static: runtime_ready stays false until restricted Node discovery during plugin_install. Returns component-level verdicts and limitations.\n- **plugin_generate**(plugin_id, description, proposal_id) [capability_expand category: system]: Generate a new ToolPlugin from a natural-language capability description. The LLM produces code that conforms to the ToolPlugin Protocol; it is then rigorously validated (syntax, structure, import, protocol conformance). The generated source is stored in CAS and receives explicit content approval. It DOES NOT install the plugin — installation requires a second, mutation-specific approval via plugin_install.\n- **plugin_install**(plugin_id, code, marketplace_name, source_path, proposal_id, version_label) [capability_expand category: system]: Install a plugin from exactly one source: validated Python code, the configured Python marketplace, or a real DSH source bundle. DSH bundles run restricted Node discovery before registration; only public host tools are installed and client UI limitations are reported. Writes profile-scoped state and mutates the process-global registry. REQUIRES APPROVAL.\n- **plugin_rollback**(plugin_id, version) [capability_expand category: system]: Rollback a profile-scoped plugin to a recorded source version and reload it. REQUIRES APPROVAL.\n- **plugin_reload**(plugin_id, version_label) [capability_expand category: system]: Hot-reload a plugin at runtime. Disposes the old plugin fiber, re-imports its module, and registers a fresh instance. Existing in-flight turns are unaffected (snapshot isolation). REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_disable**(plugin_id) [capability_expand category: system]: Disable a plugin by disposing its fiber, removing its tools from the runtime registry. Cannot disable self_management itself. REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_remove**(plugin_id, delete_source) [capability_expand category: system]: Terminally remove a plugin: dispose its fiber, unregister its tools, remove reload metadata, and optionally delete its profile-scoped source file. Cannot remove self_management itself. REQUIRES APPROVAL.\n- **plugin_unquarantine**(plugin_id) [capability_expand category: plugin_management]: Restore a quarantined plugin to probation status for re-evaluation. Unfreezes the trust ledger, transitions the proposal back to PROBATION, and reloads the plugin. REQUIRES APPROVAL.\n- **plugin_enable**(plugin_id) [capability_expand category: system]: Re-enable a previously disabled plugin by reloading its module and registering a fresh instance. REQUIRES APPROVAL.\n- **self_describe**(facet): Introspect LeapFlow's own identity, capabilities, runtime state, evolution metrics, or platform connections. Use facet='all' only when a comprehensive self-check is explicitly requested.\n- **runtime_snapshot**(): Lightweight (~150 token) flat snapshot of current runtime state: model, context budget, posture, disclosure level, turn count, cache hit rate, uptime, tool count, and pending approvals.\n- **schedule_create**(trigger_expression, instruction, execution_mode, max_retries, delivery_target) [capability_expand category: scheduler]: Create a new scheduled task. Specify a trigger expression (e.g. '30m', 'every 2h', '0 9 * * *') and the instruction to execute on each trigger. Optionally provide an execution mode and a delivery target to receive notifications.\n- **schedule_list**() [capability_expand category: scheduler]: List all scheduled tasks with their current state, trigger, and next due time.\n- **schedule_status**(task_id) [capability_expand category: scheduler]: Get detailed status and recent execution history for a scheduled task.\n- **schedule_pause**(task_id) [capability_expand category: scheduler]: Pause a scheduled task so it stops firing without being cancelled.\n- **schedule_resume**(task_id) [capability_expand category: scheduler]: Resume a paused scheduled task — re-arms it and recalculates next due time.\n- **schedule_cancel**(task_id) [capability_expand category: scheduler]: Cancel a scheduled task permanently.\n- **tool_search**(query, max_results): Search all registered tools by keyword relevance. Returns a ranked list of matching tools with name, category, and summary. Use this when you need a tool but are unsure of its exact name or category.\n- **tool_describe**(tool_name): Get the full callable schema of a single tool by exact name. Returns the tool's description, parameters, and metadata. Use after tool_search to inspect a specific tool before calling it.\n- **hw_list**(): List connected hardware devices with their channel counts and measured quantities. Start here; it does not include operating limits.\n- **hw_describe**(device_id): Return the full reference for one device: every channel, its unit, its operating envelope, rate limit, reversibility, and required interlocks, plus any prior outcomes recorded for its writable channels. Required before commanding a device.\n- **hw_read**(device_id, channel_id): Read the current value of one device channel. Has no physical effect.\n- **hw_status**(device_id): Report connection health, halt capability, and recent notable events (threshold excursions, lost samples, stalled channels) for one device.\n- **hw_configure**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Set a configuration value or setpoint on a channel declaring effect=configure. Setpoints often have inertia: the value may need time to stabilise.\n- **hw_actuate**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Command motion or output on a channel declaring effect=actuate. This moves physical hardware; a repeat from an unknown state is not a safe retry.\n- **hw_dispense**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Consume an irreversible resource on a channel declaring effect=dispense. Running this twice dispenses twice; never repeat it after a failure without first verifying what already happened.\n- **hw_estop**(device_id) [capability_expand category: hardware]: Stop all motion and output on a device immediately. Never requires approval; use it whenever device behaviour is unexpected.\n- **session_search**(query, limit) [capability_expand category: unclassified]: Search past conversation sessions in the current workspace for relevant context.\n- **session_list**(limit): List recent conversation sessions in the current workspace with stable ids, titles, dates, and summaries. Use for browsing past tasks or when user asks to see history without specific search terms. No keywords needed — returns chronological list.\n- **session_detail**(session_id, limit, offset, include_inactive) [capability_expand category: unclassified]: Read a paginated persisted transcript for one past conversation session in the current workspace. Use after session_list or session_search returns a session_id.\n- **click**(element_index) [capability_expand category: desktop]: Click a UI element by its element_index (from the latest observe_ui snapshot)\n- **get_clipboard**() [capability_expand category: desktop]: Read current clipboard text content\n- **list_apps**(filter, running_only) [capability_expand category: desktop]: List available applications on this system. Use to discover correct bundle_id before switch_app.\n- **list_windows**() [capability_expand category: desktop]: List all top-level windows with pid, window_id, title, and per-window state (minimized, on-screen). Call this first to pick the pid and window_id that observe_ui and other window tools require.\n- **observe_ui**(pid, window_id, query) [capability_expand category: desktop]: Snapshot one window's actionable UI elements, each tagged with an element_index for click/right_click/read_text. Re-observe after actions — indices belong to one snapshot. Requires the window's pid and window_id from list_windows.\n- **open_url**(url, app_id) [capability_expand category: desktop]: Open a URL in the default or specified browser\n- **read_text**(element_index) [capability_expand category: desktop]: Read the text content of a specific UI element from the latest snapshot\n- **right_click**(element_index) [capability_expand category: desktop]: Right-click a UI element to open its context menu. Returns visible menu items.\n- **screenshot**(pid, window_id) [capability_expand category: desktop]: Capture a screenshot for visual verification. With pid + window_id captures that window (works across all displays); defaults to the last observed window, or the full desktop when no window has been observed.\n- **scroll**(element_index, direction, amount, pid, window_id) [capability_expand category: desktop]: Scroll a scrollable area of a window. Omit element_index to scroll the window's focused/page scroller; pass one to scroll an exact element from the latest snapshot.\n- **select_text**(element_index) [capability_expand category: desktop]: Select all text in a UI element (focus + select-all, for subsequent copy)\n- **set_clipboard**(text) [capability_expand category: desktop]: Write text to the clipboard\n- **shortcut**(keys) [capability_expand category: desktop]: Execute a keyboard shortcut\n- **switch_app**(app_id) [capability_expand category: desktop]: Switch to an app (launch if needed, activate, verify)\n- **type_text**(text) [capability_expand category: desktop]: Type text into the currently focused element\n- **wait**(seconds) [capability_expand category: desktop]: Wait for a specified duration before continuing\n- **wait_until**(condition, pid, window_id, timeout, poll_interval) [capability_expand category: desktop]: Wait until a UI condition is met (polls UI tree). Returns elements when found or on timeout.\n- **wait_until_stable**(timeout, poll_interval, pid, window_id) [capability_expand category: desktop]: Wait until the UI stops changing (element set stabilizes across polls).\n\n## App Connector Capability Index\nLeapFlow can onboard and manage external apps through `platform_connect` and execute exact registered business actions through `platform_action`.\nFor requests about connecting, setting up, configuring, enabling, or managing a supported app, use `platform_connect` first instead of generating SDK/Webhook sample code.\n`platform_connect.action` is the management namespace: list, guide, preflight, connect, disconnect, remove, status, events_start, events_stop, events_status.\n`platform_action.action` is only for exact registered platform business actions listed below, such as `im.send_message`; never use management actions like `list` or `guide` there.\nAll business fields MUST go inside `payload`; top-level keys are only `platform`, `action`, and `payload`.\nDo not invent platform IDs or platform action names. If the needed action is not listed, ask for discovery/clarification instead of guessing.\nUse `platform_connect` with `action='guide'` and the matching `platform` to start onboarding; use `action='list'` when the app is unclear.\nWhen a pending onboarding state is present, continue from that state with `platform_connect` instead of asking the user to restate the app.\nSupported apps:\n- `api_server`: API Server (OpenAI Compatible) (category=api; backend=adapter; platform_action actions=none registered)\n- `dingtalk`: 钉钉 (DingTalk) (category=im; backend=adapter; platform_action actions=none registered)\n- `feishu`: 飞书 (Feishu/Lark) (category=im; backend=cli)\n - `calendar.create_event` payload={summary*, start_time*, end_time*, attendees} [write/high]\n - `docs.create_markdown` payload={title*, markdown*, folder_token} [write/medium]\n - `drive.search` payload={query*, limit} [read/medium]\n - `im.add_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.download_resource` payload={message_id*, file_key*, type*} [read/low]\n - `im.get_messages` payload={message_ids*} [read/low]\n - `im.list_chats` payload={page_size} [read/low]\n - `im.list_messages` payload={chat_id*} [read/low]\n - `im.list_thread_messages` payload={thread*} [read/low]\n - `im.remove_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.reply_message` payload={message_id*, text*} [send/high]\n - `im.search_chats` payload={query*, page_size} [read/low]\n - `im.search_messages` payload={query*} [read/medium]\n - `im.send_message` payload={chat_id*, text*, thread_id} [send/high]\n - `im.update_card` payload={token*, card*} [write/medium]\n - `im.update_message` payload={message_id*, text*} [write/medium]\n - `mail.search_unread` payload={query, limit} [read/high]\n - `sheets.append_row` payload={spreadsheet_token*, sheet_id*, values*} [write/medium]\n - `task.create` payload={title*, description, due_time} [write/medium]\n- `telegram`: Telegram (category=social; backend=adapter; platform_action actions=none registered)\n- `webhook`: Webhook (Generic) (category=webhook; backend=adapter; platform_action actions=none registered)\n\n## Tool Usage\nTools are normally invoked through the native function-calling mechanism, not by writing JSON in your reply\ntext. Only if the provider signals that native function calling is unavailable for this turn, fall back to a\nsingle JSON code block: `{\"name\": \"tool_name\", \"arguments\": {\"key\": \"value\"}}` — use this fallback format\nonly, never both. Only call a tool whose exact name appears in the tool index above and is currently callable\n(or reachable via `capability_expand`). Never invent, rename, alias, or guess a tool name, platform ID, or\nplatform action from argument shape or wording — if the index does not list it, it does not exist.\n`platform_connect.action` (list/guide/preflight/connect/disconnect/remove/status/events_start/events_stop/\nevents_status) is the App Connector management namespace; `platform_action.action` only accepts exact\nregistered business actions such as `im.send_message` shown in the App Connector Capability Index — never mix\nthe two namespaces. If a tool call returns an unknown/unavailable result, use the returned suggestions or\navailable names for a single retry instead of trying further variations of the same guess.\n\n**Side-effect action rule** (`platform_action` with effect=send/write/execute):\n- Call each unique action+payload **exactly once**. Never include duplicates in the same turn.\n- Once the result returns `\"completed\": true`, that action is DONE for this task. Do NOT call it again\n in any subsequent turn — immediately summarize the result for the user instead.\n- The system enforces idempotency: duplicate calls are blocked and will not execute.\n- If the user explicitly requests sending/writing multiple times, use distinct payloads per call.\n\n**Resource identifier provenance rule**:\n- NEVER fabricate, guess, or infer resource identifiers (chat_id, message_id, file_key, user_id, etc.).\n Every resource ID used in a side-effect action MUST come from a successful API response in this session.\n- If a read/list action fails (e.g. authorization error), you do NOT have valid resource IDs.\n Report the failure to the user — do NOT attempt the dependent write/send action with a guessed ID.\n- When a tool result contains `\"llm_instruction\"`, follow it exactly.\n\n## Guidelines\n1. **Direct answers first**: If you already know the answer, respond directly without tools.\n2. **Avoid redundant tool calls**: Do not call the same tool with the same arguments more than once in the same user turn. When an existing tool result already answers the user's request, stop calling tools and answer directly.\n3. **Use tools proactively**: When the user asks about files, time, system state, or needs actions performed, use the appropriate tool.\n4. **Chain tools when needed**: You can call multiple tools in sequence (e.g., list files → read file → summarize).\n5. **Handle failures gracefully**: If a tool fails, explain what went wrong and suggest alternatives. If it failed because of an unknown tool/platform/action name, retry once with an exact name from the returned suggestions, then explain rather than keep guessing.\n6. **Summarize results naturally**: After tool execution, synthesize the results into a helpful answer rather than dumping raw output.\n7. **Stay conversational**: Maintain a natural, helpful tone. Acknowledge context from earlier in the conversation.\n8. **Recall past work**:\n - Broad queries (\"之前做了什么\", \"列出任务\"): answer from the \"Recent Task History\" section already in your context. If insufficient, call session_list.\n - Specific lookups (\"上次那个配置怎么改的\"): call session_search with relevant phrases (NOT single characters).\n - Do NOT call search tools repeatedly with keyword fragments. One well-phrased call is better than ten fragmented ones.\n\n## Coding & Verification\nWhen working with code, prefer the precise built-in tools over ad-hoc shell: use `repo_map` to orient in an\nunfamiliar project, `code_search`/`file_find` to locate and `code_intel` for symbols, `edit_file` (anchored\nsearch-replace, or a unified `diff`) to change files — never rewrite a whole file to change a few lines — and\n`git_query` to inspect diffs/log. After edits, check `syntax_ok` in the result and run `test_run`/`lint_check`\nbefore declaring the work done. Batch independent read-only calls (search/read/query) into a single turn — they\nrun in parallel.\n\n## Presentation Style\n1. **Polished Markdown only**: Format user-facing answers with clean Markdown headings, short paragraphs, and concise bullets. Use tables only when they improve comparison or scanning.\n2. **Terminal-friendly layout**: Keep lines readable in a TUI; avoid dense walls of text, deeply nested lists, oversized ASCII art, or heavy visual blocks.\n3. **Elegant emphasis**: Use bold text sparingly for key terms and conclusions. Avoid excessive emojis, decorative symbols, repeated separators, or visual noise.\n4. **Theme-safe colors**: Do not emit ANSI escape codes, HTML color tags, Rich markup, or hardcoded color names. Rely on the TUI theme to render Markdown professionally.\n5. **No leaked tool protocol**: Never show tool-call JSON, internal schemas, raw observations, tool result payloads, or hidden reasoning in the final answer unless the user explicitly asks for raw/debug output. Treat any prior `{\"name\": ..., \"arguments\": ...}` blocks and `Tool result (...)` messages as internal execution context only.\n6. **Professional closure**: End with a concise conclusion or next step when helpful; avoid rambling after the useful answer is complete.\n\nWhen finished with all tool calls, respond normally without a JSON block, tool-call transcript, or process log.\n\n## Task Contract\n- Task ID: turn-1\n- Original user request: List the connected hardware devices, then describe bench_r8 in full.\n- Workspace root: \n- Allowed roots: \n- Treat relative project paths as relative to the workspace root; never infer `.` as the project root when a workspace root is provided.\n- Workspace boundary is enforced by tools: do not read, search, edit, or run commands against paths outside the allowed roots unless the user explicitly requests an external path and the tool/approval policy permits it.\n- LeapFlow workspace config is optional at `/.leapflow/config.yaml`; runtime config is loaded from `~/.leapflow/config/user.yaml` and `~/.leapflow/profiles//config/*.yaml`.\n- Preserve this task contract across summarization, compression, tool loops, and memory retrieval.\n" - }, - { - "role": "system", - "content": "\n## Recent Session Summary\n- [user] List the connected hardware devices, then describe bench_r8 in full.\n" - }, - { - "role": "user", - "content": "List the connected hardware devices, then describe bench_r8 in full.\nList the connected hardware devices, then describe bench_r8 in full." - }, - { - "role": "assistant", - "content": "", - "tool_calls": [ - "hw_list" - ] - }, - { - "role": "tool", - "content": "{\"ok\": true, \"devices\": [{\"device_id\": \"bench_r8\", \"display_name\": \"R8 Simulated Bench\", \"location\": \"journey-lab\", \"channels\": 3, \"writable\": 2, \"streaming\": 1, \"quantities\": [\"state.homed\", \"temperature\"], \"verified\": true, \"halt_supported\": true}], \"count\": 1, \"hint\": \"Call hw_describe(device_id) for channel limits before commanding a device.\", \"execution_id\": \"\", \"idempotency_key\": \"\", \"execution_policy\": \"read_only\", \"tool_call_id\": \"call_1\", \"execution_status\": \"completed\"}", - "tool_result": true - }, - { - "role": "assistant", - "content": "", - "tool_calls": [ - "hw_describe" - ] - }, - { - "role": "tool", - "content": "{\"ok\": true, \"kind\": \"\", \"truncated\": true, \"original_chars\": 846, \"budget_chars\": 819}", - "tool_result": true - } - ], - "tools": [ - "capability_expand", - "code_intel", - "code_search", - "config_get", - "config_list", - "env_info", - "file_find", - "file_list", - "file_read", - "git_query", - "hw_describe", - "hw_list", - "hw_read", - "hw_status", - "lint_check", - "memory_search", - "plugin_list", - "plugin_propose", - "plugin_status", - "plugin_versions", - "repo_map", - "research_note", - "runtime_snapshot", - "schedule_reentry", - "self_describe", - "session_list", - "skill_view", - "skills_list", - "terminal_list", - "terminal_read", - "test_run", - "text_search", - "time_get", - "tool_describe", - "tool_search", - "web_fetch" - ] - }, - "responses": [ - { - "status": 200, - "content_type": "application/json", - "body": "{\"id\": \"chatcmpl-cassette\", \"object\": \"chat.completion\", \"model\": \"cassette-model\", \"choices\": [{\"index\": 0, \"message\": {\"role\": \"assistant\", \"content\": \"Discovered the simulated bench and read its channel limits.\"}, \"finish_reason\": \"stop\"}], \"usage\": {\"prompt_tokens\": 64, \"completion_tokens\": 16, \"total_tokens\": 80}}" - } - ] -} diff --git a/tests/_fixtures/cassettes/r8_hardware/cassette-model-601b0e5ebe77c051.cassette.json b/tests/_fixtures/cassettes/r8_hardware/cassette-model-601b0e5ebe77c051.cassette.json deleted file mode 100644 index f9a8020f..00000000 --- a/tests/_fixtures/cassettes/r8_hardware/cassette-model-601b0e5ebe77c051.cassette.json +++ /dev/null @@ -1,91 +0,0 @@ -{ - "fingerprint": "601b0e5ebe77c05156ecac5f0c5e854cd57e5cb088c39878fd3f2e188b789825", - "note": "captured in seed mode", - "request": { - "model": "cassette-model", - "stream": false, - "messages": [ - { - "role": "system", - "content": "You are LeapFlow, an intelligent assistant that can both converse naturally and take real actions on the user's computer.\n\n## Capabilities\nThe tool index below lists **every** registered tool by name and a one-line summary — this is the complete\ncapability contract; nothing else exists. Only a subset is directly callable this turn (via native tool calling,\nnot a JSON block in your reply). If you need a tool from the index that is not yet callable, call\n`capability_expand` with its category name first — the matching tools become callable immediately after.\nWhen the user asks what LeapFlow itself supports, whether it supports plugins, or which runtime capabilities\nare available, use the live capability evidence exposed by `plugin_list` before making capability claims; report\nconfiguration-dependent or unavailable capabilities as limitations instead of inferring from documentation.\n- **text_search**(text, pattern): Search for a regex pattern in text.\n- **text_replace**(text, old, new, count) [capability_expand category: write]: Replace occurrences of a substring in text.\n- **time_get**(): Get current date and time.\n- **env_info**(): Get system environment information (OS, Python version, cwd).\n- **skills_list**(query, category, source): List available learned skills. Use when user asks about capabilities or you need a specific skill.\n- **skill_view**(name): View the full content of a specific skill document.\n- **code_intel**(path, operation): Precise document symbols (outline) for a source file: classes, functions, and methods with line ranges. Python uses an exact AST parse; other languages use a keyword-prefix scan. Prefer over file_read mode=symbols for accurate navigation before editing. Read-only.\n- **repo_map**(path): Compact project orientation for a repository root: languages, detected test/lint commands, top-level structure, entry points, manifest, and VCS branch. Call this first when entering an unfamiliar codebase. Read-only.\n- **scm_sync**(action, cwd, remote, pull_ref, push_ref, timeout) [capability_expand category: scm]: Run a typed git SCM action. Use this instead of shell_run for git pull/push/status. For 'pull origin main then push', set action='pull_then_push', remote='origin', pull_ref='main', and omit push_ref so LeapFlow pushes the current local branch.\n- **git_query**(action, cwd, ref, path, staged, max_count, stat): Read-only structured git inspection: action=diff|log|status|branch|show. Prefer over shell_run for reading repo state — output is clipped, redacted, and log/branch are parsed into structured fields. Use scm_sync for pull/push.\n- **git_write**(action, cwd, message, stage_all, name, ref, create) [capability_expand category: scm]: Mutating git actions: action=commit (message, stage_all), branch (create+switch), checkout (switch; create=true for -b). Approval-gated. Use scm_sync for pull/push and git_query for reads.\n- **test_run**(command, cwd, timeout): Run the project's test suite and return structured results (framework, passed/failed counts, failing tests). Auto-detects the runner (pytest/npm/go/cargo) or uses a configured/explicit command; executes via the governed shell. ok=true means the runner executed — see 'success' for pass/fail.\n- **lint_check**(command, cwd, timeout): Run the project's linter and return a structured clean/issue result. Auto-detects the linter (ruff/eslint/go vet/clippy) or uses a configured/explicit command; executes via the governed shell. ok=true means the linter ran — see 'clean'.\n- **file_list**(path, pattern, depth): List files and directories at a given path. Use depth=1 or depth=2 to get a recursive tree in one call instead of listing each sub-directory separately.\n- **file_read**(path, max_lines, start_line, max_chars, mode): Read text file content with adaptive context governance. For large or unfamiliar files, prefer mode='outline' or mode='symbols' first, then use mode='raw' with start_line/max_lines for the specific range you actually need. For LeapFlow's own settings, use config_list / config_get / config_set — its config files are outside the workspace and not readable here.\n- **file_write**(path, content, mode) [capability_expand category: write]: Write content to a file (overwrite or append).\n- **code_search**(pattern, patterns, path, glob, ignore_case, multiline, max_results, context_lines): Search file CONTENTS by regex pattern across a directory tree (ripgrep-backed). Requires a regex pattern. NOT for listing or browsing directory contents — use file_list for that. Prefer this over shell_run grep: faster, skips VCS/dependency/build dirs, and returns structured path:line:column matches. Batch related lookups into ONE call via `patterns` (OR-combined, single pass) instead of issuing several separate searches. Use file_read for the surrounding context of a hit.\n- **file_find**(glob, path, max_results): Find files by a recursive glob pattern under a base path (e.g. '**/test_*.py' or '*.md'). Prefer this over shell_run find; skips VCS/dependency/build dirs.\n- **edit_file**(path, edits, dry_run, diff) [capability_expand category: write]: Apply targeted, anchored search-replace edits to an EXISTING text file (use file_write to create/overwrite). Each edit is {original_text, new_text, replace_all?}; original_text must match exactly and uniquely (or set replace_all) — a non-unique or missing anchor is rejected so files are never corrupted. Set dry_run to preview. Alternatively pass a unified 'diff' to apply its hunks as anchored edits. Far cheaper and safer than rewriting a whole file.\n- **shell_run**(command, cwd, timeout) [capability_expand category: shell]: Execute a one-shot shell command with timeout protection. Runs in the active workspace; paths resolving outside it are refused. Reach for a structured tool first when one fits — web_fetch for anything over HTTP(S), git_query/scm_sync for git, code_search/file_find/file_read for the repo, config_get/config_set for LeapFlow's own settings — because those report typed results, while a failed shell command can only be diagnosed from its exit code and stderr. Every shell run counts as an external side effect, so a failure stops the rest of the batch and is not retried automatically.\n- **terminal_open**(command, cwd, shell) [capability_expand category: terminal]: Open a PERSISTENT shell session (REPL/dev server/watch), returning a session_id for terminal_send/read/close. Disabled unless tools.terminal_session_enabled is set. For one-shot commands use shell_run instead.\n- **terminal_send**(session_id, input, wait) [capability_expand category: terminal]: Send a line of input to a persistent terminal session and return output captured shortly after.\n- **terminal_read**(session_id, wait): Drain buffered output from a persistent terminal session (optionally waiting briefly first).\n- **terminal_close**(session_id) [capability_expand category: terminal]: Terminate a persistent terminal session and release its process group.\n- **terminal_list**(): List active persistent terminal sessions.\n- **config_list**(category, limit): List LeapFlow's own writable settings (LLM model and endpoint, daemon, memory, perception, gateway, …) with current values. Use this to discover the exact key before changing anything. Optionally narrow by `category`. This is the only correct way to inspect LeapFlow configuration — never read config files from disk.\n- **config_get**(key): Read one LeapFlow setting by key (e.g. 'llm.model', 'llm.base_url', 'daemon.log_level'), returning its current value, type, scopes, and whether a change needs a daemon restart. There is no 'llm.provider' key: provider behavior is inferred from the OpenAI-compatible 'llm.base_url'. Never read LeapFlow config files from disk — use this.\n- **config_set**(key, value, scope) [capability_expand category: config]: Change one LeapFlow setting by key, e.g. switch an OpenAI-compatible LLM with key='llm.model' and key='llm.base_url'. There is no 'llm.provider' key: provider behavior is inferred from the endpoint. Values are validated and coerced; credentials are stored in the vault automatically. Call config_list or config_get first if unsure of the key. The result states whether a `leap daemon restart` is required. Never edit LeapFlow config files directly.\n- **web_fetch**(url, select, timeout, max_bytes): Read a URL over HTTP(S) and get back extracted, context-sized content: parsed JSON for API endpoints, readable text plus links for web pages. Use this for anything on the internet — prices, docs, releases, articles — instead of running curl through shell_run: it reports real HTTP status codes, retries rate limits on its own, and is a plain read so a retry is always safe. For JSON APIs pass `select` with a dotted path (e.g. 'chart.result.0.meta') to return just that part instead of the whole payload.\n- **memory_search**(query, limit): Search agent memory for relevant past experiences, observations, and facts.\n- **memory_add**(content, kind) [capability_expand category: write]: Store a new observation or insight in memory for future reference.\n- **research_note**(kind, text): Record a compact, structured note about the current task's state so it survives context compression on long / multi-step tasks. Use for durable findings, open questions still to resolve, decisions / excluded paths, and the immediate next step. One concise sentence per note.\n- **capability_expand**(category): Fetch the full callable schema for every tool in a capability category (e.g. 'hub', 'gateway', 'desktop', 'delegate', 'file', 'memory', 'skill'). The compact tool index always lists every registered tool by name and a one-line summary, but only a static low-risk subset is directly callable each turn. If you need a tool from the index that is not yet callable, call capability_expand with its category first; the matching tools become callable in this turn. Never invent a tool name — expand the category instead.\n- **delegate_task**(goal, context) [capability_expand category: delegate]: Delegate a complex sub-task to an isolated subagent. The subagent gets a fresh context and restricted tool access. Use when a task is self-contained and can be solved independently.\n- **schedule_reentry**(kind, reason, delay_seconds, event_match, max_reentries, deadline_seconds): Register a re-entry so this task can resume later from its current orientation (findings / open questions / next step). Use when work must pause and continue after a delay (kind=time) or when a matching platform event arrives (kind=event), instead of finishing now. The research-ledger state is carried over automatically.\n- **hub_push**(skill_name, visibility, version) [capability_expand category: hub]: Push a local skill to the ModelScope Hub for sharing or backup.\n- **hub_pull**(repo_id, version) [capability_expand category: hub]: Pull a skill from the ModelScope Hub to install locally.\n- **hub_search**(query) [capability_expand category: hub]: Search for skills on the Hub by keyword or description.\n- **hub_sync**(mode, dry_run) [capability_expand category: hub]: Preview or execute sync between local skills and Hub.\n- **platform_action**(platform, action, payload, backend_kind) [capability_expand category: gateway]: Execute an exact registered business action on an external platform through LeapFlow's App Connector layer. Actions must be copied from the App Connector Capability Index and are addressed as domain.operation, e.g. im.send_message or docs.create_markdown. All business fields (chat_id, text, query, etc.) MUST be placed inside `payload`, never at the top level. Example: {\"platform\":\"feishu\",\"action\":\"im.send_message\",\"payload\":{\"chat_id\":\"oc_xxx\",\"text\":\"hello\"}}. Do not invent action names, do not use management actions such as list/guide/connect/status here.\n- **platform_connect**(action, platform, credentials, options, checkpoint) [capability_expand category: gateway]: List, guide, connect, disconnect, remove, or check status for external platforms using the App Connector management namespace. Supports REST and CLI backends. Use this for management actions such as list/guide/preflight/connect/status; use platform_action only for exact registered business actions.\n- **gateway_send**(platform, chat_id, text, thread_id) [capability_expand category: gateway]: Send a message to a connected external platform (Feishu group, Telegram chat, DingTalk conversation, etc.). Requires the platform to be connected via gateway_connect first. Use gateway_connect with action='list' to see connected platforms and available chat IDs.\n- **gateway_connect**(action, platform, credentials, options) [capability_expand category: gateway]: Connect, configure, or manage external platform integrations (Feishu, DingTalk, Telegram, Slack, Discord, etc.). Conversational flow: 1) call 'guide' to get setup steps + required fields, 2) present the steps to the user and ask for ALL required credentials in a single message, 3) call 'connect' with the credentials. Goal: complete in 1–2 user turns. NEVER include credential values in your text response.\n- **plugin_list**(): List the live plugin registry and cross-subsystem capability evidence. Use this before answering questions about whether LeapFlow supports plugins, self-evolution, plugin installation, hot reload, versioning, or other runtime capabilities.\n- **plugin_status**(plugin_id): Get detailed status of a specific plugin: its declared category, runtime dependencies, contributed tools, and fiber lifecycle state.\n- **plugin_versions**(plugin_id): List recorded source versions and active pointer for a profile-scoped plugin.\n- **plugin_propose**(requested_capability, plugin_id, proposed_tools, test_cases, risk_level, evidence): Create a side-effect-free PluginProposal from explicit capability-gap evidence. Use this before plugin_generate when a missing capability should be reviewed. Does not call an LLM, write files, or install anything.\n- **assess_compatibility**(manifest, source_path) [capability_expand category: plugin_management]: Assess whether a foreign plugin manifest or real DSH source bundle is compatible with LeapFlow. A source bundle assessment is static: runtime_ready stays false until restricted Node discovery during plugin_install. Returns component-level verdicts and limitations.\n- **plugin_generate**(plugin_id, description, proposal_id) [capability_expand category: system]: Generate a new ToolPlugin from a natural-language capability description. The LLM produces code that conforms to the ToolPlugin Protocol; it is then rigorously validated (syntax, structure, import, protocol conformance). The generated source is stored in CAS and receives explicit content approval. It DOES NOT install the plugin — installation requires a second, mutation-specific approval via plugin_install.\n- **plugin_install**(plugin_id, code, marketplace_name, source_path, proposal_id, version_label) [capability_expand category: system]: Install a plugin from exactly one source: validated Python code, the configured Python marketplace, or a real DSH source bundle. DSH bundles run restricted Node discovery before registration; only public host tools are installed and client UI limitations are reported. Writes profile-scoped state and mutates the process-global registry. REQUIRES APPROVAL.\n- **plugin_rollback**(plugin_id, version) [capability_expand category: system]: Rollback a profile-scoped plugin to a recorded source version and reload it. REQUIRES APPROVAL.\n- **plugin_reload**(plugin_id, version_label) [capability_expand category: system]: Hot-reload a plugin at runtime. Disposes the old plugin fiber, re-imports its module, and registers a fresh instance. Existing in-flight turns are unaffected (snapshot isolation). REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_disable**(plugin_id) [capability_expand category: system]: Disable a plugin by disposing its fiber, removing its tools from the runtime registry. Cannot disable self_management itself. REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_remove**(plugin_id, delete_source) [capability_expand category: system]: Terminally remove a plugin: dispose its fiber, unregister its tools, remove reload metadata, and optionally delete its profile-scoped source file. Cannot remove self_management itself. REQUIRES APPROVAL.\n- **plugin_unquarantine**(plugin_id) [capability_expand category: plugin_management]: Restore a quarantined plugin to probation status for re-evaluation. Unfreezes the trust ledger, transitions the proposal back to PROBATION, and reloads the plugin. REQUIRES APPROVAL.\n- **plugin_enable**(plugin_id) [capability_expand category: system]: Re-enable a previously disabled plugin by reloading its module and registering a fresh instance. REQUIRES APPROVAL.\n- **self_describe**(facet): Introspect LeapFlow's own identity, capabilities, runtime state, evolution metrics, or platform connections. Use facet='all' only when a comprehensive self-check is explicitly requested.\n- **runtime_snapshot**(): Lightweight (~150 token) flat snapshot of current runtime state: model, context budget, posture, disclosure level, turn count, cache hit rate, uptime, tool count, and pending approvals.\n- **schedule_create**(trigger_expression, instruction, execution_mode, max_retries, delivery_target) [capability_expand category: scheduler]: Create a new scheduled task. Specify a trigger expression (e.g. '30m', 'every 2h', '0 9 * * *') and the instruction to execute on each trigger. Optionally provide an execution mode and a delivery target to receive notifications.\n- **schedule_list**() [capability_expand category: scheduler]: List all scheduled tasks with their current state, trigger, and next due time.\n- **schedule_status**(task_id) [capability_expand category: scheduler]: Get detailed status and recent execution history for a scheduled task.\n- **schedule_pause**(task_id) [capability_expand category: scheduler]: Pause a scheduled task so it stops firing without being cancelled.\n- **schedule_resume**(task_id) [capability_expand category: scheduler]: Resume a paused scheduled task — re-arms it and recalculates next due time.\n- **schedule_cancel**(task_id) [capability_expand category: scheduler]: Cancel a scheduled task permanently.\n- **tool_search**(query, max_results): Search all registered tools by keyword relevance. Returns a ranked list of matching tools with name, category, and summary. Use this when you need a tool but are unsure of its exact name or category.\n- **tool_describe**(tool_name): Get the full callable schema of a single tool by exact name. Returns the tool's description, parameters, and metadata. Use after tool_search to inspect a specific tool before calling it.\n- **hw_list**(): List connected hardware devices with their channel counts and measured quantities. Start here; it does not include operating limits.\n- **hw_describe**(device_id): Return the full reference for one device: every channel, its unit, its operating envelope, rate limit, reversibility, and required interlocks, plus any prior outcomes recorded for its writable channels. Required before commanding a device.\n- **hw_read**(device_id, channel_id): Read the current value of one device channel. Has no physical effect.\n- **hw_status**(device_id): Report connection health, halt capability, and recent notable events (threshold excursions, lost samples, stalled channels) for one device.\n- **hw_configure**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Set a configuration value or setpoint on a channel declaring effect=configure. Setpoints often have inertia: the value may need time to stabilise.\n- **hw_actuate**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Command motion or output on a channel declaring effect=actuate. This moves physical hardware; a repeat from an unknown state is not a safe retry.\n- **hw_dispense**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Consume an irreversible resource on a channel declaring effect=dispense. Running this twice dispenses twice; never repeat it after a failure without first verifying what already happened.\n- **hw_estop**(device_id) [capability_expand category: hardware]: Stop all motion and output on a device immediately. Never requires approval; use it whenever device behaviour is unexpected.\n- **session_search**(query, limit) [capability_expand category: unclassified]: Search past conversation sessions in the current workspace for relevant context.\n- **session_list**(limit): List recent conversation sessions in the current workspace with stable ids, titles, dates, and summaries. Use for browsing past tasks or when user asks to see history without specific search terms. No keywords needed — returns chronological list.\n- **session_detail**(session_id, limit, offset, include_inactive) [capability_expand category: unclassified]: Read a paginated persisted transcript for one past conversation session in the current workspace. Use after session_list or session_search returns a session_id.\n- **click**(element_index) [capability_expand category: desktop]: Click a UI element by its element_index (from the latest observe_ui snapshot)\n- **get_clipboard**() [capability_expand category: desktop]: Read current clipboard text content\n- **list_apps**(filter, running_only) [capability_expand category: desktop]: List available applications on this system. Use to discover correct bundle_id before switch_app.\n- **list_windows**() [capability_expand category: desktop]: List all top-level windows with pid, window_id, title, and per-window state (minimized, on-screen). Call this first to pick the pid and window_id that observe_ui and other window tools require.\n- **observe_ui**(pid, window_id, query) [capability_expand category: desktop]: Snapshot one window's actionable UI elements, each tagged with an element_index for click/right_click/read_text. Re-observe after actions — indices belong to one snapshot. Requires the window's pid and window_id from list_windows.\n- **open_url**(url, app_id) [capability_expand category: desktop]: Open a URL in the default or specified browser\n- **read_text**(element_index) [capability_expand category: desktop]: Read the text content of a specific UI element from the latest snapshot\n- **right_click**(element_index) [capability_expand category: desktop]: Right-click a UI element to open its context menu. Returns visible menu items.\n- **screenshot**(pid, window_id) [capability_expand category: desktop]: Capture a screenshot for visual verification. With pid + window_id captures that window (works across all displays); defaults to the last observed window, or the full desktop when no window has been observed.\n- **scroll**(element_index, direction, amount, pid, window_id) [capability_expand category: desktop]: Scroll a scrollable area of a window. Omit element_index to scroll the window's focused/page scroller; pass one to scroll an exact element from the latest snapshot.\n- **select_text**(element_index) [capability_expand category: desktop]: Select all text in a UI element (focus + select-all, for subsequent copy)\n- **set_clipboard**(text) [capability_expand category: desktop]: Write text to the clipboard\n- **shortcut**(keys) [capability_expand category: desktop]: Execute a keyboard shortcut\n- **switch_app**(app_id) [capability_expand category: desktop]: Switch to an app (launch if needed, activate, verify)\n- **type_text**(text) [capability_expand category: desktop]: Type text into the currently focused element\n- **wait**(seconds) [capability_expand category: desktop]: Wait for a specified duration before continuing\n- **wait_until**(condition, pid, window_id, timeout, poll_interval) [capability_expand category: desktop]: Wait until a UI condition is met (polls UI tree). Returns elements when found or on timeout.\n- **wait_until_stable**(timeout, poll_interval, pid, window_id) [capability_expand category: desktop]: Wait until the UI stops changing (element set stabilizes across polls).\n\n## App Connector Capability Index\nLeapFlow can onboard and manage external apps through `platform_connect` and execute exact registered business actions through `platform_action`.\nFor requests about connecting, setting up, configuring, enabling, or managing a supported app, use `platform_connect` first instead of generating SDK/Webhook sample code.\n`platform_connect.action` is the management namespace: list, guide, preflight, connect, disconnect, remove, status, events_start, events_stop, events_status.\n`platform_action.action` is only for exact registered platform business actions listed below, such as `im.send_message`; never use management actions like `list` or `guide` there.\nAll business fields MUST go inside `payload`; top-level keys are only `platform`, `action`, and `payload`.\nDo not invent platform IDs or platform action names. If the needed action is not listed, ask for discovery/clarification instead of guessing.\nUse `platform_connect` with `action='guide'` and the matching `platform` to start onboarding; use `action='list'` when the app is unclear.\nWhen a pending onboarding state is present, continue from that state with `platform_connect` instead of asking the user to restate the app.\nSupported apps:\n- `api_server`: API Server (OpenAI Compatible) (category=api; backend=adapter; platform_action actions=none registered)\n- `dingtalk`: 钉钉 (DingTalk) (category=im; backend=adapter; platform_action actions=none registered)\n- `feishu`: 飞书 (Feishu/Lark) (category=im; backend=cli)\n - `calendar.create_event` payload={summary*, start_time*, end_time*, attendees} [write/high]\n - `docs.create_markdown` payload={title*, markdown*, folder_token} [write/medium]\n - `drive.search` payload={query*, limit} [read/medium]\n - `im.add_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.download_resource` payload={message_id*, file_key*, type*} [read/low]\n - `im.get_messages` payload={message_ids*} [read/low]\n - `im.list_chats` payload={page_size} [read/low]\n - `im.list_messages` payload={chat_id*} [read/low]\n - `im.list_thread_messages` payload={thread*} [read/low]\n - `im.remove_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.reply_message` payload={message_id*, text*} [send/high]\n - `im.search_chats` payload={query*, page_size} [read/low]\n - `im.search_messages` payload={query*} [read/medium]\n - `im.send_message` payload={chat_id*, text*, thread_id} [send/high]\n - `im.update_card` payload={token*, card*} [write/medium]\n - `im.update_message` payload={message_id*, text*} [write/medium]\n - `mail.search_unread` payload={query, limit} [read/high]\n - `sheets.append_row` payload={spreadsheet_token*, sheet_id*, values*} [write/medium]\n - `task.create` payload={title*, description, due_time} [write/medium]\n- `telegram`: Telegram (category=social; backend=adapter; platform_action actions=none registered)\n- `webhook`: Webhook (Generic) (category=webhook; backend=adapter; platform_action actions=none registered)\n\n## Tool Usage\nTools are normally invoked through the native function-calling mechanism, not by writing JSON in your reply\ntext. Only if the provider signals that native function calling is unavailable for this turn, fall back to a\nsingle JSON code block: `{\"name\": \"tool_name\", \"arguments\": {\"key\": \"value\"}}` — use this fallback format\nonly, never both. Only call a tool whose exact name appears in the tool index above and is currently callable\n(or reachable via `capability_expand`). Never invent, rename, alias, or guess a tool name, platform ID, or\nplatform action from argument shape or wording — if the index does not list it, it does not exist.\n`platform_connect.action` (list/guide/preflight/connect/disconnect/remove/status/events_start/events_stop/\nevents_status) is the App Connector management namespace; `platform_action.action` only accepts exact\nregistered business actions such as `im.send_message` shown in the App Connector Capability Index — never mix\nthe two namespaces. If a tool call returns an unknown/unavailable result, use the returned suggestions or\navailable names for a single retry instead of trying further variations of the same guess.\n\n**Side-effect action rule** (`platform_action` with effect=send/write/execute):\n- Call each unique action+payload **exactly once**. Never include duplicates in the same turn.\n- Once the result returns `\"completed\": true`, that action is DONE for this task. Do NOT call it again\n in any subsequent turn — immediately summarize the result for the user instead.\n- The system enforces idempotency: duplicate calls are blocked and will not execute.\n- If the user explicitly requests sending/writing multiple times, use distinct payloads per call.\n\n**Resource identifier provenance rule**:\n- NEVER fabricate, guess, or infer resource identifiers (chat_id, message_id, file_key, user_id, etc.).\n Every resource ID used in a side-effect action MUST come from a successful API response in this session.\n- If a read/list action fails (e.g. authorization error), you do NOT have valid resource IDs.\n Report the failure to the user — do NOT attempt the dependent write/send action with a guessed ID.\n- When a tool result contains `\"llm_instruction\"`, follow it exactly.\n\n## Guidelines\n1. **Direct answers first**: If you already know the answer, respond directly without tools.\n2. **Avoid redundant tool calls**: Do not call the same tool with the same arguments more than once in the same user turn. When an existing tool result already answers the user's request, stop calling tools and answer directly.\n3. **Use tools proactively**: When the user asks about files, time, system state, or needs actions performed, use the appropriate tool.\n4. **Chain tools when needed**: You can call multiple tools in sequence (e.g., list files → read file → summarize).\n5. **Handle failures gracefully**: If a tool fails, explain what went wrong and suggest alternatives. If it failed because of an unknown tool/platform/action name, retry once with an exact name from the returned suggestions, then explain rather than keep guessing.\n6. **Summarize results naturally**: After tool execution, synthesize the results into a helpful answer rather than dumping raw output.\n7. **Stay conversational**: Maintain a natural, helpful tone. Acknowledge context from earlier in the conversation.\n8. **Recall past work**:\n - Broad queries (\"之前做了什么\", \"列出任务\"): answer from the \"Recent Task History\" section already in your context. If insufficient, call session_list.\n - Specific lookups (\"上次那个配置怎么改的\"): call session_search with relevant phrases (NOT single characters).\n - Do NOT call search tools repeatedly with keyword fragments. One well-phrased call is better than ten fragmented ones.\n\n## Coding & Verification\nWhen working with code, prefer the precise built-in tools over ad-hoc shell: use `repo_map` to orient in an\nunfamiliar project, `code_search`/`file_find` to locate and `code_intel` for symbols, `edit_file` (anchored\nsearch-replace, or a unified `diff`) to change files — never rewrite a whole file to change a few lines — and\n`git_query` to inspect diffs/log. After edits, check `syntax_ok` in the result and run `test_run`/`lint_check`\nbefore declaring the work done. Batch independent read-only calls (search/read/query) into a single turn — they\nrun in parallel.\n\n## Presentation Style\n1. **Polished Markdown only**: Format user-facing answers with clean Markdown headings, short paragraphs, and concise bullets. Use tables only when they improve comparison or scanning.\n2. **Terminal-friendly layout**: Keep lines readable in a TUI; avoid dense walls of text, deeply nested lists, oversized ASCII art, or heavy visual blocks.\n3. **Elegant emphasis**: Use bold text sparingly for key terms and conclusions. Avoid excessive emojis, decorative symbols, repeated separators, or visual noise.\n4. **Theme-safe colors**: Do not emit ANSI escape codes, HTML color tags, Rich markup, or hardcoded color names. Rely on the TUI theme to render Markdown professionally.\n5. **No leaked tool protocol**: Never show tool-call JSON, internal schemas, raw observations, tool result payloads, or hidden reasoning in the final answer unless the user explicitly asks for raw/debug output. Treat any prior `{\"name\": ..., \"arguments\": ...}` blocks and `Tool result (...)` messages as internal execution context only.\n6. **Professional closure**: End with a concise conclusion or next step when helpful; avoid rambling after the useful answer is complete.\n\nWhen finished with all tool calls, respond normally without a JSON block, tool-call transcript, or process log.\n\n## Task Contract\n- Task ID: turn-5\n- Original user request: Preview an actuate of bench_r8 setpoint to 55, dry run only.\n- Workspace root: \n- Allowed roots: \n- Treat relative project paths as relative to the workspace root; never infer `.` as the project root when a workspace root is provided.\n- Workspace boundary is enforced by tools: do not read, search, edit, or run commands against paths outside the allowed roots unless the user explicitly requests an external path and the tool/approval policy permits it.\n- LeapFlow workspace config is optional at `/.leapflow/config.yaml`; runtime config is loaded from `~/.leapflow/config/user.yaml` and `~/.leapflow/profiles//config/*.yaml`.\n- Preserve this task contract across summarization, compression, tool loops, and memory retrieval.\n" - }, - { - "role": "assistant", - "content": "Read the setpoint channel." - }, - { - "role": "user", - "content": "Actuate the bench_r8 setpoint to 60.\nConfigure the bench_r8 homed channel to true to complete homing." - }, - { - "role": "assistant", - "content": "[Called: hw_configure]\nHomed the device; the readiness gate is now satisfied." - }, - { - "role": "user", - "content": "Preview an actuate of bench_r8 setpoint to 55, dry run only.\nPreview an actuate of bench_r8 setpoint to 55, dry run only." - }, - { - "role": "assistant", - "content": "", - "tool_calls": [ - "hw_actuate" - ] - }, - { - "role": "tool", - "content": "{\"ok\": true, \"kind\": \"\", \"truncated\": true, \"original_chars\": 888, \"budget_chars\": 819}", - "tool_result": true - } - ], - "tools": [ - "capability_expand", - "code_intel", - "code_search", - "config_get", - "config_list", - "env_info", - "file_find", - "file_list", - "file_read", - "git_query", - "hw_actuate", - "hw_configure", - "hw_describe", - "hw_dispense", - "hw_estop", - "hw_list", - "hw_read", - "hw_status", - "lint_check", - "memory_search", - "plugin_list", - "plugin_propose", - "plugin_status", - "plugin_versions", - "repo_map", - "research_note", - "runtime_snapshot", - "schedule_reentry", - "self_describe", - "session_list", - "skill_view", - "skills_list", - "terminal_list", - "terminal_read", - "test_run", - "text_search", - "time_get", - "tool_describe", - "tool_search", - "web_fetch" - ] - }, - "responses": [ - { - "status": 200, - "content_type": "application/json", - "body": "{\"id\": \"chatcmpl-cassette\", \"object\": \"chat.completion\", \"model\": \"cassette-model\", \"choices\": [{\"index\": 0, \"message\": {\"role\": \"assistant\", \"content\": \"Dry run confirmed the setpoint command is feasible after homing.\"}, \"finish_reason\": \"stop\"}], \"usage\": {\"prompt_tokens\": 64, \"completion_tokens\": 16, \"total_tokens\": 80}}" - } - ] -} diff --git a/tests/_fixtures/cassettes/r8_hardware/cassette-model-68fb63fed8d61ee1.cassette.json b/tests/_fixtures/cassettes/r8_hardware/cassette-model-68fb63fed8d61ee1.cassette.json deleted file mode 100644 index 410dc176..00000000 --- a/tests/_fixtures/cassettes/r8_hardware/cassette-model-68fb63fed8d61ee1.cassette.json +++ /dev/null @@ -1,79 +0,0 @@ -{ - "fingerprint": "68fb63fed8d61ee15c492da05b9cdf2e25a2428b72e1599e9198957a95dcb5af", - "note": "captured in seed mode", - "request": { - "model": "cassette-model", - "stream": false, - "messages": [ - { - "role": "system", - "content": "You are LeapFlow, an intelligent assistant that can both converse naturally and take real actions on the user's computer.\n\n## Capabilities\nThe tool index below lists **every** registered tool by name and a one-line summary — this is the complete\ncapability contract; nothing else exists. Only a subset is directly callable this turn (via native tool calling,\nnot a JSON block in your reply). If you need a tool from the index that is not yet callable, call\n`capability_expand` with its category name first — the matching tools become callable immediately after.\nWhen the user asks what LeapFlow itself supports, whether it supports plugins, or which runtime capabilities\nare available, use the live capability evidence exposed by `plugin_list` before making capability claims; report\nconfiguration-dependent or unavailable capabilities as limitations instead of inferring from documentation.\n- **text_search**(text, pattern): Search for a regex pattern in text.\n- **text_replace**(text, old, new, count) [capability_expand category: write]: Replace occurrences of a substring in text.\n- **time_get**(): Get current date and time.\n- **env_info**(): Get system environment information (OS, Python version, cwd).\n- **skills_list**(query, category, source): List available learned skills. Use when user asks about capabilities or you need a specific skill.\n- **skill_view**(name): View the full content of a specific skill document.\n- **code_intel**(path, operation): Precise document symbols (outline) for a source file: classes, functions, and methods with line ranges. Python uses an exact AST parse; other languages use a keyword-prefix scan. Prefer over file_read mode=symbols for accurate navigation before editing. Read-only.\n- **repo_map**(path): Compact project orientation for a repository root: languages, detected test/lint commands, top-level structure, entry points, manifest, and VCS branch. Call this first when entering an unfamiliar codebase. Read-only.\n- **scm_sync**(action, cwd, remote, pull_ref, push_ref, timeout) [capability_expand category: scm]: Run a typed git SCM action. Use this instead of shell_run for git pull/push/status. For 'pull origin main then push', set action='pull_then_push', remote='origin', pull_ref='main', and omit push_ref so LeapFlow pushes the current local branch.\n- **git_query**(action, cwd, ref, path, staged, max_count, stat): Read-only structured git inspection: action=diff|log|status|branch|show. Prefer over shell_run for reading repo state — output is clipped, redacted, and log/branch are parsed into structured fields. Use scm_sync for pull/push.\n- **git_write**(action, cwd, message, stage_all, name, ref, create) [capability_expand category: scm]: Mutating git actions: action=commit (message, stage_all), branch (create+switch), checkout (switch; create=true for -b). Approval-gated. Use scm_sync for pull/push and git_query for reads.\n- **test_run**(command, cwd, timeout): Run the project's test suite and return structured results (framework, passed/failed counts, failing tests). Auto-detects the runner (pytest/npm/go/cargo) or uses a configured/explicit command; executes via the governed shell. ok=true means the runner executed — see 'success' for pass/fail.\n- **lint_check**(command, cwd, timeout): Run the project's linter and return a structured clean/issue result. Auto-detects the linter (ruff/eslint/go vet/clippy) or uses a configured/explicit command; executes via the governed shell. ok=true means the linter ran — see 'clean'.\n- **file_list**(path, pattern, depth): List files and directories at a given path. Use depth=1 or depth=2 to get a recursive tree in one call instead of listing each sub-directory separately.\n- **file_read**(path, max_lines, start_line, max_chars, mode): Read text file content with adaptive context governance. For large or unfamiliar files, prefer mode='outline' or mode='symbols' first, then use mode='raw' with start_line/max_lines for the specific range you actually need. For LeapFlow's own settings, use config_list / config_get / config_set — its config files are outside the workspace and not readable here.\n- **file_write**(path, content, mode) [capability_expand category: write]: Write content to a file (overwrite or append).\n- **code_search**(pattern, patterns, path, glob, ignore_case, multiline, max_results, context_lines): Search file CONTENTS by regex pattern across a directory tree (ripgrep-backed). Requires a regex pattern. NOT for listing or browsing directory contents — use file_list for that. Prefer this over shell_run grep: faster, skips VCS/dependency/build dirs, and returns structured path:line:column matches. Batch related lookups into ONE call via `patterns` (OR-combined, single pass) instead of issuing several separate searches. Use file_read for the surrounding context of a hit.\n- **file_find**(glob, path, max_results): Find files by a recursive glob pattern under a base path (e.g. '**/test_*.py' or '*.md'). Prefer this over shell_run find; skips VCS/dependency/build dirs.\n- **edit_file**(path, edits, dry_run, diff) [capability_expand category: write]: Apply targeted, anchored search-replace edits to an EXISTING text file (use file_write to create/overwrite). Each edit is {original_text, new_text, replace_all?}; original_text must match exactly and uniquely (or set replace_all) — a non-unique or missing anchor is rejected so files are never corrupted. Set dry_run to preview. Alternatively pass a unified 'diff' to apply its hunks as anchored edits. Far cheaper and safer than rewriting a whole file.\n- **shell_run**(command, cwd, timeout) [capability_expand category: shell]: Execute a one-shot shell command with timeout protection. Runs in the active workspace; paths resolving outside it are refused. Reach for a structured tool first when one fits — web_fetch for anything over HTTP(S), git_query/scm_sync for git, code_search/file_find/file_read for the repo, config_get/config_set for LeapFlow's own settings — because those report typed results, while a failed shell command can only be diagnosed from its exit code and stderr. Every shell run counts as an external side effect, so a failure stops the rest of the batch and is not retried automatically.\n- **terminal_open**(command, cwd, shell) [capability_expand category: terminal]: Open a PERSISTENT shell session (REPL/dev server/watch), returning a session_id for terminal_send/read/close. Disabled unless tools.terminal_session_enabled is set. For one-shot commands use shell_run instead.\n- **terminal_send**(session_id, input, wait) [capability_expand category: terminal]: Send a line of input to a persistent terminal session and return output captured shortly after.\n- **terminal_read**(session_id, wait): Drain buffered output from a persistent terminal session (optionally waiting briefly first).\n- **terminal_close**(session_id) [capability_expand category: terminal]: Terminate a persistent terminal session and release its process group.\n- **terminal_list**(): List active persistent terminal sessions.\n- **config_list**(category, limit): List LeapFlow's own writable settings (LLM model and endpoint, daemon, memory, perception, gateway, …) with current values. Use this to discover the exact key before changing anything. Optionally narrow by `category`. This is the only correct way to inspect LeapFlow configuration — never read config files from disk.\n- **config_get**(key): Read one LeapFlow setting by key (e.g. 'llm.model', 'llm.base_url', 'daemon.log_level'), returning its current value, type, scopes, and whether a change needs a daemon restart. There is no 'llm.provider' key: provider behavior is inferred from the OpenAI-compatible 'llm.base_url'. Never read LeapFlow config files from disk — use this.\n- **config_set**(key, value, scope) [capability_expand category: config]: Change one LeapFlow setting by key, e.g. switch an OpenAI-compatible LLM with key='llm.model' and key='llm.base_url'. There is no 'llm.provider' key: provider behavior is inferred from the endpoint. Values are validated and coerced; credentials are stored in the vault automatically. Call config_list or config_get first if unsure of the key. The result states whether a `leap daemon restart` is required. Never edit LeapFlow config files directly.\n- **web_fetch**(url, select, timeout, max_bytes): Read a URL over HTTP(S) and get back extracted, context-sized content: parsed JSON for API endpoints, readable text plus links for web pages. Use this for anything on the internet — prices, docs, releases, articles — instead of running curl through shell_run: it reports real HTTP status codes, retries rate limits on its own, and is a plain read so a retry is always safe. For JSON APIs pass `select` with a dotted path (e.g. 'chart.result.0.meta') to return just that part instead of the whole payload.\n- **memory_search**(query, limit): Search agent memory for relevant past experiences, observations, and facts.\n- **memory_add**(content, kind) [capability_expand category: write]: Store a new observation or insight in memory for future reference.\n- **research_note**(kind, text): Record a compact, structured note about the current task's state so it survives context compression on long / multi-step tasks. Use for durable findings, open questions still to resolve, decisions / excluded paths, and the immediate next step. One concise sentence per note.\n- **capability_expand**(category): Fetch the full callable schema for every tool in a capability category (e.g. 'hub', 'gateway', 'desktop', 'delegate', 'file', 'memory', 'skill'). The compact tool index always lists every registered tool by name and a one-line summary, but only a static low-risk subset is directly callable each turn. If you need a tool from the index that is not yet callable, call capability_expand with its category first; the matching tools become callable in this turn. Never invent a tool name — expand the category instead.\n- **delegate_task**(goal, context) [capability_expand category: delegate]: Delegate a complex sub-task to an isolated subagent. The subagent gets a fresh context and restricted tool access. Use when a task is self-contained and can be solved independently.\n- **schedule_reentry**(kind, reason, delay_seconds, event_match, max_reentries, deadline_seconds): Register a re-entry so this task can resume later from its current orientation (findings / open questions / next step). Use when work must pause and continue after a delay (kind=time) or when a matching platform event arrives (kind=event), instead of finishing now. The research-ledger state is carried over automatically.\n- **hub_push**(skill_name, visibility, version) [capability_expand category: hub]: Push a local skill to the ModelScope Hub for sharing or backup.\n- **hub_pull**(repo_id, version) [capability_expand category: hub]: Pull a skill from the ModelScope Hub to install locally.\n- **hub_search**(query) [capability_expand category: hub]: Search for skills on the Hub by keyword or description.\n- **hub_sync**(mode, dry_run) [capability_expand category: hub]: Preview or execute sync between local skills and Hub.\n- **platform_action**(platform, action, payload, backend_kind) [capability_expand category: gateway]: Execute an exact registered business action on an external platform through LeapFlow's App Connector layer. Actions must be copied from the App Connector Capability Index and are addressed as domain.operation, e.g. im.send_message or docs.create_markdown. All business fields (chat_id, text, query, etc.) MUST be placed inside `payload`, never at the top level. Example: {\"platform\":\"feishu\",\"action\":\"im.send_message\",\"payload\":{\"chat_id\":\"oc_xxx\",\"text\":\"hello\"}}. Do not invent action names, do not use management actions such as list/guide/connect/status here.\n- **platform_connect**(action, platform, credentials, options, checkpoint) [capability_expand category: gateway]: List, guide, connect, disconnect, remove, or check status for external platforms using the App Connector management namespace. Supports REST and CLI backends. Use this for management actions such as list/guide/preflight/connect/status; use platform_action only for exact registered business actions.\n- **gateway_send**(platform, chat_id, text, thread_id) [capability_expand category: gateway]: Send a message to a connected external platform (Feishu group, Telegram chat, DingTalk conversation, etc.). Requires the platform to be connected via gateway_connect first. Use gateway_connect with action='list' to see connected platforms and available chat IDs.\n- **gateway_connect**(action, platform, credentials, options) [capability_expand category: gateway]: Connect, configure, or manage external platform integrations (Feishu, DingTalk, Telegram, Slack, Discord, etc.). Conversational flow: 1) call 'guide' to get setup steps + required fields, 2) present the steps to the user and ask for ALL required credentials in a single message, 3) call 'connect' with the credentials. Goal: complete in 1–2 user turns. NEVER include credential values in your text response.\n- **plugin_list**(): List the live plugin registry and cross-subsystem capability evidence. Use this before answering questions about whether LeapFlow supports plugins, self-evolution, plugin installation, hot reload, versioning, or other runtime capabilities.\n- **plugin_status**(plugin_id): Get detailed status of a specific plugin: its declared category, runtime dependencies, contributed tools, and fiber lifecycle state.\n- **plugin_versions**(plugin_id): List recorded source versions and active pointer for a profile-scoped plugin.\n- **plugin_propose**(requested_capability, plugin_id, proposed_tools, test_cases, risk_level, evidence): Create a side-effect-free PluginProposal from explicit capability-gap evidence. Use this before plugin_generate when a missing capability should be reviewed. Does not call an LLM, write files, or install anything.\n- **assess_compatibility**(manifest, source_path) [capability_expand category: plugin_management]: Assess whether a foreign plugin manifest or real DSH source bundle is compatible with LeapFlow. A source bundle assessment is static: runtime_ready stays false until restricted Node discovery during plugin_install. Returns component-level verdicts and limitations.\n- **plugin_generate**(plugin_id, description, proposal_id) [capability_expand category: system]: Generate a new ToolPlugin from a natural-language capability description. The LLM produces code that conforms to the ToolPlugin Protocol; it is then rigorously validated (syntax, structure, import, protocol conformance). The generated source is stored in CAS and receives explicit content approval. It DOES NOT install the plugin — installation requires a second, mutation-specific approval via plugin_install.\n- **plugin_install**(plugin_id, code, marketplace_name, source_path, proposal_id, version_label) [capability_expand category: system]: Install a plugin from exactly one source: validated Python code, the configured Python marketplace, or a real DSH source bundle. DSH bundles run restricted Node discovery before registration; only public host tools are installed and client UI limitations are reported. Writes profile-scoped state and mutates the process-global registry. REQUIRES APPROVAL.\n- **plugin_rollback**(plugin_id, version) [capability_expand category: system]: Rollback a profile-scoped plugin to a recorded source version and reload it. REQUIRES APPROVAL.\n- **plugin_reload**(plugin_id, version_label) [capability_expand category: system]: Hot-reload a plugin at runtime. Disposes the old plugin fiber, re-imports its module, and registers a fresh instance. Existing in-flight turns are unaffected (snapshot isolation). REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_disable**(plugin_id) [capability_expand category: system]: Disable a plugin by disposing its fiber, removing its tools from the runtime registry. Cannot disable self_management itself. REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_remove**(plugin_id, delete_source) [capability_expand category: system]: Terminally remove a plugin: dispose its fiber, unregister its tools, remove reload metadata, and optionally delete its profile-scoped source file. Cannot remove self_management itself. REQUIRES APPROVAL.\n- **plugin_unquarantine**(plugin_id) [capability_expand category: plugin_management]: Restore a quarantined plugin to probation status for re-evaluation. Unfreezes the trust ledger, transitions the proposal back to PROBATION, and reloads the plugin. REQUIRES APPROVAL.\n- **plugin_enable**(plugin_id) [capability_expand category: system]: Re-enable a previously disabled plugin by reloading its module and registering a fresh instance. REQUIRES APPROVAL.\n- **self_describe**(facet): Introspect LeapFlow's own identity, capabilities, runtime state, evolution metrics, or platform connections. Use facet='all' only when a comprehensive self-check is explicitly requested.\n- **runtime_snapshot**(): Lightweight (~150 token) flat snapshot of current runtime state: model, context budget, posture, disclosure level, turn count, cache hit rate, uptime, tool count, and pending approvals.\n- **schedule_create**(trigger_expression, instruction, execution_mode, max_retries, delivery_target) [capability_expand category: scheduler]: Create a new scheduled task. Specify a trigger expression (e.g. '30m', 'every 2h', '0 9 * * *') and the instruction to execute on each trigger. Optionally provide an execution mode and a delivery target to receive notifications.\n- **schedule_list**() [capability_expand category: scheduler]: List all scheduled tasks with their current state, trigger, and next due time.\n- **schedule_status**(task_id) [capability_expand category: scheduler]: Get detailed status and recent execution history for a scheduled task.\n- **schedule_pause**(task_id) [capability_expand category: scheduler]: Pause a scheduled task so it stops firing without being cancelled.\n- **schedule_resume**(task_id) [capability_expand category: scheduler]: Resume a paused scheduled task — re-arms it and recalculates next due time.\n- **schedule_cancel**(task_id) [capability_expand category: scheduler]: Cancel a scheduled task permanently.\n- **tool_search**(query, max_results): Search all registered tools by keyword relevance. Returns a ranked list of matching tools with name, category, and summary. Use this when you need a tool but are unsure of its exact name or category.\n- **tool_describe**(tool_name): Get the full callable schema of a single tool by exact name. Returns the tool's description, parameters, and metadata. Use after tool_search to inspect a specific tool before calling it.\n- **hw_list**(): List connected hardware devices with their channel counts and measured quantities. Start here; it does not include operating limits.\n- **hw_describe**(device_id): Return the full reference for one device: every channel, its unit, its operating envelope, rate limit, reversibility, and required interlocks, plus any prior outcomes recorded for its writable channels. Required before commanding a device.\n- **hw_read**(device_id, channel_id): Read the current value of one device channel. Has no physical effect.\n- **hw_status**(device_id): Report connection health, halt capability, and recent notable events (threshold excursions, lost samples, stalled channels) for one device.\n- **hw_configure**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Set a configuration value or setpoint on a channel declaring effect=configure. Setpoints often have inertia: the value may need time to stabilise.\n- **hw_actuate**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Command motion or output on a channel declaring effect=actuate. This moves physical hardware; a repeat from an unknown state is not a safe retry.\n- **hw_dispense**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Consume an irreversible resource on a channel declaring effect=dispense. Running this twice dispenses twice; never repeat it after a failure without first verifying what already happened.\n- **hw_estop**(device_id) [capability_expand category: hardware]: Stop all motion and output on a device immediately. Never requires approval; use it whenever device behaviour is unexpected.\n- **session_search**(query, limit) [capability_expand category: unclassified]: Search past conversation sessions in the current workspace for relevant context.\n- **session_list**(limit): List recent conversation sessions in the current workspace with stable ids, titles, dates, and summaries. Use for browsing past tasks or when user asks to see history without specific search terms. No keywords needed — returns chronological list.\n- **session_detail**(session_id, limit, offset, include_inactive) [capability_expand category: unclassified]: Read a paginated persisted transcript for one past conversation session in the current workspace. Use after session_list or session_search returns a session_id.\n- **click**(element_index) [capability_expand category: desktop]: Click a UI element by its element_index (from the latest observe_ui snapshot)\n- **get_clipboard**() [capability_expand category: desktop]: Read current clipboard text content\n- **list_apps**(filter, running_only) [capability_expand category: desktop]: List available applications on this system. Use to discover correct bundle_id before switch_app.\n- **list_windows**() [capability_expand category: desktop]: List all top-level windows with pid, window_id, title, and per-window state (minimized, on-screen). Call this first to pick the pid and window_id that observe_ui and other window tools require.\n- **observe_ui**(pid, window_id, query) [capability_expand category: desktop]: Snapshot one window's actionable UI elements, each tagged with an element_index for click/right_click/read_text. Re-observe after actions — indices belong to one snapshot. Requires the window's pid and window_id from list_windows.\n- **open_url**(url, app_id) [capability_expand category: desktop]: Open a URL in the default or specified browser\n- **read_text**(element_index) [capability_expand category: desktop]: Read the text content of a specific UI element from the latest snapshot\n- **right_click**(element_index) [capability_expand category: desktop]: Right-click a UI element to open its context menu. Returns visible menu items.\n- **screenshot**(pid, window_id) [capability_expand category: desktop]: Capture a screenshot for visual verification. With pid + window_id captures that window (works across all displays); defaults to the last observed window, or the full desktop when no window has been observed.\n- **scroll**(element_index, direction, amount, pid, window_id) [capability_expand category: desktop]: Scroll a scrollable area of a window. Omit element_index to scroll the window's focused/page scroller; pass one to scroll an exact element from the latest snapshot.\n- **select_text**(element_index) [capability_expand category: desktop]: Select all text in a UI element (focus + select-all, for subsequent copy)\n- **set_clipboard**(text) [capability_expand category: desktop]: Write text to the clipboard\n- **shortcut**(keys) [capability_expand category: desktop]: Execute a keyboard shortcut\n- **switch_app**(app_id) [capability_expand category: desktop]: Switch to an app (launch if needed, activate, verify)\n- **type_text**(text) [capability_expand category: desktop]: Type text into the currently focused element\n- **wait**(seconds) [capability_expand category: desktop]: Wait for a specified duration before continuing\n- **wait_until**(condition, pid, window_id, timeout, poll_interval) [capability_expand category: desktop]: Wait until a UI condition is met (polls UI tree). Returns elements when found or on timeout.\n- **wait_until_stable**(timeout, poll_interval, pid, window_id) [capability_expand category: desktop]: Wait until the UI stops changing (element set stabilizes across polls).\n\n## App Connector Capability Index\nLeapFlow can onboard and manage external apps through `platform_connect` and execute exact registered business actions through `platform_action`.\nFor requests about connecting, setting up, configuring, enabling, or managing a supported app, use `platform_connect` first instead of generating SDK/Webhook sample code.\n`platform_connect.action` is the management namespace: list, guide, preflight, connect, disconnect, remove, status, events_start, events_stop, events_status.\n`platform_action.action` is only for exact registered platform business actions listed below, such as `im.send_message`; never use management actions like `list` or `guide` there.\nAll business fields MUST go inside `payload`; top-level keys are only `platform`, `action`, and `payload`.\nDo not invent platform IDs or platform action names. If the needed action is not listed, ask for discovery/clarification instead of guessing.\nUse `platform_connect` with `action='guide'` and the matching `platform` to start onboarding; use `action='list'` when the app is unclear.\nWhen a pending onboarding state is present, continue from that state with `platform_connect` instead of asking the user to restate the app.\nSupported apps:\n- `api_server`: API Server (OpenAI Compatible) (category=api; backend=adapter; platform_action actions=none registered)\n- `dingtalk`: 钉钉 (DingTalk) (category=im; backend=adapter; platform_action actions=none registered)\n- `feishu`: 飞书 (Feishu/Lark) (category=im; backend=cli)\n - `calendar.create_event` payload={summary*, start_time*, end_time*, attendees} [write/high]\n - `docs.create_markdown` payload={title*, markdown*, folder_token} [write/medium]\n - `drive.search` payload={query*, limit} [read/medium]\n - `im.add_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.download_resource` payload={message_id*, file_key*, type*} [read/low]\n - `im.get_messages` payload={message_ids*} [read/low]\n - `im.list_chats` payload={page_size} [read/low]\n - `im.list_messages` payload={chat_id*} [read/low]\n - `im.list_thread_messages` payload={thread*} [read/low]\n - `im.remove_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.reply_message` payload={message_id*, text*} [send/high]\n - `im.search_chats` payload={query*, page_size} [read/low]\n - `im.search_messages` payload={query*} [read/medium]\n - `im.send_message` payload={chat_id*, text*, thread_id} [send/high]\n - `im.update_card` payload={token*, card*} [write/medium]\n - `im.update_message` payload={message_id*, text*} [write/medium]\n - `mail.search_unread` payload={query, limit} [read/high]\n - `sheets.append_row` payload={spreadsheet_token*, sheet_id*, values*} [write/medium]\n - `task.create` payload={title*, description, due_time} [write/medium]\n- `telegram`: Telegram (category=social; backend=adapter; platform_action actions=none registered)\n- `webhook`: Webhook (Generic) (category=webhook; backend=adapter; platform_action actions=none registered)\n\n## Tool Usage\nTools are normally invoked through the native function-calling mechanism, not by writing JSON in your reply\ntext. Only if the provider signals that native function calling is unavailable for this turn, fall back to a\nsingle JSON code block: `{\"name\": \"tool_name\", \"arguments\": {\"key\": \"value\"}}` — use this fallback format\nonly, never both. Only call a tool whose exact name appears in the tool index above and is currently callable\n(or reachable via `capability_expand`). Never invent, rename, alias, or guess a tool name, platform ID, or\nplatform action from argument shape or wording — if the index does not list it, it does not exist.\n`platform_connect.action` (list/guide/preflight/connect/disconnect/remove/status/events_start/events_stop/\nevents_status) is the App Connector management namespace; `platform_action.action` only accepts exact\nregistered business actions such as `im.send_message` shown in the App Connector Capability Index — never mix\nthe two namespaces. If a tool call returns an unknown/unavailable result, use the returned suggestions or\navailable names for a single retry instead of trying further variations of the same guess.\n\n**Side-effect action rule** (`platform_action` with effect=send/write/execute):\n- Call each unique action+payload **exactly once**. Never include duplicates in the same turn.\n- Once the result returns `\"completed\": true`, that action is DONE for this task. Do NOT call it again\n in any subsequent turn — immediately summarize the result for the user instead.\n- The system enforces idempotency: duplicate calls are blocked and will not execute.\n- If the user explicitly requests sending/writing multiple times, use distinct payloads per call.\n\n**Resource identifier provenance rule**:\n- NEVER fabricate, guess, or infer resource identifiers (chat_id, message_id, file_key, user_id, etc.).\n Every resource ID used in a side-effect action MUST come from a successful API response in this session.\n- If a read/list action fails (e.g. authorization error), you do NOT have valid resource IDs.\n Report the failure to the user — do NOT attempt the dependent write/send action with a guessed ID.\n- When a tool result contains `\"llm_instruction\"`, follow it exactly.\n\n## Guidelines\n1. **Direct answers first**: If you already know the answer, respond directly without tools.\n2. **Avoid redundant tool calls**: Do not call the same tool with the same arguments more than once in the same user turn. When an existing tool result already answers the user's request, stop calling tools and answer directly.\n3. **Use tools proactively**: When the user asks about files, time, system state, or needs actions performed, use the appropriate tool.\n4. **Chain tools when needed**: You can call multiple tools in sequence (e.g., list files → read file → summarize).\n5. **Handle failures gracefully**: If a tool fails, explain what went wrong and suggest alternatives. If it failed because of an unknown tool/platform/action name, retry once with an exact name from the returned suggestions, then explain rather than keep guessing.\n6. **Summarize results naturally**: After tool execution, synthesize the results into a helpful answer rather than dumping raw output.\n7. **Stay conversational**: Maintain a natural, helpful tone. Acknowledge context from earlier in the conversation.\n8. **Recall past work**:\n - Broad queries (\"之前做了什么\", \"列出任务\"): answer from the \"Recent Task History\" section already in your context. If insufficient, call session_list.\n - Specific lookups (\"上次那个配置怎么改的\"): call session_search with relevant phrases (NOT single characters).\n - Do NOT call search tools repeatedly with keyword fragments. One well-phrased call is better than ten fragmented ones.\n\n## Coding & Verification\nWhen working with code, prefer the precise built-in tools over ad-hoc shell: use `repo_map` to orient in an\nunfamiliar project, `code_search`/`file_find` to locate and `code_intel` for symbols, `edit_file` (anchored\nsearch-replace, or a unified `diff`) to change files — never rewrite a whole file to change a few lines — and\n`git_query` to inspect diffs/log. After edits, check `syntax_ok` in the result and run `test_run`/`lint_check`\nbefore declaring the work done. Batch independent read-only calls (search/read/query) into a single turn — they\nrun in parallel.\n\n## Presentation Style\n1. **Polished Markdown only**: Format user-facing answers with clean Markdown headings, short paragraphs, and concise bullets. Use tables only when they improve comparison or scanning.\n2. **Terminal-friendly layout**: Keep lines readable in a TUI; avoid dense walls of text, deeply nested lists, oversized ASCII art, or heavy visual blocks.\n3. **Elegant emphasis**: Use bold text sparingly for key terms and conclusions. Avoid excessive emojis, decorative symbols, repeated separators, or visual noise.\n4. **Theme-safe colors**: Do not emit ANSI escape codes, HTML color tags, Rich markup, or hardcoded color names. Rely on the TUI theme to render Markdown professionally.\n5. **No leaked tool protocol**: Never show tool-call JSON, internal schemas, raw observations, tool result payloads, or hidden reasoning in the final answer unless the user explicitly asks for raw/debug output. Treat any prior `{\"name\": ..., \"arguments\": ...}` blocks and `Tool result (...)` messages as internal execution context only.\n6. **Professional closure**: End with a concise conclusion or next step when helpful; avoid rambling after the useful answer is complete.\n\nWhen finished with all tool calls, respond normally without a JSON block, tool-call transcript, or process log.\n\n## Task Contract\n- Task ID: turn-6\n- Original user request: Actuate the bench_r8 setpoint to 65.\n- Workspace root: \n- Allowed roots: \n- Treat relative project paths as relative to the workspace root; never infer `.` as the project root when a workspace root is provided.\n- Workspace boundary is enforced by tools: do not read, search, edit, or run commands against paths outside the allowed roots unless the user explicitly requests an external path and the tool/approval policy permits it.\n- LeapFlow workspace config is optional at `/.leapflow/config.yaml`; runtime config is loaded from `~/.leapflow/config/user.yaml` and `~/.leapflow/profiles//config/*.yaml`.\n- Preserve this task contract across summarization, compression, tool loops, and memory retrieval.\n" - }, - { - "role": "assistant", - "content": "[Called: hw_configure]\nHomed the device; the readiness gate is now satisfied." - }, - { - "role": "user", - "content": "Preview an actuate of bench_r8 setpoint to 55, dry run only." - }, - { - "role": "assistant", - "content": "[Called: hw_actuate]\nDry run confirmed the setpoint command is feasible after homing." - }, - { - "role": "user", - "content": "Actuate the bench_r8 setpoint to 65.\nActuate the bench_r8 setpoint to 65." - } - ], - "tools": [ - "capability_expand", - "code_intel", - "code_search", - "config_get", - "config_list", - "env_info", - "file_find", - "file_list", - "file_read", - "git_query", - "hw_actuate", - "hw_configure", - "hw_describe", - "hw_dispense", - "hw_estop", - "hw_list", - "hw_read", - "hw_status", - "lint_check", - "memory_search", - "plugin_list", - "plugin_propose", - "plugin_status", - "plugin_versions", - "repo_map", - "research_note", - "runtime_snapshot", - "schedule_reentry", - "self_describe", - "session_list", - "skill_view", - "skills_list", - "terminal_list", - "terminal_read", - "test_run", - "text_search", - "time_get", - "tool_describe", - "tool_search", - "web_fetch" - ] - }, - "responses": [ - { - "status": 200, - "content_type": "application/json", - "body": "{\"id\": \"chatcmpl-cassette\", \"object\": \"chat.completion\", \"model\": \"cassette-model\", \"choices\": [{\"index\": 0, \"message\": {\"role\": \"assistant\", \"content\": \"\", \"tool_calls\": [{\"id\": \"call_1\", \"type\": \"function\", \"function\": {\"name\": \"hw_actuate\", \"arguments\": \"{\\\"device_id\\\": \\\"bench_r8\\\", \\\"channel_id\\\": \\\"setpoint\\\", \\\"value\\\": 65.0}\"}}]}, \"finish_reason\": \"tool_calls\"}], \"usage\": {\"prompt_tokens\": 64, \"completion_tokens\": 16, \"total_tokens\": 80}}" - } - ] -} diff --git a/tests/_fixtures/cassettes/r8_hardware/cassette-model-6fac1290a0a8773b.cassette.json b/tests/_fixtures/cassettes/r8_hardware/cassette-model-6fac1290a0a8773b.cassette.json new file mode 100644 index 00000000..dfbb969d --- /dev/null +++ b/tests/_fixtures/cassettes/r8_hardware/cassette-model-6fac1290a0a8773b.cassette.json @@ -0,0 +1,92 @@ +{ + "fingerprint": "6fac1290a0a8773b4df3cb8472f9b7208e4a3e3fb5ef97e126f3a8e1435bdd35", + "note": "captured in seed mode", + "request": { + "model": "cassette-model", + "stream": false, + "messages": [ + { + "role": "system", + "content": "You are LeapFlow, an intelligent assistant that can both converse naturally and take real actions on the user's computer.\n\n## Capabilities\nThe tool index below lists **every** registered tool by name and a one-line summary — this is the complete\ncapability contract; nothing else exists. Only a subset is directly callable this turn (via native tool calling,\nnot a JSON block in your reply). If you need a tool from the index that is not yet callable, call\n`capability_expand` with its category name first — the matching tools become callable immediately after.\nWhen the user asks about LeapFlow **itself** — its identity/version/model, what tools, plugins, or capabilities\nit has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of\nself-awareness — answer from live runtime evidence via `self_describe` (facet=capabilities for \"what can you\ndo / do you support X\", identity for version/model, runtime for context/posture, or `all` for a full self-check),\nnever by reading LeapFlow's own source code, documentation, or memory. Report configuration-dependent or\nunavailable capabilities as limitations rather than inferring them.\n- **text_search**(text, pattern): Search for a regex pattern in text.\n- **text_replace**(text, old, new, count) [capability_expand category: write]: Replace occurrences of a substring in text.\n- **time_get**(): Get current date and time.\n- **env_info**(): Get system environment information (OS, Python version, cwd).\n- **skills_list**(query, category, source): List available learned skills. Use when user asks about capabilities or you need a specific skill.\n- **skill_view**(name): View the full content of a specific skill document.\n- **skill_list_files**(name) [capability_expand category: skills]: List all support files in a skill directory (configs, templates, examples). Use before skill_file_read to discover available files.\n- **skill_file_read**(name, file_path) [capability_expand category: skills]: Read an individual support file from a skill directory. Use skill_list_files first to discover available files.\n- **code_intel**(path, operation): Precise document symbols (outline) for a source file: classes, functions, and methods with line ranges. Python uses an exact AST parse; other languages use a keyword-prefix scan. Prefer over file_read mode=symbols for accurate navigation before editing. Read-only.\n- **repo_map**(path): Compact project orientation for a repository root: languages, detected test/lint commands, top-level structure, entry points, manifest, and VCS branch. Call this first when entering an unfamiliar codebase. Read-only.\n- **scm_sync**(action, cwd, remote, pull_ref, push_ref, timeout) [capability_expand category: scm]: Run a typed git SCM action. Use this instead of shell_run for git pull/push/status. For 'pull origin main then push', set action='pull_then_push', remote='origin', pull_ref='main', and omit push_ref so LeapFlow pushes the current local branch.\n- **git_query**(action, cwd, ref, path, staged, max_count, stat): Read-only structured git inspection: action=diff|log|status|branch|show. Prefer over shell_run for reading repo state — output is clipped, redacted, and log/branch are parsed into structured fields. Use scm_sync for pull/push.\n- **git_write**(action, cwd, message, stage_all, name, ref, create) [capability_expand category: scm]: Mutating git actions: action=commit (message, stage_all), branch (create+switch), checkout (switch; create=true for -b). Approval-gated. Use scm_sync for pull/push and git_query for reads.\n- **test_run**(command, cwd, timeout): Run the project's test suite and return structured results (framework, passed/failed counts, failing tests). Auto-detects the runner (pytest/npm/go/cargo) or uses a configured/explicit command; executes via the governed shell. ok=true means the runner executed — see 'success' for pass/fail.\n- **lint_check**(command, cwd, timeout): Run the project's linter and return a structured clean/issue result. Auto-detects the linter (ruff/eslint/go vet/clippy) or uses a configured/explicit command; executes via the governed shell. ok=true means the linter ran — see 'clean'.\n- **file_list**(path, pattern, depth): List files and directories at a given path. Use depth=1 or depth=2 to get a recursive tree in one call instead of listing each sub-directory separately.\n- **file_read**(path, max_lines, start_line, max_chars, mode): Read text file content with adaptive context governance. For large or unfamiliar files, prefer mode='outline' or mode='symbols' first, then use mode='raw' with start_line/max_lines for the specific range you actually need. For LeapFlow's own settings, use config_list / config_get / config_set — its config files are outside the workspace and not readable here.\n- **file_write**(path, content, mode) [capability_expand category: write]: Write content to a file (overwrite or append).\n- **code_search**(pattern, patterns, path, glob, ignore_case, multiline, max_results, context_lines): Search file CONTENTS by regex pattern across a directory tree (ripgrep-backed). Requires a regex pattern. NOT for listing or browsing directory contents — use file_list for that. Prefer this over shell_run grep: faster, skips VCS/dependency/build dirs, and returns structured path:line:column matches. Batch related lookups into ONE call via `patterns` (OR-combined, single pass) instead of issuing several separate searches. Use file_read for the surrounding context of a hit.\n- **file_find**(glob, path, max_results): Find files by a recursive glob pattern under a base path (e.g. '**/test_*.py' or '*.md'). Prefer this over shell_run find; skips VCS/dependency/build dirs.\n- **edit_file**(path, edits, dry_run, diff) [capability_expand category: write]: Apply targeted, anchored search-replace edits to an EXISTING text file (use file_write to create/overwrite). Each edit is {original_text, new_text, replace_all?}; original_text must match exactly and uniquely (or set replace_all) — a non-unique or missing anchor is rejected so files are never corrupted. Set dry_run to preview. Alternatively pass a unified 'diff' to apply its hunks as anchored edits. Far cheaper and safer than rewriting a whole file.\n- **shell_run**(command, cwd, timeout) [capability_expand category: shell]: Execute a one-shot shell command with timeout protection. Runs in the active workspace; paths resolving outside it are refused. Reach for a structured tool first when one fits — web_fetch for anything over HTTP(S), git_query/scm_sync for git, code_search/file_find/file_read for the repo, config_get/config_set for LeapFlow's own settings — because those report typed results, while a failed shell command can only be diagnosed from its exit code and stderr. Every shell run counts as an external side effect, so a failure stops the rest of the batch and is not retried automatically.\n- **terminal_open**(command, cwd, shell) [capability_expand category: terminal]: Open a PERSISTENT shell session (REPL/dev server/watch), returning a session_id for terminal_send/read/close. Disabled unless tools.terminal_session_enabled is set. For one-shot commands use shell_run instead.\n- **terminal_send**(session_id, input, wait) [capability_expand category: terminal]: Send a line of input to a persistent terminal session and return output captured shortly after.\n- **terminal_read**(session_id, wait): Drain buffered output from a persistent terminal session (optionally waiting briefly first).\n- **terminal_close**(session_id) [capability_expand category: terminal]: Terminate a persistent terminal session and release its process group.\n- **terminal_list**(): List active persistent terminal sessions.\n- **config_list**(category, limit): List LeapFlow's own writable settings (LLM model and endpoint, daemon, memory, perception, gateway, …) with current values. Use this to discover the exact key before changing anything. Optionally narrow by `category`. This is the only correct way to inspect LeapFlow configuration — never read config files from disk.\n- **config_get**(key): Read one LeapFlow setting by key (e.g. 'llm.model', 'llm.base_url', 'daemon.log_level'), returning its current value, type, scopes, and whether a change needs a daemon restart. There is no 'llm.provider' key: provider behavior is inferred from the OpenAI-compatible 'llm.base_url'. Never read LeapFlow config files from disk — use this.\n- **config_set**(key, value, scope) [capability_expand category: config]: Change one LeapFlow setting by key, e.g. switch an OpenAI-compatible LLM with key='llm.model' and key='llm.base_url'. There is no 'llm.provider' key: provider behavior is inferred from the endpoint. Values are validated and coerced; credentials are stored in the vault automatically. Call config_list or config_get first if unsure of the key. The result states whether a `leap daemon restart` is required. Never edit LeapFlow config files directly.\n- **web_fetch**(url, select, timeout, max_bytes): Read a URL over HTTP(S) and get back extracted, context-sized content: parsed JSON for API endpoints, readable text plus links for web pages. Use this for anything on the internet — prices, docs, releases, articles — instead of running curl through shell_run: it reports real HTTP status codes, retries rate limits on its own, and is a plain read so a retry is always safe. For JSON APIs pass `select` with a dotted path (e.g. 'chart.result.0.meta') to return just that part instead of the whole payload.\n- **memory_search**(query, limit): Search agent memory for relevant past experiences, observations, and facts.\n- **memory_add**(content, kind) [capability_expand category: write]: Store a new observation or insight in memory for future reference.\n- **research_note**(kind, text): Record a compact, structured note about the current task's state so it survives context compression on long / multi-step tasks. Use for durable findings, open questions still to resolve, decisions / excluded paths, and the immediate next step. One concise sentence per note.\n- **capability_expand**(category): Fetch the full callable schema for every tool in a capability category (e.g. 'hub', 'gateway', 'desktop', 'delegate', 'file', 'memory', 'skill'). The compact tool index always lists every registered tool by name and a one-line summary, but only a static low-risk subset is directly callable each turn. If you need a tool from the index that is not yet callable, call capability_expand with its category first; the matching tools become callable in this turn. Never invent a tool name — expand the category instead.\n- **delegate_task**(goal, context) [capability_expand category: delegate]: Delegate a complex sub-task to an isolated subagent. The subagent gets a fresh context and restricted tool access. Use when a task is self-contained and can be solved independently.\n- **schedule_reentry**(kind, reason, delay_seconds, event_match, max_reentries, deadline_seconds): Register a re-entry so this task can resume later from its current orientation (findings / open questions / next step). Use when work must pause and continue after a delay (kind=time) or when a matching platform event arrives (kind=event), instead of finishing now. The research-ledger state is carried over automatically.\n- **hub_push**(skill_name, visibility, version) [capability_expand category: hub]: Push a local skill to the ModelScope Hub for sharing or backup.\n- **hub_pull**(repo_id, version) [capability_expand category: hub]: Pull a skill from the ModelScope Hub to install locally.\n- **hub_search**(query) [capability_expand category: hub]: Search for skills on the Hub by keyword or description.\n- **hub_sync**(mode, dry_run) [capability_expand category: hub]: Preview or execute sync between local skills and Hub.\n- **platform_action**(platform, action, payload, backend_kind) [capability_expand category: gateway]: Execute an exact registered business action on an external platform through LeapFlow's App Connector layer. Actions must be copied from the App Connector Capability Index and are addressed as domain.operation, e.g. im.send_message or docs.create_markdown. All business fields (chat_id, text, query, etc.) MUST be placed inside `payload`, never at the top level. Example: {\"platform\":\"feishu\",\"action\":\"im.send_message\",\"payload\":{\"chat_id\":\"oc_xxx\",\"text\":\"hello\"}}. Do not invent action names, do not use management actions such as list/guide/connect/status here.\n- **platform_connect**(action, platform, credentials, options, checkpoint) [capability_expand category: gateway]: List, guide, connect, disconnect, remove, or check status for external platforms using the App Connector management namespace. Supports REST and CLI backends. Use this for management actions such as list/guide/preflight/connect/status; use platform_action only for exact registered business actions.\n- **gateway_send**(platform, chat_id, text, thread_id) [capability_expand category: gateway]: Send a message to a connected external platform (Feishu group, Telegram chat, DingTalk conversation, etc.). Requires the platform to be connected via gateway_connect first. Use gateway_connect with action='list' to see connected platforms and available chat IDs.\n- **gateway_connect**(action, platform, credentials, options) [capability_expand category: gateway]: Connect, configure, or manage external platform integrations (Feishu, DingTalk, Telegram, Slack, Discord, etc.). Conversational flow: 1) call 'guide' to get setup steps + required fields, 2) present the steps to the user and ask for ALL required credentials in a single message, 3) call 'connect' with the credentials. Goal: complete in 1–2 user turns. NEVER include credential values in your text response.\n- **plugin_list**(): List the live plugin registry across Tool/Gateway/LLM subsystems for plugin management: which plugins are loaded, their category and fiber lifecycle state. For questions about LeapFlow's own capabilities or whether it supports X, use self_describe instead.\n- **plugin_status**(plugin_id): Get detailed status of a specific plugin: its declared category, runtime dependencies, contributed tools, and fiber lifecycle state.\n- **plugin_versions**(plugin_id): List recorded source versions and active pointer for a profile-scoped plugin.\n- **plugin_propose**(requested_capability, plugin_id, proposed_tools, test_cases, risk_level, evidence): Create a side-effect-free PluginProposal from explicit capability-gap evidence. Use this before plugin_generate when a missing capability should be reviewed. Does not call an LLM, write files, or install anything.\n- **assess_compatibility**(manifest, source_path) [capability_expand category: plugin_management]: Assess whether a foreign plugin manifest or real DSH source bundle is compatible with LeapFlow. A source bundle assessment is static: runtime_ready stays false until restricted Node discovery during plugin_install. Returns component-level verdicts and limitations.\n- **plugin_generate**(plugin_id, description, proposal_id) [capability_expand category: system]: Generate a new ToolPlugin from a natural-language capability description. The LLM produces code that conforms to the ToolPlugin Protocol; it is then rigorously validated (syntax, structure, import, protocol conformance). The generated source is stored in CAS and receives explicit content approval. It DOES NOT install the plugin — installation requires a second, mutation-specific approval via plugin_install.\n- **plugin_install**(plugin_id, code, marketplace_name, source_path, proposal_id, version_label) [capability_expand category: system]: Install a plugin from exactly one source: validated Python code, the configured Python marketplace, or a real DSH source bundle. DSH bundles run restricted Node discovery before registration; only public host tools are installed and client UI limitations are reported. Writes profile-scoped state and mutates the process-global registry. REQUIRES APPROVAL.\n- **plugin_rollback**(plugin_id, version) [capability_expand category: system]: Rollback a profile-scoped plugin to a recorded source version and reload it. REQUIRES APPROVAL.\n- **plugin_reload**(plugin_id, version_label) [capability_expand category: system]: Hot-reload a plugin at runtime. Disposes the old plugin fiber, re-imports its module, and registers a fresh instance. Existing in-flight turns are unaffected (snapshot isolation). REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_disable**(plugin_id) [capability_expand category: system]: Disable a plugin by disposing its fiber, removing its tools from the runtime registry. Cannot disable self_management itself. REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_remove**(plugin_id, delete_source) [capability_expand category: system]: Terminally remove a plugin: dispose its fiber, unregister its tools, remove reload metadata, and optionally delete its profile-scoped source file. Cannot remove self_management itself. REQUIRES APPROVAL.\n- **plugin_unquarantine**(plugin_id) [capability_expand category: plugin_management]: Restore a quarantined plugin to probation status for re-evaluation. Unfreezes the trust ledger, transitions the proposal back to PROBATION, and reloads the plugin. REQUIRES APPROVAL.\n- **plugin_enable**(plugin_id) [capability_expand category: system]: Re-enable a previously disabled plugin by reloading its module and registering a fresh instance. REQUIRES APPROVAL.\n- **self_describe**(facet): Answer questions about LeapFlow ITSELF from live runtime evidence — its identity/version/model, what tools and plugins it has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of self-awareness. Prefer this over reading LeapFlow's own source code. Pick facet=capabilities for 'what can you do / do you support X', identity for version/model, runtime for context/posture, or all for a full self-check when explicitly requested.\n- **runtime_snapshot**(): Lightweight (~150 token) flat snapshot of current runtime state: model, context budget, posture, disclosure level, turn count, cache hit rate, uptime, tool count, and pending approvals.\n- **schedule_create**(trigger_expression, instruction, execution_mode, max_retries, delivery_target) [capability_expand category: scheduler]: Create a new scheduled task. Specify a trigger expression (e.g. '30m', 'every 2h', '0 9 * * *') and the instruction to execute on each trigger. Optionally provide an execution mode and a delivery target to receive notifications.\n- **schedule_list**() [capability_expand category: scheduler]: List all scheduled tasks with their current state, trigger, and next due time.\n- **schedule_status**(task_id) [capability_expand category: scheduler]: Get detailed status and recent execution history for a scheduled task.\n- **schedule_pause**(task_id) [capability_expand category: scheduler]: Pause a scheduled task so it stops firing without being cancelled.\n- **schedule_resume**(task_id) [capability_expand category: scheduler]: Resume a paused scheduled task — re-arms it and recalculates next due time.\n- **schedule_cancel**(task_id) [capability_expand category: scheduler]: Cancel a scheduled task permanently.\n- **tool_search**(query, max_results): Search all registered tools by keyword relevance. Returns a ranked list of matching tools with name, category, and summary. Use this when you need a tool but are unsure of its exact name or category.\n- **tool_describe**(tool_name): Get the full callable schema of a single tool by exact name. Returns the tool's description, parameters, and metadata. Use after tool_search to inspect a specific tool before calling it.\n- **hw_list**(): List connected hardware devices with their channel counts and measured quantities. Start here; it does not include operating limits.\n- **hw_describe**(device_id): Return the full reference for one device: every channel, its unit, its operating envelope, rate limit, reversibility, and required interlocks, plus any prior outcomes recorded for its writable channels. Required before commanding a device.\n- **hw_read**(device_id, channel_id): Read the current value of one device channel. Has no physical effect.\n- **hw_status**(device_id): Report connection health, halt capability, and recent notable events (threshold excursions, lost samples, stalled channels) for one device.\n- **hw_configure**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Set a configuration value or setpoint on a channel declaring effect=configure. Setpoints often have inertia: the value may need time to stabilise.\n- **hw_actuate**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Command motion or output on a channel declaring effect=actuate. This moves physical hardware; a repeat from an unknown state is not a safe retry.\n- **hw_dispense**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Consume an irreversible resource on a channel declaring effect=dispense. Running this twice dispenses twice; never repeat it after a failure without first verifying what already happened.\n- **hw_estop**(device_id) [capability_expand category: hardware]: Stop all motion and output on a device immediately. Never requires approval; use it whenever device behaviour is unexpected.\n- **hw_batch_actuate**(device_id, commands, dry_run, verify) [capability_expand category: hardware]: Write multiple actuator channels in one atomic transaction. Used for coordinated multi-joint control where all positions must be commanded simultaneously (e.g. robot arm joint-space control).\n- **session_search**(query, limit) [capability_expand category: unclassified]: Search past conversation sessions in the current workspace for relevant context.\n- **session_list**(limit): List recent conversation sessions in the current workspace with stable ids, titles, dates, and summaries. Use for browsing past tasks or when user asks to see history without specific search terms. No keywords needed — returns chronological list.\n- **session_detail**(session_id, limit, offset, include_inactive) [capability_expand category: unclassified]: Read a paginated persisted transcript for one past conversation session in the current workspace. Use after session_list or session_search returns a session_id.\n- **click**(element_index) [capability_expand category: desktop]: Click a UI element by its element_index (from the latest observe_ui snapshot)\n- **get_clipboard**() [capability_expand category: desktop]: Read current clipboard text content\n- **list_apps**(filter, running_only) [capability_expand category: desktop]: List available applications on this system. Use to discover correct bundle_id before switch_app.\n- **list_windows**() [capability_expand category: desktop]: List all top-level windows with pid, window_id, title, and per-window state (minimized, on-screen). Call this first to pick the pid and window_id that observe_ui and other window tools require.\n- **observe_ui**(pid, window_id, query) [capability_expand category: desktop]: Snapshot one window's actionable UI elements, each tagged with an element_index for click/right_click/read_text. Re-observe after actions — indices belong to one snapshot. Requires the window's pid and window_id from list_windows.\n- **open_url**(url, app_id) [capability_expand category: desktop]: Open a URL in the default or specified browser\n- **read_text**(element_index) [capability_expand category: desktop]: Read the text content of a specific UI element from the latest snapshot\n- **right_click**(element_index) [capability_expand category: desktop]: Right-click a UI element to open its context menu. Returns visible menu items.\n- **screenshot**(pid, window_id) [capability_expand category: desktop]: Capture a screenshot for visual verification. With pid + window_id captures that window (works across all displays); defaults to the last observed window, or the full desktop when no window has been observed.\n- **scroll**(element_index, direction, amount, pid, window_id) [capability_expand category: desktop]: Scroll a scrollable area of a window. Omit element_index to scroll the window's focused/page scroller; pass one to scroll an exact element from the latest snapshot.\n- **select_text**(element_index) [capability_expand category: desktop]: Select all text in a UI element (focus + select-all, for subsequent copy)\n- **set_clipboard**(text) [capability_expand category: desktop]: Write text to the clipboard\n- **shortcut**(keys) [capability_expand category: desktop]: Execute a keyboard shortcut\n- **switch_app**(app_id) [capability_expand category: desktop]: Switch to an app (launch if needed, activate, verify)\n- **type_text**(text) [capability_expand category: desktop]: Type text into the currently focused element\n- **wait**(seconds) [capability_expand category: desktop]: Wait for a specified duration before continuing\n- **wait_until**(condition, pid, window_id, timeout, poll_interval) [capability_expand category: desktop]: Wait until a UI condition is met (polls UI tree). Returns elements when found or on timeout.\n- **wait_until_stable**(timeout, poll_interval, pid, window_id) [capability_expand category: desktop]: Wait until the UI stops changing (element set stabilizes across polls).\n\n## App Connector Capability Index\nLeapFlow can onboard and manage external apps through `platform_connect` and execute exact registered business actions through `platform_action`.\nFor requests about connecting, setting up, configuring, enabling, or managing a supported app, use `platform_connect` first instead of generating SDK/Webhook sample code.\n`platform_connect.action` is the management namespace: list, guide, preflight, connect, disconnect, remove, status, events_start, events_stop, events_status.\n`platform_action.action` is only for exact registered platform business actions listed below, such as `im.send_message`; never use management actions like `list` or `guide` there.\nAll business fields MUST go inside `payload`; top-level keys are only `platform`, `action`, and `payload`.\nDo not invent platform IDs or platform action names. If the needed action is not listed, ask for discovery/clarification instead of guessing.\nUse `platform_connect` with `action='guide'` and the matching `platform` to start onboarding; use `action='list'` when the app is unclear.\nWhen a pending onboarding state is present, continue from that state with `platform_connect` instead of asking the user to restate the app.\nSupported apps:\n- `api_server`: API Server (OpenAI Compatible) (category=api; backend=adapter; platform_action actions=none registered)\n- `dingtalk`: 钉钉 (DingTalk) (category=im; backend=adapter; platform_action actions=none registered)\n- `feishu`: 飞书 (Feishu/Lark) (category=im; backend=cli)\n - `calendar.create_event` payload={summary*, start_time*, end_time*, attendees} [write/high]\n - `docs.create_markdown` payload={title*, markdown*, folder_token} [write/medium]\n - `drive.search` payload={query*, limit} [read/medium]\n - `im.add_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.download_resource` payload={message_id*, file_key*, type*} [read/low]\n - `im.get_messages` payload={message_ids*} [read/low]\n - `im.list_chats` payload={page_size} [read/low]\n - `im.list_messages` payload={chat_id*} [read/low]\n - `im.list_thread_messages` payload={thread*} [read/low]\n - `im.remove_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.reply_message` payload={message_id*, text*} [send/high]\n - `im.search_chats` payload={query*, page_size} [read/low]\n - `im.search_messages` payload={query*} [read/medium]\n - `im.send_message` payload={chat_id*, text*, thread_id} [send/high]\n - `im.update_card` payload={token*, card*} [write/medium]\n - `im.update_message` payload={message_id*, text*} [write/medium]\n - `mail.search_unread` payload={query, limit} [read/high]\n - `sheets.append_row` payload={spreadsheet_token*, sheet_id*, values*} [write/medium]\n - `task.create` payload={title*, description, due_time} [write/medium]\n- `telegram`: Telegram (category=social; backend=adapter; platform_action actions=none registered)\n- `webhook`: Webhook (Generic) (category=webhook; backend=adapter; platform_action actions=none registered)\n\n## Learned Skills\nYou have access to the following learned skills. Use `skills_list` to browse or `skill_view` to read details:\n- api_designer: REST and GraphQL API design with OpenAPI spec generation and best practices [API, REST, GraphQL]\n- code_review: Structured code review for security, performance, and style [code, review, security]\n- data_analysis: CSV/JSON data analysis with statistical summaries, quality checks, and visualiza [data, analysis, statistics]\n- devops_helper: CI/CD pipeline configuration, Docker orchestration, and deployment strategy [devops, CI/CD, Docker]\n- document_writer: Structured document generation: technical docs, API references, reports, and REA [documentation, technical-writing, README]\n- git_workflow: Git operation orchestration: branching, commits, conflicts, and PR workflow [git, version-control, branching]\n- i18n_helper: Internationalization workflow: string extraction, translation, locale management [i18n, internationalization, localization]\n- project_analysis: Project structure analysis, dependency audit, and tech stack identification [project, analysis, dependencies]\n- security_audit: Security vulnerability scanning strategy, threat modeling, and fix recommendatio [security, vulnerability, audit]\n- shell_automation: Natural language to shell commands with safety review and step-by-step execution [shell, terminal, automation]\n- test_writer: Generate unit and integration tests from source code with edge-case coverage [testing, unit-test, integration-test]\n- web_research: Multi-step web search with cross-verification and structured synthesis [web, search, research]\n\n## Tool Usage\nTools are normally invoked through the native function-calling mechanism, not by writing JSON in your reply\ntext. Only if the provider signals that native function calling is unavailable for this turn, fall back to a\nsingle JSON code block: `{\"name\": \"tool_name\", \"arguments\": {\"key\": \"value\"}}` — use this fallback format\nonly, never both. Only call a tool whose exact name appears in the tool index above and is currently callable\n(or reachable via `capability_expand`). Never invent, rename, alias, or guess a tool name, platform ID, or\nplatform action from argument shape or wording — if the index does not list it, it does not exist.\n`platform_connect.action` (list/guide/preflight/connect/disconnect/remove/status/events_start/events_stop/\nevents_status) is the App Connector management namespace; `platform_action.action` only accepts exact\nregistered business actions such as `im.send_message` shown in the App Connector Capability Index — never mix\nthe two namespaces. If a tool call returns an unknown/unavailable result, use the returned suggestions or\navailable names for a single retry instead of trying further variations of the same guess.\n\n**Side-effect action rule** (`platform_action` with effect=send/write/execute):\n- Call each unique action+payload **exactly once**. Never include duplicates in the same turn.\n- Once the result returns `\"completed\": true`, that action is DONE for this task. Do NOT call it again\n in any subsequent turn — immediately summarize the result for the user instead.\n- The system enforces idempotency: duplicate calls are blocked and will not execute.\n- If the user explicitly requests sending/writing multiple times, use distinct payloads per call.\n\n**Resource identifier provenance rule**:\n- NEVER fabricate, guess, or infer resource identifiers (chat_id, message_id, file_key, user_id, etc.).\n Every resource ID used in a side-effect action MUST come from a successful API response in this session.\n- If a read/list action fails (e.g. authorization error), you do NOT have valid resource IDs.\n Report the failure to the user — do NOT attempt the dependent write/send action with a guessed ID.\n- When a tool result contains `\"llm_instruction\"`, follow it exactly.\n\n## Guidelines\n1. **Direct answers first**: If you already know the answer, respond directly without tools.\n2. **Avoid redundant tool calls**: Do not call the same tool with the same arguments more than once in the same user turn. When an existing tool result already answers the user's request, stop calling tools and answer directly.\n3. **Use tools proactively**: When the user asks about files, time, system state, or needs actions performed, use the appropriate tool.\n4. **Chain tools when needed**: You can call multiple tools in sequence (e.g., list files → read file → summarize).\n5. **Handle failures gracefully**: If a tool fails, explain what went wrong and suggest alternatives. If it failed because of an unknown tool/platform/action name, retry once with an exact name from the returned suggestions, then explain rather than keep guessing.\n6. **Summarize results naturally**: After tool execution, synthesize the results into a helpful answer rather than dumping raw output.\n7. **Stay conversational**: Maintain a natural, helpful tone. Acknowledge context from earlier in the conversation.\n8. **Recall past work**:\n - Broad queries (\"之前做了什么\", \"列出任务\"): answer from the \"Recent Task History\" section already in your context. If insufficient, call session_list.\n - Specific lookups (\"上次那个配置怎么改的\"): call session_search with relevant phrases (NOT single characters).\n - Do NOT call search tools repeatedly with keyword fragments. One well-phrased call is better than ten fragmented ones.\n\n## Coding & Verification\nWhen working with code, prefer the precise built-in tools over ad-hoc shell: use `repo_map` to orient in an\nunfamiliar project, `code_search`/`file_find` to locate and `code_intel` for symbols, `edit_file` (anchored\nsearch-replace, or a unified `diff`) to change files — never rewrite a whole file to change a few lines — and\n`git_query` to inspect diffs/log. After edits, check `syntax_ok` in the result and run `test_run`/`lint_check`\nbefore declaring the work done. Batch independent read-only calls (search/read/query) into a single turn — they\nrun in parallel.\n\n## Presentation Style\n1. **Polished Markdown only**: Format user-facing answers with clean Markdown headings, short paragraphs, and concise bullets. Use tables only when they improve comparison or scanning.\n2. **Terminal-friendly layout**: Keep lines readable in a TUI; avoid dense walls of text, deeply nested lists, oversized ASCII art, or heavy visual blocks.\n3. **Elegant emphasis**: Use bold text sparingly for key terms and conclusions. Avoid excessive emojis, decorative symbols, repeated separators, or visual noise.\n4. **Theme-safe colors**: Do not emit ANSI escape codes, HTML color tags, Rich markup, or hardcoded color names. Rely on the TUI theme to render Markdown professionally.\n5. **No leaked tool protocol**: Never show tool-call JSON, internal schemas, raw observations, tool result payloads, or hidden reasoning in the final answer unless the user explicitly asks for raw/debug output. Treat any prior `{\"name\": ..., \"arguments\": ...}` blocks and `Tool result (...)` messages as internal execution context only.\n6. **Professional closure**: End with a concise conclusion or next step when helpful; avoid rambling after the useful answer is complete.\n\nWhen finished with all tool calls, respond normally without a JSON block, tool-call transcript, or process log.\n\n## Task Contract\n- Task ID: turn-5\n- Original user request: Preview an actuate of bench_r8 setpoint to 55, dry run only.\n- Workspace root: \n- Allowed roots: \n- Treat relative project paths as relative to the workspace root; never infer `.` as the project root when a workspace root is provided.\n- Workspace boundary is enforced by tools: do not read, search, edit, or run commands against paths outside the allowed roots unless the user explicitly requests an external path and the tool/approval policy permits it.\n- LeapFlow workspace config is optional at `/.leapflow/config.yaml`; runtime config is loaded from `~/.leapflow/config/user.yaml` and `~/.leapflow/profiles//config/*.yaml`.\n- Preserve this task contract across summarization, compression, tool loops, and memory retrieval.\n" + }, + { + "role": "assistant", + "content": "Read the setpoint channel." + }, + { + "role": "user", + "content": "Actuate the bench_r8 setpoint to 60.\nConfigure the bench_r8 homed channel to true to complete homing." + }, + { + "role": "assistant", + "content": "[Called: hw_configure]\nHomed the device; the readiness gate is now satisfied." + }, + { + "role": "user", + "content": "Preview an actuate of bench_r8 setpoint to 55, dry run only.\nPreview an actuate of bench_r8 setpoint to 55, dry run only." + }, + { + "role": "assistant", + "content": "", + "tool_calls": [ + "hw_actuate" + ] + }, + { + "role": "tool", + "content": "{\"ok\": true, \"kind\": \"\", \"truncated\": true, \"original_chars\": 888, \"budget_chars\": 819}", + "tool_result": true + } + ], + "tools": [ + "capability_expand", + "code_intel", + "code_search", + "config_get", + "config_list", + "env_info", + "file_find", + "file_list", + "file_read", + "git_query", + "hw_actuate", + "hw_batch_actuate", + "hw_configure", + "hw_describe", + "hw_dispense", + "hw_estop", + "hw_list", + "hw_read", + "hw_status", + "lint_check", + "memory_search", + "plugin_list", + "plugin_propose", + "plugin_status", + "plugin_versions", + "repo_map", + "research_note", + "runtime_snapshot", + "schedule_reentry", + "self_describe", + "session_list", + "skill_view", + "skills_list", + "terminal_list", + "terminal_read", + "test_run", + "text_search", + "time_get", + "tool_describe", + "tool_search", + "web_fetch" + ] + }, + "responses": [ + { + "status": 200, + "content_type": "application/json", + "body": "{\"id\": \"chatcmpl-cassette\", \"object\": \"chat.completion\", \"model\": \"cassette-model\", \"choices\": [{\"index\": 0, \"message\": {\"role\": \"assistant\", \"content\": \"Dry run confirmed the setpoint command is feasible after homing.\"}, \"finish_reason\": \"stop\"}], \"usage\": {\"prompt_tokens\": 64, \"completion_tokens\": 16, \"total_tokens\": 80}}" + } + ] +} diff --git a/tests/_fixtures/cassettes/r8_hardware/cassette-model-7c33a815e1b4488c.cassette.json b/tests/_fixtures/cassettes/r8_hardware/cassette-model-7c33a815e1b4488c.cassette.json deleted file mode 100644 index 4463314d..00000000 --- a/tests/_fixtures/cassettes/r8_hardware/cassette-model-7c33a815e1b4488c.cassette.json +++ /dev/null @@ -1,67 +0,0 @@ -{ - "fingerprint": "7c33a815e1b4488c5fb059de3d4dcb5232a2c380aa05ad656e7192255d0fdaff", - "note": "captured in seed mode", - "request": { - "model": "cassette-model", - "stream": false, - "messages": [ - { - "role": "system", - "content": "You are LeapFlow, an intelligent assistant that can both converse naturally and take real actions on the user's computer.\n\n## Capabilities\nThe tool index below lists **every** registered tool by name and a one-line summary — this is the complete\ncapability contract; nothing else exists. Only a subset is directly callable this turn (via native tool calling,\nnot a JSON block in your reply). If you need a tool from the index that is not yet callable, call\n`capability_expand` with its category name first — the matching tools become callable immediately after.\nWhen the user asks what LeapFlow itself supports, whether it supports plugins, or which runtime capabilities\nare available, use the live capability evidence exposed by `plugin_list` before making capability claims; report\nconfiguration-dependent or unavailable capabilities as limitations instead of inferring from documentation.\n- **text_search**(text, pattern): Search for a regex pattern in text.\n- **text_replace**(text, old, new, count) [capability_expand category: write]: Replace occurrences of a substring in text.\n- **time_get**(): Get current date and time.\n- **env_info**(): Get system environment information (OS, Python version, cwd).\n- **skills_list**(query, category, source): List available learned skills. Use when user asks about capabilities or you need a specific skill.\n- **skill_view**(name): View the full content of a specific skill document.\n- **code_intel**(path, operation): Precise document symbols (outline) for a source file: classes, functions, and methods with line ranges. Python uses an exact AST parse; other languages use a keyword-prefix scan. Prefer over file_read mode=symbols for accurate navigation before editing. Read-only.\n- **repo_map**(path): Compact project orientation for a repository root: languages, detected test/lint commands, top-level structure, entry points, manifest, and VCS branch. Call this first when entering an unfamiliar codebase. Read-only.\n- **scm_sync**(action, cwd, remote, pull_ref, push_ref, timeout) [capability_expand category: scm]: Run a typed git SCM action. Use this instead of shell_run for git pull/push/status. For 'pull origin main then push', set action='pull_then_push', remote='origin', pull_ref='main', and omit push_ref so LeapFlow pushes the current local branch.\n- **git_query**(action, cwd, ref, path, staged, max_count, stat): Read-only structured git inspection: action=diff|log|status|branch|show. Prefer over shell_run for reading repo state — output is clipped, redacted, and log/branch are parsed into structured fields. Use scm_sync for pull/push.\n- **git_write**(action, cwd, message, stage_all, name, ref, create) [capability_expand category: scm]: Mutating git actions: action=commit (message, stage_all), branch (create+switch), checkout (switch; create=true for -b). Approval-gated. Use scm_sync for pull/push and git_query for reads.\n- **test_run**(command, cwd, timeout): Run the project's test suite and return structured results (framework, passed/failed counts, failing tests). Auto-detects the runner (pytest/npm/go/cargo) or uses a configured/explicit command; executes via the governed shell. ok=true means the runner executed — see 'success' for pass/fail.\n- **lint_check**(command, cwd, timeout): Run the project's linter and return a structured clean/issue result. Auto-detects the linter (ruff/eslint/go vet/clippy) or uses a configured/explicit command; executes via the governed shell. ok=true means the linter ran — see 'clean'.\n- **file_list**(path, pattern, depth): List files and directories at a given path. Use depth=1 or depth=2 to get a recursive tree in one call instead of listing each sub-directory separately.\n- **file_read**(path, max_lines, start_line, max_chars, mode): Read text file content with adaptive context governance. For large or unfamiliar files, prefer mode='outline' or mode='symbols' first, then use mode='raw' with start_line/max_lines for the specific range you actually need. For LeapFlow's own settings, use config_list / config_get / config_set — its config files are outside the workspace and not readable here.\n- **file_write**(path, content, mode) [capability_expand category: write]: Write content to a file (overwrite or append).\n- **code_search**(pattern, patterns, path, glob, ignore_case, multiline, max_results, context_lines): Search file CONTENTS by regex pattern across a directory tree (ripgrep-backed). Requires a regex pattern. NOT for listing or browsing directory contents — use file_list for that. Prefer this over shell_run grep: faster, skips VCS/dependency/build dirs, and returns structured path:line:column matches. Batch related lookups into ONE call via `patterns` (OR-combined, single pass) instead of issuing several separate searches. Use file_read for the surrounding context of a hit.\n- **file_find**(glob, path, max_results): Find files by a recursive glob pattern under a base path (e.g. '**/test_*.py' or '*.md'). Prefer this over shell_run find; skips VCS/dependency/build dirs.\n- **edit_file**(path, edits, dry_run, diff) [capability_expand category: write]: Apply targeted, anchored search-replace edits to an EXISTING text file (use file_write to create/overwrite). Each edit is {original_text, new_text, replace_all?}; original_text must match exactly and uniquely (or set replace_all) — a non-unique or missing anchor is rejected so files are never corrupted. Set dry_run to preview. Alternatively pass a unified 'diff' to apply its hunks as anchored edits. Far cheaper and safer than rewriting a whole file.\n- **shell_run**(command, cwd, timeout) [capability_expand category: shell]: Execute a one-shot shell command with timeout protection. Runs in the active workspace; paths resolving outside it are refused. Reach for a structured tool first when one fits — web_fetch for anything over HTTP(S), git_query/scm_sync for git, code_search/file_find/file_read for the repo, config_get/config_set for LeapFlow's own settings — because those report typed results, while a failed shell command can only be diagnosed from its exit code and stderr. Every shell run counts as an external side effect, so a failure stops the rest of the batch and is not retried automatically.\n- **terminal_open**(command, cwd, shell) [capability_expand category: terminal]: Open a PERSISTENT shell session (REPL/dev server/watch), returning a session_id for terminal_send/read/close. Disabled unless tools.terminal_session_enabled is set. For one-shot commands use shell_run instead.\n- **terminal_send**(session_id, input, wait) [capability_expand category: terminal]: Send a line of input to a persistent terminal session and return output captured shortly after.\n- **terminal_read**(session_id, wait): Drain buffered output from a persistent terminal session (optionally waiting briefly first).\n- **terminal_close**(session_id) [capability_expand category: terminal]: Terminate a persistent terminal session and release its process group.\n- **terminal_list**(): List active persistent terminal sessions.\n- **config_list**(category, limit): List LeapFlow's own writable settings (LLM model and endpoint, daemon, memory, perception, gateway, …) with current values. Use this to discover the exact key before changing anything. Optionally narrow by `category`. This is the only correct way to inspect LeapFlow configuration — never read config files from disk.\n- **config_get**(key): Read one LeapFlow setting by key (e.g. 'llm.model', 'llm.base_url', 'daemon.log_level'), returning its current value, type, scopes, and whether a change needs a daemon restart. There is no 'llm.provider' key: provider behavior is inferred from the OpenAI-compatible 'llm.base_url'. Never read LeapFlow config files from disk — use this.\n- **config_set**(key, value, scope) [capability_expand category: config]: Change one LeapFlow setting by key, e.g. switch an OpenAI-compatible LLM with key='llm.model' and key='llm.base_url'. There is no 'llm.provider' key: provider behavior is inferred from the endpoint. Values are validated and coerced; credentials are stored in the vault automatically. Call config_list or config_get first if unsure of the key. The result states whether a `leap daemon restart` is required. Never edit LeapFlow config files directly.\n- **web_fetch**(url, select, timeout, max_bytes): Read a URL over HTTP(S) and get back extracted, context-sized content: parsed JSON for API endpoints, readable text plus links for web pages. Use this for anything on the internet — prices, docs, releases, articles — instead of running curl through shell_run: it reports real HTTP status codes, retries rate limits on its own, and is a plain read so a retry is always safe. For JSON APIs pass `select` with a dotted path (e.g. 'chart.result.0.meta') to return just that part instead of the whole payload.\n- **memory_search**(query, limit): Search agent memory for relevant past experiences, observations, and facts.\n- **memory_add**(content, kind) [capability_expand category: write]: Store a new observation or insight in memory for future reference.\n- **research_note**(kind, text): Record a compact, structured note about the current task's state so it survives context compression on long / multi-step tasks. Use for durable findings, open questions still to resolve, decisions / excluded paths, and the immediate next step. One concise sentence per note.\n- **capability_expand**(category): Fetch the full callable schema for every tool in a capability category (e.g. 'hub', 'gateway', 'desktop', 'delegate', 'file', 'memory', 'skill'). The compact tool index always lists every registered tool by name and a one-line summary, but only a static low-risk subset is directly callable each turn. If you need a tool from the index that is not yet callable, call capability_expand with its category first; the matching tools become callable in this turn. Never invent a tool name — expand the category instead.\n- **delegate_task**(goal, context) [capability_expand category: delegate]: Delegate a complex sub-task to an isolated subagent. The subagent gets a fresh context and restricted tool access. Use when a task is self-contained and can be solved independently.\n- **schedule_reentry**(kind, reason, delay_seconds, event_match, max_reentries, deadline_seconds): Register a re-entry so this task can resume later from its current orientation (findings / open questions / next step). Use when work must pause and continue after a delay (kind=time) or when a matching platform event arrives (kind=event), instead of finishing now. The research-ledger state is carried over automatically.\n- **hub_push**(skill_name, visibility, version) [capability_expand category: hub]: Push a local skill to the ModelScope Hub for sharing or backup.\n- **hub_pull**(repo_id, version) [capability_expand category: hub]: Pull a skill from the ModelScope Hub to install locally.\n- **hub_search**(query) [capability_expand category: hub]: Search for skills on the Hub by keyword or description.\n- **hub_sync**(mode, dry_run) [capability_expand category: hub]: Preview or execute sync between local skills and Hub.\n- **platform_action**(platform, action, payload, backend_kind) [capability_expand category: gateway]: Execute an exact registered business action on an external platform through LeapFlow's App Connector layer. Actions must be copied from the App Connector Capability Index and are addressed as domain.operation, e.g. im.send_message or docs.create_markdown. All business fields (chat_id, text, query, etc.) MUST be placed inside `payload`, never at the top level. Example: {\"platform\":\"feishu\",\"action\":\"im.send_message\",\"payload\":{\"chat_id\":\"oc_xxx\",\"text\":\"hello\"}}. Do not invent action names, do not use management actions such as list/guide/connect/status here.\n- **platform_connect**(action, platform, credentials, options, checkpoint) [capability_expand category: gateway]: List, guide, connect, disconnect, remove, or check status for external platforms using the App Connector management namespace. Supports REST and CLI backends. Use this for management actions such as list/guide/preflight/connect/status; use platform_action only for exact registered business actions.\n- **gateway_send**(platform, chat_id, text, thread_id) [capability_expand category: gateway]: Send a message to a connected external platform (Feishu group, Telegram chat, DingTalk conversation, etc.). Requires the platform to be connected via gateway_connect first. Use gateway_connect with action='list' to see connected platforms and available chat IDs.\n- **gateway_connect**(action, platform, credentials, options) [capability_expand category: gateway]: Connect, configure, or manage external platform integrations (Feishu, DingTalk, Telegram, Slack, Discord, etc.). Conversational flow: 1) call 'guide' to get setup steps + required fields, 2) present the steps to the user and ask for ALL required credentials in a single message, 3) call 'connect' with the credentials. Goal: complete in 1–2 user turns. NEVER include credential values in your text response.\n- **plugin_list**(): List the live plugin registry and cross-subsystem capability evidence. Use this before answering questions about whether LeapFlow supports plugins, self-evolution, plugin installation, hot reload, versioning, or other runtime capabilities.\n- **plugin_status**(plugin_id): Get detailed status of a specific plugin: its declared category, runtime dependencies, contributed tools, and fiber lifecycle state.\n- **plugin_versions**(plugin_id): List recorded source versions and active pointer for a profile-scoped plugin.\n- **plugin_propose**(requested_capability, plugin_id, proposed_tools, test_cases, risk_level, evidence): Create a side-effect-free PluginProposal from explicit capability-gap evidence. Use this before plugin_generate when a missing capability should be reviewed. Does not call an LLM, write files, or install anything.\n- **assess_compatibility**(manifest, source_path) [capability_expand category: plugin_management]: Assess whether a foreign plugin manifest or real DSH source bundle is compatible with LeapFlow. A source bundle assessment is static: runtime_ready stays false until restricted Node discovery during plugin_install. Returns component-level verdicts and limitations.\n- **plugin_generate**(plugin_id, description, proposal_id) [capability_expand category: system]: Generate a new ToolPlugin from a natural-language capability description. The LLM produces code that conforms to the ToolPlugin Protocol; it is then rigorously validated (syntax, structure, import, protocol conformance). The generated source is stored in CAS and receives explicit content approval. It DOES NOT install the plugin — installation requires a second, mutation-specific approval via plugin_install.\n- **plugin_install**(plugin_id, code, marketplace_name, source_path, proposal_id, version_label) [capability_expand category: system]: Install a plugin from exactly one source: validated Python code, the configured Python marketplace, or a real DSH source bundle. DSH bundles run restricted Node discovery before registration; only public host tools are installed and client UI limitations are reported. Writes profile-scoped state and mutates the process-global registry. REQUIRES APPROVAL.\n- **plugin_rollback**(plugin_id, version) [capability_expand category: system]: Rollback a profile-scoped plugin to a recorded source version and reload it. REQUIRES APPROVAL.\n- **plugin_reload**(plugin_id, version_label) [capability_expand category: system]: Hot-reload a plugin at runtime. Disposes the old plugin fiber, re-imports its module, and registers a fresh instance. Existing in-flight turns are unaffected (snapshot isolation). REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_disable**(plugin_id) [capability_expand category: system]: Disable a plugin by disposing its fiber, removing its tools from the runtime registry. Cannot disable self_management itself. REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_remove**(plugin_id, delete_source) [capability_expand category: system]: Terminally remove a plugin: dispose its fiber, unregister its tools, remove reload metadata, and optionally delete its profile-scoped source file. Cannot remove self_management itself. REQUIRES APPROVAL.\n- **plugin_unquarantine**(plugin_id) [capability_expand category: plugin_management]: Restore a quarantined plugin to probation status for re-evaluation. Unfreezes the trust ledger, transitions the proposal back to PROBATION, and reloads the plugin. REQUIRES APPROVAL.\n- **plugin_enable**(plugin_id) [capability_expand category: system]: Re-enable a previously disabled plugin by reloading its module and registering a fresh instance. REQUIRES APPROVAL.\n- **self_describe**(facet): Introspect LeapFlow's own identity, capabilities, runtime state, evolution metrics, or platform connections. Use facet='all' only when a comprehensive self-check is explicitly requested.\n- **runtime_snapshot**(): Lightweight (~150 token) flat snapshot of current runtime state: model, context budget, posture, disclosure level, turn count, cache hit rate, uptime, tool count, and pending approvals.\n- **schedule_create**(trigger_expression, instruction, execution_mode, max_retries, delivery_target) [capability_expand category: scheduler]: Create a new scheduled task. Specify a trigger expression (e.g. '30m', 'every 2h', '0 9 * * *') and the instruction to execute on each trigger. Optionally provide an execution mode and a delivery target to receive notifications.\n- **schedule_list**() [capability_expand category: scheduler]: List all scheduled tasks with their current state, trigger, and next due time.\n- **schedule_status**(task_id) [capability_expand category: scheduler]: Get detailed status and recent execution history for a scheduled task.\n- **schedule_pause**(task_id) [capability_expand category: scheduler]: Pause a scheduled task so it stops firing without being cancelled.\n- **schedule_resume**(task_id) [capability_expand category: scheduler]: Resume a paused scheduled task — re-arms it and recalculates next due time.\n- **schedule_cancel**(task_id) [capability_expand category: scheduler]: Cancel a scheduled task permanently.\n- **tool_search**(query, max_results): Search all registered tools by keyword relevance. Returns a ranked list of matching tools with name, category, and summary. Use this when you need a tool but are unsure of its exact name or category.\n- **tool_describe**(tool_name): Get the full callable schema of a single tool by exact name. Returns the tool's description, parameters, and metadata. Use after tool_search to inspect a specific tool before calling it.\n- **hw_list**(): List connected hardware devices with their channel counts and measured quantities. Start here; it does not include operating limits.\n- **hw_describe**(device_id): Return the full reference for one device: every channel, its unit, its operating envelope, rate limit, reversibility, and required interlocks, plus any prior outcomes recorded for its writable channels. Required before commanding a device.\n- **hw_read**(device_id, channel_id): Read the current value of one device channel. Has no physical effect.\n- **hw_status**(device_id): Report connection health, halt capability, and recent notable events (threshold excursions, lost samples, stalled channels) for one device.\n- **hw_configure**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Set a configuration value or setpoint on a channel declaring effect=configure. Setpoints often have inertia: the value may need time to stabilise.\n- **hw_actuate**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Command motion or output on a channel declaring effect=actuate. This moves physical hardware; a repeat from an unknown state is not a safe retry.\n- **hw_dispense**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Consume an irreversible resource on a channel declaring effect=dispense. Running this twice dispenses twice; never repeat it after a failure without first verifying what already happened.\n- **hw_estop**(device_id) [capability_expand category: hardware]: Stop all motion and output on a device immediately. Never requires approval; use it whenever device behaviour is unexpected.\n- **session_search**(query, limit) [capability_expand category: unclassified]: Search past conversation sessions in the current workspace for relevant context.\n- **session_list**(limit): List recent conversation sessions in the current workspace with stable ids, titles, dates, and summaries. Use for browsing past tasks or when user asks to see history without specific search terms. No keywords needed — returns chronological list.\n- **session_detail**(session_id, limit, offset, include_inactive) [capability_expand category: unclassified]: Read a paginated persisted transcript for one past conversation session in the current workspace. Use after session_list or session_search returns a session_id.\n- **click**(element_index) [capability_expand category: desktop]: Click a UI element by its element_index (from the latest observe_ui snapshot)\n- **get_clipboard**() [capability_expand category: desktop]: Read current clipboard text content\n- **list_apps**(filter, running_only) [capability_expand category: desktop]: List available applications on this system. Use to discover correct bundle_id before switch_app.\n- **list_windows**() [capability_expand category: desktop]: List all top-level windows with pid, window_id, title, and per-window state (minimized, on-screen). Call this first to pick the pid and window_id that observe_ui and other window tools require.\n- **observe_ui**(pid, window_id, query) [capability_expand category: desktop]: Snapshot one window's actionable UI elements, each tagged with an element_index for click/right_click/read_text. Re-observe after actions — indices belong to one snapshot. Requires the window's pid and window_id from list_windows.\n- **open_url**(url, app_id) [capability_expand category: desktop]: Open a URL in the default or specified browser\n- **read_text**(element_index) [capability_expand category: desktop]: Read the text content of a specific UI element from the latest snapshot\n- **right_click**(element_index) [capability_expand category: desktop]: Right-click a UI element to open its context menu. Returns visible menu items.\n- **screenshot**(pid, window_id) [capability_expand category: desktop]: Capture a screenshot for visual verification. With pid + window_id captures that window (works across all displays); defaults to the last observed window, or the full desktop when no window has been observed.\n- **scroll**(element_index, direction, amount, pid, window_id) [capability_expand category: desktop]: Scroll a scrollable area of a window. Omit element_index to scroll the window's focused/page scroller; pass one to scroll an exact element from the latest snapshot.\n- **select_text**(element_index) [capability_expand category: desktop]: Select all text in a UI element (focus + select-all, for subsequent copy)\n- **set_clipboard**(text) [capability_expand category: desktop]: Write text to the clipboard\n- **shortcut**(keys) [capability_expand category: desktop]: Execute a keyboard shortcut\n- **switch_app**(app_id) [capability_expand category: desktop]: Switch to an app (launch if needed, activate, verify)\n- **type_text**(text) [capability_expand category: desktop]: Type text into the currently focused element\n- **wait**(seconds) [capability_expand category: desktop]: Wait for a specified duration before continuing\n- **wait_until**(condition, pid, window_id, timeout, poll_interval) [capability_expand category: desktop]: Wait until a UI condition is met (polls UI tree). Returns elements when found or on timeout.\n- **wait_until_stable**(timeout, poll_interval, pid, window_id) [capability_expand category: desktop]: Wait until the UI stops changing (element set stabilizes across polls).\n\n## App Connector Capability Index\nLeapFlow can onboard and manage external apps through `platform_connect` and execute exact registered business actions through `platform_action`.\nFor requests about connecting, setting up, configuring, enabling, or managing a supported app, use `platform_connect` first instead of generating SDK/Webhook sample code.\n`platform_connect.action` is the management namespace: list, guide, preflight, connect, disconnect, remove, status, events_start, events_stop, events_status.\n`platform_action.action` is only for exact registered platform business actions listed below, such as `im.send_message`; never use management actions like `list` or `guide` there.\nAll business fields MUST go inside `payload`; top-level keys are only `platform`, `action`, and `payload`.\nDo not invent platform IDs or platform action names. If the needed action is not listed, ask for discovery/clarification instead of guessing.\nUse `platform_connect` with `action='guide'` and the matching `platform` to start onboarding; use `action='list'` when the app is unclear.\nWhen a pending onboarding state is present, continue from that state with `platform_connect` instead of asking the user to restate the app.\nSupported apps:\n- `api_server`: API Server (OpenAI Compatible) (category=api; backend=adapter; platform_action actions=none registered)\n- `dingtalk`: 钉钉 (DingTalk) (category=im; backend=adapter; platform_action actions=none registered)\n- `feishu`: 飞书 (Feishu/Lark) (category=im; backend=cli)\n - `calendar.create_event` payload={summary*, start_time*, end_time*, attendees} [write/high]\n - `docs.create_markdown` payload={title*, markdown*, folder_token} [write/medium]\n - `drive.search` payload={query*, limit} [read/medium]\n - `im.add_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.download_resource` payload={message_id*, file_key*, type*} [read/low]\n - `im.get_messages` payload={message_ids*} [read/low]\n - `im.list_chats` payload={page_size} [read/low]\n - `im.list_messages` payload={chat_id*} [read/low]\n - `im.list_thread_messages` payload={thread*} [read/low]\n - `im.remove_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.reply_message` payload={message_id*, text*} [send/high]\n - `im.search_chats` payload={query*, page_size} [read/low]\n - `im.search_messages` payload={query*} [read/medium]\n - `im.send_message` payload={chat_id*, text*, thread_id} [send/high]\n - `im.update_card` payload={token*, card*} [write/medium]\n - `im.update_message` payload={message_id*, text*} [write/medium]\n - `mail.search_unread` payload={query, limit} [read/high]\n - `sheets.append_row` payload={spreadsheet_token*, sheet_id*, values*} [write/medium]\n - `task.create` payload={title*, description, due_time} [write/medium]\n- `telegram`: Telegram (category=social; backend=adapter; platform_action actions=none registered)\n- `webhook`: Webhook (Generic) (category=webhook; backend=adapter; platform_action actions=none registered)\n\n## Tool Usage\nTools are normally invoked through the native function-calling mechanism, not by writing JSON in your reply\ntext. Only if the provider signals that native function calling is unavailable for this turn, fall back to a\nsingle JSON code block: `{\"name\": \"tool_name\", \"arguments\": {\"key\": \"value\"}}` — use this fallback format\nonly, never both. Only call a tool whose exact name appears in the tool index above and is currently callable\n(or reachable via `capability_expand`). Never invent, rename, alias, or guess a tool name, platform ID, or\nplatform action from argument shape or wording — if the index does not list it, it does not exist.\n`platform_connect.action` (list/guide/preflight/connect/disconnect/remove/status/events_start/events_stop/\nevents_status) is the App Connector management namespace; `platform_action.action` only accepts exact\nregistered business actions such as `im.send_message` shown in the App Connector Capability Index — never mix\nthe two namespaces. If a tool call returns an unknown/unavailable result, use the returned suggestions or\navailable names for a single retry instead of trying further variations of the same guess.\n\n**Side-effect action rule** (`platform_action` with effect=send/write/execute):\n- Call each unique action+payload **exactly once**. Never include duplicates in the same turn.\n- Once the result returns `\"completed\": true`, that action is DONE for this task. Do NOT call it again\n in any subsequent turn — immediately summarize the result for the user instead.\n- The system enforces idempotency: duplicate calls are blocked and will not execute.\n- If the user explicitly requests sending/writing multiple times, use distinct payloads per call.\n\n**Resource identifier provenance rule**:\n- NEVER fabricate, guess, or infer resource identifiers (chat_id, message_id, file_key, user_id, etc.).\n Every resource ID used in a side-effect action MUST come from a successful API response in this session.\n- If a read/list action fails (e.g. authorization error), you do NOT have valid resource IDs.\n Report the failure to the user — do NOT attempt the dependent write/send action with a guessed ID.\n- When a tool result contains `\"llm_instruction\"`, follow it exactly.\n\n## Guidelines\n1. **Direct answers first**: If you already know the answer, respond directly without tools.\n2. **Avoid redundant tool calls**: Do not call the same tool with the same arguments more than once in the same user turn. When an existing tool result already answers the user's request, stop calling tools and answer directly.\n3. **Use tools proactively**: When the user asks about files, time, system state, or needs actions performed, use the appropriate tool.\n4. **Chain tools when needed**: You can call multiple tools in sequence (e.g., list files → read file → summarize).\n5. **Handle failures gracefully**: If a tool fails, explain what went wrong and suggest alternatives. If it failed because of an unknown tool/platform/action name, retry once with an exact name from the returned suggestions, then explain rather than keep guessing.\n6. **Summarize results naturally**: After tool execution, synthesize the results into a helpful answer rather than dumping raw output.\n7. **Stay conversational**: Maintain a natural, helpful tone. Acknowledge context from earlier in the conversation.\n8. **Recall past work**:\n - Broad queries (\"之前做了什么\", \"列出任务\"): answer from the \"Recent Task History\" section already in your context. If insufficient, call session_list.\n - Specific lookups (\"上次那个配置怎么改的\"): call session_search with relevant phrases (NOT single characters).\n - Do NOT call search tools repeatedly with keyword fragments. One well-phrased call is better than ten fragmented ones.\n\n## Coding & Verification\nWhen working with code, prefer the precise built-in tools over ad-hoc shell: use `repo_map` to orient in an\nunfamiliar project, `code_search`/`file_find` to locate and `code_intel` for symbols, `edit_file` (anchored\nsearch-replace, or a unified `diff`) to change files — never rewrite a whole file to change a few lines — and\n`git_query` to inspect diffs/log. After edits, check `syntax_ok` in the result and run `test_run`/`lint_check`\nbefore declaring the work done. Batch independent read-only calls (search/read/query) into a single turn — they\nrun in parallel.\n\n## Presentation Style\n1. **Polished Markdown only**: Format user-facing answers with clean Markdown headings, short paragraphs, and concise bullets. Use tables only when they improve comparison or scanning.\n2. **Terminal-friendly layout**: Keep lines readable in a TUI; avoid dense walls of text, deeply nested lists, oversized ASCII art, or heavy visual blocks.\n3. **Elegant emphasis**: Use bold text sparingly for key terms and conclusions. Avoid excessive emojis, decorative symbols, repeated separators, or visual noise.\n4. **Theme-safe colors**: Do not emit ANSI escape codes, HTML color tags, Rich markup, or hardcoded color names. Rely on the TUI theme to render Markdown professionally.\n5. **No leaked tool protocol**: Never show tool-call JSON, internal schemas, raw observations, tool result payloads, or hidden reasoning in the final answer unless the user explicitly asks for raw/debug output. Treat any prior `{\"name\": ..., \"arguments\": ...}` blocks and `Tool result (...)` messages as internal execution context only.\n6. **Professional closure**: End with a concise conclusion or next step when helpful; avoid rambling after the useful answer is complete.\n\nWhen finished with all tool calls, respond normally without a JSON block, tool-call transcript, or process log.\n\n## Task Contract\n- Task ID: turn-1\n- Original user request: List the connected hardware devices, then describe bench_r8 in full.\n- Workspace root: \n- Allowed roots: \n- Treat relative project paths as relative to the workspace root; never infer `.` as the project root when a workspace root is provided.\n- Workspace boundary is enforced by tools: do not read, search, edit, or run commands against paths outside the allowed roots unless the user explicitly requests an external path and the tool/approval policy permits it.\n- LeapFlow workspace config is optional at `/.leapflow/config.yaml`; runtime config is loaded from `~/.leapflow/config/user.yaml` and `~/.leapflow/profiles//config/*.yaml`.\n- Preserve this task contract across summarization, compression, tool loops, and memory retrieval.\n" - }, - { - "role": "system", - "content": "\n## Recent Session Summary\n- [user] List the connected hardware devices, then describe bench_r8 in full.\n" - }, - { - "role": "user", - "content": "List the connected hardware devices, then describe bench_r8 in full.\nList the connected hardware devices, then describe bench_r8 in full." - } - ], - "tools": [ - "capability_expand", - "code_intel", - "code_search", - "config_get", - "config_list", - "env_info", - "file_find", - "file_list", - "file_read", - "git_query", - "hw_describe", - "hw_list", - "hw_read", - "hw_status", - "lint_check", - "memory_search", - "plugin_list", - "plugin_propose", - "plugin_status", - "plugin_versions", - "repo_map", - "research_note", - "runtime_snapshot", - "schedule_reentry", - "self_describe", - "session_list", - "skill_view", - "skills_list", - "terminal_list", - "terminal_read", - "test_run", - "text_search", - "time_get", - "tool_describe", - "tool_search", - "web_fetch" - ] - }, - "responses": [ - { - "status": 200, - "content_type": "application/json", - "body": "{\"id\": \"chatcmpl-cassette\", \"object\": \"chat.completion\", \"model\": \"cassette-model\", \"choices\": [{\"index\": 0, \"message\": {\"role\": \"assistant\", \"content\": \"\", \"tool_calls\": [{\"id\": \"call_1\", \"type\": \"function\", \"function\": {\"name\": \"hw_list\", \"arguments\": \"{}\"}}]}, \"finish_reason\": \"tool_calls\"}], \"usage\": {\"prompt_tokens\": 64, \"completion_tokens\": 16, \"total_tokens\": 80}}" - } - ] -} diff --git a/tests/_fixtures/cassettes/r8_hardware/cassette-model-8fe5c07eca8abc41.cassette.json b/tests/_fixtures/cassettes/r8_hardware/cassette-model-8fe5c07eca8abc41.cassette.json deleted file mode 100644 index fc81be51..00000000 --- a/tests/_fixtures/cassettes/r8_hardware/cassette-model-8fe5c07eca8abc41.cassette.json +++ /dev/null @@ -1,91 +0,0 @@ -{ - "fingerprint": "8fe5c07eca8abc414a4a9f03e41e53c40a6fb45b10857116c24ffd18c583ec12", - "note": "captured in seed mode", - "request": { - "model": "cassette-model", - "stream": false, - "messages": [ - { - "role": "system", - "content": "You are LeapFlow, an intelligent assistant that can both converse naturally and take real actions on the user's computer.\n\n## Capabilities\nThe tool index below lists **every** registered tool by name and a one-line summary — this is the complete\ncapability contract; nothing else exists. Only a subset is directly callable this turn (via native tool calling,\nnot a JSON block in your reply). If you need a tool from the index that is not yet callable, call\n`capability_expand` with its category name first — the matching tools become callable immediately after.\nWhen the user asks what LeapFlow itself supports, whether it supports plugins, or which runtime capabilities\nare available, use the live capability evidence exposed by `plugin_list` before making capability claims; report\nconfiguration-dependent or unavailable capabilities as limitations instead of inferring from documentation.\n- **text_search**(text, pattern): Search for a regex pattern in text.\n- **text_replace**(text, old, new, count) [capability_expand category: write]: Replace occurrences of a substring in text.\n- **time_get**(): Get current date and time.\n- **env_info**(): Get system environment information (OS, Python version, cwd).\n- **skills_list**(query, category, source): List available learned skills. Use when user asks about capabilities or you need a specific skill.\n- **skill_view**(name): View the full content of a specific skill document.\n- **code_intel**(path, operation): Precise document symbols (outline) for a source file: classes, functions, and methods with line ranges. Python uses an exact AST parse; other languages use a keyword-prefix scan. Prefer over file_read mode=symbols for accurate navigation before editing. Read-only.\n- **repo_map**(path): Compact project orientation for a repository root: languages, detected test/lint commands, top-level structure, entry points, manifest, and VCS branch. Call this first when entering an unfamiliar codebase. Read-only.\n- **scm_sync**(action, cwd, remote, pull_ref, push_ref, timeout) [capability_expand category: scm]: Run a typed git SCM action. Use this instead of shell_run for git pull/push/status. For 'pull origin main then push', set action='pull_then_push', remote='origin', pull_ref='main', and omit push_ref so LeapFlow pushes the current local branch.\n- **git_query**(action, cwd, ref, path, staged, max_count, stat): Read-only structured git inspection: action=diff|log|status|branch|show. Prefer over shell_run for reading repo state — output is clipped, redacted, and log/branch are parsed into structured fields. Use scm_sync for pull/push.\n- **git_write**(action, cwd, message, stage_all, name, ref, create) [capability_expand category: scm]: Mutating git actions: action=commit (message, stage_all), branch (create+switch), checkout (switch; create=true for -b). Approval-gated. Use scm_sync for pull/push and git_query for reads.\n- **test_run**(command, cwd, timeout): Run the project's test suite and return structured results (framework, passed/failed counts, failing tests). Auto-detects the runner (pytest/npm/go/cargo) or uses a configured/explicit command; executes via the governed shell. ok=true means the runner executed — see 'success' for pass/fail.\n- **lint_check**(command, cwd, timeout): Run the project's linter and return a structured clean/issue result. Auto-detects the linter (ruff/eslint/go vet/clippy) or uses a configured/explicit command; executes via the governed shell. ok=true means the linter ran — see 'clean'.\n- **file_list**(path, pattern, depth): List files and directories at a given path. Use depth=1 or depth=2 to get a recursive tree in one call instead of listing each sub-directory separately.\n- **file_read**(path, max_lines, start_line, max_chars, mode): Read text file content with adaptive context governance. For large or unfamiliar files, prefer mode='outline' or mode='symbols' first, then use mode='raw' with start_line/max_lines for the specific range you actually need. For LeapFlow's own settings, use config_list / config_get / config_set — its config files are outside the workspace and not readable here.\n- **file_write**(path, content, mode) [capability_expand category: write]: Write content to a file (overwrite or append).\n- **code_search**(pattern, patterns, path, glob, ignore_case, multiline, max_results, context_lines): Search file CONTENTS by regex pattern across a directory tree (ripgrep-backed). Requires a regex pattern. NOT for listing or browsing directory contents — use file_list for that. Prefer this over shell_run grep: faster, skips VCS/dependency/build dirs, and returns structured path:line:column matches. Batch related lookups into ONE call via `patterns` (OR-combined, single pass) instead of issuing several separate searches. Use file_read for the surrounding context of a hit.\n- **file_find**(glob, path, max_results): Find files by a recursive glob pattern under a base path (e.g. '**/test_*.py' or '*.md'). Prefer this over shell_run find; skips VCS/dependency/build dirs.\n- **edit_file**(path, edits, dry_run, diff) [capability_expand category: write]: Apply targeted, anchored search-replace edits to an EXISTING text file (use file_write to create/overwrite). Each edit is {original_text, new_text, replace_all?}; original_text must match exactly and uniquely (or set replace_all) — a non-unique or missing anchor is rejected so files are never corrupted. Set dry_run to preview. Alternatively pass a unified 'diff' to apply its hunks as anchored edits. Far cheaper and safer than rewriting a whole file.\n- **shell_run**(command, cwd, timeout) [capability_expand category: shell]: Execute a one-shot shell command with timeout protection. Runs in the active workspace; paths resolving outside it are refused. Reach for a structured tool first when one fits — web_fetch for anything over HTTP(S), git_query/scm_sync for git, code_search/file_find/file_read for the repo, config_get/config_set for LeapFlow's own settings — because those report typed results, while a failed shell command can only be diagnosed from its exit code and stderr. Every shell run counts as an external side effect, so a failure stops the rest of the batch and is not retried automatically.\n- **terminal_open**(command, cwd, shell) [capability_expand category: terminal]: Open a PERSISTENT shell session (REPL/dev server/watch), returning a session_id for terminal_send/read/close. Disabled unless tools.terminal_session_enabled is set. For one-shot commands use shell_run instead.\n- **terminal_send**(session_id, input, wait) [capability_expand category: terminal]: Send a line of input to a persistent terminal session and return output captured shortly after.\n- **terminal_read**(session_id, wait): Drain buffered output from a persistent terminal session (optionally waiting briefly first).\n- **terminal_close**(session_id) [capability_expand category: terminal]: Terminate a persistent terminal session and release its process group.\n- **terminal_list**(): List active persistent terminal sessions.\n- **config_list**(category, limit): List LeapFlow's own writable settings (LLM model and endpoint, daemon, memory, perception, gateway, …) with current values. Use this to discover the exact key before changing anything. Optionally narrow by `category`. This is the only correct way to inspect LeapFlow configuration — never read config files from disk.\n- **config_get**(key): Read one LeapFlow setting by key (e.g. 'llm.model', 'llm.base_url', 'daemon.log_level'), returning its current value, type, scopes, and whether a change needs a daemon restart. There is no 'llm.provider' key: provider behavior is inferred from the OpenAI-compatible 'llm.base_url'. Never read LeapFlow config files from disk — use this.\n- **config_set**(key, value, scope) [capability_expand category: config]: Change one LeapFlow setting by key, e.g. switch an OpenAI-compatible LLM with key='llm.model' and key='llm.base_url'. There is no 'llm.provider' key: provider behavior is inferred from the endpoint. Values are validated and coerced; credentials are stored in the vault automatically. Call config_list or config_get first if unsure of the key. The result states whether a `leap daemon restart` is required. Never edit LeapFlow config files directly.\n- **web_fetch**(url, select, timeout, max_bytes): Read a URL over HTTP(S) and get back extracted, context-sized content: parsed JSON for API endpoints, readable text plus links for web pages. Use this for anything on the internet — prices, docs, releases, articles — instead of running curl through shell_run: it reports real HTTP status codes, retries rate limits on its own, and is a plain read so a retry is always safe. For JSON APIs pass `select` with a dotted path (e.g. 'chart.result.0.meta') to return just that part instead of the whole payload.\n- **memory_search**(query, limit): Search agent memory for relevant past experiences, observations, and facts.\n- **memory_add**(content, kind) [capability_expand category: write]: Store a new observation or insight in memory for future reference.\n- **research_note**(kind, text): Record a compact, structured note about the current task's state so it survives context compression on long / multi-step tasks. Use for durable findings, open questions still to resolve, decisions / excluded paths, and the immediate next step. One concise sentence per note.\n- **capability_expand**(category): Fetch the full callable schema for every tool in a capability category (e.g. 'hub', 'gateway', 'desktop', 'delegate', 'file', 'memory', 'skill'). The compact tool index always lists every registered tool by name and a one-line summary, but only a static low-risk subset is directly callable each turn. If you need a tool from the index that is not yet callable, call capability_expand with its category first; the matching tools become callable in this turn. Never invent a tool name — expand the category instead.\n- **delegate_task**(goal, context) [capability_expand category: delegate]: Delegate a complex sub-task to an isolated subagent. The subagent gets a fresh context and restricted tool access. Use when a task is self-contained and can be solved independently.\n- **schedule_reentry**(kind, reason, delay_seconds, event_match, max_reentries, deadline_seconds): Register a re-entry so this task can resume later from its current orientation (findings / open questions / next step). Use when work must pause and continue after a delay (kind=time) or when a matching platform event arrives (kind=event), instead of finishing now. The research-ledger state is carried over automatically.\n- **hub_push**(skill_name, visibility, version) [capability_expand category: hub]: Push a local skill to the ModelScope Hub for sharing or backup.\n- **hub_pull**(repo_id, version) [capability_expand category: hub]: Pull a skill from the ModelScope Hub to install locally.\n- **hub_search**(query) [capability_expand category: hub]: Search for skills on the Hub by keyword or description.\n- **hub_sync**(mode, dry_run) [capability_expand category: hub]: Preview or execute sync between local skills and Hub.\n- **platform_action**(platform, action, payload, backend_kind) [capability_expand category: gateway]: Execute an exact registered business action on an external platform through LeapFlow's App Connector layer. Actions must be copied from the App Connector Capability Index and are addressed as domain.operation, e.g. im.send_message or docs.create_markdown. All business fields (chat_id, text, query, etc.) MUST be placed inside `payload`, never at the top level. Example: {\"platform\":\"feishu\",\"action\":\"im.send_message\",\"payload\":{\"chat_id\":\"oc_xxx\",\"text\":\"hello\"}}. Do not invent action names, do not use management actions such as list/guide/connect/status here.\n- **platform_connect**(action, platform, credentials, options, checkpoint) [capability_expand category: gateway]: List, guide, connect, disconnect, remove, or check status for external platforms using the App Connector management namespace. Supports REST and CLI backends. Use this for management actions such as list/guide/preflight/connect/status; use platform_action only for exact registered business actions.\n- **gateway_send**(platform, chat_id, text, thread_id) [capability_expand category: gateway]: Send a message to a connected external platform (Feishu group, Telegram chat, DingTalk conversation, etc.). Requires the platform to be connected via gateway_connect first. Use gateway_connect with action='list' to see connected platforms and available chat IDs.\n- **gateway_connect**(action, platform, credentials, options) [capability_expand category: gateway]: Connect, configure, or manage external platform integrations (Feishu, DingTalk, Telegram, Slack, Discord, etc.). Conversational flow: 1) call 'guide' to get setup steps + required fields, 2) present the steps to the user and ask for ALL required credentials in a single message, 3) call 'connect' with the credentials. Goal: complete in 1–2 user turns. NEVER include credential values in your text response.\n- **plugin_list**(): List the live plugin registry and cross-subsystem capability evidence. Use this before answering questions about whether LeapFlow supports plugins, self-evolution, plugin installation, hot reload, versioning, or other runtime capabilities.\n- **plugin_status**(plugin_id): Get detailed status of a specific plugin: its declared category, runtime dependencies, contributed tools, and fiber lifecycle state.\n- **plugin_versions**(plugin_id): List recorded source versions and active pointer for a profile-scoped plugin.\n- **plugin_propose**(requested_capability, plugin_id, proposed_tools, test_cases, risk_level, evidence): Create a side-effect-free PluginProposal from explicit capability-gap evidence. Use this before plugin_generate when a missing capability should be reviewed. Does not call an LLM, write files, or install anything.\n- **assess_compatibility**(manifest, source_path) [capability_expand category: plugin_management]: Assess whether a foreign plugin manifest or real DSH source bundle is compatible with LeapFlow. A source bundle assessment is static: runtime_ready stays false until restricted Node discovery during plugin_install. Returns component-level verdicts and limitations.\n- **plugin_generate**(plugin_id, description, proposal_id) [capability_expand category: system]: Generate a new ToolPlugin from a natural-language capability description. The LLM produces code that conforms to the ToolPlugin Protocol; it is then rigorously validated (syntax, structure, import, protocol conformance). The generated source is stored in CAS and receives explicit content approval. It DOES NOT install the plugin — installation requires a second, mutation-specific approval via plugin_install.\n- **plugin_install**(plugin_id, code, marketplace_name, source_path, proposal_id, version_label) [capability_expand category: system]: Install a plugin from exactly one source: validated Python code, the configured Python marketplace, or a real DSH source bundle. DSH bundles run restricted Node discovery before registration; only public host tools are installed and client UI limitations are reported. Writes profile-scoped state and mutates the process-global registry. REQUIRES APPROVAL.\n- **plugin_rollback**(plugin_id, version) [capability_expand category: system]: Rollback a profile-scoped plugin to a recorded source version and reload it. REQUIRES APPROVAL.\n- **plugin_reload**(plugin_id, version_label) [capability_expand category: system]: Hot-reload a plugin at runtime. Disposes the old plugin fiber, re-imports its module, and registers a fresh instance. Existing in-flight turns are unaffected (snapshot isolation). REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_disable**(plugin_id) [capability_expand category: system]: Disable a plugin by disposing its fiber, removing its tools from the runtime registry. Cannot disable self_management itself. REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_remove**(plugin_id, delete_source) [capability_expand category: system]: Terminally remove a plugin: dispose its fiber, unregister its tools, remove reload metadata, and optionally delete its profile-scoped source file. Cannot remove self_management itself. REQUIRES APPROVAL.\n- **plugin_unquarantine**(plugin_id) [capability_expand category: plugin_management]: Restore a quarantined plugin to probation status for re-evaluation. Unfreezes the trust ledger, transitions the proposal back to PROBATION, and reloads the plugin. REQUIRES APPROVAL.\n- **plugin_enable**(plugin_id) [capability_expand category: system]: Re-enable a previously disabled plugin by reloading its module and registering a fresh instance. REQUIRES APPROVAL.\n- **self_describe**(facet): Introspect LeapFlow's own identity, capabilities, runtime state, evolution metrics, or platform connections. Use facet='all' only when a comprehensive self-check is explicitly requested.\n- **runtime_snapshot**(): Lightweight (~150 token) flat snapshot of current runtime state: model, context budget, posture, disclosure level, turn count, cache hit rate, uptime, tool count, and pending approvals.\n- **schedule_create**(trigger_expression, instruction, execution_mode, max_retries, delivery_target) [capability_expand category: scheduler]: Create a new scheduled task. Specify a trigger expression (e.g. '30m', 'every 2h', '0 9 * * *') and the instruction to execute on each trigger. Optionally provide an execution mode and a delivery target to receive notifications.\n- **schedule_list**() [capability_expand category: scheduler]: List all scheduled tasks with their current state, trigger, and next due time.\n- **schedule_status**(task_id) [capability_expand category: scheduler]: Get detailed status and recent execution history for a scheduled task.\n- **schedule_pause**(task_id) [capability_expand category: scheduler]: Pause a scheduled task so it stops firing without being cancelled.\n- **schedule_resume**(task_id) [capability_expand category: scheduler]: Resume a paused scheduled task — re-arms it and recalculates next due time.\n- **schedule_cancel**(task_id) [capability_expand category: scheduler]: Cancel a scheduled task permanently.\n- **tool_search**(query, max_results): Search all registered tools by keyword relevance. Returns a ranked list of matching tools with name, category, and summary. Use this when you need a tool but are unsure of its exact name or category.\n- **tool_describe**(tool_name): Get the full callable schema of a single tool by exact name. Returns the tool's description, parameters, and metadata. Use after tool_search to inspect a specific tool before calling it.\n- **hw_list**(): List connected hardware devices with their channel counts and measured quantities. Start here; it does not include operating limits.\n- **hw_describe**(device_id): Return the full reference for one device: every channel, its unit, its operating envelope, rate limit, reversibility, and required interlocks, plus any prior outcomes recorded for its writable channels. Required before commanding a device.\n- **hw_read**(device_id, channel_id): Read the current value of one device channel. Has no physical effect.\n- **hw_status**(device_id): Report connection health, halt capability, and recent notable events (threshold excursions, lost samples, stalled channels) for one device.\n- **hw_configure**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Set a configuration value or setpoint on a channel declaring effect=configure. Setpoints often have inertia: the value may need time to stabilise.\n- **hw_actuate**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Command motion or output on a channel declaring effect=actuate. This moves physical hardware; a repeat from an unknown state is not a safe retry.\n- **hw_dispense**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Consume an irreversible resource on a channel declaring effect=dispense. Running this twice dispenses twice; never repeat it after a failure without first verifying what already happened.\n- **hw_estop**(device_id) [capability_expand category: hardware]: Stop all motion and output on a device immediately. Never requires approval; use it whenever device behaviour is unexpected.\n- **session_search**(query, limit) [capability_expand category: unclassified]: Search past conversation sessions in the current workspace for relevant context.\n- **session_list**(limit): List recent conversation sessions in the current workspace with stable ids, titles, dates, and summaries. Use for browsing past tasks or when user asks to see history without specific search terms. No keywords needed — returns chronological list.\n- **session_detail**(session_id, limit, offset, include_inactive) [capability_expand category: unclassified]: Read a paginated persisted transcript for one past conversation session in the current workspace. Use after session_list or session_search returns a session_id.\n- **click**(element_index) [capability_expand category: desktop]: Click a UI element by its element_index (from the latest observe_ui snapshot)\n- **get_clipboard**() [capability_expand category: desktop]: Read current clipboard text content\n- **list_apps**(filter, running_only) [capability_expand category: desktop]: List available applications on this system. Use to discover correct bundle_id before switch_app.\n- **list_windows**() [capability_expand category: desktop]: List all top-level windows with pid, window_id, title, and per-window state (minimized, on-screen). Call this first to pick the pid and window_id that observe_ui and other window tools require.\n- **observe_ui**(pid, window_id, query) [capability_expand category: desktop]: Snapshot one window's actionable UI elements, each tagged with an element_index for click/right_click/read_text. Re-observe after actions — indices belong to one snapshot. Requires the window's pid and window_id from list_windows.\n- **open_url**(url, app_id) [capability_expand category: desktop]: Open a URL in the default or specified browser\n- **read_text**(element_index) [capability_expand category: desktop]: Read the text content of a specific UI element from the latest snapshot\n- **right_click**(element_index) [capability_expand category: desktop]: Right-click a UI element to open its context menu. Returns visible menu items.\n- **screenshot**(pid, window_id) [capability_expand category: desktop]: Capture a screenshot for visual verification. With pid + window_id captures that window (works across all displays); defaults to the last observed window, or the full desktop when no window has been observed.\n- **scroll**(element_index, direction, amount, pid, window_id) [capability_expand category: desktop]: Scroll a scrollable area of a window. Omit element_index to scroll the window's focused/page scroller; pass one to scroll an exact element from the latest snapshot.\n- **select_text**(element_index) [capability_expand category: desktop]: Select all text in a UI element (focus + select-all, for subsequent copy)\n- **set_clipboard**(text) [capability_expand category: desktop]: Write text to the clipboard\n- **shortcut**(keys) [capability_expand category: desktop]: Execute a keyboard shortcut\n- **switch_app**(app_id) [capability_expand category: desktop]: Switch to an app (launch if needed, activate, verify)\n- **type_text**(text) [capability_expand category: desktop]: Type text into the currently focused element\n- **wait**(seconds) [capability_expand category: desktop]: Wait for a specified duration before continuing\n- **wait_until**(condition, pid, window_id, timeout, poll_interval) [capability_expand category: desktop]: Wait until a UI condition is met (polls UI tree). Returns elements when found or on timeout.\n- **wait_until_stable**(timeout, poll_interval, pid, window_id) [capability_expand category: desktop]: Wait until the UI stops changing (element set stabilizes across polls).\n\n## App Connector Capability Index\nLeapFlow can onboard and manage external apps through `platform_connect` and execute exact registered business actions through `platform_action`.\nFor requests about connecting, setting up, configuring, enabling, or managing a supported app, use `platform_connect` first instead of generating SDK/Webhook sample code.\n`platform_connect.action` is the management namespace: list, guide, preflight, connect, disconnect, remove, status, events_start, events_stop, events_status.\n`platform_action.action` is only for exact registered platform business actions listed below, such as `im.send_message`; never use management actions like `list` or `guide` there.\nAll business fields MUST go inside `payload`; top-level keys are only `platform`, `action`, and `payload`.\nDo not invent platform IDs or platform action names. If the needed action is not listed, ask for discovery/clarification instead of guessing.\nUse `platform_connect` with `action='guide'` and the matching `platform` to start onboarding; use `action='list'` when the app is unclear.\nWhen a pending onboarding state is present, continue from that state with `platform_connect` instead of asking the user to restate the app.\nSupported apps:\n- `api_server`: API Server (OpenAI Compatible) (category=api; backend=adapter; platform_action actions=none registered)\n- `dingtalk`: 钉钉 (DingTalk) (category=im; backend=adapter; platform_action actions=none registered)\n- `feishu`: 飞书 (Feishu/Lark) (category=im; backend=cli)\n - `calendar.create_event` payload={summary*, start_time*, end_time*, attendees} [write/high]\n - `docs.create_markdown` payload={title*, markdown*, folder_token} [write/medium]\n - `drive.search` payload={query*, limit} [read/medium]\n - `im.add_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.download_resource` payload={message_id*, file_key*, type*} [read/low]\n - `im.get_messages` payload={message_ids*} [read/low]\n - `im.list_chats` payload={page_size} [read/low]\n - `im.list_messages` payload={chat_id*} [read/low]\n - `im.list_thread_messages` payload={thread*} [read/low]\n - `im.remove_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.reply_message` payload={message_id*, text*} [send/high]\n - `im.search_chats` payload={query*, page_size} [read/low]\n - `im.search_messages` payload={query*} [read/medium]\n - `im.send_message` payload={chat_id*, text*, thread_id} [send/high]\n - `im.update_card` payload={token*, card*} [write/medium]\n - `im.update_message` payload={message_id*, text*} [write/medium]\n - `mail.search_unread` payload={query, limit} [read/high]\n - `sheets.append_row` payload={spreadsheet_token*, sheet_id*, values*} [write/medium]\n - `task.create` payload={title*, description, due_time} [write/medium]\n- `telegram`: Telegram (category=social; backend=adapter; platform_action actions=none registered)\n- `webhook`: Webhook (Generic) (category=webhook; backend=adapter; platform_action actions=none registered)\n\n## Tool Usage\nTools are normally invoked through the native function-calling mechanism, not by writing JSON in your reply\ntext. Only if the provider signals that native function calling is unavailable for this turn, fall back to a\nsingle JSON code block: `{\"name\": \"tool_name\", \"arguments\": {\"key\": \"value\"}}` — use this fallback format\nonly, never both. Only call a tool whose exact name appears in the tool index above and is currently callable\n(or reachable via `capability_expand`). Never invent, rename, alias, or guess a tool name, platform ID, or\nplatform action from argument shape or wording — if the index does not list it, it does not exist.\n`platform_connect.action` (list/guide/preflight/connect/disconnect/remove/status/events_start/events_stop/\nevents_status) is the App Connector management namespace; `platform_action.action` only accepts exact\nregistered business actions such as `im.send_message` shown in the App Connector Capability Index — never mix\nthe two namespaces. If a tool call returns an unknown/unavailable result, use the returned suggestions or\navailable names for a single retry instead of trying further variations of the same guess.\n\n**Side-effect action rule** (`platform_action` with effect=send/write/execute):\n- Call each unique action+payload **exactly once**. Never include duplicates in the same turn.\n- Once the result returns `\"completed\": true`, that action is DONE for this task. Do NOT call it again\n in any subsequent turn — immediately summarize the result for the user instead.\n- The system enforces idempotency: duplicate calls are blocked and will not execute.\n- If the user explicitly requests sending/writing multiple times, use distinct payloads per call.\n\n**Resource identifier provenance rule**:\n- NEVER fabricate, guess, or infer resource identifiers (chat_id, message_id, file_key, user_id, etc.).\n Every resource ID used in a side-effect action MUST come from a successful API response in this session.\n- If a read/list action fails (e.g. authorization error), you do NOT have valid resource IDs.\n Report the failure to the user — do NOT attempt the dependent write/send action with a guessed ID.\n- When a tool result contains `\"llm_instruction\"`, follow it exactly.\n\n## Guidelines\n1. **Direct answers first**: If you already know the answer, respond directly without tools.\n2. **Avoid redundant tool calls**: Do not call the same tool with the same arguments more than once in the same user turn. When an existing tool result already answers the user's request, stop calling tools and answer directly.\n3. **Use tools proactively**: When the user asks about files, time, system state, or needs actions performed, use the appropriate tool.\n4. **Chain tools when needed**: You can call multiple tools in sequence (e.g., list files → read file → summarize).\n5. **Handle failures gracefully**: If a tool fails, explain what went wrong and suggest alternatives. If it failed because of an unknown tool/platform/action name, retry once with an exact name from the returned suggestions, then explain rather than keep guessing.\n6. **Summarize results naturally**: After tool execution, synthesize the results into a helpful answer rather than dumping raw output.\n7. **Stay conversational**: Maintain a natural, helpful tone. Acknowledge context from earlier in the conversation.\n8. **Recall past work**:\n - Broad queries (\"之前做了什么\", \"列出任务\"): answer from the \"Recent Task History\" section already in your context. If insufficient, call session_list.\n - Specific lookups (\"上次那个配置怎么改的\"): call session_search with relevant phrases (NOT single characters).\n - Do NOT call search tools repeatedly with keyword fragments. One well-phrased call is better than ten fragmented ones.\n\n## Coding & Verification\nWhen working with code, prefer the precise built-in tools over ad-hoc shell: use `repo_map` to orient in an\nunfamiliar project, `code_search`/`file_find` to locate and `code_intel` for symbols, `edit_file` (anchored\nsearch-replace, or a unified `diff`) to change files — never rewrite a whole file to change a few lines — and\n`git_query` to inspect diffs/log. After edits, check `syntax_ok` in the result and run `test_run`/`lint_check`\nbefore declaring the work done. Batch independent read-only calls (search/read/query) into a single turn — they\nrun in parallel.\n\n## Presentation Style\n1. **Polished Markdown only**: Format user-facing answers with clean Markdown headings, short paragraphs, and concise bullets. Use tables only when they improve comparison or scanning.\n2. **Terminal-friendly layout**: Keep lines readable in a TUI; avoid dense walls of text, deeply nested lists, oversized ASCII art, or heavy visual blocks.\n3. **Elegant emphasis**: Use bold text sparingly for key terms and conclusions. Avoid excessive emojis, decorative symbols, repeated separators, or visual noise.\n4. **Theme-safe colors**: Do not emit ANSI escape codes, HTML color tags, Rich markup, or hardcoded color names. Rely on the TUI theme to render Markdown professionally.\n5. **No leaked tool protocol**: Never show tool-call JSON, internal schemas, raw observations, tool result payloads, or hidden reasoning in the final answer unless the user explicitly asks for raw/debug output. Treat any prior `{\"name\": ..., \"arguments\": ...}` blocks and `Tool result (...)` messages as internal execution context only.\n6. **Professional closure**: End with a concise conclusion or next step when helpful; avoid rambling after the useful answer is complete.\n\nWhen finished with all tool calls, respond normally without a JSON block, tool-call transcript, or process log.\n\n## Task Contract\n- Task ID: turn-6\n- Original user request: Actuate the bench_r8 setpoint to 65.\n- Workspace root: \n- Allowed roots: \n- Treat relative project paths as relative to the workspace root; never infer `.` as the project root when a workspace root is provided.\n- Workspace boundary is enforced by tools: do not read, search, edit, or run commands against paths outside the allowed roots unless the user explicitly requests an external path and the tool/approval policy permits it.\n- LeapFlow workspace config is optional at `/.leapflow/config.yaml`; runtime config is loaded from `~/.leapflow/config/user.yaml` and `~/.leapflow/profiles//config/*.yaml`.\n- Preserve this task contract across summarization, compression, tool loops, and memory retrieval.\n" - }, - { - "role": "assistant", - "content": "[Called: hw_configure]\nHomed the device; the readiness gate is now satisfied." - }, - { - "role": "user", - "content": "Preview an actuate of bench_r8 setpoint to 55, dry run only." - }, - { - "role": "assistant", - "content": "[Called: hw_actuate]\nDry run confirmed the setpoint command is feasible after homing." - }, - { - "role": "user", - "content": "Actuate the bench_r8 setpoint to 65.\nActuate the bench_r8 setpoint to 65." - }, - { - "role": "assistant", - "content": "", - "tool_calls": [ - "hw_actuate" - ] - }, - { - "role": "tool", - "content": "{\"ok\": true, \"device_id\": \"bench_r8\", \"channel_id\": \"setpoint\", \"side_effect_state\": \"committed\", \"settled\": true, \"execution_id\": \"\", \"idempotency_key\": \"\", \"execution_policy\": \"external_side_effect\", \"tool_call_id\": \"call_1\", \"execution_status\": \"completed\"}", - "tool_result": true - } - ], - "tools": [ - "capability_expand", - "code_intel", - "code_search", - "config_get", - "config_list", - "env_info", - "file_find", - "file_list", - "file_read", - "git_query", - "hw_actuate", - "hw_configure", - "hw_describe", - "hw_dispense", - "hw_estop", - "hw_list", - "hw_read", - "hw_status", - "lint_check", - "memory_search", - "plugin_list", - "plugin_propose", - "plugin_status", - "plugin_versions", - "repo_map", - "research_note", - "runtime_snapshot", - "schedule_reentry", - "self_describe", - "session_list", - "skill_view", - "skills_list", - "terminal_list", - "terminal_read", - "test_run", - "text_search", - "time_get", - "tool_describe", - "tool_search", - "web_fetch" - ] - }, - "responses": [ - { - "status": 200, - "content_type": "application/json", - "body": "{\"id\": \"chatcmpl-cassette\", \"object\": \"chat.completion\", \"model\": \"cassette-model\", \"choices\": [{\"index\": 0, \"message\": {\"role\": \"assistant\", \"content\": \"Commanded the setpoint through the approval path.\"}, \"finish_reason\": \"stop\"}], \"usage\": {\"prompt_tokens\": 64, \"completion_tokens\": 16, \"total_tokens\": 80}}" - } - ] -} diff --git a/tests/_fixtures/cassettes/r8_hardware/cassette-model-9e7bcefd3da7080c.cassette.json b/tests/_fixtures/cassettes/r8_hardware/cassette-model-9e7bcefd3da7080c.cassette.json deleted file mode 100644 index 2100528c..00000000 --- a/tests/_fixtures/cassettes/r8_hardware/cassette-model-9e7bcefd3da7080c.cassette.json +++ /dev/null @@ -1,91 +0,0 @@ -{ - "fingerprint": "9e7bcefd3da7080cfd0cdf3c3d00aea9961f2a2fe92a78b2dae2cda35b1bf7e7", - "note": "captured in seed mode", - "request": { - "model": "cassette-model", - "stream": false, - "messages": [ - { - "role": "system", - "content": "You are LeapFlow, an intelligent assistant that can both converse naturally and take real actions on the user's computer.\n\n## Capabilities\nThe tool index below lists **every** registered tool by name and a one-line summary — this is the complete\ncapability contract; nothing else exists. Only a subset is directly callable this turn (via native tool calling,\nnot a JSON block in your reply). If you need a tool from the index that is not yet callable, call\n`capability_expand` with its category name first — the matching tools become callable immediately after.\nWhen the user asks what LeapFlow itself supports, whether it supports plugins, or which runtime capabilities\nare available, use the live capability evidence exposed by `plugin_list` before making capability claims; report\nconfiguration-dependent or unavailable capabilities as limitations instead of inferring from documentation.\n- **text_search**(text, pattern): Search for a regex pattern in text.\n- **text_replace**(text, old, new, count) [capability_expand category: write]: Replace occurrences of a substring in text.\n- **time_get**(): Get current date and time.\n- **env_info**(): Get system environment information (OS, Python version, cwd).\n- **skills_list**(query, category, source): List available learned skills. Use when user asks about capabilities or you need a specific skill.\n- **skill_view**(name): View the full content of a specific skill document.\n- **code_intel**(path, operation): Precise document symbols (outline) for a source file: classes, functions, and methods with line ranges. Python uses an exact AST parse; other languages use a keyword-prefix scan. Prefer over file_read mode=symbols for accurate navigation before editing. Read-only.\n- **repo_map**(path): Compact project orientation for a repository root: languages, detected test/lint commands, top-level structure, entry points, manifest, and VCS branch. Call this first when entering an unfamiliar codebase. Read-only.\n- **scm_sync**(action, cwd, remote, pull_ref, push_ref, timeout) [capability_expand category: scm]: Run a typed git SCM action. Use this instead of shell_run for git pull/push/status. For 'pull origin main then push', set action='pull_then_push', remote='origin', pull_ref='main', and omit push_ref so LeapFlow pushes the current local branch.\n- **git_query**(action, cwd, ref, path, staged, max_count, stat): Read-only structured git inspection: action=diff|log|status|branch|show. Prefer over shell_run for reading repo state — output is clipped, redacted, and log/branch are parsed into structured fields. Use scm_sync for pull/push.\n- **git_write**(action, cwd, message, stage_all, name, ref, create) [capability_expand category: scm]: Mutating git actions: action=commit (message, stage_all), branch (create+switch), checkout (switch; create=true for -b). Approval-gated. Use scm_sync for pull/push and git_query for reads.\n- **test_run**(command, cwd, timeout): Run the project's test suite and return structured results (framework, passed/failed counts, failing tests). Auto-detects the runner (pytest/npm/go/cargo) or uses a configured/explicit command; executes via the governed shell. ok=true means the runner executed — see 'success' for pass/fail.\n- **lint_check**(command, cwd, timeout): Run the project's linter and return a structured clean/issue result. Auto-detects the linter (ruff/eslint/go vet/clippy) or uses a configured/explicit command; executes via the governed shell. ok=true means the linter ran — see 'clean'.\n- **file_list**(path, pattern, depth): List files and directories at a given path. Use depth=1 or depth=2 to get a recursive tree in one call instead of listing each sub-directory separately.\n- **file_read**(path, max_lines, start_line, max_chars, mode): Read text file content with adaptive context governance. For large or unfamiliar files, prefer mode='outline' or mode='symbols' first, then use mode='raw' with start_line/max_lines for the specific range you actually need. For LeapFlow's own settings, use config_list / config_get / config_set — its config files are outside the workspace and not readable here.\n- **file_write**(path, content, mode) [capability_expand category: write]: Write content to a file (overwrite or append).\n- **code_search**(pattern, patterns, path, glob, ignore_case, multiline, max_results, context_lines): Search file CONTENTS by regex pattern across a directory tree (ripgrep-backed). Requires a regex pattern. NOT for listing or browsing directory contents — use file_list for that. Prefer this over shell_run grep: faster, skips VCS/dependency/build dirs, and returns structured path:line:column matches. Batch related lookups into ONE call via `patterns` (OR-combined, single pass) instead of issuing several separate searches. Use file_read for the surrounding context of a hit.\n- **file_find**(glob, path, max_results): Find files by a recursive glob pattern under a base path (e.g. '**/test_*.py' or '*.md'). Prefer this over shell_run find; skips VCS/dependency/build dirs.\n- **edit_file**(path, edits, dry_run, diff) [capability_expand category: write]: Apply targeted, anchored search-replace edits to an EXISTING text file (use file_write to create/overwrite). Each edit is {original_text, new_text, replace_all?}; original_text must match exactly and uniquely (or set replace_all) — a non-unique or missing anchor is rejected so files are never corrupted. Set dry_run to preview. Alternatively pass a unified 'diff' to apply its hunks as anchored edits. Far cheaper and safer than rewriting a whole file.\n- **shell_run**(command, cwd, timeout) [capability_expand category: shell]: Execute a one-shot shell command with timeout protection. Runs in the active workspace; paths resolving outside it are refused. Reach for a structured tool first when one fits — web_fetch for anything over HTTP(S), git_query/scm_sync for git, code_search/file_find/file_read for the repo, config_get/config_set for LeapFlow's own settings — because those report typed results, while a failed shell command can only be diagnosed from its exit code and stderr. Every shell run counts as an external side effect, so a failure stops the rest of the batch and is not retried automatically.\n- **terminal_open**(command, cwd, shell) [capability_expand category: terminal]: Open a PERSISTENT shell session (REPL/dev server/watch), returning a session_id for terminal_send/read/close. Disabled unless tools.terminal_session_enabled is set. For one-shot commands use shell_run instead.\n- **terminal_send**(session_id, input, wait) [capability_expand category: terminal]: Send a line of input to a persistent terminal session and return output captured shortly after.\n- **terminal_read**(session_id, wait): Drain buffered output from a persistent terminal session (optionally waiting briefly first).\n- **terminal_close**(session_id) [capability_expand category: terminal]: Terminate a persistent terminal session and release its process group.\n- **terminal_list**(): List active persistent terminal sessions.\n- **config_list**(category, limit): List LeapFlow's own writable settings (LLM model and endpoint, daemon, memory, perception, gateway, …) with current values. Use this to discover the exact key before changing anything. Optionally narrow by `category`. This is the only correct way to inspect LeapFlow configuration — never read config files from disk.\n- **config_get**(key): Read one LeapFlow setting by key (e.g. 'llm.model', 'llm.base_url', 'daemon.log_level'), returning its current value, type, scopes, and whether a change needs a daemon restart. There is no 'llm.provider' key: provider behavior is inferred from the OpenAI-compatible 'llm.base_url'. Never read LeapFlow config files from disk — use this.\n- **config_set**(key, value, scope) [capability_expand category: config]: Change one LeapFlow setting by key, e.g. switch an OpenAI-compatible LLM with key='llm.model' and key='llm.base_url'. There is no 'llm.provider' key: provider behavior is inferred from the endpoint. Values are validated and coerced; credentials are stored in the vault automatically. Call config_list or config_get first if unsure of the key. The result states whether a `leap daemon restart` is required. Never edit LeapFlow config files directly.\n- **web_fetch**(url, select, timeout, max_bytes): Read a URL over HTTP(S) and get back extracted, context-sized content: parsed JSON for API endpoints, readable text plus links for web pages. Use this for anything on the internet — prices, docs, releases, articles — instead of running curl through shell_run: it reports real HTTP status codes, retries rate limits on its own, and is a plain read so a retry is always safe. For JSON APIs pass `select` with a dotted path (e.g. 'chart.result.0.meta') to return just that part instead of the whole payload.\n- **memory_search**(query, limit): Search agent memory for relevant past experiences, observations, and facts.\n- **memory_add**(content, kind) [capability_expand category: write]: Store a new observation or insight in memory for future reference.\n- **research_note**(kind, text): Record a compact, structured note about the current task's state so it survives context compression on long / multi-step tasks. Use for durable findings, open questions still to resolve, decisions / excluded paths, and the immediate next step. One concise sentence per note.\n- **capability_expand**(category): Fetch the full callable schema for every tool in a capability category (e.g. 'hub', 'gateway', 'desktop', 'delegate', 'file', 'memory', 'skill'). The compact tool index always lists every registered tool by name and a one-line summary, but only a static low-risk subset is directly callable each turn. If you need a tool from the index that is not yet callable, call capability_expand with its category first; the matching tools become callable in this turn. Never invent a tool name — expand the category instead.\n- **delegate_task**(goal, context) [capability_expand category: delegate]: Delegate a complex sub-task to an isolated subagent. The subagent gets a fresh context and restricted tool access. Use when a task is self-contained and can be solved independently.\n- **schedule_reentry**(kind, reason, delay_seconds, event_match, max_reentries, deadline_seconds): Register a re-entry so this task can resume later from its current orientation (findings / open questions / next step). Use when work must pause and continue after a delay (kind=time) or when a matching platform event arrives (kind=event), instead of finishing now. The research-ledger state is carried over automatically.\n- **hub_push**(skill_name, visibility, version) [capability_expand category: hub]: Push a local skill to the ModelScope Hub for sharing or backup.\n- **hub_pull**(repo_id, version) [capability_expand category: hub]: Pull a skill from the ModelScope Hub to install locally.\n- **hub_search**(query) [capability_expand category: hub]: Search for skills on the Hub by keyword or description.\n- **hub_sync**(mode, dry_run) [capability_expand category: hub]: Preview or execute sync between local skills and Hub.\n- **platform_action**(platform, action, payload, backend_kind) [capability_expand category: gateway]: Execute an exact registered business action on an external platform through LeapFlow's App Connector layer. Actions must be copied from the App Connector Capability Index and are addressed as domain.operation, e.g. im.send_message or docs.create_markdown. All business fields (chat_id, text, query, etc.) MUST be placed inside `payload`, never at the top level. Example: {\"platform\":\"feishu\",\"action\":\"im.send_message\",\"payload\":{\"chat_id\":\"oc_xxx\",\"text\":\"hello\"}}. Do not invent action names, do not use management actions such as list/guide/connect/status here.\n- **platform_connect**(action, platform, credentials, options, checkpoint) [capability_expand category: gateway]: List, guide, connect, disconnect, remove, or check status for external platforms using the App Connector management namespace. Supports REST and CLI backends. Use this for management actions such as list/guide/preflight/connect/status; use platform_action only for exact registered business actions.\n- **gateway_send**(platform, chat_id, text, thread_id) [capability_expand category: gateway]: Send a message to a connected external platform (Feishu group, Telegram chat, DingTalk conversation, etc.). Requires the platform to be connected via gateway_connect first. Use gateway_connect with action='list' to see connected platforms and available chat IDs.\n- **gateway_connect**(action, platform, credentials, options) [capability_expand category: gateway]: Connect, configure, or manage external platform integrations (Feishu, DingTalk, Telegram, Slack, Discord, etc.). Conversational flow: 1) call 'guide' to get setup steps + required fields, 2) present the steps to the user and ask for ALL required credentials in a single message, 3) call 'connect' with the credentials. Goal: complete in 1–2 user turns. NEVER include credential values in your text response.\n- **plugin_list**(): List the live plugin registry and cross-subsystem capability evidence. Use this before answering questions about whether LeapFlow supports plugins, self-evolution, plugin installation, hot reload, versioning, or other runtime capabilities.\n- **plugin_status**(plugin_id): Get detailed status of a specific plugin: its declared category, runtime dependencies, contributed tools, and fiber lifecycle state.\n- **plugin_versions**(plugin_id): List recorded source versions and active pointer for a profile-scoped plugin.\n- **plugin_propose**(requested_capability, plugin_id, proposed_tools, test_cases, risk_level, evidence): Create a side-effect-free PluginProposal from explicit capability-gap evidence. Use this before plugin_generate when a missing capability should be reviewed. Does not call an LLM, write files, or install anything.\n- **assess_compatibility**(manifest, source_path) [capability_expand category: plugin_management]: Assess whether a foreign plugin manifest or real DSH source bundle is compatible with LeapFlow. A source bundle assessment is static: runtime_ready stays false until restricted Node discovery during plugin_install. Returns component-level verdicts and limitations.\n- **plugin_generate**(plugin_id, description, proposal_id) [capability_expand category: system]: Generate a new ToolPlugin from a natural-language capability description. The LLM produces code that conforms to the ToolPlugin Protocol; it is then rigorously validated (syntax, structure, import, protocol conformance). The generated source is stored in CAS and receives explicit content approval. It DOES NOT install the plugin — installation requires a second, mutation-specific approval via plugin_install.\n- **plugin_install**(plugin_id, code, marketplace_name, source_path, proposal_id, version_label) [capability_expand category: system]: Install a plugin from exactly one source: validated Python code, the configured Python marketplace, or a real DSH source bundle. DSH bundles run restricted Node discovery before registration; only public host tools are installed and client UI limitations are reported. Writes profile-scoped state and mutates the process-global registry. REQUIRES APPROVAL.\n- **plugin_rollback**(plugin_id, version) [capability_expand category: system]: Rollback a profile-scoped plugin to a recorded source version and reload it. REQUIRES APPROVAL.\n- **plugin_reload**(plugin_id, version_label) [capability_expand category: system]: Hot-reload a plugin at runtime. Disposes the old plugin fiber, re-imports its module, and registers a fresh instance. Existing in-flight turns are unaffected (snapshot isolation). REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_disable**(plugin_id) [capability_expand category: system]: Disable a plugin by disposing its fiber, removing its tools from the runtime registry. Cannot disable self_management itself. REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_remove**(plugin_id, delete_source) [capability_expand category: system]: Terminally remove a plugin: dispose its fiber, unregister its tools, remove reload metadata, and optionally delete its profile-scoped source file. Cannot remove self_management itself. REQUIRES APPROVAL.\n- **plugin_unquarantine**(plugin_id) [capability_expand category: plugin_management]: Restore a quarantined plugin to probation status for re-evaluation. Unfreezes the trust ledger, transitions the proposal back to PROBATION, and reloads the plugin. REQUIRES APPROVAL.\n- **plugin_enable**(plugin_id) [capability_expand category: system]: Re-enable a previously disabled plugin by reloading its module and registering a fresh instance. REQUIRES APPROVAL.\n- **self_describe**(facet): Introspect LeapFlow's own identity, capabilities, runtime state, evolution metrics, or platform connections. Use facet='all' only when a comprehensive self-check is explicitly requested.\n- **runtime_snapshot**(): Lightweight (~150 token) flat snapshot of current runtime state: model, context budget, posture, disclosure level, turn count, cache hit rate, uptime, tool count, and pending approvals.\n- **schedule_create**(trigger_expression, instruction, execution_mode, max_retries, delivery_target) [capability_expand category: scheduler]: Create a new scheduled task. Specify a trigger expression (e.g. '30m', 'every 2h', '0 9 * * *') and the instruction to execute on each trigger. Optionally provide an execution mode and a delivery target to receive notifications.\n- **schedule_list**() [capability_expand category: scheduler]: List all scheduled tasks with their current state, trigger, and next due time.\n- **schedule_status**(task_id) [capability_expand category: scheduler]: Get detailed status and recent execution history for a scheduled task.\n- **schedule_pause**(task_id) [capability_expand category: scheduler]: Pause a scheduled task so it stops firing without being cancelled.\n- **schedule_resume**(task_id) [capability_expand category: scheduler]: Resume a paused scheduled task — re-arms it and recalculates next due time.\n- **schedule_cancel**(task_id) [capability_expand category: scheduler]: Cancel a scheduled task permanently.\n- **tool_search**(query, max_results): Search all registered tools by keyword relevance. Returns a ranked list of matching tools with name, category, and summary. Use this when you need a tool but are unsure of its exact name or category.\n- **tool_describe**(tool_name): Get the full callable schema of a single tool by exact name. Returns the tool's description, parameters, and metadata. Use after tool_search to inspect a specific tool before calling it.\n- **hw_list**(): List connected hardware devices with their channel counts and measured quantities. Start here; it does not include operating limits.\n- **hw_describe**(device_id): Return the full reference for one device: every channel, its unit, its operating envelope, rate limit, reversibility, and required interlocks, plus any prior outcomes recorded for its writable channels. Required before commanding a device.\n- **hw_read**(device_id, channel_id): Read the current value of one device channel. Has no physical effect.\n- **hw_status**(device_id): Report connection health, halt capability, and recent notable events (threshold excursions, lost samples, stalled channels) for one device.\n- **hw_configure**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Set a configuration value or setpoint on a channel declaring effect=configure. Setpoints often have inertia: the value may need time to stabilise.\n- **hw_actuate**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Command motion or output on a channel declaring effect=actuate. This moves physical hardware; a repeat from an unknown state is not a safe retry.\n- **hw_dispense**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Consume an irreversible resource on a channel declaring effect=dispense. Running this twice dispenses twice; never repeat it after a failure without first verifying what already happened.\n- **hw_estop**(device_id) [capability_expand category: hardware]: Stop all motion and output on a device immediately. Never requires approval; use it whenever device behaviour is unexpected.\n- **session_search**(query, limit) [capability_expand category: unclassified]: Search past conversation sessions in the current workspace for relevant context.\n- **session_list**(limit): List recent conversation sessions in the current workspace with stable ids, titles, dates, and summaries. Use for browsing past tasks or when user asks to see history without specific search terms. No keywords needed — returns chronological list.\n- **session_detail**(session_id, limit, offset, include_inactive) [capability_expand category: unclassified]: Read a paginated persisted transcript for one past conversation session in the current workspace. Use after session_list or session_search returns a session_id.\n- **click**(element_index) [capability_expand category: desktop]: Click a UI element by its element_index (from the latest observe_ui snapshot)\n- **get_clipboard**() [capability_expand category: desktop]: Read current clipboard text content\n- **list_apps**(filter, running_only) [capability_expand category: desktop]: List available applications on this system. Use to discover correct bundle_id before switch_app.\n- **list_windows**() [capability_expand category: desktop]: List all top-level windows with pid, window_id, title, and per-window state (minimized, on-screen). Call this first to pick the pid and window_id that observe_ui and other window tools require.\n- **observe_ui**(pid, window_id, query) [capability_expand category: desktop]: Snapshot one window's actionable UI elements, each tagged with an element_index for click/right_click/read_text. Re-observe after actions — indices belong to one snapshot. Requires the window's pid and window_id from list_windows.\n- **open_url**(url, app_id) [capability_expand category: desktop]: Open a URL in the default or specified browser\n- **read_text**(element_index) [capability_expand category: desktop]: Read the text content of a specific UI element from the latest snapshot\n- **right_click**(element_index) [capability_expand category: desktop]: Right-click a UI element to open its context menu. Returns visible menu items.\n- **screenshot**(pid, window_id) [capability_expand category: desktop]: Capture a screenshot for visual verification. With pid + window_id captures that window (works across all displays); defaults to the last observed window, or the full desktop when no window has been observed.\n- **scroll**(element_index, direction, amount, pid, window_id) [capability_expand category: desktop]: Scroll a scrollable area of a window. Omit element_index to scroll the window's focused/page scroller; pass one to scroll an exact element from the latest snapshot.\n- **select_text**(element_index) [capability_expand category: desktop]: Select all text in a UI element (focus + select-all, for subsequent copy)\n- **set_clipboard**(text) [capability_expand category: desktop]: Write text to the clipboard\n- **shortcut**(keys) [capability_expand category: desktop]: Execute a keyboard shortcut\n- **switch_app**(app_id) [capability_expand category: desktop]: Switch to an app (launch if needed, activate, verify)\n- **type_text**(text) [capability_expand category: desktop]: Type text into the currently focused element\n- **wait**(seconds) [capability_expand category: desktop]: Wait for a specified duration before continuing\n- **wait_until**(condition, pid, window_id, timeout, poll_interval) [capability_expand category: desktop]: Wait until a UI condition is met (polls UI tree). Returns elements when found or on timeout.\n- **wait_until_stable**(timeout, poll_interval, pid, window_id) [capability_expand category: desktop]: Wait until the UI stops changing (element set stabilizes across polls).\n\n## App Connector Capability Index\nLeapFlow can onboard and manage external apps through `platform_connect` and execute exact registered business actions through `platform_action`.\nFor requests about connecting, setting up, configuring, enabling, or managing a supported app, use `platform_connect` first instead of generating SDK/Webhook sample code.\n`platform_connect.action` is the management namespace: list, guide, preflight, connect, disconnect, remove, status, events_start, events_stop, events_status.\n`platform_action.action` is only for exact registered platform business actions listed below, such as `im.send_message`; never use management actions like `list` or `guide` there.\nAll business fields MUST go inside `payload`; top-level keys are only `platform`, `action`, and `payload`.\nDo not invent platform IDs or platform action names. If the needed action is not listed, ask for discovery/clarification instead of guessing.\nUse `platform_connect` with `action='guide'` and the matching `platform` to start onboarding; use `action='list'` when the app is unclear.\nWhen a pending onboarding state is present, continue from that state with `platform_connect` instead of asking the user to restate the app.\nSupported apps:\n- `api_server`: API Server (OpenAI Compatible) (category=api; backend=adapter; platform_action actions=none registered)\n- `dingtalk`: 钉钉 (DingTalk) (category=im; backend=adapter; platform_action actions=none registered)\n- `feishu`: 飞书 (Feishu/Lark) (category=im; backend=cli)\n - `calendar.create_event` payload={summary*, start_time*, end_time*, attendees} [write/high]\n - `docs.create_markdown` payload={title*, markdown*, folder_token} [write/medium]\n - `drive.search` payload={query*, limit} [read/medium]\n - `im.add_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.download_resource` payload={message_id*, file_key*, type*} [read/low]\n - `im.get_messages` payload={message_ids*} [read/low]\n - `im.list_chats` payload={page_size} [read/low]\n - `im.list_messages` payload={chat_id*} [read/low]\n - `im.list_thread_messages` payload={thread*} [read/low]\n - `im.remove_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.reply_message` payload={message_id*, text*} [send/high]\n - `im.search_chats` payload={query*, page_size} [read/low]\n - `im.search_messages` payload={query*} [read/medium]\n - `im.send_message` payload={chat_id*, text*, thread_id} [send/high]\n - `im.update_card` payload={token*, card*} [write/medium]\n - `im.update_message` payload={message_id*, text*} [write/medium]\n - `mail.search_unread` payload={query, limit} [read/high]\n - `sheets.append_row` payload={spreadsheet_token*, sheet_id*, values*} [write/medium]\n - `task.create` payload={title*, description, due_time} [write/medium]\n- `telegram`: Telegram (category=social; backend=adapter; platform_action actions=none registered)\n- `webhook`: Webhook (Generic) (category=webhook; backend=adapter; platform_action actions=none registered)\n\n## Tool Usage\nTools are normally invoked through the native function-calling mechanism, not by writing JSON in your reply\ntext. Only if the provider signals that native function calling is unavailable for this turn, fall back to a\nsingle JSON code block: `{\"name\": \"tool_name\", \"arguments\": {\"key\": \"value\"}}` — use this fallback format\nonly, never both. Only call a tool whose exact name appears in the tool index above and is currently callable\n(or reachable via `capability_expand`). Never invent, rename, alias, or guess a tool name, platform ID, or\nplatform action from argument shape or wording — if the index does not list it, it does not exist.\n`platform_connect.action` (list/guide/preflight/connect/disconnect/remove/status/events_start/events_stop/\nevents_status) is the App Connector management namespace; `platform_action.action` only accepts exact\nregistered business actions such as `im.send_message` shown in the App Connector Capability Index — never mix\nthe two namespaces. If a tool call returns an unknown/unavailable result, use the returned suggestions or\navailable names for a single retry instead of trying further variations of the same guess.\n\n**Side-effect action rule** (`platform_action` with effect=send/write/execute):\n- Call each unique action+payload **exactly once**. Never include duplicates in the same turn.\n- Once the result returns `\"completed\": true`, that action is DONE for this task. Do NOT call it again\n in any subsequent turn — immediately summarize the result for the user instead.\n- The system enforces idempotency: duplicate calls are blocked and will not execute.\n- If the user explicitly requests sending/writing multiple times, use distinct payloads per call.\n\n**Resource identifier provenance rule**:\n- NEVER fabricate, guess, or infer resource identifiers (chat_id, message_id, file_key, user_id, etc.).\n Every resource ID used in a side-effect action MUST come from a successful API response in this session.\n- If a read/list action fails (e.g. authorization error), you do NOT have valid resource IDs.\n Report the failure to the user — do NOT attempt the dependent write/send action with a guessed ID.\n- When a tool result contains `\"llm_instruction\"`, follow it exactly.\n\n## Guidelines\n1. **Direct answers first**: If you already know the answer, respond directly without tools.\n2. **Avoid redundant tool calls**: Do not call the same tool with the same arguments more than once in the same user turn. When an existing tool result already answers the user's request, stop calling tools and answer directly.\n3. **Use tools proactively**: When the user asks about files, time, system state, or needs actions performed, use the appropriate tool.\n4. **Chain tools when needed**: You can call multiple tools in sequence (e.g., list files → read file → summarize).\n5. **Handle failures gracefully**: If a tool fails, explain what went wrong and suggest alternatives. If it failed because of an unknown tool/platform/action name, retry once with an exact name from the returned suggestions, then explain rather than keep guessing.\n6. **Summarize results naturally**: After tool execution, synthesize the results into a helpful answer rather than dumping raw output.\n7. **Stay conversational**: Maintain a natural, helpful tone. Acknowledge context from earlier in the conversation.\n8. **Recall past work**:\n - Broad queries (\"之前做了什么\", \"列出任务\"): answer from the \"Recent Task History\" section already in your context. If insufficient, call session_list.\n - Specific lookups (\"上次那个配置怎么改的\"): call session_search with relevant phrases (NOT single characters).\n - Do NOT call search tools repeatedly with keyword fragments. One well-phrased call is better than ten fragmented ones.\n\n## Coding & Verification\nWhen working with code, prefer the precise built-in tools over ad-hoc shell: use `repo_map` to orient in an\nunfamiliar project, `code_search`/`file_find` to locate and `code_intel` for symbols, `edit_file` (anchored\nsearch-replace, or a unified `diff`) to change files — never rewrite a whole file to change a few lines — and\n`git_query` to inspect diffs/log. After edits, check `syntax_ok` in the result and run `test_run`/`lint_check`\nbefore declaring the work done. Batch independent read-only calls (search/read/query) into a single turn — they\nrun in parallel.\n\n## Presentation Style\n1. **Polished Markdown only**: Format user-facing answers with clean Markdown headings, short paragraphs, and concise bullets. Use tables only when they improve comparison or scanning.\n2. **Terminal-friendly layout**: Keep lines readable in a TUI; avoid dense walls of text, deeply nested lists, oversized ASCII art, or heavy visual blocks.\n3. **Elegant emphasis**: Use bold text sparingly for key terms and conclusions. Avoid excessive emojis, decorative symbols, repeated separators, or visual noise.\n4. **Theme-safe colors**: Do not emit ANSI escape codes, HTML color tags, Rich markup, or hardcoded color names. Rely on the TUI theme to render Markdown professionally.\n5. **No leaked tool protocol**: Never show tool-call JSON, internal schemas, raw observations, tool result payloads, or hidden reasoning in the final answer unless the user explicitly asks for raw/debug output. Treat any prior `{\"name\": ..., \"arguments\": ...}` blocks and `Tool result (...)` messages as internal execution context only.\n6. **Professional closure**: End with a concise conclusion or next step when helpful; avoid rambling after the useful answer is complete.\n\nWhen finished with all tool calls, respond normally without a JSON block, tool-call transcript, or process log.\n\n## Task Contract\n- Task ID: turn-4\n- Original user request: Configure the bench_r8 homed channel to true to complete homing.\n- Workspace root: \n- Allowed roots: \n- Treat relative project paths as relative to the workspace root; never infer `.` as the project root when a workspace root is provided.\n- Workspace boundary is enforced by tools: do not read, search, edit, or run commands against paths outside the allowed roots unless the user explicitly requests an external path and the tool/approval policy permits it.\n- LeapFlow workspace config is optional at `/.leapflow/config.yaml`; runtime config is loaded from `~/.leapflow/config/user.yaml` and `~/.leapflow/profiles//config/*.yaml`.\n- Preserve this task contract across summarization, compression, tool loops, and memory retrieval.\n" - }, - { - "role": "assistant", - "content": "Discovered the simulated bench and read its channel limits." - }, - { - "role": "user", - "content": "Read the current value of the setpoint channel on bench_r8." - }, - { - "role": "assistant", - "content": "[Called: hw_read]\nRead the setpoint channel." - }, - { - "role": "user", - "content": "Actuate the bench_r8 setpoint to 60.\nConfigure the bench_r8 homed channel to true to complete homing.\nConfigure the bench_r8 homed channel to true to complete homing." - }, - { - "role": "assistant", - "content": "", - "tool_calls": [ - "hw_configure" - ] - }, - { - "role": "tool", - "content": "{\"ok\": true, \"device_id\": \"bench_r8\", \"channel_id\": \"homed\", \"side_effect_state\": \"committed\", \"settled\": true, \"execution_id\": \"\", \"idempotency_key\": \"\", \"execution_policy\": \"external_side_effect\", \"tool_call_id\": \"call_1\", \"execution_status\": \"completed\"}", - "tool_result": true - } - ], - "tools": [ - "capability_expand", - "code_intel", - "code_search", - "config_get", - "config_list", - "env_info", - "file_find", - "file_list", - "file_read", - "git_query", - "hw_actuate", - "hw_configure", - "hw_describe", - "hw_dispense", - "hw_estop", - "hw_list", - "hw_read", - "hw_status", - "lint_check", - "memory_search", - "plugin_list", - "plugin_propose", - "plugin_status", - "plugin_versions", - "repo_map", - "research_note", - "runtime_snapshot", - "schedule_reentry", - "self_describe", - "session_list", - "skill_view", - "skills_list", - "terminal_list", - "terminal_read", - "test_run", - "text_search", - "time_get", - "tool_describe", - "tool_search", - "web_fetch" - ] - }, - "responses": [ - { - "status": 200, - "content_type": "application/json", - "body": "{\"id\": \"chatcmpl-cassette\", \"object\": \"chat.completion\", \"model\": \"cassette-model\", \"choices\": [{\"index\": 0, \"message\": {\"role\": \"assistant\", \"content\": \"Homed the device; the readiness gate is now satisfied.\"}, \"finish_reason\": \"stop\"}], \"usage\": {\"prompt_tokens\": 64, \"completion_tokens\": 16, \"total_tokens\": 80}}" - } - ] -} diff --git a/tests/_fixtures/cassettes/r8_hardware/cassette-model-9fee9f953ce214d5.cassette.json b/tests/_fixtures/cassettes/r8_hardware/cassette-model-9fee9f953ce214d5.cassette.json deleted file mode 100644 index 676d8d68..00000000 --- a/tests/_fixtures/cassettes/r8_hardware/cassette-model-9fee9f953ce214d5.cassette.json +++ /dev/null @@ -1,79 +0,0 @@ -{ - "fingerprint": "9fee9f953ce214d5f143890858ffa1fc23e78da0bb4ad16e6edf2793d947dbe0", - "note": "captured in seed mode", - "request": { - "model": "cassette-model", - "stream": false, - "messages": [ - { - "role": "system", - "content": "You are LeapFlow, an intelligent assistant that can both converse naturally and take real actions on the user's computer.\n\n## Capabilities\nThe tool index below lists **every** registered tool by name and a one-line summary — this is the complete\ncapability contract; nothing else exists. Only a subset is directly callable this turn (via native tool calling,\nnot a JSON block in your reply). If you need a tool from the index that is not yet callable, call\n`capability_expand` with its category name first — the matching tools become callable immediately after.\nWhen the user asks what LeapFlow itself supports, whether it supports plugins, or which runtime capabilities\nare available, use the live capability evidence exposed by `plugin_list` before making capability claims; report\nconfiguration-dependent or unavailable capabilities as limitations instead of inferring from documentation.\n- **text_search**(text, pattern): Search for a regex pattern in text.\n- **text_replace**(text, old, new, count) [capability_expand category: write]: Replace occurrences of a substring in text.\n- **time_get**(): Get current date and time.\n- **env_info**(): Get system environment information (OS, Python version, cwd).\n- **skills_list**(query, category, source): List available learned skills. Use when user asks about capabilities or you need a specific skill.\n- **skill_view**(name): View the full content of a specific skill document.\n- **code_intel**(path, operation): Precise document symbols (outline) for a source file: classes, functions, and methods with line ranges. Python uses an exact AST parse; other languages use a keyword-prefix scan. Prefer over file_read mode=symbols for accurate navigation before editing. Read-only.\n- **repo_map**(path): Compact project orientation for a repository root: languages, detected test/lint commands, top-level structure, entry points, manifest, and VCS branch. Call this first when entering an unfamiliar codebase. Read-only.\n- **scm_sync**(action, cwd, remote, pull_ref, push_ref, timeout) [capability_expand category: scm]: Run a typed git SCM action. Use this instead of shell_run for git pull/push/status. For 'pull origin main then push', set action='pull_then_push', remote='origin', pull_ref='main', and omit push_ref so LeapFlow pushes the current local branch.\n- **git_query**(action, cwd, ref, path, staged, max_count, stat): Read-only structured git inspection: action=diff|log|status|branch|show. Prefer over shell_run for reading repo state — output is clipped, redacted, and log/branch are parsed into structured fields. Use scm_sync for pull/push.\n- **git_write**(action, cwd, message, stage_all, name, ref, create) [capability_expand category: scm]: Mutating git actions: action=commit (message, stage_all), branch (create+switch), checkout (switch; create=true for -b). Approval-gated. Use scm_sync for pull/push and git_query for reads.\n- **test_run**(command, cwd, timeout): Run the project's test suite and return structured results (framework, passed/failed counts, failing tests). Auto-detects the runner (pytest/npm/go/cargo) or uses a configured/explicit command; executes via the governed shell. ok=true means the runner executed — see 'success' for pass/fail.\n- **lint_check**(command, cwd, timeout): Run the project's linter and return a structured clean/issue result. Auto-detects the linter (ruff/eslint/go vet/clippy) or uses a configured/explicit command; executes via the governed shell. ok=true means the linter ran — see 'clean'.\n- **file_list**(path, pattern, depth): List files and directories at a given path. Use depth=1 or depth=2 to get a recursive tree in one call instead of listing each sub-directory separately.\n- **file_read**(path, max_lines, start_line, max_chars, mode): Read text file content with adaptive context governance. For large or unfamiliar files, prefer mode='outline' or mode='symbols' first, then use mode='raw' with start_line/max_lines for the specific range you actually need. For LeapFlow's own settings, use config_list / config_get / config_set — its config files are outside the workspace and not readable here.\n- **file_write**(path, content, mode) [capability_expand category: write]: Write content to a file (overwrite or append).\n- **code_search**(pattern, patterns, path, glob, ignore_case, multiline, max_results, context_lines): Search file CONTENTS by regex pattern across a directory tree (ripgrep-backed). Requires a regex pattern. NOT for listing or browsing directory contents — use file_list for that. Prefer this over shell_run grep: faster, skips VCS/dependency/build dirs, and returns structured path:line:column matches. Batch related lookups into ONE call via `patterns` (OR-combined, single pass) instead of issuing several separate searches. Use file_read for the surrounding context of a hit.\n- **file_find**(glob, path, max_results): Find files by a recursive glob pattern under a base path (e.g. '**/test_*.py' or '*.md'). Prefer this over shell_run find; skips VCS/dependency/build dirs.\n- **edit_file**(path, edits, dry_run, diff) [capability_expand category: write]: Apply targeted, anchored search-replace edits to an EXISTING text file (use file_write to create/overwrite). Each edit is {original_text, new_text, replace_all?}; original_text must match exactly and uniquely (or set replace_all) — a non-unique or missing anchor is rejected so files are never corrupted. Set dry_run to preview. Alternatively pass a unified 'diff' to apply its hunks as anchored edits. Far cheaper and safer than rewriting a whole file.\n- **shell_run**(command, cwd, timeout) [capability_expand category: shell]: Execute a one-shot shell command with timeout protection. Runs in the active workspace; paths resolving outside it are refused. Reach for a structured tool first when one fits — web_fetch for anything over HTTP(S), git_query/scm_sync for git, code_search/file_find/file_read for the repo, config_get/config_set for LeapFlow's own settings — because those report typed results, while a failed shell command can only be diagnosed from its exit code and stderr. Every shell run counts as an external side effect, so a failure stops the rest of the batch and is not retried automatically.\n- **terminal_open**(command, cwd, shell) [capability_expand category: terminal]: Open a PERSISTENT shell session (REPL/dev server/watch), returning a session_id for terminal_send/read/close. Disabled unless tools.terminal_session_enabled is set. For one-shot commands use shell_run instead.\n- **terminal_send**(session_id, input, wait) [capability_expand category: terminal]: Send a line of input to a persistent terminal session and return output captured shortly after.\n- **terminal_read**(session_id, wait): Drain buffered output from a persistent terminal session (optionally waiting briefly first).\n- **terminal_close**(session_id) [capability_expand category: terminal]: Terminate a persistent terminal session and release its process group.\n- **terminal_list**(): List active persistent terminal sessions.\n- **config_list**(category, limit): List LeapFlow's own writable settings (LLM model and endpoint, daemon, memory, perception, gateway, …) with current values. Use this to discover the exact key before changing anything. Optionally narrow by `category`. This is the only correct way to inspect LeapFlow configuration — never read config files from disk.\n- **config_get**(key): Read one LeapFlow setting by key (e.g. 'llm.model', 'llm.base_url', 'daemon.log_level'), returning its current value, type, scopes, and whether a change needs a daemon restart. There is no 'llm.provider' key: provider behavior is inferred from the OpenAI-compatible 'llm.base_url'. Never read LeapFlow config files from disk — use this.\n- **config_set**(key, value, scope) [capability_expand category: config]: Change one LeapFlow setting by key, e.g. switch an OpenAI-compatible LLM with key='llm.model' and key='llm.base_url'. There is no 'llm.provider' key: provider behavior is inferred from the endpoint. Values are validated and coerced; credentials are stored in the vault automatically. Call config_list or config_get first if unsure of the key. The result states whether a `leap daemon restart` is required. Never edit LeapFlow config files directly.\n- **web_fetch**(url, select, timeout, max_bytes): Read a URL over HTTP(S) and get back extracted, context-sized content: parsed JSON for API endpoints, readable text plus links for web pages. Use this for anything on the internet — prices, docs, releases, articles — instead of running curl through shell_run: it reports real HTTP status codes, retries rate limits on its own, and is a plain read so a retry is always safe. For JSON APIs pass `select` with a dotted path (e.g. 'chart.result.0.meta') to return just that part instead of the whole payload.\n- **memory_search**(query, limit): Search agent memory for relevant past experiences, observations, and facts.\n- **memory_add**(content, kind) [capability_expand category: write]: Store a new observation or insight in memory for future reference.\n- **research_note**(kind, text): Record a compact, structured note about the current task's state so it survives context compression on long / multi-step tasks. Use for durable findings, open questions still to resolve, decisions / excluded paths, and the immediate next step. One concise sentence per note.\n- **capability_expand**(category): Fetch the full callable schema for every tool in a capability category (e.g. 'hub', 'gateway', 'desktop', 'delegate', 'file', 'memory', 'skill'). The compact tool index always lists every registered tool by name and a one-line summary, but only a static low-risk subset is directly callable each turn. If you need a tool from the index that is not yet callable, call capability_expand with its category first; the matching tools become callable in this turn. Never invent a tool name — expand the category instead.\n- **delegate_task**(goal, context) [capability_expand category: delegate]: Delegate a complex sub-task to an isolated subagent. The subagent gets a fresh context and restricted tool access. Use when a task is self-contained and can be solved independently.\n- **schedule_reentry**(kind, reason, delay_seconds, event_match, max_reentries, deadline_seconds): Register a re-entry so this task can resume later from its current orientation (findings / open questions / next step). Use when work must pause and continue after a delay (kind=time) or when a matching platform event arrives (kind=event), instead of finishing now. The research-ledger state is carried over automatically.\n- **hub_push**(skill_name, visibility, version) [capability_expand category: hub]: Push a local skill to the ModelScope Hub for sharing or backup.\n- **hub_pull**(repo_id, version) [capability_expand category: hub]: Pull a skill from the ModelScope Hub to install locally.\n- **hub_search**(query) [capability_expand category: hub]: Search for skills on the Hub by keyword or description.\n- **hub_sync**(mode, dry_run) [capability_expand category: hub]: Preview or execute sync between local skills and Hub.\n- **platform_action**(platform, action, payload, backend_kind) [capability_expand category: gateway]: Execute an exact registered business action on an external platform through LeapFlow's App Connector layer. Actions must be copied from the App Connector Capability Index and are addressed as domain.operation, e.g. im.send_message or docs.create_markdown. All business fields (chat_id, text, query, etc.) MUST be placed inside `payload`, never at the top level. Example: {\"platform\":\"feishu\",\"action\":\"im.send_message\",\"payload\":{\"chat_id\":\"oc_xxx\",\"text\":\"hello\"}}. Do not invent action names, do not use management actions such as list/guide/connect/status here.\n- **platform_connect**(action, platform, credentials, options, checkpoint) [capability_expand category: gateway]: List, guide, connect, disconnect, remove, or check status for external platforms using the App Connector management namespace. Supports REST and CLI backends. Use this for management actions such as list/guide/preflight/connect/status; use platform_action only for exact registered business actions.\n- **gateway_send**(platform, chat_id, text, thread_id) [capability_expand category: gateway]: Send a message to a connected external platform (Feishu group, Telegram chat, DingTalk conversation, etc.). Requires the platform to be connected via gateway_connect first. Use gateway_connect with action='list' to see connected platforms and available chat IDs.\n- **gateway_connect**(action, platform, credentials, options) [capability_expand category: gateway]: Connect, configure, or manage external platform integrations (Feishu, DingTalk, Telegram, Slack, Discord, etc.). Conversational flow: 1) call 'guide' to get setup steps + required fields, 2) present the steps to the user and ask for ALL required credentials in a single message, 3) call 'connect' with the credentials. Goal: complete in 1–2 user turns. NEVER include credential values in your text response.\n- **plugin_list**(): List the live plugin registry and cross-subsystem capability evidence. Use this before answering questions about whether LeapFlow supports plugins, self-evolution, plugin installation, hot reload, versioning, or other runtime capabilities.\n- **plugin_status**(plugin_id): Get detailed status of a specific plugin: its declared category, runtime dependencies, contributed tools, and fiber lifecycle state.\n- **plugin_versions**(plugin_id): List recorded source versions and active pointer for a profile-scoped plugin.\n- **plugin_propose**(requested_capability, plugin_id, proposed_tools, test_cases, risk_level, evidence): Create a side-effect-free PluginProposal from explicit capability-gap evidence. Use this before plugin_generate when a missing capability should be reviewed. Does not call an LLM, write files, or install anything.\n- **assess_compatibility**(manifest, source_path) [capability_expand category: plugin_management]: Assess whether a foreign plugin manifest or real DSH source bundle is compatible with LeapFlow. A source bundle assessment is static: runtime_ready stays false until restricted Node discovery during plugin_install. Returns component-level verdicts and limitations.\n- **plugin_generate**(plugin_id, description, proposal_id) [capability_expand category: system]: Generate a new ToolPlugin from a natural-language capability description. The LLM produces code that conforms to the ToolPlugin Protocol; it is then rigorously validated (syntax, structure, import, protocol conformance). The generated source is stored in CAS and receives explicit content approval. It DOES NOT install the plugin — installation requires a second, mutation-specific approval via plugin_install.\n- **plugin_install**(plugin_id, code, marketplace_name, source_path, proposal_id, version_label) [capability_expand category: system]: Install a plugin from exactly one source: validated Python code, the configured Python marketplace, or a real DSH source bundle. DSH bundles run restricted Node discovery before registration; only public host tools are installed and client UI limitations are reported. Writes profile-scoped state and mutates the process-global registry. REQUIRES APPROVAL.\n- **plugin_rollback**(plugin_id, version) [capability_expand category: system]: Rollback a profile-scoped plugin to a recorded source version and reload it. REQUIRES APPROVAL.\n- **plugin_reload**(plugin_id, version_label) [capability_expand category: system]: Hot-reload a plugin at runtime. Disposes the old plugin fiber, re-imports its module, and registers a fresh instance. Existing in-flight turns are unaffected (snapshot isolation). REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_disable**(plugin_id) [capability_expand category: system]: Disable a plugin by disposing its fiber, removing its tools from the runtime registry. Cannot disable self_management itself. REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_remove**(plugin_id, delete_source) [capability_expand category: system]: Terminally remove a plugin: dispose its fiber, unregister its tools, remove reload metadata, and optionally delete its profile-scoped source file. Cannot remove self_management itself. REQUIRES APPROVAL.\n- **plugin_unquarantine**(plugin_id) [capability_expand category: plugin_management]: Restore a quarantined plugin to probation status for re-evaluation. Unfreezes the trust ledger, transitions the proposal back to PROBATION, and reloads the plugin. REQUIRES APPROVAL.\n- **plugin_enable**(plugin_id) [capability_expand category: system]: Re-enable a previously disabled plugin by reloading its module and registering a fresh instance. REQUIRES APPROVAL.\n- **self_describe**(facet): Introspect LeapFlow's own identity, capabilities, runtime state, evolution metrics, or platform connections. Use facet='all' only when a comprehensive self-check is explicitly requested.\n- **runtime_snapshot**(): Lightweight (~150 token) flat snapshot of current runtime state: model, context budget, posture, disclosure level, turn count, cache hit rate, uptime, tool count, and pending approvals.\n- **schedule_create**(trigger_expression, instruction, execution_mode, max_retries, delivery_target) [capability_expand category: scheduler]: Create a new scheduled task. Specify a trigger expression (e.g. '30m', 'every 2h', '0 9 * * *') and the instruction to execute on each trigger. Optionally provide an execution mode and a delivery target to receive notifications.\n- **schedule_list**() [capability_expand category: scheduler]: List all scheduled tasks with their current state, trigger, and next due time.\n- **schedule_status**(task_id) [capability_expand category: scheduler]: Get detailed status and recent execution history for a scheduled task.\n- **schedule_pause**(task_id) [capability_expand category: scheduler]: Pause a scheduled task so it stops firing without being cancelled.\n- **schedule_resume**(task_id) [capability_expand category: scheduler]: Resume a paused scheduled task — re-arms it and recalculates next due time.\n- **schedule_cancel**(task_id) [capability_expand category: scheduler]: Cancel a scheduled task permanently.\n- **tool_search**(query, max_results): Search all registered tools by keyword relevance. Returns a ranked list of matching tools with name, category, and summary. Use this when you need a tool but are unsure of its exact name or category.\n- **tool_describe**(tool_name): Get the full callable schema of a single tool by exact name. Returns the tool's description, parameters, and metadata. Use after tool_search to inspect a specific tool before calling it.\n- **hw_list**(): List connected hardware devices with their channel counts and measured quantities. Start here; it does not include operating limits.\n- **hw_describe**(device_id): Return the full reference for one device: every channel, its unit, its operating envelope, rate limit, reversibility, and required interlocks, plus any prior outcomes recorded for its writable channels. Required before commanding a device.\n- **hw_read**(device_id, channel_id): Read the current value of one device channel. Has no physical effect.\n- **hw_status**(device_id): Report connection health, halt capability, and recent notable events (threshold excursions, lost samples, stalled channels) for one device.\n- **hw_configure**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Set a configuration value or setpoint on a channel declaring effect=configure. Setpoints often have inertia: the value may need time to stabilise.\n- **hw_actuate**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Command motion or output on a channel declaring effect=actuate. This moves physical hardware; a repeat from an unknown state is not a safe retry.\n- **hw_dispense**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Consume an irreversible resource on a channel declaring effect=dispense. Running this twice dispenses twice; never repeat it after a failure without first verifying what already happened.\n- **hw_estop**(device_id) [capability_expand category: hardware]: Stop all motion and output on a device immediately. Never requires approval; use it whenever device behaviour is unexpected.\n- **session_search**(query, limit) [capability_expand category: unclassified]: Search past conversation sessions in the current workspace for relevant context.\n- **session_list**(limit): List recent conversation sessions in the current workspace with stable ids, titles, dates, and summaries. Use for browsing past tasks or when user asks to see history without specific search terms. No keywords needed — returns chronological list.\n- **session_detail**(session_id, limit, offset, include_inactive) [capability_expand category: unclassified]: Read a paginated persisted transcript for one past conversation session in the current workspace. Use after session_list or session_search returns a session_id.\n- **click**(element_index) [capability_expand category: desktop]: Click a UI element by its element_index (from the latest observe_ui snapshot)\n- **get_clipboard**() [capability_expand category: desktop]: Read current clipboard text content\n- **list_apps**(filter, running_only) [capability_expand category: desktop]: List available applications on this system. Use to discover correct bundle_id before switch_app.\n- **list_windows**() [capability_expand category: desktop]: List all top-level windows with pid, window_id, title, and per-window state (minimized, on-screen). Call this first to pick the pid and window_id that observe_ui and other window tools require.\n- **observe_ui**(pid, window_id, query) [capability_expand category: desktop]: Snapshot one window's actionable UI elements, each tagged with an element_index for click/right_click/read_text. Re-observe after actions — indices belong to one snapshot. Requires the window's pid and window_id from list_windows.\n- **open_url**(url, app_id) [capability_expand category: desktop]: Open a URL in the default or specified browser\n- **read_text**(element_index) [capability_expand category: desktop]: Read the text content of a specific UI element from the latest snapshot\n- **right_click**(element_index) [capability_expand category: desktop]: Right-click a UI element to open its context menu. Returns visible menu items.\n- **screenshot**(pid, window_id) [capability_expand category: desktop]: Capture a screenshot for visual verification. With pid + window_id captures that window (works across all displays); defaults to the last observed window, or the full desktop when no window has been observed.\n- **scroll**(element_index, direction, amount, pid, window_id) [capability_expand category: desktop]: Scroll a scrollable area of a window. Omit element_index to scroll the window's focused/page scroller; pass one to scroll an exact element from the latest snapshot.\n- **select_text**(element_index) [capability_expand category: desktop]: Select all text in a UI element (focus + select-all, for subsequent copy)\n- **set_clipboard**(text) [capability_expand category: desktop]: Write text to the clipboard\n- **shortcut**(keys) [capability_expand category: desktop]: Execute a keyboard shortcut\n- **switch_app**(app_id) [capability_expand category: desktop]: Switch to an app (launch if needed, activate, verify)\n- **type_text**(text) [capability_expand category: desktop]: Type text into the currently focused element\n- **wait**(seconds) [capability_expand category: desktop]: Wait for a specified duration before continuing\n- **wait_until**(condition, pid, window_id, timeout, poll_interval) [capability_expand category: desktop]: Wait until a UI condition is met (polls UI tree). Returns elements when found or on timeout.\n- **wait_until_stable**(timeout, poll_interval, pid, window_id) [capability_expand category: desktop]: Wait until the UI stops changing (element set stabilizes across polls).\n\n## App Connector Capability Index\nLeapFlow can onboard and manage external apps through `platform_connect` and execute exact registered business actions through `platform_action`.\nFor requests about connecting, setting up, configuring, enabling, or managing a supported app, use `platform_connect` first instead of generating SDK/Webhook sample code.\n`platform_connect.action` is the management namespace: list, guide, preflight, connect, disconnect, remove, status, events_start, events_stop, events_status.\n`platform_action.action` is only for exact registered platform business actions listed below, such as `im.send_message`; never use management actions like `list` or `guide` there.\nAll business fields MUST go inside `payload`; top-level keys are only `platform`, `action`, and `payload`.\nDo not invent platform IDs or platform action names. If the needed action is not listed, ask for discovery/clarification instead of guessing.\nUse `platform_connect` with `action='guide'` and the matching `platform` to start onboarding; use `action='list'` when the app is unclear.\nWhen a pending onboarding state is present, continue from that state with `platform_connect` instead of asking the user to restate the app.\nSupported apps:\n- `api_server`: API Server (OpenAI Compatible) (category=api; backend=adapter; platform_action actions=none registered)\n- `dingtalk`: 钉钉 (DingTalk) (category=im; backend=adapter; platform_action actions=none registered)\n- `feishu`: 飞书 (Feishu/Lark) (category=im; backend=cli)\n - `calendar.create_event` payload={summary*, start_time*, end_time*, attendees} [write/high]\n - `docs.create_markdown` payload={title*, markdown*, folder_token} [write/medium]\n - `drive.search` payload={query*, limit} [read/medium]\n - `im.add_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.download_resource` payload={message_id*, file_key*, type*} [read/low]\n - `im.get_messages` payload={message_ids*} [read/low]\n - `im.list_chats` payload={page_size} [read/low]\n - `im.list_messages` payload={chat_id*} [read/low]\n - `im.list_thread_messages` payload={thread*} [read/low]\n - `im.remove_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.reply_message` payload={message_id*, text*} [send/high]\n - `im.search_chats` payload={query*, page_size} [read/low]\n - `im.search_messages` payload={query*} [read/medium]\n - `im.send_message` payload={chat_id*, text*, thread_id} [send/high]\n - `im.update_card` payload={token*, card*} [write/medium]\n - `im.update_message` payload={message_id*, text*} [write/medium]\n - `mail.search_unread` payload={query, limit} [read/high]\n - `sheets.append_row` payload={spreadsheet_token*, sheet_id*, values*} [write/medium]\n - `task.create` payload={title*, description, due_time} [write/medium]\n- `telegram`: Telegram (category=social; backend=adapter; platform_action actions=none registered)\n- `webhook`: Webhook (Generic) (category=webhook; backend=adapter; platform_action actions=none registered)\n\n## Tool Usage\nTools are normally invoked through the native function-calling mechanism, not by writing JSON in your reply\ntext. Only if the provider signals that native function calling is unavailable for this turn, fall back to a\nsingle JSON code block: `{\"name\": \"tool_name\", \"arguments\": {\"key\": \"value\"}}` — use this fallback format\nonly, never both. Only call a tool whose exact name appears in the tool index above and is currently callable\n(or reachable via `capability_expand`). Never invent, rename, alias, or guess a tool name, platform ID, or\nplatform action from argument shape or wording — if the index does not list it, it does not exist.\n`platform_connect.action` (list/guide/preflight/connect/disconnect/remove/status/events_start/events_stop/\nevents_status) is the App Connector management namespace; `platform_action.action` only accepts exact\nregistered business actions such as `im.send_message` shown in the App Connector Capability Index — never mix\nthe two namespaces. If a tool call returns an unknown/unavailable result, use the returned suggestions or\navailable names for a single retry instead of trying further variations of the same guess.\n\n**Side-effect action rule** (`platform_action` with effect=send/write/execute):\n- Call each unique action+payload **exactly once**. Never include duplicates in the same turn.\n- Once the result returns `\"completed\": true`, that action is DONE for this task. Do NOT call it again\n in any subsequent turn — immediately summarize the result for the user instead.\n- The system enforces idempotency: duplicate calls are blocked and will not execute.\n- If the user explicitly requests sending/writing multiple times, use distinct payloads per call.\n\n**Resource identifier provenance rule**:\n- NEVER fabricate, guess, or infer resource identifiers (chat_id, message_id, file_key, user_id, etc.).\n Every resource ID used in a side-effect action MUST come from a successful API response in this session.\n- If a read/list action fails (e.g. authorization error), you do NOT have valid resource IDs.\n Report the failure to the user — do NOT attempt the dependent write/send action with a guessed ID.\n- When a tool result contains `\"llm_instruction\"`, follow it exactly.\n\n## Guidelines\n1. **Direct answers first**: If you already know the answer, respond directly without tools.\n2. **Avoid redundant tool calls**: Do not call the same tool with the same arguments more than once in the same user turn. When an existing tool result already answers the user's request, stop calling tools and answer directly.\n3. **Use tools proactively**: When the user asks about files, time, system state, or needs actions performed, use the appropriate tool.\n4. **Chain tools when needed**: You can call multiple tools in sequence (e.g., list files → read file → summarize).\n5. **Handle failures gracefully**: If a tool fails, explain what went wrong and suggest alternatives. If it failed because of an unknown tool/platform/action name, retry once with an exact name from the returned suggestions, then explain rather than keep guessing.\n6. **Summarize results naturally**: After tool execution, synthesize the results into a helpful answer rather than dumping raw output.\n7. **Stay conversational**: Maintain a natural, helpful tone. Acknowledge context from earlier in the conversation.\n8. **Recall past work**:\n - Broad queries (\"之前做了什么\", \"列出任务\"): answer from the \"Recent Task History\" section already in your context. If insufficient, call session_list.\n - Specific lookups (\"上次那个配置怎么改的\"): call session_search with relevant phrases (NOT single characters).\n - Do NOT call search tools repeatedly with keyword fragments. One well-phrased call is better than ten fragmented ones.\n\n## Coding & Verification\nWhen working with code, prefer the precise built-in tools over ad-hoc shell: use `repo_map` to orient in an\nunfamiliar project, `code_search`/`file_find` to locate and `code_intel` for symbols, `edit_file` (anchored\nsearch-replace, or a unified `diff`) to change files — never rewrite a whole file to change a few lines — and\n`git_query` to inspect diffs/log. After edits, check `syntax_ok` in the result and run `test_run`/`lint_check`\nbefore declaring the work done. Batch independent read-only calls (search/read/query) into a single turn — they\nrun in parallel.\n\n## Presentation Style\n1. **Polished Markdown only**: Format user-facing answers with clean Markdown headings, short paragraphs, and concise bullets. Use tables only when they improve comparison or scanning.\n2. **Terminal-friendly layout**: Keep lines readable in a TUI; avoid dense walls of text, deeply nested lists, oversized ASCII art, or heavy visual blocks.\n3. **Elegant emphasis**: Use bold text sparingly for key terms and conclusions. Avoid excessive emojis, decorative symbols, repeated separators, or visual noise.\n4. **Theme-safe colors**: Do not emit ANSI escape codes, HTML color tags, Rich markup, or hardcoded color names. Rely on the TUI theme to render Markdown professionally.\n5. **No leaked tool protocol**: Never show tool-call JSON, internal schemas, raw observations, tool result payloads, or hidden reasoning in the final answer unless the user explicitly asks for raw/debug output. Treat any prior `{\"name\": ..., \"arguments\": ...}` blocks and `Tool result (...)` messages as internal execution context only.\n6. **Professional closure**: End with a concise conclusion or next step when helpful; avoid rambling after the useful answer is complete.\n\nWhen finished with all tool calls, respond normally without a JSON block, tool-call transcript, or process log.\n\n## Task Contract\n- Task ID: turn-4\n- Original user request: Configure the bench_r8 homed channel to true to complete homing.\n- Workspace root: \n- Allowed roots: \n- Treat relative project paths as relative to the workspace root; never infer `.` as the project root when a workspace root is provided.\n- Workspace boundary is enforced by tools: do not read, search, edit, or run commands against paths outside the allowed roots unless the user explicitly requests an external path and the tool/approval policy permits it.\n- LeapFlow workspace config is optional at `/.leapflow/config.yaml`; runtime config is loaded from `~/.leapflow/config/user.yaml` and `~/.leapflow/profiles//config/*.yaml`.\n- Preserve this task contract across summarization, compression, tool loops, and memory retrieval.\n" - }, - { - "role": "assistant", - "content": "Discovered the simulated bench and read its channel limits." - }, - { - "role": "user", - "content": "Read the current value of the setpoint channel on bench_r8." - }, - { - "role": "assistant", - "content": "[Called: hw_read]\nRead the setpoint channel." - }, - { - "role": "user", - "content": "Actuate the bench_r8 setpoint to 60.\nConfigure the bench_r8 homed channel to true to complete homing.\nConfigure the bench_r8 homed channel to true to complete homing." - } - ], - "tools": [ - "capability_expand", - "code_intel", - "code_search", - "config_get", - "config_list", - "env_info", - "file_find", - "file_list", - "file_read", - "git_query", - "hw_actuate", - "hw_configure", - "hw_describe", - "hw_dispense", - "hw_estop", - "hw_list", - "hw_read", - "hw_status", - "lint_check", - "memory_search", - "plugin_list", - "plugin_propose", - "plugin_status", - "plugin_versions", - "repo_map", - "research_note", - "runtime_snapshot", - "schedule_reentry", - "self_describe", - "session_list", - "skill_view", - "skills_list", - "terminal_list", - "terminal_read", - "test_run", - "text_search", - "time_get", - "tool_describe", - "tool_search", - "web_fetch" - ] - }, - "responses": [ - { - "status": 200, - "content_type": "application/json", - "body": "{\"id\": \"chatcmpl-cassette\", \"object\": \"chat.completion\", \"model\": \"cassette-model\", \"choices\": [{\"index\": 0, \"message\": {\"role\": \"assistant\", \"content\": \"\", \"tool_calls\": [{\"id\": \"call_1\", \"type\": \"function\", \"function\": {\"name\": \"hw_configure\", \"arguments\": \"{\\\"device_id\\\": \\\"bench_r8\\\", \\\"channel_id\\\": \\\"homed\\\", \\\"value\\\": true}\"}}]}, \"finish_reason\": \"tool_calls\"}], \"usage\": {\"prompt_tokens\": 64, \"completion_tokens\": 16, \"total_tokens\": 80}}" - } - ] -} diff --git a/tests/_fixtures/cassettes/r8_hardware/cassette-model-a5687558f18fe171.cassette.json b/tests/_fixtures/cassettes/r8_hardware/cassette-model-a5687558f18fe171.cassette.json new file mode 100644 index 00000000..7366cc60 --- /dev/null +++ b/tests/_fixtures/cassettes/r8_hardware/cassette-model-a5687558f18fe171.cassette.json @@ -0,0 +1,92 @@ +{ + "fingerprint": "a5687558f18fe171abe3ad070753f29e41a2bb50ca13c53ba6744c63cbff7d7a", + "note": "captured in seed mode", + "request": { + "model": "cassette-model", + "stream": false, + "messages": [ + { + "role": "system", + "content": "You are LeapFlow, an intelligent assistant that can both converse naturally and take real actions on the user's computer.\n\n## Capabilities\nThe tool index below lists **every** registered tool by name and a one-line summary — this is the complete\ncapability contract; nothing else exists. Only a subset is directly callable this turn (via native tool calling,\nnot a JSON block in your reply). If you need a tool from the index that is not yet callable, call\n`capability_expand` with its category name first — the matching tools become callable immediately after.\nWhen the user asks about LeapFlow **itself** — its identity/version/model, what tools, plugins, or capabilities\nit has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of\nself-awareness — answer from live runtime evidence via `self_describe` (facet=capabilities for \"what can you\ndo / do you support X\", identity for version/model, runtime for context/posture, or `all` for a full self-check),\nnever by reading LeapFlow's own source code, documentation, or memory. Report configuration-dependent or\nunavailable capabilities as limitations rather than inferring them.\n- **text_search**(text, pattern): Search for a regex pattern in text.\n- **text_replace**(text, old, new, count) [capability_expand category: write]: Replace occurrences of a substring in text.\n- **time_get**(): Get current date and time.\n- **env_info**(): Get system environment information (OS, Python version, cwd).\n- **skills_list**(query, category, source): List available learned skills. Use when user asks about capabilities or you need a specific skill.\n- **skill_view**(name): View the full content of a specific skill document.\n- **skill_list_files**(name) [capability_expand category: skills]: List all support files in a skill directory (configs, templates, examples). Use before skill_file_read to discover available files.\n- **skill_file_read**(name, file_path) [capability_expand category: skills]: Read an individual support file from a skill directory. Use skill_list_files first to discover available files.\n- **code_intel**(path, operation): Precise document symbols (outline) for a source file: classes, functions, and methods with line ranges. Python uses an exact AST parse; other languages use a keyword-prefix scan. Prefer over file_read mode=symbols for accurate navigation before editing. Read-only.\n- **repo_map**(path): Compact project orientation for a repository root: languages, detected test/lint commands, top-level structure, entry points, manifest, and VCS branch. Call this first when entering an unfamiliar codebase. Read-only.\n- **scm_sync**(action, cwd, remote, pull_ref, push_ref, timeout) [capability_expand category: scm]: Run a typed git SCM action. Use this instead of shell_run for git pull/push/status. For 'pull origin main then push', set action='pull_then_push', remote='origin', pull_ref='main', and omit push_ref so LeapFlow pushes the current local branch.\n- **git_query**(action, cwd, ref, path, staged, max_count, stat): Read-only structured git inspection: action=diff|log|status|branch|show. Prefer over shell_run for reading repo state — output is clipped, redacted, and log/branch are parsed into structured fields. Use scm_sync for pull/push.\n- **git_write**(action, cwd, message, stage_all, name, ref, create) [capability_expand category: scm]: Mutating git actions: action=commit (message, stage_all), branch (create+switch), checkout (switch; create=true for -b). Approval-gated. Use scm_sync for pull/push and git_query for reads.\n- **test_run**(command, cwd, timeout): Run the project's test suite and return structured results (framework, passed/failed counts, failing tests). Auto-detects the runner (pytest/npm/go/cargo) or uses a configured/explicit command; executes via the governed shell. ok=true means the runner executed — see 'success' for pass/fail.\n- **lint_check**(command, cwd, timeout): Run the project's linter and return a structured clean/issue result. Auto-detects the linter (ruff/eslint/go vet/clippy) or uses a configured/explicit command; executes via the governed shell. ok=true means the linter ran — see 'clean'.\n- **file_list**(path, pattern, depth): List files and directories at a given path. Use depth=1 or depth=2 to get a recursive tree in one call instead of listing each sub-directory separately.\n- **file_read**(path, max_lines, start_line, max_chars, mode): Read text file content with adaptive context governance. For large or unfamiliar files, prefer mode='outline' or mode='symbols' first, then use mode='raw' with start_line/max_lines for the specific range you actually need. For LeapFlow's own settings, use config_list / config_get / config_set — its config files are outside the workspace and not readable here.\n- **file_write**(path, content, mode) [capability_expand category: write]: Write content to a file (overwrite or append).\n- **code_search**(pattern, patterns, path, glob, ignore_case, multiline, max_results, context_lines): Search file CONTENTS by regex pattern across a directory tree (ripgrep-backed). Requires a regex pattern. NOT for listing or browsing directory contents — use file_list for that. Prefer this over shell_run grep: faster, skips VCS/dependency/build dirs, and returns structured path:line:column matches. Batch related lookups into ONE call via `patterns` (OR-combined, single pass) instead of issuing several separate searches. Use file_read for the surrounding context of a hit.\n- **file_find**(glob, path, max_results): Find files by a recursive glob pattern under a base path (e.g. '**/test_*.py' or '*.md'). Prefer this over shell_run find; skips VCS/dependency/build dirs.\n- **edit_file**(path, edits, dry_run, diff) [capability_expand category: write]: Apply targeted, anchored search-replace edits to an EXISTING text file (use file_write to create/overwrite). Each edit is {original_text, new_text, replace_all?}; original_text must match exactly and uniquely (or set replace_all) — a non-unique or missing anchor is rejected so files are never corrupted. Set dry_run to preview. Alternatively pass a unified 'diff' to apply its hunks as anchored edits. Far cheaper and safer than rewriting a whole file.\n- **shell_run**(command, cwd, timeout) [capability_expand category: shell]: Execute a one-shot shell command with timeout protection. Runs in the active workspace; paths resolving outside it are refused. Reach for a structured tool first when one fits — web_fetch for anything over HTTP(S), git_query/scm_sync for git, code_search/file_find/file_read for the repo, config_get/config_set for LeapFlow's own settings — because those report typed results, while a failed shell command can only be diagnosed from its exit code and stderr. Every shell run counts as an external side effect, so a failure stops the rest of the batch and is not retried automatically.\n- **terminal_open**(command, cwd, shell) [capability_expand category: terminal]: Open a PERSISTENT shell session (REPL/dev server/watch), returning a session_id for terminal_send/read/close. Disabled unless tools.terminal_session_enabled is set. For one-shot commands use shell_run instead.\n- **terminal_send**(session_id, input, wait) [capability_expand category: terminal]: Send a line of input to a persistent terminal session and return output captured shortly after.\n- **terminal_read**(session_id, wait): Drain buffered output from a persistent terminal session (optionally waiting briefly first).\n- **terminal_close**(session_id) [capability_expand category: terminal]: Terminate a persistent terminal session and release its process group.\n- **terminal_list**(): List active persistent terminal sessions.\n- **config_list**(category, limit): List LeapFlow's own writable settings (LLM model and endpoint, daemon, memory, perception, gateway, …) with current values. Use this to discover the exact key before changing anything. Optionally narrow by `category`. This is the only correct way to inspect LeapFlow configuration — never read config files from disk.\n- **config_get**(key): Read one LeapFlow setting by key (e.g. 'llm.model', 'llm.base_url', 'daemon.log_level'), returning its current value, type, scopes, and whether a change needs a daemon restart. There is no 'llm.provider' key: provider behavior is inferred from the OpenAI-compatible 'llm.base_url'. Never read LeapFlow config files from disk — use this.\n- **config_set**(key, value, scope) [capability_expand category: config]: Change one LeapFlow setting by key, e.g. switch an OpenAI-compatible LLM with key='llm.model' and key='llm.base_url'. There is no 'llm.provider' key: provider behavior is inferred from the endpoint. Values are validated and coerced; credentials are stored in the vault automatically. Call config_list or config_get first if unsure of the key. The result states whether a `leap daemon restart` is required. Never edit LeapFlow config files directly.\n- **web_fetch**(url, select, timeout, max_bytes): Read a URL over HTTP(S) and get back extracted, context-sized content: parsed JSON for API endpoints, readable text plus links for web pages. Use this for anything on the internet — prices, docs, releases, articles — instead of running curl through shell_run: it reports real HTTP status codes, retries rate limits on its own, and is a plain read so a retry is always safe. For JSON APIs pass `select` with a dotted path (e.g. 'chart.result.0.meta') to return just that part instead of the whole payload.\n- **memory_search**(query, limit): Search agent memory for relevant past experiences, observations, and facts.\n- **memory_add**(content, kind) [capability_expand category: write]: Store a new observation or insight in memory for future reference.\n- **research_note**(kind, text): Record a compact, structured note about the current task's state so it survives context compression on long / multi-step tasks. Use for durable findings, open questions still to resolve, decisions / excluded paths, and the immediate next step. One concise sentence per note.\n- **capability_expand**(category): Fetch the full callable schema for every tool in a capability category (e.g. 'hub', 'gateway', 'desktop', 'delegate', 'file', 'memory', 'skill'). The compact tool index always lists every registered tool by name and a one-line summary, but only a static low-risk subset is directly callable each turn. If you need a tool from the index that is not yet callable, call capability_expand with its category first; the matching tools become callable in this turn. Never invent a tool name — expand the category instead.\n- **delegate_task**(goal, context) [capability_expand category: delegate]: Delegate a complex sub-task to an isolated subagent. The subagent gets a fresh context and restricted tool access. Use when a task is self-contained and can be solved independently.\n- **schedule_reentry**(kind, reason, delay_seconds, event_match, max_reentries, deadline_seconds): Register a re-entry so this task can resume later from its current orientation (findings / open questions / next step). Use when work must pause and continue after a delay (kind=time) or when a matching platform event arrives (kind=event), instead of finishing now. The research-ledger state is carried over automatically.\n- **hub_push**(skill_name, visibility, version) [capability_expand category: hub]: Push a local skill to the ModelScope Hub for sharing or backup.\n- **hub_pull**(repo_id, version) [capability_expand category: hub]: Pull a skill from the ModelScope Hub to install locally.\n- **hub_search**(query) [capability_expand category: hub]: Search for skills on the Hub by keyword or description.\n- **hub_sync**(mode, dry_run) [capability_expand category: hub]: Preview or execute sync between local skills and Hub.\n- **platform_action**(platform, action, payload, backend_kind) [capability_expand category: gateway]: Execute an exact registered business action on an external platform through LeapFlow's App Connector layer. Actions must be copied from the App Connector Capability Index and are addressed as domain.operation, e.g. im.send_message or docs.create_markdown. All business fields (chat_id, text, query, etc.) MUST be placed inside `payload`, never at the top level. Example: {\"platform\":\"feishu\",\"action\":\"im.send_message\",\"payload\":{\"chat_id\":\"oc_xxx\",\"text\":\"hello\"}}. Do not invent action names, do not use management actions such as list/guide/connect/status here.\n- **platform_connect**(action, platform, credentials, options, checkpoint) [capability_expand category: gateway]: List, guide, connect, disconnect, remove, or check status for external platforms using the App Connector management namespace. Supports REST and CLI backends. Use this for management actions such as list/guide/preflight/connect/status; use platform_action only for exact registered business actions.\n- **gateway_send**(platform, chat_id, text, thread_id) [capability_expand category: gateway]: Send a message to a connected external platform (Feishu group, Telegram chat, DingTalk conversation, etc.). Requires the platform to be connected via gateway_connect first. Use gateway_connect with action='list' to see connected platforms and available chat IDs.\n- **gateway_connect**(action, platform, credentials, options) [capability_expand category: gateway]: Connect, configure, or manage external platform integrations (Feishu, DingTalk, Telegram, Slack, Discord, etc.). Conversational flow: 1) call 'guide' to get setup steps + required fields, 2) present the steps to the user and ask for ALL required credentials in a single message, 3) call 'connect' with the credentials. Goal: complete in 1–2 user turns. NEVER include credential values in your text response.\n- **plugin_list**(): List the live plugin registry across Tool/Gateway/LLM subsystems for plugin management: which plugins are loaded, their category and fiber lifecycle state. For questions about LeapFlow's own capabilities or whether it supports X, use self_describe instead.\n- **plugin_status**(plugin_id): Get detailed status of a specific plugin: its declared category, runtime dependencies, contributed tools, and fiber lifecycle state.\n- **plugin_versions**(plugin_id): List recorded source versions and active pointer for a profile-scoped plugin.\n- **plugin_propose**(requested_capability, plugin_id, proposed_tools, test_cases, risk_level, evidence): Create a side-effect-free PluginProposal from explicit capability-gap evidence. Use this before plugin_generate when a missing capability should be reviewed. Does not call an LLM, write files, or install anything.\n- **assess_compatibility**(manifest, source_path) [capability_expand category: plugin_management]: Assess whether a foreign plugin manifest or real DSH source bundle is compatible with LeapFlow. A source bundle assessment is static: runtime_ready stays false until restricted Node discovery during plugin_install. Returns component-level verdicts and limitations.\n- **plugin_generate**(plugin_id, description, proposal_id) [capability_expand category: system]: Generate a new ToolPlugin from a natural-language capability description. The LLM produces code that conforms to the ToolPlugin Protocol; it is then rigorously validated (syntax, structure, import, protocol conformance). The generated source is stored in CAS and receives explicit content approval. It DOES NOT install the plugin — installation requires a second, mutation-specific approval via plugin_install.\n- **plugin_install**(plugin_id, code, marketplace_name, source_path, proposal_id, version_label) [capability_expand category: system]: Install a plugin from exactly one source: validated Python code, the configured Python marketplace, or a real DSH source bundle. DSH bundles run restricted Node discovery before registration; only public host tools are installed and client UI limitations are reported. Writes profile-scoped state and mutates the process-global registry. REQUIRES APPROVAL.\n- **plugin_rollback**(plugin_id, version) [capability_expand category: system]: Rollback a profile-scoped plugin to a recorded source version and reload it. REQUIRES APPROVAL.\n- **plugin_reload**(plugin_id, version_label) [capability_expand category: system]: Hot-reload a plugin at runtime. Disposes the old plugin fiber, re-imports its module, and registers a fresh instance. Existing in-flight turns are unaffected (snapshot isolation). REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_disable**(plugin_id) [capability_expand category: system]: Disable a plugin by disposing its fiber, removing its tools from the runtime registry. Cannot disable self_management itself. REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_remove**(plugin_id, delete_source) [capability_expand category: system]: Terminally remove a plugin: dispose its fiber, unregister its tools, remove reload metadata, and optionally delete its profile-scoped source file. Cannot remove self_management itself. REQUIRES APPROVAL.\n- **plugin_unquarantine**(plugin_id) [capability_expand category: plugin_management]: Restore a quarantined plugin to probation status for re-evaluation. Unfreezes the trust ledger, transitions the proposal back to PROBATION, and reloads the plugin. REQUIRES APPROVAL.\n- **plugin_enable**(plugin_id) [capability_expand category: system]: Re-enable a previously disabled plugin by reloading its module and registering a fresh instance. REQUIRES APPROVAL.\n- **self_describe**(facet): Answer questions about LeapFlow ITSELF from live runtime evidence — its identity/version/model, what tools and plugins it has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of self-awareness. Prefer this over reading LeapFlow's own source code. Pick facet=capabilities for 'what can you do / do you support X', identity for version/model, runtime for context/posture, or all for a full self-check when explicitly requested.\n- **runtime_snapshot**(): Lightweight (~150 token) flat snapshot of current runtime state: model, context budget, posture, disclosure level, turn count, cache hit rate, uptime, tool count, and pending approvals.\n- **schedule_create**(trigger_expression, instruction, execution_mode, max_retries, delivery_target) [capability_expand category: scheduler]: Create a new scheduled task. Specify a trigger expression (e.g. '30m', 'every 2h', '0 9 * * *') and the instruction to execute on each trigger. Optionally provide an execution mode and a delivery target to receive notifications.\n- **schedule_list**() [capability_expand category: scheduler]: List all scheduled tasks with their current state, trigger, and next due time.\n- **schedule_status**(task_id) [capability_expand category: scheduler]: Get detailed status and recent execution history for a scheduled task.\n- **schedule_pause**(task_id) [capability_expand category: scheduler]: Pause a scheduled task so it stops firing without being cancelled.\n- **schedule_resume**(task_id) [capability_expand category: scheduler]: Resume a paused scheduled task — re-arms it and recalculates next due time.\n- **schedule_cancel**(task_id) [capability_expand category: scheduler]: Cancel a scheduled task permanently.\n- **tool_search**(query, max_results): Search all registered tools by keyword relevance. Returns a ranked list of matching tools with name, category, and summary. Use this when you need a tool but are unsure of its exact name or category.\n- **tool_describe**(tool_name): Get the full callable schema of a single tool by exact name. Returns the tool's description, parameters, and metadata. Use after tool_search to inspect a specific tool before calling it.\n- **hw_list**(): List connected hardware devices with their channel counts and measured quantities. Start here; it does not include operating limits.\n- **hw_describe**(device_id): Return the full reference for one device: every channel, its unit, its operating envelope, rate limit, reversibility, and required interlocks, plus any prior outcomes recorded for its writable channels. Required before commanding a device.\n- **hw_read**(device_id, channel_id): Read the current value of one device channel. Has no physical effect.\n- **hw_status**(device_id): Report connection health, halt capability, and recent notable events (threshold excursions, lost samples, stalled channels) for one device.\n- **hw_configure**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Set a configuration value or setpoint on a channel declaring effect=configure. Setpoints often have inertia: the value may need time to stabilise.\n- **hw_actuate**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Command motion or output on a channel declaring effect=actuate. This moves physical hardware; a repeat from an unknown state is not a safe retry.\n- **hw_dispense**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Consume an irreversible resource on a channel declaring effect=dispense. Running this twice dispenses twice; never repeat it after a failure without first verifying what already happened.\n- **hw_estop**(device_id) [capability_expand category: hardware]: Stop all motion and output on a device immediately. Never requires approval; use it whenever device behaviour is unexpected.\n- **hw_batch_actuate**(device_id, commands, dry_run, verify) [capability_expand category: hardware]: Write multiple actuator channels in one atomic transaction. Used for coordinated multi-joint control where all positions must be commanded simultaneously (e.g. robot arm joint-space control).\n- **session_search**(query, limit) [capability_expand category: unclassified]: Search past conversation sessions in the current workspace for relevant context.\n- **session_list**(limit): List recent conversation sessions in the current workspace with stable ids, titles, dates, and summaries. Use for browsing past tasks or when user asks to see history without specific search terms. No keywords needed — returns chronological list.\n- **session_detail**(session_id, limit, offset, include_inactive) [capability_expand category: unclassified]: Read a paginated persisted transcript for one past conversation session in the current workspace. Use after session_list or session_search returns a session_id.\n- **click**(element_index) [capability_expand category: desktop]: Click a UI element by its element_index (from the latest observe_ui snapshot)\n- **get_clipboard**() [capability_expand category: desktop]: Read current clipboard text content\n- **list_apps**(filter, running_only) [capability_expand category: desktop]: List available applications on this system. Use to discover correct bundle_id before switch_app.\n- **list_windows**() [capability_expand category: desktop]: List all top-level windows with pid, window_id, title, and per-window state (minimized, on-screen). Call this first to pick the pid and window_id that observe_ui and other window tools require.\n- **observe_ui**(pid, window_id, query) [capability_expand category: desktop]: Snapshot one window's actionable UI elements, each tagged with an element_index for click/right_click/read_text. Re-observe after actions — indices belong to one snapshot. Requires the window's pid and window_id from list_windows.\n- **open_url**(url, app_id) [capability_expand category: desktop]: Open a URL in the default or specified browser\n- **read_text**(element_index) [capability_expand category: desktop]: Read the text content of a specific UI element from the latest snapshot\n- **right_click**(element_index) [capability_expand category: desktop]: Right-click a UI element to open its context menu. Returns visible menu items.\n- **screenshot**(pid, window_id) [capability_expand category: desktop]: Capture a screenshot for visual verification. With pid + window_id captures that window (works across all displays); defaults to the last observed window, or the full desktop when no window has been observed.\n- **scroll**(element_index, direction, amount, pid, window_id) [capability_expand category: desktop]: Scroll a scrollable area of a window. Omit element_index to scroll the window's focused/page scroller; pass one to scroll an exact element from the latest snapshot.\n- **select_text**(element_index) [capability_expand category: desktop]: Select all text in a UI element (focus + select-all, for subsequent copy)\n- **set_clipboard**(text) [capability_expand category: desktop]: Write text to the clipboard\n- **shortcut**(keys) [capability_expand category: desktop]: Execute a keyboard shortcut\n- **switch_app**(app_id) [capability_expand category: desktop]: Switch to an app (launch if needed, activate, verify)\n- **type_text**(text) [capability_expand category: desktop]: Type text into the currently focused element\n- **wait**(seconds) [capability_expand category: desktop]: Wait for a specified duration before continuing\n- **wait_until**(condition, pid, window_id, timeout, poll_interval) [capability_expand category: desktop]: Wait until a UI condition is met (polls UI tree). Returns elements when found or on timeout.\n- **wait_until_stable**(timeout, poll_interval, pid, window_id) [capability_expand category: desktop]: Wait until the UI stops changing (element set stabilizes across polls).\n\n## App Connector Capability Index\nLeapFlow can onboard and manage external apps through `platform_connect` and execute exact registered business actions through `platform_action`.\nFor requests about connecting, setting up, configuring, enabling, or managing a supported app, use `platform_connect` first instead of generating SDK/Webhook sample code.\n`platform_connect.action` is the management namespace: list, guide, preflight, connect, disconnect, remove, status, events_start, events_stop, events_status.\n`platform_action.action` is only for exact registered platform business actions listed below, such as `im.send_message`; never use management actions like `list` or `guide` there.\nAll business fields MUST go inside `payload`; top-level keys are only `platform`, `action`, and `payload`.\nDo not invent platform IDs or platform action names. If the needed action is not listed, ask for discovery/clarification instead of guessing.\nUse `platform_connect` with `action='guide'` and the matching `platform` to start onboarding; use `action='list'` when the app is unclear.\nWhen a pending onboarding state is present, continue from that state with `platform_connect` instead of asking the user to restate the app.\nSupported apps:\n- `api_server`: API Server (OpenAI Compatible) (category=api; backend=adapter; platform_action actions=none registered)\n- `dingtalk`: 钉钉 (DingTalk) (category=im; backend=adapter; platform_action actions=none registered)\n- `feishu`: 飞书 (Feishu/Lark) (category=im; backend=cli)\n - `calendar.create_event` payload={summary*, start_time*, end_time*, attendees} [write/high]\n - `docs.create_markdown` payload={title*, markdown*, folder_token} [write/medium]\n - `drive.search` payload={query*, limit} [read/medium]\n - `im.add_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.download_resource` payload={message_id*, file_key*, type*} [read/low]\n - `im.get_messages` payload={message_ids*} [read/low]\n - `im.list_chats` payload={page_size} [read/low]\n - `im.list_messages` payload={chat_id*} [read/low]\n - `im.list_thread_messages` payload={thread*} [read/low]\n - `im.remove_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.reply_message` payload={message_id*, text*} [send/high]\n - `im.search_chats` payload={query*, page_size} [read/low]\n - `im.search_messages` payload={query*} [read/medium]\n - `im.send_message` payload={chat_id*, text*, thread_id} [send/high]\n - `im.update_card` payload={token*, card*} [write/medium]\n - `im.update_message` payload={message_id*, text*} [write/medium]\n - `mail.search_unread` payload={query, limit} [read/high]\n - `sheets.append_row` payload={spreadsheet_token*, sheet_id*, values*} [write/medium]\n - `task.create` payload={title*, description, due_time} [write/medium]\n- `telegram`: Telegram (category=social; backend=adapter; platform_action actions=none registered)\n- `webhook`: Webhook (Generic) (category=webhook; backend=adapter; platform_action actions=none registered)\n\n## Learned Skills\nYou have access to the following learned skills. Use `skills_list` to browse or `skill_view` to read details:\n- api_designer: REST and GraphQL API design with OpenAPI spec generation and best practices [API, REST, GraphQL]\n- code_review: Structured code review for security, performance, and style [code, review, security]\n- data_analysis: CSV/JSON data analysis with statistical summaries, quality checks, and visualiza [data, analysis, statistics]\n- devops_helper: CI/CD pipeline configuration, Docker orchestration, and deployment strategy [devops, CI/CD, Docker]\n- document_writer: Structured document generation: technical docs, API references, reports, and REA [documentation, technical-writing, README]\n- git_workflow: Git operation orchestration: branching, commits, conflicts, and PR workflow [git, version-control, branching]\n- i18n_helper: Internationalization workflow: string extraction, translation, locale management [i18n, internationalization, localization]\n- project_analysis: Project structure analysis, dependency audit, and tech stack identification [project, analysis, dependencies]\n- security_audit: Security vulnerability scanning strategy, threat modeling, and fix recommendatio [security, vulnerability, audit]\n- shell_automation: Natural language to shell commands with safety review and step-by-step execution [shell, terminal, automation]\n- test_writer: Generate unit and integration tests from source code with edge-case coverage [testing, unit-test, integration-test]\n- web_research: Multi-step web search with cross-verification and structured synthesis [web, search, research]\n\n## Tool Usage\nTools are normally invoked through the native function-calling mechanism, not by writing JSON in your reply\ntext. Only if the provider signals that native function calling is unavailable for this turn, fall back to a\nsingle JSON code block: `{\"name\": \"tool_name\", \"arguments\": {\"key\": \"value\"}}` — use this fallback format\nonly, never both. Only call a tool whose exact name appears in the tool index above and is currently callable\n(or reachable via `capability_expand`). Never invent, rename, alias, or guess a tool name, platform ID, or\nplatform action from argument shape or wording — if the index does not list it, it does not exist.\n`platform_connect.action` (list/guide/preflight/connect/disconnect/remove/status/events_start/events_stop/\nevents_status) is the App Connector management namespace; `platform_action.action` only accepts exact\nregistered business actions such as `im.send_message` shown in the App Connector Capability Index — never mix\nthe two namespaces. If a tool call returns an unknown/unavailable result, use the returned suggestions or\navailable names for a single retry instead of trying further variations of the same guess.\n\n**Side-effect action rule** (`platform_action` with effect=send/write/execute):\n- Call each unique action+payload **exactly once**. Never include duplicates in the same turn.\n- Once the result returns `\"completed\": true`, that action is DONE for this task. Do NOT call it again\n in any subsequent turn — immediately summarize the result for the user instead.\n- The system enforces idempotency: duplicate calls are blocked and will not execute.\n- If the user explicitly requests sending/writing multiple times, use distinct payloads per call.\n\n**Resource identifier provenance rule**:\n- NEVER fabricate, guess, or infer resource identifiers (chat_id, message_id, file_key, user_id, etc.).\n Every resource ID used in a side-effect action MUST come from a successful API response in this session.\n- If a read/list action fails (e.g. authorization error), you do NOT have valid resource IDs.\n Report the failure to the user — do NOT attempt the dependent write/send action with a guessed ID.\n- When a tool result contains `\"llm_instruction\"`, follow it exactly.\n\n## Guidelines\n1. **Direct answers first**: If you already know the answer, respond directly without tools.\n2. **Avoid redundant tool calls**: Do not call the same tool with the same arguments more than once in the same user turn. When an existing tool result already answers the user's request, stop calling tools and answer directly.\n3. **Use tools proactively**: When the user asks about files, time, system state, or needs actions performed, use the appropriate tool.\n4. **Chain tools when needed**: You can call multiple tools in sequence (e.g., list files → read file → summarize).\n5. **Handle failures gracefully**: If a tool fails, explain what went wrong and suggest alternatives. If it failed because of an unknown tool/platform/action name, retry once with an exact name from the returned suggestions, then explain rather than keep guessing.\n6. **Summarize results naturally**: After tool execution, synthesize the results into a helpful answer rather than dumping raw output.\n7. **Stay conversational**: Maintain a natural, helpful tone. Acknowledge context from earlier in the conversation.\n8. **Recall past work**:\n - Broad queries (\"之前做了什么\", \"列出任务\"): answer from the \"Recent Task History\" section already in your context. If insufficient, call session_list.\n - Specific lookups (\"上次那个配置怎么改的\"): call session_search with relevant phrases (NOT single characters).\n - Do NOT call search tools repeatedly with keyword fragments. One well-phrased call is better than ten fragmented ones.\n\n## Coding & Verification\nWhen working with code, prefer the precise built-in tools over ad-hoc shell: use `repo_map` to orient in an\nunfamiliar project, `code_search`/`file_find` to locate and `code_intel` for symbols, `edit_file` (anchored\nsearch-replace, or a unified `diff`) to change files — never rewrite a whole file to change a few lines — and\n`git_query` to inspect diffs/log. After edits, check `syntax_ok` in the result and run `test_run`/`lint_check`\nbefore declaring the work done. Batch independent read-only calls (search/read/query) into a single turn — they\nrun in parallel.\n\n## Presentation Style\n1. **Polished Markdown only**: Format user-facing answers with clean Markdown headings, short paragraphs, and concise bullets. Use tables only when they improve comparison or scanning.\n2. **Terminal-friendly layout**: Keep lines readable in a TUI; avoid dense walls of text, deeply nested lists, oversized ASCII art, or heavy visual blocks.\n3. **Elegant emphasis**: Use bold text sparingly for key terms and conclusions. Avoid excessive emojis, decorative symbols, repeated separators, or visual noise.\n4. **Theme-safe colors**: Do not emit ANSI escape codes, HTML color tags, Rich markup, or hardcoded color names. Rely on the TUI theme to render Markdown professionally.\n5. **No leaked tool protocol**: Never show tool-call JSON, internal schemas, raw observations, tool result payloads, or hidden reasoning in the final answer unless the user explicitly asks for raw/debug output. Treat any prior `{\"name\": ..., \"arguments\": ...}` blocks and `Tool result (...)` messages as internal execution context only.\n6. **Professional closure**: End with a concise conclusion or next step when helpful; avoid rambling after the useful answer is complete.\n\nWhen finished with all tool calls, respond normally without a JSON block, tool-call transcript, or process log.\n\n## Task Contract\n- Task ID: turn-6\n- Original user request: Actuate the bench_r8 setpoint to 65.\n- Workspace root: \n- Allowed roots: \n- Treat relative project paths as relative to the workspace root; never infer `.` as the project root when a workspace root is provided.\n- Workspace boundary is enforced by tools: do not read, search, edit, or run commands against paths outside the allowed roots unless the user explicitly requests an external path and the tool/approval policy permits it.\n- LeapFlow workspace config is optional at `/.leapflow/config.yaml`; runtime config is loaded from `~/.leapflow/config/user.yaml` and `~/.leapflow/profiles//config/*.yaml`.\n- Preserve this task contract across summarization, compression, tool loops, and memory retrieval.\n" + }, + { + "role": "assistant", + "content": "[Called: hw_configure]\nHomed the device; the readiness gate is now satisfied." + }, + { + "role": "user", + "content": "Preview an actuate of bench_r8 setpoint to 55, dry run only." + }, + { + "role": "assistant", + "content": "[Called: hw_actuate]\nDry run confirmed the setpoint command is feasible after homing." + }, + { + "role": "user", + "content": "Actuate the bench_r8 setpoint to 65.\nActuate the bench_r8 setpoint to 65." + }, + { + "role": "assistant", + "content": "", + "tool_calls": [ + "hw_actuate" + ] + }, + { + "role": "tool", + "content": "{\"ok\": true, \"device_id\": \"bench_r8\", \"channel_id\": \"setpoint\", \"side_effect_state\": \"committed\", \"settled\": true, \"execution_id\": \"\", \"idempotency_key\": \"\", \"execution_policy\": \"external_side_effect\", \"tool_call_id\": \"call_1\", \"execution_status\": \"completed\"}", + "tool_result": true + } + ], + "tools": [ + "capability_expand", + "code_intel", + "code_search", + "config_get", + "config_list", + "env_info", + "file_find", + "file_list", + "file_read", + "git_query", + "hw_actuate", + "hw_batch_actuate", + "hw_configure", + "hw_describe", + "hw_dispense", + "hw_estop", + "hw_list", + "hw_read", + "hw_status", + "lint_check", + "memory_search", + "plugin_list", + "plugin_propose", + "plugin_status", + "plugin_versions", + "repo_map", + "research_note", + "runtime_snapshot", + "schedule_reentry", + "self_describe", + "session_list", + "skill_view", + "skills_list", + "terminal_list", + "terminal_read", + "test_run", + "text_search", + "time_get", + "tool_describe", + "tool_search", + "web_fetch" + ] + }, + "responses": [ + { + "status": 200, + "content_type": "application/json", + "body": "{\"id\": \"chatcmpl-cassette\", \"object\": \"chat.completion\", \"model\": \"cassette-model\", \"choices\": [{\"index\": 0, \"message\": {\"role\": \"assistant\", \"content\": \"Commanded the setpoint through the approval path.\"}, \"finish_reason\": \"stop\"}], \"usage\": {\"prompt_tokens\": 64, \"completion_tokens\": 16, \"total_tokens\": 80}}" + } + ] +} diff --git a/tests/_fixtures/cassettes/r8_hardware/cassette-model-aedf2373abf08185.cassette.json b/tests/_fixtures/cassettes/r8_hardware/cassette-model-aedf2373abf08185.cassette.json deleted file mode 100644 index d8a83174..00000000 --- a/tests/_fixtures/cassettes/r8_hardware/cassette-model-aedf2373abf08185.cassette.json +++ /dev/null @@ -1,79 +0,0 @@ -{ - "fingerprint": "aedf2373abf08185d4d710168e74e58e82dd9081bd3bad7491596f532e392baf", - "note": "captured in seed mode", - "request": { - "model": "cassette-model", - "stream": false, - "messages": [ - { - "role": "system", - "content": "You are LeapFlow, an intelligent assistant that can both converse naturally and take real actions on the user's computer.\n\n## Capabilities\nThe tool index below lists **every** registered tool by name and a one-line summary — this is the complete\ncapability contract; nothing else exists. Only a subset is directly callable this turn (via native tool calling,\nnot a JSON block in your reply). If you need a tool from the index that is not yet callable, call\n`capability_expand` with its category name first — the matching tools become callable immediately after.\nWhen the user asks what LeapFlow itself supports, whether it supports plugins, or which runtime capabilities\nare available, use the live capability evidence exposed by `plugin_list` before making capability claims; report\nconfiguration-dependent or unavailable capabilities as limitations instead of inferring from documentation.\n- **text_search**(text, pattern): Search for a regex pattern in text.\n- **text_replace**(text, old, new, count) [capability_expand category: write]: Replace occurrences of a substring in text.\n- **time_get**(): Get current date and time.\n- **env_info**(): Get system environment information (OS, Python version, cwd).\n- **skills_list**(query, category, source): List available learned skills. Use when user asks about capabilities or you need a specific skill.\n- **skill_view**(name): View the full content of a specific skill document.\n- **code_intel**(path, operation): Precise document symbols (outline) for a source file: classes, functions, and methods with line ranges. Python uses an exact AST parse; other languages use a keyword-prefix scan. Prefer over file_read mode=symbols for accurate navigation before editing. Read-only.\n- **repo_map**(path): Compact project orientation for a repository root: languages, detected test/lint commands, top-level structure, entry points, manifest, and VCS branch. Call this first when entering an unfamiliar codebase. Read-only.\n- **scm_sync**(action, cwd, remote, pull_ref, push_ref, timeout) [capability_expand category: scm]: Run a typed git SCM action. Use this instead of shell_run for git pull/push/status. For 'pull origin main then push', set action='pull_then_push', remote='origin', pull_ref='main', and omit push_ref so LeapFlow pushes the current local branch.\n- **git_query**(action, cwd, ref, path, staged, max_count, stat): Read-only structured git inspection: action=diff|log|status|branch|show. Prefer over shell_run for reading repo state — output is clipped, redacted, and log/branch are parsed into structured fields. Use scm_sync for pull/push.\n- **git_write**(action, cwd, message, stage_all, name, ref, create) [capability_expand category: scm]: Mutating git actions: action=commit (message, stage_all), branch (create+switch), checkout (switch; create=true for -b). Approval-gated. Use scm_sync for pull/push and git_query for reads.\n- **test_run**(command, cwd, timeout): Run the project's test suite and return structured results (framework, passed/failed counts, failing tests). Auto-detects the runner (pytest/npm/go/cargo) or uses a configured/explicit command; executes via the governed shell. ok=true means the runner executed — see 'success' for pass/fail.\n- **lint_check**(command, cwd, timeout): Run the project's linter and return a structured clean/issue result. Auto-detects the linter (ruff/eslint/go vet/clippy) or uses a configured/explicit command; executes via the governed shell. ok=true means the linter ran — see 'clean'.\n- **file_list**(path, pattern, depth): List files and directories at a given path. Use depth=1 or depth=2 to get a recursive tree in one call instead of listing each sub-directory separately.\n- **file_read**(path, max_lines, start_line, max_chars, mode): Read text file content with adaptive context governance. For large or unfamiliar files, prefer mode='outline' or mode='symbols' first, then use mode='raw' with start_line/max_lines for the specific range you actually need. For LeapFlow's own settings, use config_list / config_get / config_set — its config files are outside the workspace and not readable here.\n- **file_write**(path, content, mode) [capability_expand category: write]: Write content to a file (overwrite or append).\n- **code_search**(pattern, patterns, path, glob, ignore_case, multiline, max_results, context_lines): Search file CONTENTS by regex pattern across a directory tree (ripgrep-backed). Requires a regex pattern. NOT for listing or browsing directory contents — use file_list for that. Prefer this over shell_run grep: faster, skips VCS/dependency/build dirs, and returns structured path:line:column matches. Batch related lookups into ONE call via `patterns` (OR-combined, single pass) instead of issuing several separate searches. Use file_read for the surrounding context of a hit.\n- **file_find**(glob, path, max_results): Find files by a recursive glob pattern under a base path (e.g. '**/test_*.py' or '*.md'). Prefer this over shell_run find; skips VCS/dependency/build dirs.\n- **edit_file**(path, edits, dry_run, diff) [capability_expand category: write]: Apply targeted, anchored search-replace edits to an EXISTING text file (use file_write to create/overwrite). Each edit is {original_text, new_text, replace_all?}; original_text must match exactly and uniquely (or set replace_all) — a non-unique or missing anchor is rejected so files are never corrupted. Set dry_run to preview. Alternatively pass a unified 'diff' to apply its hunks as anchored edits. Far cheaper and safer than rewriting a whole file.\n- **shell_run**(command, cwd, timeout) [capability_expand category: shell]: Execute a one-shot shell command with timeout protection. Runs in the active workspace; paths resolving outside it are refused. Reach for a structured tool first when one fits — web_fetch for anything over HTTP(S), git_query/scm_sync for git, code_search/file_find/file_read for the repo, config_get/config_set for LeapFlow's own settings — because those report typed results, while a failed shell command can only be diagnosed from its exit code and stderr. Every shell run counts as an external side effect, so a failure stops the rest of the batch and is not retried automatically.\n- **terminal_open**(command, cwd, shell) [capability_expand category: terminal]: Open a PERSISTENT shell session (REPL/dev server/watch), returning a session_id for terminal_send/read/close. Disabled unless tools.terminal_session_enabled is set. For one-shot commands use shell_run instead.\n- **terminal_send**(session_id, input, wait) [capability_expand category: terminal]: Send a line of input to a persistent terminal session and return output captured shortly after.\n- **terminal_read**(session_id, wait): Drain buffered output from a persistent terminal session (optionally waiting briefly first).\n- **terminal_close**(session_id) [capability_expand category: terminal]: Terminate a persistent terminal session and release its process group.\n- **terminal_list**(): List active persistent terminal sessions.\n- **config_list**(category, limit): List LeapFlow's own writable settings (LLM model and endpoint, daemon, memory, perception, gateway, …) with current values. Use this to discover the exact key before changing anything. Optionally narrow by `category`. This is the only correct way to inspect LeapFlow configuration — never read config files from disk.\n- **config_get**(key): Read one LeapFlow setting by key (e.g. 'llm.model', 'llm.base_url', 'daemon.log_level'), returning its current value, type, scopes, and whether a change needs a daemon restart. There is no 'llm.provider' key: provider behavior is inferred from the OpenAI-compatible 'llm.base_url'. Never read LeapFlow config files from disk — use this.\n- **config_set**(key, value, scope) [capability_expand category: config]: Change one LeapFlow setting by key, e.g. switch an OpenAI-compatible LLM with key='llm.model' and key='llm.base_url'. There is no 'llm.provider' key: provider behavior is inferred from the endpoint. Values are validated and coerced; credentials are stored in the vault automatically. Call config_list or config_get first if unsure of the key. The result states whether a `leap daemon restart` is required. Never edit LeapFlow config files directly.\n- **web_fetch**(url, select, timeout, max_bytes): Read a URL over HTTP(S) and get back extracted, context-sized content: parsed JSON for API endpoints, readable text plus links for web pages. Use this for anything on the internet — prices, docs, releases, articles — instead of running curl through shell_run: it reports real HTTP status codes, retries rate limits on its own, and is a plain read so a retry is always safe. For JSON APIs pass `select` with a dotted path (e.g. 'chart.result.0.meta') to return just that part instead of the whole payload.\n- **memory_search**(query, limit): Search agent memory for relevant past experiences, observations, and facts.\n- **memory_add**(content, kind) [capability_expand category: write]: Store a new observation or insight in memory for future reference.\n- **research_note**(kind, text): Record a compact, structured note about the current task's state so it survives context compression on long / multi-step tasks. Use for durable findings, open questions still to resolve, decisions / excluded paths, and the immediate next step. One concise sentence per note.\n- **capability_expand**(category): Fetch the full callable schema for every tool in a capability category (e.g. 'hub', 'gateway', 'desktop', 'delegate', 'file', 'memory', 'skill'). The compact tool index always lists every registered tool by name and a one-line summary, but only a static low-risk subset is directly callable each turn. If you need a tool from the index that is not yet callable, call capability_expand with its category first; the matching tools become callable in this turn. Never invent a tool name — expand the category instead.\n- **delegate_task**(goal, context) [capability_expand category: delegate]: Delegate a complex sub-task to an isolated subagent. The subagent gets a fresh context and restricted tool access. Use when a task is self-contained and can be solved independently.\n- **schedule_reentry**(kind, reason, delay_seconds, event_match, max_reentries, deadline_seconds): Register a re-entry so this task can resume later from its current orientation (findings / open questions / next step). Use when work must pause and continue after a delay (kind=time) or when a matching platform event arrives (kind=event), instead of finishing now. The research-ledger state is carried over automatically.\n- **hub_push**(skill_name, visibility, version) [capability_expand category: hub]: Push a local skill to the ModelScope Hub for sharing or backup.\n- **hub_pull**(repo_id, version) [capability_expand category: hub]: Pull a skill from the ModelScope Hub to install locally.\n- **hub_search**(query) [capability_expand category: hub]: Search for skills on the Hub by keyword or description.\n- **hub_sync**(mode, dry_run) [capability_expand category: hub]: Preview or execute sync between local skills and Hub.\n- **platform_action**(platform, action, payload, backend_kind) [capability_expand category: gateway]: Execute an exact registered business action on an external platform through LeapFlow's App Connector layer. Actions must be copied from the App Connector Capability Index and are addressed as domain.operation, e.g. im.send_message or docs.create_markdown. All business fields (chat_id, text, query, etc.) MUST be placed inside `payload`, never at the top level. Example: {\"platform\":\"feishu\",\"action\":\"im.send_message\",\"payload\":{\"chat_id\":\"oc_xxx\",\"text\":\"hello\"}}. Do not invent action names, do not use management actions such as list/guide/connect/status here.\n- **platform_connect**(action, platform, credentials, options, checkpoint) [capability_expand category: gateway]: List, guide, connect, disconnect, remove, or check status for external platforms using the App Connector management namespace. Supports REST and CLI backends. Use this for management actions such as list/guide/preflight/connect/status; use platform_action only for exact registered business actions.\n- **gateway_send**(platform, chat_id, text, thread_id) [capability_expand category: gateway]: Send a message to a connected external platform (Feishu group, Telegram chat, DingTalk conversation, etc.). Requires the platform to be connected via gateway_connect first. Use gateway_connect with action='list' to see connected platforms and available chat IDs.\n- **gateway_connect**(action, platform, credentials, options) [capability_expand category: gateway]: Connect, configure, or manage external platform integrations (Feishu, DingTalk, Telegram, Slack, Discord, etc.). Conversational flow: 1) call 'guide' to get setup steps + required fields, 2) present the steps to the user and ask for ALL required credentials in a single message, 3) call 'connect' with the credentials. Goal: complete in 1–2 user turns. NEVER include credential values in your text response.\n- **plugin_list**(): List the live plugin registry and cross-subsystem capability evidence. Use this before answering questions about whether LeapFlow supports plugins, self-evolution, plugin installation, hot reload, versioning, or other runtime capabilities.\n- **plugin_status**(plugin_id): Get detailed status of a specific plugin: its declared category, runtime dependencies, contributed tools, and fiber lifecycle state.\n- **plugin_versions**(plugin_id): List recorded source versions and active pointer for a profile-scoped plugin.\n- **plugin_propose**(requested_capability, plugin_id, proposed_tools, test_cases, risk_level, evidence): Create a side-effect-free PluginProposal from explicit capability-gap evidence. Use this before plugin_generate when a missing capability should be reviewed. Does not call an LLM, write files, or install anything.\n- **assess_compatibility**(manifest, source_path) [capability_expand category: plugin_management]: Assess whether a foreign plugin manifest or real DSH source bundle is compatible with LeapFlow. A source bundle assessment is static: runtime_ready stays false until restricted Node discovery during plugin_install. Returns component-level verdicts and limitations.\n- **plugin_generate**(plugin_id, description, proposal_id) [capability_expand category: system]: Generate a new ToolPlugin from a natural-language capability description. The LLM produces code that conforms to the ToolPlugin Protocol; it is then rigorously validated (syntax, structure, import, protocol conformance). The generated source is stored in CAS and receives explicit content approval. It DOES NOT install the plugin — installation requires a second, mutation-specific approval via plugin_install.\n- **plugin_install**(plugin_id, code, marketplace_name, source_path, proposal_id, version_label) [capability_expand category: system]: Install a plugin from exactly one source: validated Python code, the configured Python marketplace, or a real DSH source bundle. DSH bundles run restricted Node discovery before registration; only public host tools are installed and client UI limitations are reported. Writes profile-scoped state and mutates the process-global registry. REQUIRES APPROVAL.\n- **plugin_rollback**(plugin_id, version) [capability_expand category: system]: Rollback a profile-scoped plugin to a recorded source version and reload it. REQUIRES APPROVAL.\n- **plugin_reload**(plugin_id, version_label) [capability_expand category: system]: Hot-reload a plugin at runtime. Disposes the old plugin fiber, re-imports its module, and registers a fresh instance. Existing in-flight turns are unaffected (snapshot isolation). REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_disable**(plugin_id) [capability_expand category: system]: Disable a plugin by disposing its fiber, removing its tools from the runtime registry. Cannot disable self_management itself. REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_remove**(plugin_id, delete_source) [capability_expand category: system]: Terminally remove a plugin: dispose its fiber, unregister its tools, remove reload metadata, and optionally delete its profile-scoped source file. Cannot remove self_management itself. REQUIRES APPROVAL.\n- **plugin_unquarantine**(plugin_id) [capability_expand category: plugin_management]: Restore a quarantined plugin to probation status for re-evaluation. Unfreezes the trust ledger, transitions the proposal back to PROBATION, and reloads the plugin. REQUIRES APPROVAL.\n- **plugin_enable**(plugin_id) [capability_expand category: system]: Re-enable a previously disabled plugin by reloading its module and registering a fresh instance. REQUIRES APPROVAL.\n- **self_describe**(facet): Introspect LeapFlow's own identity, capabilities, runtime state, evolution metrics, or platform connections. Use facet='all' only when a comprehensive self-check is explicitly requested.\n- **runtime_snapshot**(): Lightweight (~150 token) flat snapshot of current runtime state: model, context budget, posture, disclosure level, turn count, cache hit rate, uptime, tool count, and pending approvals.\n- **schedule_create**(trigger_expression, instruction, execution_mode, max_retries, delivery_target) [capability_expand category: scheduler]: Create a new scheduled task. Specify a trigger expression (e.g. '30m', 'every 2h', '0 9 * * *') and the instruction to execute on each trigger. Optionally provide an execution mode and a delivery target to receive notifications.\n- **schedule_list**() [capability_expand category: scheduler]: List all scheduled tasks with their current state, trigger, and next due time.\n- **schedule_status**(task_id) [capability_expand category: scheduler]: Get detailed status and recent execution history for a scheduled task.\n- **schedule_pause**(task_id) [capability_expand category: scheduler]: Pause a scheduled task so it stops firing without being cancelled.\n- **schedule_resume**(task_id) [capability_expand category: scheduler]: Resume a paused scheduled task — re-arms it and recalculates next due time.\n- **schedule_cancel**(task_id) [capability_expand category: scheduler]: Cancel a scheduled task permanently.\n- **tool_search**(query, max_results): Search all registered tools by keyword relevance. Returns a ranked list of matching tools with name, category, and summary. Use this when you need a tool but are unsure of its exact name or category.\n- **tool_describe**(tool_name): Get the full callable schema of a single tool by exact name. Returns the tool's description, parameters, and metadata. Use after tool_search to inspect a specific tool before calling it.\n- **hw_list**(): List connected hardware devices with their channel counts and measured quantities. Start here; it does not include operating limits.\n- **hw_describe**(device_id): Return the full reference for one device: every channel, its unit, its operating envelope, rate limit, reversibility, and required interlocks, plus any prior outcomes recorded for its writable channels. Required before commanding a device.\n- **hw_read**(device_id, channel_id): Read the current value of one device channel. Has no physical effect.\n- **hw_status**(device_id): Report connection health, halt capability, and recent notable events (threshold excursions, lost samples, stalled channels) for one device.\n- **hw_configure**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Set a configuration value or setpoint on a channel declaring effect=configure. Setpoints often have inertia: the value may need time to stabilise.\n- **hw_actuate**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Command motion or output on a channel declaring effect=actuate. This moves physical hardware; a repeat from an unknown state is not a safe retry.\n- **hw_dispense**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Consume an irreversible resource on a channel declaring effect=dispense. Running this twice dispenses twice; never repeat it after a failure without first verifying what already happened.\n- **hw_estop**(device_id) [capability_expand category: hardware]: Stop all motion and output on a device immediately. Never requires approval; use it whenever device behaviour is unexpected.\n- **session_search**(query, limit) [capability_expand category: unclassified]: Search past conversation sessions in the current workspace for relevant context.\n- **session_list**(limit): List recent conversation sessions in the current workspace with stable ids, titles, dates, and summaries. Use for browsing past tasks or when user asks to see history without specific search terms. No keywords needed — returns chronological list.\n- **session_detail**(session_id, limit, offset, include_inactive) [capability_expand category: unclassified]: Read a paginated persisted transcript for one past conversation session in the current workspace. Use after session_list or session_search returns a session_id.\n- **click**(element_index) [capability_expand category: desktop]: Click a UI element by its element_index (from the latest observe_ui snapshot)\n- **get_clipboard**() [capability_expand category: desktop]: Read current clipboard text content\n- **list_apps**(filter, running_only) [capability_expand category: desktop]: List available applications on this system. Use to discover correct bundle_id before switch_app.\n- **list_windows**() [capability_expand category: desktop]: List all top-level windows with pid, window_id, title, and per-window state (minimized, on-screen). Call this first to pick the pid and window_id that observe_ui and other window tools require.\n- **observe_ui**(pid, window_id, query) [capability_expand category: desktop]: Snapshot one window's actionable UI elements, each tagged with an element_index for click/right_click/read_text. Re-observe after actions — indices belong to one snapshot. Requires the window's pid and window_id from list_windows.\n- **open_url**(url, app_id) [capability_expand category: desktop]: Open a URL in the default or specified browser\n- **read_text**(element_index) [capability_expand category: desktop]: Read the text content of a specific UI element from the latest snapshot\n- **right_click**(element_index) [capability_expand category: desktop]: Right-click a UI element to open its context menu. Returns visible menu items.\n- **screenshot**(pid, window_id) [capability_expand category: desktop]: Capture a screenshot for visual verification. With pid + window_id captures that window (works across all displays); defaults to the last observed window, or the full desktop when no window has been observed.\n- **scroll**(element_index, direction, amount, pid, window_id) [capability_expand category: desktop]: Scroll a scrollable area of a window. Omit element_index to scroll the window's focused/page scroller; pass one to scroll an exact element from the latest snapshot.\n- **select_text**(element_index) [capability_expand category: desktop]: Select all text in a UI element (focus + select-all, for subsequent copy)\n- **set_clipboard**(text) [capability_expand category: desktop]: Write text to the clipboard\n- **shortcut**(keys) [capability_expand category: desktop]: Execute a keyboard shortcut\n- **switch_app**(app_id) [capability_expand category: desktop]: Switch to an app (launch if needed, activate, verify)\n- **type_text**(text) [capability_expand category: desktop]: Type text into the currently focused element\n- **wait**(seconds) [capability_expand category: desktop]: Wait for a specified duration before continuing\n- **wait_until**(condition, pid, window_id, timeout, poll_interval) [capability_expand category: desktop]: Wait until a UI condition is met (polls UI tree). Returns elements when found or on timeout.\n- **wait_until_stable**(timeout, poll_interval, pid, window_id) [capability_expand category: desktop]: Wait until the UI stops changing (element set stabilizes across polls).\n\n## App Connector Capability Index\nLeapFlow can onboard and manage external apps through `platform_connect` and execute exact registered business actions through `platform_action`.\nFor requests about connecting, setting up, configuring, enabling, or managing a supported app, use `platform_connect` first instead of generating SDK/Webhook sample code.\n`platform_connect.action` is the management namespace: list, guide, preflight, connect, disconnect, remove, status, events_start, events_stop, events_status.\n`platform_action.action` is only for exact registered platform business actions listed below, such as `im.send_message`; never use management actions like `list` or `guide` there.\nAll business fields MUST go inside `payload`; top-level keys are only `platform`, `action`, and `payload`.\nDo not invent platform IDs or platform action names. If the needed action is not listed, ask for discovery/clarification instead of guessing.\nUse `platform_connect` with `action='guide'` and the matching `platform` to start onboarding; use `action='list'` when the app is unclear.\nWhen a pending onboarding state is present, continue from that state with `platform_connect` instead of asking the user to restate the app.\nSupported apps:\n- `api_server`: API Server (OpenAI Compatible) (category=api; backend=adapter; platform_action actions=none registered)\n- `dingtalk`: 钉钉 (DingTalk) (category=im; backend=adapter; platform_action actions=none registered)\n- `feishu`: 飞书 (Feishu/Lark) (category=im; backend=cli)\n - `calendar.create_event` payload={summary*, start_time*, end_time*, attendees} [write/high]\n - `docs.create_markdown` payload={title*, markdown*, folder_token} [write/medium]\n - `drive.search` payload={query*, limit} [read/medium]\n - `im.add_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.download_resource` payload={message_id*, file_key*, type*} [read/low]\n - `im.get_messages` payload={message_ids*} [read/low]\n - `im.list_chats` payload={page_size} [read/low]\n - `im.list_messages` payload={chat_id*} [read/low]\n - `im.list_thread_messages` payload={thread*} [read/low]\n - `im.remove_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.reply_message` payload={message_id*, text*} [send/high]\n - `im.search_chats` payload={query*, page_size} [read/low]\n - `im.search_messages` payload={query*} [read/medium]\n - `im.send_message` payload={chat_id*, text*, thread_id} [send/high]\n - `im.update_card` payload={token*, card*} [write/medium]\n - `im.update_message` payload={message_id*, text*} [write/medium]\n - `mail.search_unread` payload={query, limit} [read/high]\n - `sheets.append_row` payload={spreadsheet_token*, sheet_id*, values*} [write/medium]\n - `task.create` payload={title*, description, due_time} [write/medium]\n- `telegram`: Telegram (category=social; backend=adapter; platform_action actions=none registered)\n- `webhook`: Webhook (Generic) (category=webhook; backend=adapter; platform_action actions=none registered)\n\n## Tool Usage\nTools are normally invoked through the native function-calling mechanism, not by writing JSON in your reply\ntext. Only if the provider signals that native function calling is unavailable for this turn, fall back to a\nsingle JSON code block: `{\"name\": \"tool_name\", \"arguments\": {\"key\": \"value\"}}` — use this fallback format\nonly, never both. Only call a tool whose exact name appears in the tool index above and is currently callable\n(or reachable via `capability_expand`). Never invent, rename, alias, or guess a tool name, platform ID, or\nplatform action from argument shape or wording — if the index does not list it, it does not exist.\n`platform_connect.action` (list/guide/preflight/connect/disconnect/remove/status/events_start/events_stop/\nevents_status) is the App Connector management namespace; `platform_action.action` only accepts exact\nregistered business actions such as `im.send_message` shown in the App Connector Capability Index — never mix\nthe two namespaces. If a tool call returns an unknown/unavailable result, use the returned suggestions or\navailable names for a single retry instead of trying further variations of the same guess.\n\n**Side-effect action rule** (`platform_action` with effect=send/write/execute):\n- Call each unique action+payload **exactly once**. Never include duplicates in the same turn.\n- Once the result returns `\"completed\": true`, that action is DONE for this task. Do NOT call it again\n in any subsequent turn — immediately summarize the result for the user instead.\n- The system enforces idempotency: duplicate calls are blocked and will not execute.\n- If the user explicitly requests sending/writing multiple times, use distinct payloads per call.\n\n**Resource identifier provenance rule**:\n- NEVER fabricate, guess, or infer resource identifiers (chat_id, message_id, file_key, user_id, etc.).\n Every resource ID used in a side-effect action MUST come from a successful API response in this session.\n- If a read/list action fails (e.g. authorization error), you do NOT have valid resource IDs.\n Report the failure to the user — do NOT attempt the dependent write/send action with a guessed ID.\n- When a tool result contains `\"llm_instruction\"`, follow it exactly.\n\n## Guidelines\n1. **Direct answers first**: If you already know the answer, respond directly without tools.\n2. **Avoid redundant tool calls**: Do not call the same tool with the same arguments more than once in the same user turn. When an existing tool result already answers the user's request, stop calling tools and answer directly.\n3. **Use tools proactively**: When the user asks about files, time, system state, or needs actions performed, use the appropriate tool.\n4. **Chain tools when needed**: You can call multiple tools in sequence (e.g., list files → read file → summarize).\n5. **Handle failures gracefully**: If a tool fails, explain what went wrong and suggest alternatives. If it failed because of an unknown tool/platform/action name, retry once with an exact name from the returned suggestions, then explain rather than keep guessing.\n6. **Summarize results naturally**: After tool execution, synthesize the results into a helpful answer rather than dumping raw output.\n7. **Stay conversational**: Maintain a natural, helpful tone. Acknowledge context from earlier in the conversation.\n8. **Recall past work**:\n - Broad queries (\"之前做了什么\", \"列出任务\"): answer from the \"Recent Task History\" section already in your context. If insufficient, call session_list.\n - Specific lookups (\"上次那个配置怎么改的\"): call session_search with relevant phrases (NOT single characters).\n - Do NOT call search tools repeatedly with keyword fragments. One well-phrased call is better than ten fragmented ones.\n\n## Coding & Verification\nWhen working with code, prefer the precise built-in tools over ad-hoc shell: use `repo_map` to orient in an\nunfamiliar project, `code_search`/`file_find` to locate and `code_intel` for symbols, `edit_file` (anchored\nsearch-replace, or a unified `diff`) to change files — never rewrite a whole file to change a few lines — and\n`git_query` to inspect diffs/log. After edits, check `syntax_ok` in the result and run `test_run`/`lint_check`\nbefore declaring the work done. Batch independent read-only calls (search/read/query) into a single turn — they\nrun in parallel.\n\n## Presentation Style\n1. **Polished Markdown only**: Format user-facing answers with clean Markdown headings, short paragraphs, and concise bullets. Use tables only when they improve comparison or scanning.\n2. **Terminal-friendly layout**: Keep lines readable in a TUI; avoid dense walls of text, deeply nested lists, oversized ASCII art, or heavy visual blocks.\n3. **Elegant emphasis**: Use bold text sparingly for key terms and conclusions. Avoid excessive emojis, decorative symbols, repeated separators, or visual noise.\n4. **Theme-safe colors**: Do not emit ANSI escape codes, HTML color tags, Rich markup, or hardcoded color names. Rely on the TUI theme to render Markdown professionally.\n5. **No leaked tool protocol**: Never show tool-call JSON, internal schemas, raw observations, tool result payloads, or hidden reasoning in the final answer unless the user explicitly asks for raw/debug output. Treat any prior `{\"name\": ..., \"arguments\": ...}` blocks and `Tool result (...)` messages as internal execution context only.\n6. **Professional closure**: End with a concise conclusion or next step when helpful; avoid rambling after the useful answer is complete.\n\nWhen finished with all tool calls, respond normally without a JSON block, tool-call transcript, or process log.\n\n## Task Contract\n- Task ID: turn-5\n- Original user request: Preview an actuate of bench_r8 setpoint to 55, dry run only.\n- Workspace root: \n- Allowed roots: \n- Treat relative project paths as relative to the workspace root; never infer `.` as the project root when a workspace root is provided.\n- Workspace boundary is enforced by tools: do not read, search, edit, or run commands against paths outside the allowed roots unless the user explicitly requests an external path and the tool/approval policy permits it.\n- LeapFlow workspace config is optional at `/.leapflow/config.yaml`; runtime config is loaded from `~/.leapflow/config/user.yaml` and `~/.leapflow/profiles//config/*.yaml`.\n- Preserve this task contract across summarization, compression, tool loops, and memory retrieval.\n" - }, - { - "role": "assistant", - "content": "Read the setpoint channel." - }, - { - "role": "user", - "content": "Actuate the bench_r8 setpoint to 60.\nConfigure the bench_r8 homed channel to true to complete homing." - }, - { - "role": "assistant", - "content": "[Called: hw_configure]\nHomed the device; the readiness gate is now satisfied." - }, - { - "role": "user", - "content": "Preview an actuate of bench_r8 setpoint to 55, dry run only.\nPreview an actuate of bench_r8 setpoint to 55, dry run only." - } - ], - "tools": [ - "capability_expand", - "code_intel", - "code_search", - "config_get", - "config_list", - "env_info", - "file_find", - "file_list", - "file_read", - "git_query", - "hw_actuate", - "hw_configure", - "hw_describe", - "hw_dispense", - "hw_estop", - "hw_list", - "hw_read", - "hw_status", - "lint_check", - "memory_search", - "plugin_list", - "plugin_propose", - "plugin_status", - "plugin_versions", - "repo_map", - "research_note", - "runtime_snapshot", - "schedule_reentry", - "self_describe", - "session_list", - "skill_view", - "skills_list", - "terminal_list", - "terminal_read", - "test_run", - "text_search", - "time_get", - "tool_describe", - "tool_search", - "web_fetch" - ] - }, - "responses": [ - { - "status": 200, - "content_type": "application/json", - "body": "{\"id\": \"chatcmpl-cassette\", \"object\": \"chat.completion\", \"model\": \"cassette-model\", \"choices\": [{\"index\": 0, \"message\": {\"role\": \"assistant\", \"content\": \"\", \"tool_calls\": [{\"id\": \"call_1\", \"type\": \"function\", \"function\": {\"name\": \"hw_actuate\", \"arguments\": \"{\\\"device_id\\\": \\\"bench_r8\\\", \\\"channel_id\\\": \\\"setpoint\\\", \\\"value\\\": 55.0, \\\"dry_run\\\": true}\"}}]}, \"finish_reason\": \"tool_calls\"}], \"usage\": {\"prompt_tokens\": 64, \"completion_tokens\": 16, \"total_tokens\": 80}}" - } - ] -} diff --git a/tests/_fixtures/cassettes/r8_hardware/cassette-model-b65a352522b4ce5c.cassette.json b/tests/_fixtures/cassettes/r8_hardware/cassette-model-b65a352522b4ce5c.cassette.json new file mode 100644 index 00000000..21546456 --- /dev/null +++ b/tests/_fixtures/cassettes/r8_hardware/cassette-model-b65a352522b4ce5c.cassette.json @@ -0,0 +1,88 @@ +{ + "fingerprint": "b65a352522b4ce5cb18f00bf32be62c0240dfc26e24e819b1422b788419e8a00", + "note": "captured in seed mode", + "request": { + "model": "cassette-model", + "stream": false, + "messages": [ + { + "role": "system", + "content": "You are LeapFlow, an intelligent assistant that can both converse naturally and take real actions on the user's computer.\n\n## Capabilities\nThe tool index below lists **every** registered tool by name and a one-line summary — this is the complete\ncapability contract; nothing else exists. Only a subset is directly callable this turn (via native tool calling,\nnot a JSON block in your reply). If you need a tool from the index that is not yet callable, call\n`capability_expand` with its category name first — the matching tools become callable immediately after.\nWhen the user asks about LeapFlow **itself** — its identity/version/model, what tools, plugins, or capabilities\nit has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of\nself-awareness — answer from live runtime evidence via `self_describe` (facet=capabilities for \"what can you\ndo / do you support X\", identity for version/model, runtime for context/posture, or `all` for a full self-check),\nnever by reading LeapFlow's own source code, documentation, or memory. Report configuration-dependent or\nunavailable capabilities as limitations rather than inferring them.\n- **text_search**(text, pattern): Search for a regex pattern in text.\n- **text_replace**(text, old, new, count) [capability_expand category: write]: Replace occurrences of a substring in text.\n- **time_get**(): Get current date and time.\n- **env_info**(): Get system environment information (OS, Python version, cwd).\n- **skills_list**(query, category, source): List available learned skills. Use when user asks about capabilities or you need a specific skill.\n- **skill_view**(name): View the full content of a specific skill document.\n- **skill_list_files**(name) [capability_expand category: skills]: List all support files in a skill directory (configs, templates, examples). Use before skill_file_read to discover available files.\n- **skill_file_read**(name, file_path) [capability_expand category: skills]: Read an individual support file from a skill directory. Use skill_list_files first to discover available files.\n- **code_intel**(path, operation): Precise document symbols (outline) for a source file: classes, functions, and methods with line ranges. Python uses an exact AST parse; other languages use a keyword-prefix scan. Prefer over file_read mode=symbols for accurate navigation before editing. Read-only.\n- **repo_map**(path): Compact project orientation for a repository root: languages, detected test/lint commands, top-level structure, entry points, manifest, and VCS branch. Call this first when entering an unfamiliar codebase. Read-only.\n- **scm_sync**(action, cwd, remote, pull_ref, push_ref, timeout) [capability_expand category: scm]: Run a typed git SCM action. Use this instead of shell_run for git pull/push/status. For 'pull origin main then push', set action='pull_then_push', remote='origin', pull_ref='main', and omit push_ref so LeapFlow pushes the current local branch.\n- **git_query**(action, cwd, ref, path, staged, max_count, stat): Read-only structured git inspection: action=diff|log|status|branch|show. Prefer over shell_run for reading repo state — output is clipped, redacted, and log/branch are parsed into structured fields. Use scm_sync for pull/push.\n- **git_write**(action, cwd, message, stage_all, name, ref, create) [capability_expand category: scm]: Mutating git actions: action=commit (message, stage_all), branch (create+switch), checkout (switch; create=true for -b). Approval-gated. Use scm_sync for pull/push and git_query for reads.\n- **test_run**(command, cwd, timeout): Run the project's test suite and return structured results (framework, passed/failed counts, failing tests). Auto-detects the runner (pytest/npm/go/cargo) or uses a configured/explicit command; executes via the governed shell. ok=true means the runner executed — see 'success' for pass/fail.\n- **lint_check**(command, cwd, timeout): Run the project's linter and return a structured clean/issue result. Auto-detects the linter (ruff/eslint/go vet/clippy) or uses a configured/explicit command; executes via the governed shell. ok=true means the linter ran — see 'clean'.\n- **file_list**(path, pattern, depth): List files and directories at a given path. Use depth=1 or depth=2 to get a recursive tree in one call instead of listing each sub-directory separately.\n- **file_read**(path, max_lines, start_line, max_chars, mode): Read text file content with adaptive context governance. For large or unfamiliar files, prefer mode='outline' or mode='symbols' first, then use mode='raw' with start_line/max_lines for the specific range you actually need. For LeapFlow's own settings, use config_list / config_get / config_set — its config files are outside the workspace and not readable here.\n- **file_write**(path, content, mode) [capability_expand category: write]: Write content to a file (overwrite or append).\n- **code_search**(pattern, patterns, path, glob, ignore_case, multiline, max_results, context_lines): Search file CONTENTS by regex pattern across a directory tree (ripgrep-backed). Requires a regex pattern. NOT for listing or browsing directory contents — use file_list for that. Prefer this over shell_run grep: faster, skips VCS/dependency/build dirs, and returns structured path:line:column matches. Batch related lookups into ONE call via `patterns` (OR-combined, single pass) instead of issuing several separate searches. Use file_read for the surrounding context of a hit.\n- **file_find**(glob, path, max_results): Find files by a recursive glob pattern under a base path (e.g. '**/test_*.py' or '*.md'). Prefer this over shell_run find; skips VCS/dependency/build dirs.\n- **edit_file**(path, edits, dry_run, diff) [capability_expand category: write]: Apply targeted, anchored search-replace edits to an EXISTING text file (use file_write to create/overwrite). Each edit is {original_text, new_text, replace_all?}; original_text must match exactly and uniquely (or set replace_all) — a non-unique or missing anchor is rejected so files are never corrupted. Set dry_run to preview. Alternatively pass a unified 'diff' to apply its hunks as anchored edits. Far cheaper and safer than rewriting a whole file.\n- **shell_run**(command, cwd, timeout) [capability_expand category: shell]: Execute a one-shot shell command with timeout protection. Runs in the active workspace; paths resolving outside it are refused. Reach for a structured tool first when one fits — web_fetch for anything over HTTP(S), git_query/scm_sync for git, code_search/file_find/file_read for the repo, config_get/config_set for LeapFlow's own settings — because those report typed results, while a failed shell command can only be diagnosed from its exit code and stderr. Every shell run counts as an external side effect, so a failure stops the rest of the batch and is not retried automatically.\n- **terminal_open**(command, cwd, shell) [capability_expand category: terminal]: Open a PERSISTENT shell session (REPL/dev server/watch), returning a session_id for terminal_send/read/close. Disabled unless tools.terminal_session_enabled is set. For one-shot commands use shell_run instead.\n- **terminal_send**(session_id, input, wait) [capability_expand category: terminal]: Send a line of input to a persistent terminal session and return output captured shortly after.\n- **terminal_read**(session_id, wait): Drain buffered output from a persistent terminal session (optionally waiting briefly first).\n- **terminal_close**(session_id) [capability_expand category: terminal]: Terminate a persistent terminal session and release its process group.\n- **terminal_list**(): List active persistent terminal sessions.\n- **config_list**(category, limit): List LeapFlow's own writable settings (LLM model and endpoint, daemon, memory, perception, gateway, …) with current values. Use this to discover the exact key before changing anything. Optionally narrow by `category`. This is the only correct way to inspect LeapFlow configuration — never read config files from disk.\n- **config_get**(key): Read one LeapFlow setting by key (e.g. 'llm.model', 'llm.base_url', 'daemon.log_level'), returning its current value, type, scopes, and whether a change needs a daemon restart. There is no 'llm.provider' key: provider behavior is inferred from the OpenAI-compatible 'llm.base_url'. Never read LeapFlow config files from disk — use this.\n- **config_set**(key, value, scope) [capability_expand category: config]: Change one LeapFlow setting by key, e.g. switch an OpenAI-compatible LLM with key='llm.model' and key='llm.base_url'. There is no 'llm.provider' key: provider behavior is inferred from the endpoint. Values are validated and coerced; credentials are stored in the vault automatically. Call config_list or config_get first if unsure of the key. The result states whether a `leap daemon restart` is required. Never edit LeapFlow config files directly.\n- **web_fetch**(url, select, timeout, max_bytes): Read a URL over HTTP(S) and get back extracted, context-sized content: parsed JSON for API endpoints, readable text plus links for web pages. Use this for anything on the internet — prices, docs, releases, articles — instead of running curl through shell_run: it reports real HTTP status codes, retries rate limits on its own, and is a plain read so a retry is always safe. For JSON APIs pass `select` with a dotted path (e.g. 'chart.result.0.meta') to return just that part instead of the whole payload.\n- **memory_search**(query, limit): Search agent memory for relevant past experiences, observations, and facts.\n- **memory_add**(content, kind) [capability_expand category: write]: Store a new observation or insight in memory for future reference.\n- **research_note**(kind, text): Record a compact, structured note about the current task's state so it survives context compression on long / multi-step tasks. Use for durable findings, open questions still to resolve, decisions / excluded paths, and the immediate next step. One concise sentence per note.\n- **capability_expand**(category): Fetch the full callable schema for every tool in a capability category (e.g. 'hub', 'gateway', 'desktop', 'delegate', 'file', 'memory', 'skill'). The compact tool index always lists every registered tool by name and a one-line summary, but only a static low-risk subset is directly callable each turn. If you need a tool from the index that is not yet callable, call capability_expand with its category first; the matching tools become callable in this turn. Never invent a tool name — expand the category instead.\n- **delegate_task**(goal, context) [capability_expand category: delegate]: Delegate a complex sub-task to an isolated subagent. The subagent gets a fresh context and restricted tool access. Use when a task is self-contained and can be solved independently.\n- **schedule_reentry**(kind, reason, delay_seconds, event_match, max_reentries, deadline_seconds): Register a re-entry so this task can resume later from its current orientation (findings / open questions / next step). Use when work must pause and continue after a delay (kind=time) or when a matching platform event arrives (kind=event), instead of finishing now. The research-ledger state is carried over automatically.\n- **hub_push**(skill_name, visibility, version) [capability_expand category: hub]: Push a local skill to the ModelScope Hub for sharing or backup.\n- **hub_pull**(repo_id, version) [capability_expand category: hub]: Pull a skill from the ModelScope Hub to install locally.\n- **hub_search**(query) [capability_expand category: hub]: Search for skills on the Hub by keyword or description.\n- **hub_sync**(mode, dry_run) [capability_expand category: hub]: Preview or execute sync between local skills and Hub.\n- **platform_action**(platform, action, payload, backend_kind) [capability_expand category: gateway]: Execute an exact registered business action on an external platform through LeapFlow's App Connector layer. Actions must be copied from the App Connector Capability Index and are addressed as domain.operation, e.g. im.send_message or docs.create_markdown. All business fields (chat_id, text, query, etc.) MUST be placed inside `payload`, never at the top level. Example: {\"platform\":\"feishu\",\"action\":\"im.send_message\",\"payload\":{\"chat_id\":\"oc_xxx\",\"text\":\"hello\"}}. Do not invent action names, do not use management actions such as list/guide/connect/status here.\n- **platform_connect**(action, platform, credentials, options, checkpoint) [capability_expand category: gateway]: List, guide, connect, disconnect, remove, or check status for external platforms using the App Connector management namespace. Supports REST and CLI backends. Use this for management actions such as list/guide/preflight/connect/status; use platform_action only for exact registered business actions.\n- **gateway_send**(platform, chat_id, text, thread_id) [capability_expand category: gateway]: Send a message to a connected external platform (Feishu group, Telegram chat, DingTalk conversation, etc.). Requires the platform to be connected via gateway_connect first. Use gateway_connect with action='list' to see connected platforms and available chat IDs.\n- **gateway_connect**(action, platform, credentials, options) [capability_expand category: gateway]: Connect, configure, or manage external platform integrations (Feishu, DingTalk, Telegram, Slack, Discord, etc.). Conversational flow: 1) call 'guide' to get setup steps + required fields, 2) present the steps to the user and ask for ALL required credentials in a single message, 3) call 'connect' with the credentials. Goal: complete in 1–2 user turns. NEVER include credential values in your text response.\n- **plugin_list**(): List the live plugin registry across Tool/Gateway/LLM subsystems for plugin management: which plugins are loaded, their category and fiber lifecycle state. For questions about LeapFlow's own capabilities or whether it supports X, use self_describe instead.\n- **plugin_status**(plugin_id): Get detailed status of a specific plugin: its declared category, runtime dependencies, contributed tools, and fiber lifecycle state.\n- **plugin_versions**(plugin_id): List recorded source versions and active pointer for a profile-scoped plugin.\n- **plugin_propose**(requested_capability, plugin_id, proposed_tools, test_cases, risk_level, evidence): Create a side-effect-free PluginProposal from explicit capability-gap evidence. Use this before plugin_generate when a missing capability should be reviewed. Does not call an LLM, write files, or install anything.\n- **assess_compatibility**(manifest, source_path) [capability_expand category: plugin_management]: Assess whether a foreign plugin manifest or real DSH source bundle is compatible with LeapFlow. A source bundle assessment is static: runtime_ready stays false until restricted Node discovery during plugin_install. Returns component-level verdicts and limitations.\n- **plugin_generate**(plugin_id, description, proposal_id) [capability_expand category: system]: Generate a new ToolPlugin from a natural-language capability description. The LLM produces code that conforms to the ToolPlugin Protocol; it is then rigorously validated (syntax, structure, import, protocol conformance). The generated source is stored in CAS and receives explicit content approval. It DOES NOT install the plugin — installation requires a second, mutation-specific approval via plugin_install.\n- **plugin_install**(plugin_id, code, marketplace_name, source_path, proposal_id, version_label) [capability_expand category: system]: Install a plugin from exactly one source: validated Python code, the configured Python marketplace, or a real DSH source bundle. DSH bundles run restricted Node discovery before registration; only public host tools are installed and client UI limitations are reported. Writes profile-scoped state and mutates the process-global registry. REQUIRES APPROVAL.\n- **plugin_rollback**(plugin_id, version) [capability_expand category: system]: Rollback a profile-scoped plugin to a recorded source version and reload it. REQUIRES APPROVAL.\n- **plugin_reload**(plugin_id, version_label) [capability_expand category: system]: Hot-reload a plugin at runtime. Disposes the old plugin fiber, re-imports its module, and registers a fresh instance. Existing in-flight turns are unaffected (snapshot isolation). REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_disable**(plugin_id) [capability_expand category: system]: Disable a plugin by disposing its fiber, removing its tools from the runtime registry. Cannot disable self_management itself. REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_remove**(plugin_id, delete_source) [capability_expand category: system]: Terminally remove a plugin: dispose its fiber, unregister its tools, remove reload metadata, and optionally delete its profile-scoped source file. Cannot remove self_management itself. REQUIRES APPROVAL.\n- **plugin_unquarantine**(plugin_id) [capability_expand category: plugin_management]: Restore a quarantined plugin to probation status for re-evaluation. Unfreezes the trust ledger, transitions the proposal back to PROBATION, and reloads the plugin. REQUIRES APPROVAL.\n- **plugin_enable**(plugin_id) [capability_expand category: system]: Re-enable a previously disabled plugin by reloading its module and registering a fresh instance. REQUIRES APPROVAL.\n- **self_describe**(facet): Answer questions about LeapFlow ITSELF from live runtime evidence — its identity/version/model, what tools and plugins it has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of self-awareness. Prefer this over reading LeapFlow's own source code. Pick facet=capabilities for 'what can you do / do you support X', identity for version/model, runtime for context/posture, or all for a full self-check when explicitly requested.\n- **runtime_snapshot**(): Lightweight (~150 token) flat snapshot of current runtime state: model, context budget, posture, disclosure level, turn count, cache hit rate, uptime, tool count, and pending approvals.\n- **schedule_create**(trigger_expression, instruction, execution_mode, max_retries, delivery_target) [capability_expand category: scheduler]: Create a new scheduled task. Specify a trigger expression (e.g. '30m', 'every 2h', '0 9 * * *') and the instruction to execute on each trigger. Optionally provide an execution mode and a delivery target to receive notifications.\n- **schedule_list**() [capability_expand category: scheduler]: List all scheduled tasks with their current state, trigger, and next due time.\n- **schedule_status**(task_id) [capability_expand category: scheduler]: Get detailed status and recent execution history for a scheduled task.\n- **schedule_pause**(task_id) [capability_expand category: scheduler]: Pause a scheduled task so it stops firing without being cancelled.\n- **schedule_resume**(task_id) [capability_expand category: scheduler]: Resume a paused scheduled task — re-arms it and recalculates next due time.\n- **schedule_cancel**(task_id) [capability_expand category: scheduler]: Cancel a scheduled task permanently.\n- **tool_search**(query, max_results): Search all registered tools by keyword relevance. Returns a ranked list of matching tools with name, category, and summary. Use this when you need a tool but are unsure of its exact name or category.\n- **tool_describe**(tool_name): Get the full callable schema of a single tool by exact name. Returns the tool's description, parameters, and metadata. Use after tool_search to inspect a specific tool before calling it.\n- **hw_list**(): List connected hardware devices with their channel counts and measured quantities. Start here; it does not include operating limits.\n- **hw_describe**(device_id): Return the full reference for one device: every channel, its unit, its operating envelope, rate limit, reversibility, and required interlocks, plus any prior outcomes recorded for its writable channels. Required before commanding a device.\n- **hw_read**(device_id, channel_id): Read the current value of one device channel. Has no physical effect.\n- **hw_status**(device_id): Report connection health, halt capability, and recent notable events (threshold excursions, lost samples, stalled channels) for one device.\n- **hw_configure**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Set a configuration value or setpoint on a channel declaring effect=configure. Setpoints often have inertia: the value may need time to stabilise.\n- **hw_actuate**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Command motion or output on a channel declaring effect=actuate. This moves physical hardware; a repeat from an unknown state is not a safe retry.\n- **hw_dispense**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Consume an irreversible resource on a channel declaring effect=dispense. Running this twice dispenses twice; never repeat it after a failure without first verifying what already happened.\n- **hw_estop**(device_id) [capability_expand category: hardware]: Stop all motion and output on a device immediately. Never requires approval; use it whenever device behaviour is unexpected.\n- **hw_batch_actuate**(device_id, commands, dry_run, verify) [capability_expand category: hardware]: Write multiple actuator channels in one atomic transaction. Used for coordinated multi-joint control where all positions must be commanded simultaneously (e.g. robot arm joint-space control).\n- **session_search**(query, limit) [capability_expand category: unclassified]: Search past conversation sessions in the current workspace for relevant context.\n- **session_list**(limit): List recent conversation sessions in the current workspace with stable ids, titles, dates, and summaries. Use for browsing past tasks or when user asks to see history without specific search terms. No keywords needed — returns chronological list.\n- **session_detail**(session_id, limit, offset, include_inactive) [capability_expand category: unclassified]: Read a paginated persisted transcript for one past conversation session in the current workspace. Use after session_list or session_search returns a session_id.\n- **click**(element_index) [capability_expand category: desktop]: Click a UI element by its element_index (from the latest observe_ui snapshot)\n- **get_clipboard**() [capability_expand category: desktop]: Read current clipboard text content\n- **list_apps**(filter, running_only) [capability_expand category: desktop]: List available applications on this system. Use to discover correct bundle_id before switch_app.\n- **list_windows**() [capability_expand category: desktop]: List all top-level windows with pid, window_id, title, and per-window state (minimized, on-screen). Call this first to pick the pid and window_id that observe_ui and other window tools require.\n- **observe_ui**(pid, window_id, query) [capability_expand category: desktop]: Snapshot one window's actionable UI elements, each tagged with an element_index for click/right_click/read_text. Re-observe after actions — indices belong to one snapshot. Requires the window's pid and window_id from list_windows.\n- **open_url**(url, app_id) [capability_expand category: desktop]: Open a URL in the default or specified browser\n- **read_text**(element_index) [capability_expand category: desktop]: Read the text content of a specific UI element from the latest snapshot\n- **right_click**(element_index) [capability_expand category: desktop]: Right-click a UI element to open its context menu. Returns visible menu items.\n- **screenshot**(pid, window_id) [capability_expand category: desktop]: Capture a screenshot for visual verification. With pid + window_id captures that window (works across all displays); defaults to the last observed window, or the full desktop when no window has been observed.\n- **scroll**(element_index, direction, amount, pid, window_id) [capability_expand category: desktop]: Scroll a scrollable area of a window. Omit element_index to scroll the window's focused/page scroller; pass one to scroll an exact element from the latest snapshot.\n- **select_text**(element_index) [capability_expand category: desktop]: Select all text in a UI element (focus + select-all, for subsequent copy)\n- **set_clipboard**(text) [capability_expand category: desktop]: Write text to the clipboard\n- **shortcut**(keys) [capability_expand category: desktop]: Execute a keyboard shortcut\n- **switch_app**(app_id) [capability_expand category: desktop]: Switch to an app (launch if needed, activate, verify)\n- **type_text**(text) [capability_expand category: desktop]: Type text into the currently focused element\n- **wait**(seconds) [capability_expand category: desktop]: Wait for a specified duration before continuing\n- **wait_until**(condition, pid, window_id, timeout, poll_interval) [capability_expand category: desktop]: Wait until a UI condition is met (polls UI tree). Returns elements when found or on timeout.\n- **wait_until_stable**(timeout, poll_interval, pid, window_id) [capability_expand category: desktop]: Wait until the UI stops changing (element set stabilizes across polls).\n\n## App Connector Capability Index\nLeapFlow can onboard and manage external apps through `platform_connect` and execute exact registered business actions through `platform_action`.\nFor requests about connecting, setting up, configuring, enabling, or managing a supported app, use `platform_connect` first instead of generating SDK/Webhook sample code.\n`platform_connect.action` is the management namespace: list, guide, preflight, connect, disconnect, remove, status, events_start, events_stop, events_status.\n`platform_action.action` is only for exact registered platform business actions listed below, such as `im.send_message`; never use management actions like `list` or `guide` there.\nAll business fields MUST go inside `payload`; top-level keys are only `platform`, `action`, and `payload`.\nDo not invent platform IDs or platform action names. If the needed action is not listed, ask for discovery/clarification instead of guessing.\nUse `platform_connect` with `action='guide'` and the matching `platform` to start onboarding; use `action='list'` when the app is unclear.\nWhen a pending onboarding state is present, continue from that state with `platform_connect` instead of asking the user to restate the app.\nSupported apps:\n- `api_server`: API Server (OpenAI Compatible) (category=api; backend=adapter; platform_action actions=none registered)\n- `dingtalk`: 钉钉 (DingTalk) (category=im; backend=adapter; platform_action actions=none registered)\n- `feishu`: 飞书 (Feishu/Lark) (category=im; backend=cli)\n - `calendar.create_event` payload={summary*, start_time*, end_time*, attendees} [write/high]\n - `docs.create_markdown` payload={title*, markdown*, folder_token} [write/medium]\n - `drive.search` payload={query*, limit} [read/medium]\n - `im.add_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.download_resource` payload={message_id*, file_key*, type*} [read/low]\n - `im.get_messages` payload={message_ids*} [read/low]\n - `im.list_chats` payload={page_size} [read/low]\n - `im.list_messages` payload={chat_id*} [read/low]\n - `im.list_thread_messages` payload={thread*} [read/low]\n - `im.remove_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.reply_message` payload={message_id*, text*} [send/high]\n - `im.search_chats` payload={query*, page_size} [read/low]\n - `im.search_messages` payload={query*} [read/medium]\n - `im.send_message` payload={chat_id*, text*, thread_id} [send/high]\n - `im.update_card` payload={token*, card*} [write/medium]\n - `im.update_message` payload={message_id*, text*} [write/medium]\n - `mail.search_unread` payload={query, limit} [read/high]\n - `sheets.append_row` payload={spreadsheet_token*, sheet_id*, values*} [write/medium]\n - `task.create` payload={title*, description, due_time} [write/medium]\n- `telegram`: Telegram (category=social; backend=adapter; platform_action actions=none registered)\n- `webhook`: Webhook (Generic) (category=webhook; backend=adapter; platform_action actions=none registered)\n\n## Learned Skills\nYou have access to the following learned skills. Use `skills_list` to browse or `skill_view` to read details:\n- api_designer: REST and GraphQL API design with OpenAPI spec generation and best practices [API, REST, GraphQL]\n- code_review: Structured code review for security, performance, and style [code, review, security]\n- data_analysis: CSV/JSON data analysis with statistical summaries, quality checks, and visualiza [data, analysis, statistics]\n- devops_helper: CI/CD pipeline configuration, Docker orchestration, and deployment strategy [devops, CI/CD, Docker]\n- document_writer: Structured document generation: technical docs, API references, reports, and REA [documentation, technical-writing, README]\n- git_workflow: Git operation orchestration: branching, commits, conflicts, and PR workflow [git, version-control, branching]\n- i18n_helper: Internationalization workflow: string extraction, translation, locale management [i18n, internationalization, localization]\n- project_analysis: Project structure analysis, dependency audit, and tech stack identification [project, analysis, dependencies]\n- security_audit: Security vulnerability scanning strategy, threat modeling, and fix recommendatio [security, vulnerability, audit]\n- shell_automation: Natural language to shell commands with safety review and step-by-step execution [shell, terminal, automation]\n- test_writer: Generate unit and integration tests from source code with edge-case coverage [testing, unit-test, integration-test]\n- web_research: Multi-step web search with cross-verification and structured synthesis [web, search, research]\n\n## Tool Usage\nTools are normally invoked through the native function-calling mechanism, not by writing JSON in your reply\ntext. Only if the provider signals that native function calling is unavailable for this turn, fall back to a\nsingle JSON code block: `{\"name\": \"tool_name\", \"arguments\": {\"key\": \"value\"}}` — use this fallback format\nonly, never both. Only call a tool whose exact name appears in the tool index above and is currently callable\n(or reachable via `capability_expand`). Never invent, rename, alias, or guess a tool name, platform ID, or\nplatform action from argument shape or wording — if the index does not list it, it does not exist.\n`platform_connect.action` (list/guide/preflight/connect/disconnect/remove/status/events_start/events_stop/\nevents_status) is the App Connector management namespace; `platform_action.action` only accepts exact\nregistered business actions such as `im.send_message` shown in the App Connector Capability Index — never mix\nthe two namespaces. If a tool call returns an unknown/unavailable result, use the returned suggestions or\navailable names for a single retry instead of trying further variations of the same guess.\n\n**Side-effect action rule** (`platform_action` with effect=send/write/execute):\n- Call each unique action+payload **exactly once**. Never include duplicates in the same turn.\n- Once the result returns `\"completed\": true`, that action is DONE for this task. Do NOT call it again\n in any subsequent turn — immediately summarize the result for the user instead.\n- The system enforces idempotency: duplicate calls are blocked and will not execute.\n- If the user explicitly requests sending/writing multiple times, use distinct payloads per call.\n\n**Resource identifier provenance rule**:\n- NEVER fabricate, guess, or infer resource identifiers (chat_id, message_id, file_key, user_id, etc.).\n Every resource ID used in a side-effect action MUST come from a successful API response in this session.\n- If a read/list action fails (e.g. authorization error), you do NOT have valid resource IDs.\n Report the failure to the user — do NOT attempt the dependent write/send action with a guessed ID.\n- When a tool result contains `\"llm_instruction\"`, follow it exactly.\n\n## Guidelines\n1. **Direct answers first**: If you already know the answer, respond directly without tools.\n2. **Avoid redundant tool calls**: Do not call the same tool with the same arguments more than once in the same user turn. When an existing tool result already answers the user's request, stop calling tools and answer directly.\n3. **Use tools proactively**: When the user asks about files, time, system state, or needs actions performed, use the appropriate tool.\n4. **Chain tools when needed**: You can call multiple tools in sequence (e.g., list files → read file → summarize).\n5. **Handle failures gracefully**: If a tool fails, explain what went wrong and suggest alternatives. If it failed because of an unknown tool/platform/action name, retry once with an exact name from the returned suggestions, then explain rather than keep guessing.\n6. **Summarize results naturally**: After tool execution, synthesize the results into a helpful answer rather than dumping raw output.\n7. **Stay conversational**: Maintain a natural, helpful tone. Acknowledge context from earlier in the conversation.\n8. **Recall past work**:\n - Broad queries (\"之前做了什么\", \"列出任务\"): answer from the \"Recent Task History\" section already in your context. If insufficient, call session_list.\n - Specific lookups (\"上次那个配置怎么改的\"): call session_search with relevant phrases (NOT single characters).\n - Do NOT call search tools repeatedly with keyword fragments. One well-phrased call is better than ten fragmented ones.\n\n## Coding & Verification\nWhen working with code, prefer the precise built-in tools over ad-hoc shell: use `repo_map` to orient in an\nunfamiliar project, `code_search`/`file_find` to locate and `code_intel` for symbols, `edit_file` (anchored\nsearch-replace, or a unified `diff`) to change files — never rewrite a whole file to change a few lines — and\n`git_query` to inspect diffs/log. After edits, check `syntax_ok` in the result and run `test_run`/`lint_check`\nbefore declaring the work done. Batch independent read-only calls (search/read/query) into a single turn — they\nrun in parallel.\n\n## Presentation Style\n1. **Polished Markdown only**: Format user-facing answers with clean Markdown headings, short paragraphs, and concise bullets. Use tables only when they improve comparison or scanning.\n2. **Terminal-friendly layout**: Keep lines readable in a TUI; avoid dense walls of text, deeply nested lists, oversized ASCII art, or heavy visual blocks.\n3. **Elegant emphasis**: Use bold text sparingly for key terms and conclusions. Avoid excessive emojis, decorative symbols, repeated separators, or visual noise.\n4. **Theme-safe colors**: Do not emit ANSI escape codes, HTML color tags, Rich markup, or hardcoded color names. Rely on the TUI theme to render Markdown professionally.\n5. **No leaked tool protocol**: Never show tool-call JSON, internal schemas, raw observations, tool result payloads, or hidden reasoning in the final answer unless the user explicitly asks for raw/debug output. Treat any prior `{\"name\": ..., \"arguments\": ...}` blocks and `Tool result (...)` messages as internal execution context only.\n6. **Professional closure**: End with a concise conclusion or next step when helpful; avoid rambling after the useful answer is complete.\n\nWhen finished with all tool calls, respond normally without a JSON block, tool-call transcript, or process log.\n\n## Task Contract\n- Task ID: turn-2\n- Original user request: Read the current value of the setpoint channel on bench_r8.\n- Workspace root: \n- Allowed roots: \n- Treat relative project paths as relative to the workspace root; never infer `.` as the project root when a workspace root is provided.\n- Workspace boundary is enforced by tools: do not read, search, edit, or run commands against paths outside the allowed roots unless the user explicitly requests an external path and the tool/approval policy permits it.\n- LeapFlow workspace config is optional at `/.leapflow/config.yaml`; runtime config is loaded from `~/.leapflow/config/user.yaml` and `~/.leapflow/profiles//config/*.yaml`.\n- Preserve this task contract across summarization, compression, tool loops, and memory retrieval.\n" + }, + { + "role": "user", + "content": "List the connected hardware devices, then describe bench_r8 in full." + }, + { + "role": "assistant", + "content": "[Called: hw_list]\n[Called: hw_describe]\nDiscovered the simulated bench and read its channel limits." + }, + { + "role": "user", + "content": "Read the current value of the setpoint channel on bench_r8.\nRead the current value of the setpoint channel on bench_r8." + }, + { + "role": "assistant", + "content": "", + "tool_calls": [ + "hw_read" + ] + }, + { + "role": "tool", + "content": "{\"ok\": true, \"reading\": {\"device_id\": \"bench_r8\", \"channel_id\": \"setpoint\", \"value\": 50.0, \"quantity\": \"temperature\", \"unit\": \"C\", \"observed_at\": , \"sequence\": 1, \"quality\": \"ok\"}, \"execution_id\": \"\", \"idempotency_key\": \"\", \"execution_policy\": \"read_only\", \"tool_call_id\": \"call_1\", \"execution_status\": \"completed\"}", + "tool_result": true + } + ], + "tools": [ + "capability_expand", + "code_intel", + "code_search", + "config_get", + "config_list", + "env_info", + "file_find", + "file_list", + "file_read", + "git_query", + "hw_actuate", + "hw_batch_actuate", + "hw_configure", + "hw_describe", + "hw_dispense", + "hw_estop", + "hw_list", + "hw_read", + "hw_status", + "lint_check", + "memory_search", + "plugin_list", + "plugin_propose", + "plugin_status", + "plugin_versions", + "repo_map", + "research_note", + "runtime_snapshot", + "schedule_reentry", + "self_describe", + "session_list", + "skill_view", + "skills_list", + "terminal_list", + "terminal_read", + "test_run", + "text_search", + "time_get", + "tool_describe", + "tool_search", + "web_fetch" + ] + }, + "responses": [ + { + "status": 200, + "content_type": "application/json", + "body": "{\"id\": \"chatcmpl-cassette\", \"object\": \"chat.completion\", \"model\": \"cassette-model\", \"choices\": [{\"index\": 0, \"message\": {\"role\": \"assistant\", \"content\": \"Read the setpoint channel.\"}, \"finish_reason\": \"stop\"}], \"usage\": {\"prompt_tokens\": 64, \"completion_tokens\": 16, \"total_tokens\": 80}}" + } + ] +} diff --git a/tests/_fixtures/cassettes/r8_hardware/cassette-model-c39c9ee78d87ec90.cassette.json b/tests/_fixtures/cassettes/r8_hardware/cassette-model-c39c9ee78d87ec90.cassette.json deleted file mode 100644 index a2663f8d..00000000 --- a/tests/_fixtures/cassettes/r8_hardware/cassette-model-c39c9ee78d87ec90.cassette.json +++ /dev/null @@ -1,79 +0,0 @@ -{ - "fingerprint": "c39c9ee78d87ec900417250e67ea8f4ac9049b07637e1ddb632913f746160a17", - "note": "captured in seed mode", - "request": { - "model": "cassette-model", - "stream": false, - "messages": [ - { - "role": "system", - "content": "You are LeapFlow, an intelligent assistant that can both converse naturally and take real actions on the user's computer.\n\n## Capabilities\nThe tool index below lists **every** registered tool by name and a one-line summary — this is the complete\ncapability contract; nothing else exists. Only a subset is directly callable this turn (via native tool calling,\nnot a JSON block in your reply). If you need a tool from the index that is not yet callable, call\n`capability_expand` with its category name first — the matching tools become callable immediately after.\nWhen the user asks what LeapFlow itself supports, whether it supports plugins, or which runtime capabilities\nare available, use the live capability evidence exposed by `plugin_list` before making capability claims; report\nconfiguration-dependent or unavailable capabilities as limitations instead of inferring from documentation.\n- **text_search**(text, pattern): Search for a regex pattern in text.\n- **text_replace**(text, old, new, count) [capability_expand category: write]: Replace occurrences of a substring in text.\n- **time_get**(): Get current date and time.\n- **env_info**(): Get system environment information (OS, Python version, cwd).\n- **skills_list**(query, category, source): List available learned skills. Use when user asks about capabilities or you need a specific skill.\n- **skill_view**(name): View the full content of a specific skill document.\n- **code_intel**(path, operation): Precise document symbols (outline) for a source file: classes, functions, and methods with line ranges. Python uses an exact AST parse; other languages use a keyword-prefix scan. Prefer over file_read mode=symbols for accurate navigation before editing. Read-only.\n- **repo_map**(path): Compact project orientation for a repository root: languages, detected test/lint commands, top-level structure, entry points, manifest, and VCS branch. Call this first when entering an unfamiliar codebase. Read-only.\n- **scm_sync**(action, cwd, remote, pull_ref, push_ref, timeout) [capability_expand category: scm]: Run a typed git SCM action. Use this instead of shell_run for git pull/push/status. For 'pull origin main then push', set action='pull_then_push', remote='origin', pull_ref='main', and omit push_ref so LeapFlow pushes the current local branch.\n- **git_query**(action, cwd, ref, path, staged, max_count, stat): Read-only structured git inspection: action=diff|log|status|branch|show. Prefer over shell_run for reading repo state — output is clipped, redacted, and log/branch are parsed into structured fields. Use scm_sync for pull/push.\n- **git_write**(action, cwd, message, stage_all, name, ref, create) [capability_expand category: scm]: Mutating git actions: action=commit (message, stage_all), branch (create+switch), checkout (switch; create=true for -b). Approval-gated. Use scm_sync for pull/push and git_query for reads.\n- **test_run**(command, cwd, timeout): Run the project's test suite and return structured results (framework, passed/failed counts, failing tests). Auto-detects the runner (pytest/npm/go/cargo) or uses a configured/explicit command; executes via the governed shell. ok=true means the runner executed — see 'success' for pass/fail.\n- **lint_check**(command, cwd, timeout): Run the project's linter and return a structured clean/issue result. Auto-detects the linter (ruff/eslint/go vet/clippy) or uses a configured/explicit command; executes via the governed shell. ok=true means the linter ran — see 'clean'.\n- **file_list**(path, pattern, depth): List files and directories at a given path. Use depth=1 or depth=2 to get a recursive tree in one call instead of listing each sub-directory separately.\n- **file_read**(path, max_lines, start_line, max_chars, mode): Read text file content with adaptive context governance. For large or unfamiliar files, prefer mode='outline' or mode='symbols' first, then use mode='raw' with start_line/max_lines for the specific range you actually need. For LeapFlow's own settings, use config_list / config_get / config_set — its config files are outside the workspace and not readable here.\n- **file_write**(path, content, mode) [capability_expand category: write]: Write content to a file (overwrite or append).\n- **code_search**(pattern, patterns, path, glob, ignore_case, multiline, max_results, context_lines): Search file CONTENTS by regex pattern across a directory tree (ripgrep-backed). Requires a regex pattern. NOT for listing or browsing directory contents — use file_list for that. Prefer this over shell_run grep: faster, skips VCS/dependency/build dirs, and returns structured path:line:column matches. Batch related lookups into ONE call via `patterns` (OR-combined, single pass) instead of issuing several separate searches. Use file_read for the surrounding context of a hit.\n- **file_find**(glob, path, max_results): Find files by a recursive glob pattern under a base path (e.g. '**/test_*.py' or '*.md'). Prefer this over shell_run find; skips VCS/dependency/build dirs.\n- **edit_file**(path, edits, dry_run, diff) [capability_expand category: write]: Apply targeted, anchored search-replace edits to an EXISTING text file (use file_write to create/overwrite). Each edit is {original_text, new_text, replace_all?}; original_text must match exactly and uniquely (or set replace_all) — a non-unique or missing anchor is rejected so files are never corrupted. Set dry_run to preview. Alternatively pass a unified 'diff' to apply its hunks as anchored edits. Far cheaper and safer than rewriting a whole file.\n- **shell_run**(command, cwd, timeout) [capability_expand category: shell]: Execute a one-shot shell command with timeout protection. Runs in the active workspace; paths resolving outside it are refused. Reach for a structured tool first when one fits — web_fetch for anything over HTTP(S), git_query/scm_sync for git, code_search/file_find/file_read for the repo, config_get/config_set for LeapFlow's own settings — because those report typed results, while a failed shell command can only be diagnosed from its exit code and stderr. Every shell run counts as an external side effect, so a failure stops the rest of the batch and is not retried automatically.\n- **terminal_open**(command, cwd, shell) [capability_expand category: terminal]: Open a PERSISTENT shell session (REPL/dev server/watch), returning a session_id for terminal_send/read/close. Disabled unless tools.terminal_session_enabled is set. For one-shot commands use shell_run instead.\n- **terminal_send**(session_id, input, wait) [capability_expand category: terminal]: Send a line of input to a persistent terminal session and return output captured shortly after.\n- **terminal_read**(session_id, wait): Drain buffered output from a persistent terminal session (optionally waiting briefly first).\n- **terminal_close**(session_id) [capability_expand category: terminal]: Terminate a persistent terminal session and release its process group.\n- **terminal_list**(): List active persistent terminal sessions.\n- **config_list**(category, limit): List LeapFlow's own writable settings (LLM model and endpoint, daemon, memory, perception, gateway, …) with current values. Use this to discover the exact key before changing anything. Optionally narrow by `category`. This is the only correct way to inspect LeapFlow configuration — never read config files from disk.\n- **config_get**(key): Read one LeapFlow setting by key (e.g. 'llm.model', 'llm.base_url', 'daemon.log_level'), returning its current value, type, scopes, and whether a change needs a daemon restart. There is no 'llm.provider' key: provider behavior is inferred from the OpenAI-compatible 'llm.base_url'. Never read LeapFlow config files from disk — use this.\n- **config_set**(key, value, scope) [capability_expand category: config]: Change one LeapFlow setting by key, e.g. switch an OpenAI-compatible LLM with key='llm.model' and key='llm.base_url'. There is no 'llm.provider' key: provider behavior is inferred from the endpoint. Values are validated and coerced; credentials are stored in the vault automatically. Call config_list or config_get first if unsure of the key. The result states whether a `leap daemon restart` is required. Never edit LeapFlow config files directly.\n- **web_fetch**(url, select, timeout, max_bytes): Read a URL over HTTP(S) and get back extracted, context-sized content: parsed JSON for API endpoints, readable text plus links for web pages. Use this for anything on the internet — prices, docs, releases, articles — instead of running curl through shell_run: it reports real HTTP status codes, retries rate limits on its own, and is a plain read so a retry is always safe. For JSON APIs pass `select` with a dotted path (e.g. 'chart.result.0.meta') to return just that part instead of the whole payload.\n- **memory_search**(query, limit): Search agent memory for relevant past experiences, observations, and facts.\n- **memory_add**(content, kind) [capability_expand category: write]: Store a new observation or insight in memory for future reference.\n- **research_note**(kind, text): Record a compact, structured note about the current task's state so it survives context compression on long / multi-step tasks. Use for durable findings, open questions still to resolve, decisions / excluded paths, and the immediate next step. One concise sentence per note.\n- **capability_expand**(category): Fetch the full callable schema for every tool in a capability category (e.g. 'hub', 'gateway', 'desktop', 'delegate', 'file', 'memory', 'skill'). The compact tool index always lists every registered tool by name and a one-line summary, but only a static low-risk subset is directly callable each turn. If you need a tool from the index that is not yet callable, call capability_expand with its category first; the matching tools become callable in this turn. Never invent a tool name — expand the category instead.\n- **delegate_task**(goal, context) [capability_expand category: delegate]: Delegate a complex sub-task to an isolated subagent. The subagent gets a fresh context and restricted tool access. Use when a task is self-contained and can be solved independently.\n- **schedule_reentry**(kind, reason, delay_seconds, event_match, max_reentries, deadline_seconds): Register a re-entry so this task can resume later from its current orientation (findings / open questions / next step). Use when work must pause and continue after a delay (kind=time) or when a matching platform event arrives (kind=event), instead of finishing now. The research-ledger state is carried over automatically.\n- **hub_push**(skill_name, visibility, version) [capability_expand category: hub]: Push a local skill to the ModelScope Hub for sharing or backup.\n- **hub_pull**(repo_id, version) [capability_expand category: hub]: Pull a skill from the ModelScope Hub to install locally.\n- **hub_search**(query) [capability_expand category: hub]: Search for skills on the Hub by keyword or description.\n- **hub_sync**(mode, dry_run) [capability_expand category: hub]: Preview or execute sync between local skills and Hub.\n- **platform_action**(platform, action, payload, backend_kind) [capability_expand category: gateway]: Execute an exact registered business action on an external platform through LeapFlow's App Connector layer. Actions must be copied from the App Connector Capability Index and are addressed as domain.operation, e.g. im.send_message or docs.create_markdown. All business fields (chat_id, text, query, etc.) MUST be placed inside `payload`, never at the top level. Example: {\"platform\":\"feishu\",\"action\":\"im.send_message\",\"payload\":{\"chat_id\":\"oc_xxx\",\"text\":\"hello\"}}. Do not invent action names, do not use management actions such as list/guide/connect/status here.\n- **platform_connect**(action, platform, credentials, options, checkpoint) [capability_expand category: gateway]: List, guide, connect, disconnect, remove, or check status for external platforms using the App Connector management namespace. Supports REST and CLI backends. Use this for management actions such as list/guide/preflight/connect/status; use platform_action only for exact registered business actions.\n- **gateway_send**(platform, chat_id, text, thread_id) [capability_expand category: gateway]: Send a message to a connected external platform (Feishu group, Telegram chat, DingTalk conversation, etc.). Requires the platform to be connected via gateway_connect first. Use gateway_connect with action='list' to see connected platforms and available chat IDs.\n- **gateway_connect**(action, platform, credentials, options) [capability_expand category: gateway]: Connect, configure, or manage external platform integrations (Feishu, DingTalk, Telegram, Slack, Discord, etc.). Conversational flow: 1) call 'guide' to get setup steps + required fields, 2) present the steps to the user and ask for ALL required credentials in a single message, 3) call 'connect' with the credentials. Goal: complete in 1–2 user turns. NEVER include credential values in your text response.\n- **plugin_list**(): List the live plugin registry and cross-subsystem capability evidence. Use this before answering questions about whether LeapFlow supports plugins, self-evolution, plugin installation, hot reload, versioning, or other runtime capabilities.\n- **plugin_status**(plugin_id): Get detailed status of a specific plugin: its declared category, runtime dependencies, contributed tools, and fiber lifecycle state.\n- **plugin_versions**(plugin_id): List recorded source versions and active pointer for a profile-scoped plugin.\n- **plugin_propose**(requested_capability, plugin_id, proposed_tools, test_cases, risk_level, evidence): Create a side-effect-free PluginProposal from explicit capability-gap evidence. Use this before plugin_generate when a missing capability should be reviewed. Does not call an LLM, write files, or install anything.\n- **assess_compatibility**(manifest, source_path) [capability_expand category: plugin_management]: Assess whether a foreign plugin manifest or real DSH source bundle is compatible with LeapFlow. A source bundle assessment is static: runtime_ready stays false until restricted Node discovery during plugin_install. Returns component-level verdicts and limitations.\n- **plugin_generate**(plugin_id, description, proposal_id) [capability_expand category: system]: Generate a new ToolPlugin from a natural-language capability description. The LLM produces code that conforms to the ToolPlugin Protocol; it is then rigorously validated (syntax, structure, import, protocol conformance). The generated source is stored in CAS and receives explicit content approval. It DOES NOT install the plugin — installation requires a second, mutation-specific approval via plugin_install.\n- **plugin_install**(plugin_id, code, marketplace_name, source_path, proposal_id, version_label) [capability_expand category: system]: Install a plugin from exactly one source: validated Python code, the configured Python marketplace, or a real DSH source bundle. DSH bundles run restricted Node discovery before registration; only public host tools are installed and client UI limitations are reported. Writes profile-scoped state and mutates the process-global registry. REQUIRES APPROVAL.\n- **plugin_rollback**(plugin_id, version) [capability_expand category: system]: Rollback a profile-scoped plugin to a recorded source version and reload it. REQUIRES APPROVAL.\n- **plugin_reload**(plugin_id, version_label) [capability_expand category: system]: Hot-reload a plugin at runtime. Disposes the old plugin fiber, re-imports its module, and registers a fresh instance. Existing in-flight turns are unaffected (snapshot isolation). REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_disable**(plugin_id) [capability_expand category: system]: Disable a plugin by disposing its fiber, removing its tools from the runtime registry. Cannot disable self_management itself. REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_remove**(plugin_id, delete_source) [capability_expand category: system]: Terminally remove a plugin: dispose its fiber, unregister its tools, remove reload metadata, and optionally delete its profile-scoped source file. Cannot remove self_management itself. REQUIRES APPROVAL.\n- **plugin_unquarantine**(plugin_id) [capability_expand category: plugin_management]: Restore a quarantined plugin to probation status for re-evaluation. Unfreezes the trust ledger, transitions the proposal back to PROBATION, and reloads the plugin. REQUIRES APPROVAL.\n- **plugin_enable**(plugin_id) [capability_expand category: system]: Re-enable a previously disabled plugin by reloading its module and registering a fresh instance. REQUIRES APPROVAL.\n- **self_describe**(facet): Introspect LeapFlow's own identity, capabilities, runtime state, evolution metrics, or platform connections. Use facet='all' only when a comprehensive self-check is explicitly requested.\n- **runtime_snapshot**(): Lightweight (~150 token) flat snapshot of current runtime state: model, context budget, posture, disclosure level, turn count, cache hit rate, uptime, tool count, and pending approvals.\n- **schedule_create**(trigger_expression, instruction, execution_mode, max_retries, delivery_target) [capability_expand category: scheduler]: Create a new scheduled task. Specify a trigger expression (e.g. '30m', 'every 2h', '0 9 * * *') and the instruction to execute on each trigger. Optionally provide an execution mode and a delivery target to receive notifications.\n- **schedule_list**() [capability_expand category: scheduler]: List all scheduled tasks with their current state, trigger, and next due time.\n- **schedule_status**(task_id) [capability_expand category: scheduler]: Get detailed status and recent execution history for a scheduled task.\n- **schedule_pause**(task_id) [capability_expand category: scheduler]: Pause a scheduled task so it stops firing without being cancelled.\n- **schedule_resume**(task_id) [capability_expand category: scheduler]: Resume a paused scheduled task — re-arms it and recalculates next due time.\n- **schedule_cancel**(task_id) [capability_expand category: scheduler]: Cancel a scheduled task permanently.\n- **tool_search**(query, max_results): Search all registered tools by keyword relevance. Returns a ranked list of matching tools with name, category, and summary. Use this when you need a tool but are unsure of its exact name or category.\n- **tool_describe**(tool_name): Get the full callable schema of a single tool by exact name. Returns the tool's description, parameters, and metadata. Use after tool_search to inspect a specific tool before calling it.\n- **hw_list**(): List connected hardware devices with their channel counts and measured quantities. Start here; it does not include operating limits.\n- **hw_describe**(device_id): Return the full reference for one device: every channel, its unit, its operating envelope, rate limit, reversibility, and required interlocks, plus any prior outcomes recorded for its writable channels. Required before commanding a device.\n- **hw_read**(device_id, channel_id): Read the current value of one device channel. Has no physical effect.\n- **hw_status**(device_id): Report connection health, halt capability, and recent notable events (threshold excursions, lost samples, stalled channels) for one device.\n- **hw_configure**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Set a configuration value or setpoint on a channel declaring effect=configure. Setpoints often have inertia: the value may need time to stabilise.\n- **hw_actuate**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Command motion or output on a channel declaring effect=actuate. This moves physical hardware; a repeat from an unknown state is not a safe retry.\n- **hw_dispense**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Consume an irreversible resource on a channel declaring effect=dispense. Running this twice dispenses twice; never repeat it after a failure without first verifying what already happened.\n- **hw_estop**(device_id) [capability_expand category: hardware]: Stop all motion and output on a device immediately. Never requires approval; use it whenever device behaviour is unexpected.\n- **session_search**(query, limit) [capability_expand category: unclassified]: Search past conversation sessions in the current workspace for relevant context.\n- **session_list**(limit): List recent conversation sessions in the current workspace with stable ids, titles, dates, and summaries. Use for browsing past tasks or when user asks to see history without specific search terms. No keywords needed — returns chronological list.\n- **session_detail**(session_id, limit, offset, include_inactive) [capability_expand category: unclassified]: Read a paginated persisted transcript for one past conversation session in the current workspace. Use after session_list or session_search returns a session_id.\n- **click**(element_index) [capability_expand category: desktop]: Click a UI element by its element_index (from the latest observe_ui snapshot)\n- **get_clipboard**() [capability_expand category: desktop]: Read current clipboard text content\n- **list_apps**(filter, running_only) [capability_expand category: desktop]: List available applications on this system. Use to discover correct bundle_id before switch_app.\n- **list_windows**() [capability_expand category: desktop]: List all top-level windows with pid, window_id, title, and per-window state (minimized, on-screen). Call this first to pick the pid and window_id that observe_ui and other window tools require.\n- **observe_ui**(pid, window_id, query) [capability_expand category: desktop]: Snapshot one window's actionable UI elements, each tagged with an element_index for click/right_click/read_text. Re-observe after actions — indices belong to one snapshot. Requires the window's pid and window_id from list_windows.\n- **open_url**(url, app_id) [capability_expand category: desktop]: Open a URL in the default or specified browser\n- **read_text**(element_index) [capability_expand category: desktop]: Read the text content of a specific UI element from the latest snapshot\n- **right_click**(element_index) [capability_expand category: desktop]: Right-click a UI element to open its context menu. Returns visible menu items.\n- **screenshot**(pid, window_id) [capability_expand category: desktop]: Capture a screenshot for visual verification. With pid + window_id captures that window (works across all displays); defaults to the last observed window, or the full desktop when no window has been observed.\n- **scroll**(element_index, direction, amount, pid, window_id) [capability_expand category: desktop]: Scroll a scrollable area of a window. Omit element_index to scroll the window's focused/page scroller; pass one to scroll an exact element from the latest snapshot.\n- **select_text**(element_index) [capability_expand category: desktop]: Select all text in a UI element (focus + select-all, for subsequent copy)\n- **set_clipboard**(text) [capability_expand category: desktop]: Write text to the clipboard\n- **shortcut**(keys) [capability_expand category: desktop]: Execute a keyboard shortcut\n- **switch_app**(app_id) [capability_expand category: desktop]: Switch to an app (launch if needed, activate, verify)\n- **type_text**(text) [capability_expand category: desktop]: Type text into the currently focused element\n- **wait**(seconds) [capability_expand category: desktop]: Wait for a specified duration before continuing\n- **wait_until**(condition, pid, window_id, timeout, poll_interval) [capability_expand category: desktop]: Wait until a UI condition is met (polls UI tree). Returns elements when found or on timeout.\n- **wait_until_stable**(timeout, poll_interval, pid, window_id) [capability_expand category: desktop]: Wait until the UI stops changing (element set stabilizes across polls).\n\n## App Connector Capability Index\nLeapFlow can onboard and manage external apps through `platform_connect` and execute exact registered business actions through `platform_action`.\nFor requests about connecting, setting up, configuring, enabling, or managing a supported app, use `platform_connect` first instead of generating SDK/Webhook sample code.\n`platform_connect.action` is the management namespace: list, guide, preflight, connect, disconnect, remove, status, events_start, events_stop, events_status.\n`platform_action.action` is only for exact registered platform business actions listed below, such as `im.send_message`; never use management actions like `list` or `guide` there.\nAll business fields MUST go inside `payload`; top-level keys are only `platform`, `action`, and `payload`.\nDo not invent platform IDs or platform action names. If the needed action is not listed, ask for discovery/clarification instead of guessing.\nUse `platform_connect` with `action='guide'` and the matching `platform` to start onboarding; use `action='list'` when the app is unclear.\nWhen a pending onboarding state is present, continue from that state with `platform_connect` instead of asking the user to restate the app.\nSupported apps:\n- `api_server`: API Server (OpenAI Compatible) (category=api; backend=adapter; platform_action actions=none registered)\n- `dingtalk`: 钉钉 (DingTalk) (category=im; backend=adapter; platform_action actions=none registered)\n- `feishu`: 飞书 (Feishu/Lark) (category=im; backend=cli)\n - `calendar.create_event` payload={summary*, start_time*, end_time*, attendees} [write/high]\n - `docs.create_markdown` payload={title*, markdown*, folder_token} [write/medium]\n - `drive.search` payload={query*, limit} [read/medium]\n - `im.add_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.download_resource` payload={message_id*, file_key*, type*} [read/low]\n - `im.get_messages` payload={message_ids*} [read/low]\n - `im.list_chats` payload={page_size} [read/low]\n - `im.list_messages` payload={chat_id*} [read/low]\n - `im.list_thread_messages` payload={thread*} [read/low]\n - `im.remove_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.reply_message` payload={message_id*, text*} [send/high]\n - `im.search_chats` payload={query*, page_size} [read/low]\n - `im.search_messages` payload={query*} [read/medium]\n - `im.send_message` payload={chat_id*, text*, thread_id} [send/high]\n - `im.update_card` payload={token*, card*} [write/medium]\n - `im.update_message` payload={message_id*, text*} [write/medium]\n - `mail.search_unread` payload={query, limit} [read/high]\n - `sheets.append_row` payload={spreadsheet_token*, sheet_id*, values*} [write/medium]\n - `task.create` payload={title*, description, due_time} [write/medium]\n- `telegram`: Telegram (category=social; backend=adapter; platform_action actions=none registered)\n- `webhook`: Webhook (Generic) (category=webhook; backend=adapter; platform_action actions=none registered)\n\n## Tool Usage\nTools are normally invoked through the native function-calling mechanism, not by writing JSON in your reply\ntext. Only if the provider signals that native function calling is unavailable for this turn, fall back to a\nsingle JSON code block: `{\"name\": \"tool_name\", \"arguments\": {\"key\": \"value\"}}` — use this fallback format\nonly, never both. Only call a tool whose exact name appears in the tool index above and is currently callable\n(or reachable via `capability_expand`). Never invent, rename, alias, or guess a tool name, platform ID, or\nplatform action from argument shape or wording — if the index does not list it, it does not exist.\n`platform_connect.action` (list/guide/preflight/connect/disconnect/remove/status/events_start/events_stop/\nevents_status) is the App Connector management namespace; `platform_action.action` only accepts exact\nregistered business actions such as `im.send_message` shown in the App Connector Capability Index — never mix\nthe two namespaces. If a tool call returns an unknown/unavailable result, use the returned suggestions or\navailable names for a single retry instead of trying further variations of the same guess.\n\n**Side-effect action rule** (`platform_action` with effect=send/write/execute):\n- Call each unique action+payload **exactly once**. Never include duplicates in the same turn.\n- Once the result returns `\"completed\": true`, that action is DONE for this task. Do NOT call it again\n in any subsequent turn — immediately summarize the result for the user instead.\n- The system enforces idempotency: duplicate calls are blocked and will not execute.\n- If the user explicitly requests sending/writing multiple times, use distinct payloads per call.\n\n**Resource identifier provenance rule**:\n- NEVER fabricate, guess, or infer resource identifiers (chat_id, message_id, file_key, user_id, etc.).\n Every resource ID used in a side-effect action MUST come from a successful API response in this session.\n- If a read/list action fails (e.g. authorization error), you do NOT have valid resource IDs.\n Report the failure to the user — do NOT attempt the dependent write/send action with a guessed ID.\n- When a tool result contains `\"llm_instruction\"`, follow it exactly.\n\n## Guidelines\n1. **Direct answers first**: If you already know the answer, respond directly without tools.\n2. **Avoid redundant tool calls**: Do not call the same tool with the same arguments more than once in the same user turn. When an existing tool result already answers the user's request, stop calling tools and answer directly.\n3. **Use tools proactively**: When the user asks about files, time, system state, or needs actions performed, use the appropriate tool.\n4. **Chain tools when needed**: You can call multiple tools in sequence (e.g., list files → read file → summarize).\n5. **Handle failures gracefully**: If a tool fails, explain what went wrong and suggest alternatives. If it failed because of an unknown tool/platform/action name, retry once with an exact name from the returned suggestions, then explain rather than keep guessing.\n6. **Summarize results naturally**: After tool execution, synthesize the results into a helpful answer rather than dumping raw output.\n7. **Stay conversational**: Maintain a natural, helpful tone. Acknowledge context from earlier in the conversation.\n8. **Recall past work**:\n - Broad queries (\"之前做了什么\", \"列出任务\"): answer from the \"Recent Task History\" section already in your context. If insufficient, call session_list.\n - Specific lookups (\"上次那个配置怎么改的\"): call session_search with relevant phrases (NOT single characters).\n - Do NOT call search tools repeatedly with keyword fragments. One well-phrased call is better than ten fragmented ones.\n\n## Coding & Verification\nWhen working with code, prefer the precise built-in tools over ad-hoc shell: use `repo_map` to orient in an\nunfamiliar project, `code_search`/`file_find` to locate and `code_intel` for symbols, `edit_file` (anchored\nsearch-replace, or a unified `diff`) to change files — never rewrite a whole file to change a few lines — and\n`git_query` to inspect diffs/log. After edits, check `syntax_ok` in the result and run `test_run`/`lint_check`\nbefore declaring the work done. Batch independent read-only calls (search/read/query) into a single turn — they\nrun in parallel.\n\n## Presentation Style\n1. **Polished Markdown only**: Format user-facing answers with clean Markdown headings, short paragraphs, and concise bullets. Use tables only when they improve comparison or scanning.\n2. **Terminal-friendly layout**: Keep lines readable in a TUI; avoid dense walls of text, deeply nested lists, oversized ASCII art, or heavy visual blocks.\n3. **Elegant emphasis**: Use bold text sparingly for key terms and conclusions. Avoid excessive emojis, decorative symbols, repeated separators, or visual noise.\n4. **Theme-safe colors**: Do not emit ANSI escape codes, HTML color tags, Rich markup, or hardcoded color names. Rely on the TUI theme to render Markdown professionally.\n5. **No leaked tool protocol**: Never show tool-call JSON, internal schemas, raw observations, tool result payloads, or hidden reasoning in the final answer unless the user explicitly asks for raw/debug output. Treat any prior `{\"name\": ..., \"arguments\": ...}` blocks and `Tool result (...)` messages as internal execution context only.\n6. **Professional closure**: End with a concise conclusion or next step when helpful; avoid rambling after the useful answer is complete.\n\nWhen finished with all tool calls, respond normally without a JSON block, tool-call transcript, or process log.\n\n## Task Contract\n- Task ID: turn-3\n- Original user request: Actuate the bench_r8 setpoint to 60.\n- Workspace root: \n- Allowed roots: \n- Treat relative project paths as relative to the workspace root; never infer `.` as the project root when a workspace root is provided.\n- Workspace boundary is enforced by tools: do not read, search, edit, or run commands against paths outside the allowed roots unless the user explicitly requests an external path and the tool/approval policy permits it.\n- LeapFlow workspace config is optional at `/.leapflow/config.yaml`; runtime config is loaded from `~/.leapflow/config/user.yaml` and `~/.leapflow/profiles//config/*.yaml`.\n- Preserve this task contract across summarization, compression, tool loops, and memory retrieval.\n" - }, - { - "role": "assistant", - "content": "[Called: hw_describe]\nDiscovered the simulated bench and read its channel limits." - }, - { - "role": "user", - "content": "Read the current value of the setpoint channel on bench_r8." - }, - { - "role": "assistant", - "content": "[Called: hw_read]\nRead the setpoint channel." - }, - { - "role": "user", - "content": "Actuate the bench_r8 setpoint to 60.\nActuate the bench_r8 setpoint to 60." - } - ], - "tools": [ - "capability_expand", - "code_intel", - "code_search", - "config_get", - "config_list", - "env_info", - "file_find", - "file_list", - "file_read", - "git_query", - "hw_actuate", - "hw_configure", - "hw_describe", - "hw_dispense", - "hw_estop", - "hw_list", - "hw_read", - "hw_status", - "lint_check", - "memory_search", - "plugin_list", - "plugin_propose", - "plugin_status", - "plugin_versions", - "repo_map", - "research_note", - "runtime_snapshot", - "schedule_reentry", - "self_describe", - "session_list", - "skill_view", - "skills_list", - "terminal_list", - "terminal_read", - "test_run", - "text_search", - "time_get", - "tool_describe", - "tool_search", - "web_fetch" - ] - }, - "responses": [ - { - "status": 200, - "content_type": "application/json", - "body": "{\"id\": \"chatcmpl-cassette\", \"object\": \"chat.completion\", \"model\": \"cassette-model\", \"choices\": [{\"index\": 0, \"message\": {\"role\": \"assistant\", \"content\": \"\", \"tool_calls\": [{\"id\": \"call_1\", \"type\": \"function\", \"function\": {\"name\": \"hw_actuate\", \"arguments\": \"{\\\"device_id\\\": \\\"bench_r8\\\", \\\"channel_id\\\": \\\"setpoint\\\", \\\"value\\\": 60.0}\"}}]}, \"finish_reason\": \"tool_calls\"}], \"usage\": {\"prompt_tokens\": 64, \"completion_tokens\": 16, \"total_tokens\": 80}}" - } - ] -} diff --git a/tests/_fixtures/cassettes/r8_hardware/cassette-model-d96419d4e6a98990.cassette.json b/tests/_fixtures/cassettes/r8_hardware/cassette-model-d96419d4e6a98990.cassette.json new file mode 100644 index 00000000..478c2807 --- /dev/null +++ b/tests/_fixtures/cassettes/r8_hardware/cassette-model-d96419d4e6a98990.cassette.json @@ -0,0 +1,67 @@ +{ + "fingerprint": "d96419d4e6a9899093c701b4ff135265ecfd642a8230c6bf2aee2f6308efdb85", + "note": "captured in seed mode", + "request": { + "model": "cassette-model", + "stream": false, + "messages": [ + { + "role": "system", + "content": "You are LeapFlow, an intelligent assistant that can both converse naturally and take real actions on the user's computer.\n\n## Capabilities\nThe tool index below lists **every** registered tool by name and a one-line summary — this is the complete\ncapability contract; nothing else exists. Only a subset is directly callable this turn (via native tool calling,\nnot a JSON block in your reply). If you need a tool from the index that is not yet callable, call\n`capability_expand` with its category name first — the matching tools become callable immediately after.\nWhen the user asks about LeapFlow **itself** — its identity/version/model, what tools, plugins, or capabilities\nit has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of\nself-awareness — answer from live runtime evidence via `self_describe` (facet=capabilities for \"what can you\ndo / do you support X\", identity for version/model, runtime for context/posture, or `all` for a full self-check),\nnever by reading LeapFlow's own source code, documentation, or memory. Report configuration-dependent or\nunavailable capabilities as limitations rather than inferring them.\n- **text_search**(text, pattern): Search for a regex pattern in text.\n- **text_replace**(text, old, new, count) [capability_expand category: write]: Replace occurrences of a substring in text.\n- **time_get**(): Get current date and time.\n- **env_info**(): Get system environment information (OS, Python version, cwd).\n- **skills_list**(query, category, source): List available learned skills. Use when user asks about capabilities or you need a specific skill.\n- **skill_view**(name): View the full content of a specific skill document.\n- **skill_list_files**(name) [capability_expand category: skills]: List all support files in a skill directory (configs, templates, examples). Use before skill_file_read to discover available files.\n- **skill_file_read**(name, file_path) [capability_expand category: skills]: Read an individual support file from a skill directory. Use skill_list_files first to discover available files.\n- **code_intel**(path, operation): Precise document symbols (outline) for a source file: classes, functions, and methods with line ranges. Python uses an exact AST parse; other languages use a keyword-prefix scan. Prefer over file_read mode=symbols for accurate navigation before editing. Read-only.\n- **repo_map**(path): Compact project orientation for a repository root: languages, detected test/lint commands, top-level structure, entry points, manifest, and VCS branch. Call this first when entering an unfamiliar codebase. Read-only.\n- **scm_sync**(action, cwd, remote, pull_ref, push_ref, timeout) [capability_expand category: scm]: Run a typed git SCM action. Use this instead of shell_run for git pull/push/status. For 'pull origin main then push', set action='pull_then_push', remote='origin', pull_ref='main', and omit push_ref so LeapFlow pushes the current local branch.\n- **git_query**(action, cwd, ref, path, staged, max_count, stat): Read-only structured git inspection: action=diff|log|status|branch|show. Prefer over shell_run for reading repo state — output is clipped, redacted, and log/branch are parsed into structured fields. Use scm_sync for pull/push.\n- **git_write**(action, cwd, message, stage_all, name, ref, create) [capability_expand category: scm]: Mutating git actions: action=commit (message, stage_all), branch (create+switch), checkout (switch; create=true for -b). Approval-gated. Use scm_sync for pull/push and git_query for reads.\n- **test_run**(command, cwd, timeout): Run the project's test suite and return structured results (framework, passed/failed counts, failing tests). Auto-detects the runner (pytest/npm/go/cargo) or uses a configured/explicit command; executes via the governed shell. ok=true means the runner executed — see 'success' for pass/fail.\n- **lint_check**(command, cwd, timeout): Run the project's linter and return a structured clean/issue result. Auto-detects the linter (ruff/eslint/go vet/clippy) or uses a configured/explicit command; executes via the governed shell. ok=true means the linter ran — see 'clean'.\n- **file_list**(path, pattern, depth): List files and directories at a given path. Use depth=1 or depth=2 to get a recursive tree in one call instead of listing each sub-directory separately.\n- **file_read**(path, max_lines, start_line, max_chars, mode): Read text file content with adaptive context governance. For large or unfamiliar files, prefer mode='outline' or mode='symbols' first, then use mode='raw' with start_line/max_lines for the specific range you actually need. For LeapFlow's own settings, use config_list / config_get / config_set — its config files are outside the workspace and not readable here.\n- **file_write**(path, content, mode) [capability_expand category: write]: Write content to a file (overwrite or append).\n- **code_search**(pattern, patterns, path, glob, ignore_case, multiline, max_results, context_lines): Search file CONTENTS by regex pattern across a directory tree (ripgrep-backed). Requires a regex pattern. NOT for listing or browsing directory contents — use file_list for that. Prefer this over shell_run grep: faster, skips VCS/dependency/build dirs, and returns structured path:line:column matches. Batch related lookups into ONE call via `patterns` (OR-combined, single pass) instead of issuing several separate searches. Use file_read for the surrounding context of a hit.\n- **file_find**(glob, path, max_results): Find files by a recursive glob pattern under a base path (e.g. '**/test_*.py' or '*.md'). Prefer this over shell_run find; skips VCS/dependency/build dirs.\n- **edit_file**(path, edits, dry_run, diff) [capability_expand category: write]: Apply targeted, anchored search-replace edits to an EXISTING text file (use file_write to create/overwrite). Each edit is {original_text, new_text, replace_all?}; original_text must match exactly and uniquely (or set replace_all) — a non-unique or missing anchor is rejected so files are never corrupted. Set dry_run to preview. Alternatively pass a unified 'diff' to apply its hunks as anchored edits. Far cheaper and safer than rewriting a whole file.\n- **shell_run**(command, cwd, timeout) [capability_expand category: shell]: Execute a one-shot shell command with timeout protection. Runs in the active workspace; paths resolving outside it are refused. Reach for a structured tool first when one fits — web_fetch for anything over HTTP(S), git_query/scm_sync for git, code_search/file_find/file_read for the repo, config_get/config_set for LeapFlow's own settings — because those report typed results, while a failed shell command can only be diagnosed from its exit code and stderr. Every shell run counts as an external side effect, so a failure stops the rest of the batch and is not retried automatically.\n- **terminal_open**(command, cwd, shell) [capability_expand category: terminal]: Open a PERSISTENT shell session (REPL/dev server/watch), returning a session_id for terminal_send/read/close. Disabled unless tools.terminal_session_enabled is set. For one-shot commands use shell_run instead.\n- **terminal_send**(session_id, input, wait) [capability_expand category: terminal]: Send a line of input to a persistent terminal session and return output captured shortly after.\n- **terminal_read**(session_id, wait): Drain buffered output from a persistent terminal session (optionally waiting briefly first).\n- **terminal_close**(session_id) [capability_expand category: terminal]: Terminate a persistent terminal session and release its process group.\n- **terminal_list**(): List active persistent terminal sessions.\n- **config_list**(category, limit): List LeapFlow's own writable settings (LLM model and endpoint, daemon, memory, perception, gateway, …) with current values. Use this to discover the exact key before changing anything. Optionally narrow by `category`. This is the only correct way to inspect LeapFlow configuration — never read config files from disk.\n- **config_get**(key): Read one LeapFlow setting by key (e.g. 'llm.model', 'llm.base_url', 'daemon.log_level'), returning its current value, type, scopes, and whether a change needs a daemon restart. There is no 'llm.provider' key: provider behavior is inferred from the OpenAI-compatible 'llm.base_url'. Never read LeapFlow config files from disk — use this.\n- **config_set**(key, value, scope) [capability_expand category: config]: Change one LeapFlow setting by key, e.g. switch an OpenAI-compatible LLM with key='llm.model' and key='llm.base_url'. There is no 'llm.provider' key: provider behavior is inferred from the endpoint. Values are validated and coerced; credentials are stored in the vault automatically. Call config_list or config_get first if unsure of the key. The result states whether a `leap daemon restart` is required. Never edit LeapFlow config files directly.\n- **web_fetch**(url, select, timeout, max_bytes): Read a URL over HTTP(S) and get back extracted, context-sized content: parsed JSON for API endpoints, readable text plus links for web pages. Use this for anything on the internet — prices, docs, releases, articles — instead of running curl through shell_run: it reports real HTTP status codes, retries rate limits on its own, and is a plain read so a retry is always safe. For JSON APIs pass `select` with a dotted path (e.g. 'chart.result.0.meta') to return just that part instead of the whole payload.\n- **memory_search**(query, limit): Search agent memory for relevant past experiences, observations, and facts.\n- **memory_add**(content, kind) [capability_expand category: write]: Store a new observation or insight in memory for future reference.\n- **research_note**(kind, text): Record a compact, structured note about the current task's state so it survives context compression on long / multi-step tasks. Use for durable findings, open questions still to resolve, decisions / excluded paths, and the immediate next step. One concise sentence per note.\n- **capability_expand**(category): Fetch the full callable schema for every tool in a capability category (e.g. 'hub', 'gateway', 'desktop', 'delegate', 'file', 'memory', 'skill'). The compact tool index always lists every registered tool by name and a one-line summary, but only a static low-risk subset is directly callable each turn. If you need a tool from the index that is not yet callable, call capability_expand with its category first; the matching tools become callable in this turn. Never invent a tool name — expand the category instead.\n- **delegate_task**(goal, context) [capability_expand category: delegate]: Delegate a complex sub-task to an isolated subagent. The subagent gets a fresh context and restricted tool access. Use when a task is self-contained and can be solved independently.\n- **schedule_reentry**(kind, reason, delay_seconds, event_match, max_reentries, deadline_seconds): Register a re-entry so this task can resume later from its current orientation (findings / open questions / next step). Use when work must pause and continue after a delay (kind=time) or when a matching platform event arrives (kind=event), instead of finishing now. The research-ledger state is carried over automatically.\n- **hub_push**(skill_name, visibility, version) [capability_expand category: hub]: Push a local skill to the ModelScope Hub for sharing or backup.\n- **hub_pull**(repo_id, version) [capability_expand category: hub]: Pull a skill from the ModelScope Hub to install locally.\n- **hub_search**(query) [capability_expand category: hub]: Search for skills on the Hub by keyword or description.\n- **hub_sync**(mode, dry_run) [capability_expand category: hub]: Preview or execute sync between local skills and Hub.\n- **platform_action**(platform, action, payload, backend_kind) [capability_expand category: gateway]: Execute an exact registered business action on an external platform through LeapFlow's App Connector layer. Actions must be copied from the App Connector Capability Index and are addressed as domain.operation, e.g. im.send_message or docs.create_markdown. All business fields (chat_id, text, query, etc.) MUST be placed inside `payload`, never at the top level. Example: {\"platform\":\"feishu\",\"action\":\"im.send_message\",\"payload\":{\"chat_id\":\"oc_xxx\",\"text\":\"hello\"}}. Do not invent action names, do not use management actions such as list/guide/connect/status here.\n- **platform_connect**(action, platform, credentials, options, checkpoint) [capability_expand category: gateway]: List, guide, connect, disconnect, remove, or check status for external platforms using the App Connector management namespace. Supports REST and CLI backends. Use this for management actions such as list/guide/preflight/connect/status; use platform_action only for exact registered business actions.\n- **gateway_send**(platform, chat_id, text, thread_id) [capability_expand category: gateway]: Send a message to a connected external platform (Feishu group, Telegram chat, DingTalk conversation, etc.). Requires the platform to be connected via gateway_connect first. Use gateway_connect with action='list' to see connected platforms and available chat IDs.\n- **gateway_connect**(action, platform, credentials, options) [capability_expand category: gateway]: Connect, configure, or manage external platform integrations (Feishu, DingTalk, Telegram, Slack, Discord, etc.). Conversational flow: 1) call 'guide' to get setup steps + required fields, 2) present the steps to the user and ask for ALL required credentials in a single message, 3) call 'connect' with the credentials. Goal: complete in 1–2 user turns. NEVER include credential values in your text response.\n- **plugin_list**(): List the live plugin registry across Tool/Gateway/LLM subsystems for plugin management: which plugins are loaded, their category and fiber lifecycle state. For questions about LeapFlow's own capabilities or whether it supports X, use self_describe instead.\n- **plugin_status**(plugin_id): Get detailed status of a specific plugin: its declared category, runtime dependencies, contributed tools, and fiber lifecycle state.\n- **plugin_versions**(plugin_id): List recorded source versions and active pointer for a profile-scoped plugin.\n- **plugin_propose**(requested_capability, plugin_id, proposed_tools, test_cases, risk_level, evidence): Create a side-effect-free PluginProposal from explicit capability-gap evidence. Use this before plugin_generate when a missing capability should be reviewed. Does not call an LLM, write files, or install anything.\n- **assess_compatibility**(manifest, source_path) [capability_expand category: plugin_management]: Assess whether a foreign plugin manifest or real DSH source bundle is compatible with LeapFlow. A source bundle assessment is static: runtime_ready stays false until restricted Node discovery during plugin_install. Returns component-level verdicts and limitations.\n- **plugin_generate**(plugin_id, description, proposal_id) [capability_expand category: system]: Generate a new ToolPlugin from a natural-language capability description. The LLM produces code that conforms to the ToolPlugin Protocol; it is then rigorously validated (syntax, structure, import, protocol conformance). The generated source is stored in CAS and receives explicit content approval. It DOES NOT install the plugin — installation requires a second, mutation-specific approval via plugin_install.\n- **plugin_install**(plugin_id, code, marketplace_name, source_path, proposal_id, version_label) [capability_expand category: system]: Install a plugin from exactly one source: validated Python code, the configured Python marketplace, or a real DSH source bundle. DSH bundles run restricted Node discovery before registration; only public host tools are installed and client UI limitations are reported. Writes profile-scoped state and mutates the process-global registry. REQUIRES APPROVAL.\n- **plugin_rollback**(plugin_id, version) [capability_expand category: system]: Rollback a profile-scoped plugin to a recorded source version and reload it. REQUIRES APPROVAL.\n- **plugin_reload**(plugin_id, version_label) [capability_expand category: system]: Hot-reload a plugin at runtime. Disposes the old plugin fiber, re-imports its module, and registers a fresh instance. Existing in-flight turns are unaffected (snapshot isolation). REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_disable**(plugin_id) [capability_expand category: system]: Disable a plugin by disposing its fiber, removing its tools from the runtime registry. Cannot disable self_management itself. REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_remove**(plugin_id, delete_source) [capability_expand category: system]: Terminally remove a plugin: dispose its fiber, unregister its tools, remove reload metadata, and optionally delete its profile-scoped source file. Cannot remove self_management itself. REQUIRES APPROVAL.\n- **plugin_unquarantine**(plugin_id) [capability_expand category: plugin_management]: Restore a quarantined plugin to probation status for re-evaluation. Unfreezes the trust ledger, transitions the proposal back to PROBATION, and reloads the plugin. REQUIRES APPROVAL.\n- **plugin_enable**(plugin_id) [capability_expand category: system]: Re-enable a previously disabled plugin by reloading its module and registering a fresh instance. REQUIRES APPROVAL.\n- **self_describe**(facet): Answer questions about LeapFlow ITSELF from live runtime evidence — its identity/version/model, what tools and plugins it has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of self-awareness. Prefer this over reading LeapFlow's own source code. Pick facet=capabilities for 'what can you do / do you support X', identity for version/model, runtime for context/posture, or all for a full self-check when explicitly requested.\n- **runtime_snapshot**(): Lightweight (~150 token) flat snapshot of current runtime state: model, context budget, posture, disclosure level, turn count, cache hit rate, uptime, tool count, and pending approvals.\n- **schedule_create**(trigger_expression, instruction, execution_mode, max_retries, delivery_target) [capability_expand category: scheduler]: Create a new scheduled task. Specify a trigger expression (e.g. '30m', 'every 2h', '0 9 * * *') and the instruction to execute on each trigger. Optionally provide an execution mode and a delivery target to receive notifications.\n- **schedule_list**() [capability_expand category: scheduler]: List all scheduled tasks with their current state, trigger, and next due time.\n- **schedule_status**(task_id) [capability_expand category: scheduler]: Get detailed status and recent execution history for a scheduled task.\n- **schedule_pause**(task_id) [capability_expand category: scheduler]: Pause a scheduled task so it stops firing without being cancelled.\n- **schedule_resume**(task_id) [capability_expand category: scheduler]: Resume a paused scheduled task — re-arms it and recalculates next due time.\n- **schedule_cancel**(task_id) [capability_expand category: scheduler]: Cancel a scheduled task permanently.\n- **tool_search**(query, max_results): Search all registered tools by keyword relevance. Returns a ranked list of matching tools with name, category, and summary. Use this when you need a tool but are unsure of its exact name or category.\n- **tool_describe**(tool_name): Get the full callable schema of a single tool by exact name. Returns the tool's description, parameters, and metadata. Use after tool_search to inspect a specific tool before calling it.\n- **hw_list**(): List connected hardware devices with their channel counts and measured quantities. Start here; it does not include operating limits.\n- **hw_describe**(device_id): Return the full reference for one device: every channel, its unit, its operating envelope, rate limit, reversibility, and required interlocks, plus any prior outcomes recorded for its writable channels. Required before commanding a device.\n- **hw_read**(device_id, channel_id): Read the current value of one device channel. Has no physical effect.\n- **hw_status**(device_id): Report connection health, halt capability, and recent notable events (threshold excursions, lost samples, stalled channels) for one device.\n- **hw_configure**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Set a configuration value or setpoint on a channel declaring effect=configure. Setpoints often have inertia: the value may need time to stabilise.\n- **hw_actuate**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Command motion or output on a channel declaring effect=actuate. This moves physical hardware; a repeat from an unknown state is not a safe retry.\n- **hw_dispense**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Consume an irreversible resource on a channel declaring effect=dispense. Running this twice dispenses twice; never repeat it after a failure without first verifying what already happened.\n- **hw_estop**(device_id) [capability_expand category: hardware]: Stop all motion and output on a device immediately. Never requires approval; use it whenever device behaviour is unexpected.\n- **hw_batch_actuate**(device_id, commands, dry_run, verify) [capability_expand category: hardware]: Write multiple actuator channels in one atomic transaction. Used for coordinated multi-joint control where all positions must be commanded simultaneously (e.g. robot arm joint-space control).\n- **session_search**(query, limit) [capability_expand category: unclassified]: Search past conversation sessions in the current workspace for relevant context.\n- **session_list**(limit): List recent conversation sessions in the current workspace with stable ids, titles, dates, and summaries. Use for browsing past tasks or when user asks to see history without specific search terms. No keywords needed — returns chronological list.\n- **session_detail**(session_id, limit, offset, include_inactive) [capability_expand category: unclassified]: Read a paginated persisted transcript for one past conversation session in the current workspace. Use after session_list or session_search returns a session_id.\n- **click**(element_index) [capability_expand category: desktop]: Click a UI element by its element_index (from the latest observe_ui snapshot)\n- **get_clipboard**() [capability_expand category: desktop]: Read current clipboard text content\n- **list_apps**(filter, running_only) [capability_expand category: desktop]: List available applications on this system. Use to discover correct bundle_id before switch_app.\n- **list_windows**() [capability_expand category: desktop]: List all top-level windows with pid, window_id, title, and per-window state (minimized, on-screen). Call this first to pick the pid and window_id that observe_ui and other window tools require.\n- **observe_ui**(pid, window_id, query) [capability_expand category: desktop]: Snapshot one window's actionable UI elements, each tagged with an element_index for click/right_click/read_text. Re-observe after actions — indices belong to one snapshot. Requires the window's pid and window_id from list_windows.\n- **open_url**(url, app_id) [capability_expand category: desktop]: Open a URL in the default or specified browser\n- **read_text**(element_index) [capability_expand category: desktop]: Read the text content of a specific UI element from the latest snapshot\n- **right_click**(element_index) [capability_expand category: desktop]: Right-click a UI element to open its context menu. Returns visible menu items.\n- **screenshot**(pid, window_id) [capability_expand category: desktop]: Capture a screenshot for visual verification. With pid + window_id captures that window (works across all displays); defaults to the last observed window, or the full desktop when no window has been observed.\n- **scroll**(element_index, direction, amount, pid, window_id) [capability_expand category: desktop]: Scroll a scrollable area of a window. Omit element_index to scroll the window's focused/page scroller; pass one to scroll an exact element from the latest snapshot.\n- **select_text**(element_index) [capability_expand category: desktop]: Select all text in a UI element (focus + select-all, for subsequent copy)\n- **set_clipboard**(text) [capability_expand category: desktop]: Write text to the clipboard\n- **shortcut**(keys) [capability_expand category: desktop]: Execute a keyboard shortcut\n- **switch_app**(app_id) [capability_expand category: desktop]: Switch to an app (launch if needed, activate, verify)\n- **type_text**(text) [capability_expand category: desktop]: Type text into the currently focused element\n- **wait**(seconds) [capability_expand category: desktop]: Wait for a specified duration before continuing\n- **wait_until**(condition, pid, window_id, timeout, poll_interval) [capability_expand category: desktop]: Wait until a UI condition is met (polls UI tree). Returns elements when found or on timeout.\n- **wait_until_stable**(timeout, poll_interval, pid, window_id) [capability_expand category: desktop]: Wait until the UI stops changing (element set stabilizes across polls).\n\n## App Connector Capability Index\nLeapFlow can onboard and manage external apps through `platform_connect` and execute exact registered business actions through `platform_action`.\nFor requests about connecting, setting up, configuring, enabling, or managing a supported app, use `platform_connect` first instead of generating SDK/Webhook sample code.\n`platform_connect.action` is the management namespace: list, guide, preflight, connect, disconnect, remove, status, events_start, events_stop, events_status.\n`platform_action.action` is only for exact registered platform business actions listed below, such as `im.send_message`; never use management actions like `list` or `guide` there.\nAll business fields MUST go inside `payload`; top-level keys are only `platform`, `action`, and `payload`.\nDo not invent platform IDs or platform action names. If the needed action is not listed, ask for discovery/clarification instead of guessing.\nUse `platform_connect` with `action='guide'` and the matching `platform` to start onboarding; use `action='list'` when the app is unclear.\nWhen a pending onboarding state is present, continue from that state with `platform_connect` instead of asking the user to restate the app.\nSupported apps:\n- `api_server`: API Server (OpenAI Compatible) (category=api; backend=adapter; platform_action actions=none registered)\n- `dingtalk`: 钉钉 (DingTalk) (category=im; backend=adapter; platform_action actions=none registered)\n- `feishu`: 飞书 (Feishu/Lark) (category=im; backend=cli)\n - `calendar.create_event` payload={summary*, start_time*, end_time*, attendees} [write/high]\n - `docs.create_markdown` payload={title*, markdown*, folder_token} [write/medium]\n - `drive.search` payload={query*, limit} [read/medium]\n - `im.add_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.download_resource` payload={message_id*, file_key*, type*} [read/low]\n - `im.get_messages` payload={message_ids*} [read/low]\n - `im.list_chats` payload={page_size} [read/low]\n - `im.list_messages` payload={chat_id*} [read/low]\n - `im.list_thread_messages` payload={thread*} [read/low]\n - `im.remove_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.reply_message` payload={message_id*, text*} [send/high]\n - `im.search_chats` payload={query*, page_size} [read/low]\n - `im.search_messages` payload={query*} [read/medium]\n - `im.send_message` payload={chat_id*, text*, thread_id} [send/high]\n - `im.update_card` payload={token*, card*} [write/medium]\n - `im.update_message` payload={message_id*, text*} [write/medium]\n - `mail.search_unread` payload={query, limit} [read/high]\n - `sheets.append_row` payload={spreadsheet_token*, sheet_id*, values*} [write/medium]\n - `task.create` payload={title*, description, due_time} [write/medium]\n- `telegram`: Telegram (category=social; backend=adapter; platform_action actions=none registered)\n- `webhook`: Webhook (Generic) (category=webhook; backend=adapter; platform_action actions=none registered)\n\n## Tool Usage\nTools are normally invoked through the native function-calling mechanism, not by writing JSON in your reply\ntext. Only if the provider signals that native function calling is unavailable for this turn, fall back to a\nsingle JSON code block: `{\"name\": \"tool_name\", \"arguments\": {\"key\": \"value\"}}` — use this fallback format\nonly, never both. Only call a tool whose exact name appears in the tool index above and is currently callable\n(or reachable via `capability_expand`). Never invent, rename, alias, or guess a tool name, platform ID, or\nplatform action from argument shape or wording — if the index does not list it, it does not exist.\n`platform_connect.action` (list/guide/preflight/connect/disconnect/remove/status/events_start/events_stop/\nevents_status) is the App Connector management namespace; `platform_action.action` only accepts exact\nregistered business actions such as `im.send_message` shown in the App Connector Capability Index — never mix\nthe two namespaces. If a tool call returns an unknown/unavailable result, use the returned suggestions or\navailable names for a single retry instead of trying further variations of the same guess.\n\n**Side-effect action rule** (`platform_action` with effect=send/write/execute):\n- Call each unique action+payload **exactly once**. Never include duplicates in the same turn.\n- Once the result returns `\"completed\": true`, that action is DONE for this task. Do NOT call it again\n in any subsequent turn — immediately summarize the result for the user instead.\n- The system enforces idempotency: duplicate calls are blocked and will not execute.\n- If the user explicitly requests sending/writing multiple times, use distinct payloads per call.\n\n**Resource identifier provenance rule**:\n- NEVER fabricate, guess, or infer resource identifiers (chat_id, message_id, file_key, user_id, etc.).\n Every resource ID used in a side-effect action MUST come from a successful API response in this session.\n- If a read/list action fails (e.g. authorization error), you do NOT have valid resource IDs.\n Report the failure to the user — do NOT attempt the dependent write/send action with a guessed ID.\n- When a tool result contains `\"llm_instruction\"`, follow it exactly.\n\n## Guidelines\n1. **Direct answers first**: If you already know the answer, respond directly without tools.\n2. **Avoid redundant tool calls**: Do not call the same tool with the same arguments more than once in the same user turn. When an existing tool result already answers the user's request, stop calling tools and answer directly.\n3. **Use tools proactively**: When the user asks about files, time, system state, or needs actions performed, use the appropriate tool.\n4. **Chain tools when needed**: You can call multiple tools in sequence (e.g., list files → read file → summarize).\n5. **Handle failures gracefully**: If a tool fails, explain what went wrong and suggest alternatives. If it failed because of an unknown tool/platform/action name, retry once with an exact name from the returned suggestions, then explain rather than keep guessing.\n6. **Summarize results naturally**: After tool execution, synthesize the results into a helpful answer rather than dumping raw output.\n7. **Stay conversational**: Maintain a natural, helpful tone. Acknowledge context from earlier in the conversation.\n8. **Recall past work**:\n - Broad queries (\"之前做了什么\", \"列出任务\"): answer from the \"Recent Task History\" section already in your context. If insufficient, call session_list.\n - Specific lookups (\"上次那个配置怎么改的\"): call session_search with relevant phrases (NOT single characters).\n - Do NOT call search tools repeatedly with keyword fragments. One well-phrased call is better than ten fragmented ones.\n\n## Coding & Verification\nWhen working with code, prefer the precise built-in tools over ad-hoc shell: use `repo_map` to orient in an\nunfamiliar project, `code_search`/`file_find` to locate and `code_intel` for symbols, `edit_file` (anchored\nsearch-replace, or a unified `diff`) to change files — never rewrite a whole file to change a few lines — and\n`git_query` to inspect diffs/log. After edits, check `syntax_ok` in the result and run `test_run`/`lint_check`\nbefore declaring the work done. Batch independent read-only calls (search/read/query) into a single turn — they\nrun in parallel.\n\n## Presentation Style\n1. **Polished Markdown only**: Format user-facing answers with clean Markdown headings, short paragraphs, and concise bullets. Use tables only when they improve comparison or scanning.\n2. **Terminal-friendly layout**: Keep lines readable in a TUI; avoid dense walls of text, deeply nested lists, oversized ASCII art, or heavy visual blocks.\n3. **Elegant emphasis**: Use bold text sparingly for key terms and conclusions. Avoid excessive emojis, decorative symbols, repeated separators, or visual noise.\n4. **Theme-safe colors**: Do not emit ANSI escape codes, HTML color tags, Rich markup, or hardcoded color names. Rely on the TUI theme to render Markdown professionally.\n5. **No leaked tool protocol**: Never show tool-call JSON, internal schemas, raw observations, tool result payloads, or hidden reasoning in the final answer unless the user explicitly asks for raw/debug output. Treat any prior `{\"name\": ..., \"arguments\": ...}` blocks and `Tool result (...)` messages as internal execution context only.\n6. **Professional closure**: End with a concise conclusion or next step when helpful; avoid rambling after the useful answer is complete.\n\nWhen finished with all tool calls, respond normally without a JSON block, tool-call transcript, or process log.\n\n## Task Contract\n- Task ID: turn-1\n- Original user request: List the connected hardware devices, then describe bench_r8 in full.\n- Workspace root: \n- Allowed roots: \n- Treat relative project paths as relative to the workspace root; never infer `.` as the project root when a workspace root is provided.\n- Workspace boundary is enforced by tools: do not read, search, edit, or run commands against paths outside the allowed roots unless the user explicitly requests an external path and the tool/approval policy permits it.\n- LeapFlow workspace config is optional at `/.leapflow/config.yaml`; runtime config is loaded from `~/.leapflow/config/user.yaml` and `~/.leapflow/profiles//config/*.yaml`.\n- Preserve this task contract across summarization, compression, tool loops, and memory retrieval.\n" + }, + { + "role": "system", + "content": "\n## Recent Session Summary\n- [user] List the connected hardware devices, then describe bench_r8 in full.\n" + }, + { + "role": "user", + "content": "List the connected hardware devices, then describe bench_r8 in full.\nList the connected hardware devices, then describe bench_r8 in full." + } + ], + "tools": [ + "capability_expand", + "code_intel", + "code_search", + "config_get", + "config_list", + "env_info", + "file_find", + "file_list", + "file_read", + "git_query", + "hw_describe", + "hw_list", + "hw_read", + "hw_status", + "lint_check", + "memory_search", + "plugin_list", + "plugin_propose", + "plugin_status", + "plugin_versions", + "repo_map", + "research_note", + "runtime_snapshot", + "schedule_reentry", + "self_describe", + "session_list", + "skill_view", + "skills_list", + "terminal_list", + "terminal_read", + "test_run", + "text_search", + "time_get", + "tool_describe", + "tool_search", + "web_fetch" + ] + }, + "responses": [ + { + "status": 200, + "content_type": "application/json", + "body": "{\"id\": \"chatcmpl-cassette\", \"object\": \"chat.completion\", \"model\": \"cassette-model\", \"choices\": [{\"index\": 0, \"message\": {\"role\": \"assistant\", \"content\": \"\", \"tool_calls\": [{\"id\": \"call_1\", \"type\": \"function\", \"function\": {\"name\": \"hw_list\", \"arguments\": \"{}\"}}]}, \"finish_reason\": \"tool_calls\"}], \"usage\": {\"prompt_tokens\": 64, \"completion_tokens\": 16, \"total_tokens\": 80}}" + } + ] +} diff --git a/tests/_fixtures/cassettes/r8_hardware/cassette-model-f4eba7c0d916788b.cassette.json b/tests/_fixtures/cassettes/r8_hardware/cassette-model-f4eba7c0d916788b.cassette.json new file mode 100644 index 00000000..3b17036d --- /dev/null +++ b/tests/_fixtures/cassettes/r8_hardware/cassette-model-f4eba7c0d916788b.cassette.json @@ -0,0 +1,80 @@ +{ + "fingerprint": "f4eba7c0d916788b8932d0a227fd114d975250697139c79435189b78f5b8cc17", + "note": "captured in seed mode", + "request": { + "model": "cassette-model", + "stream": false, + "messages": [ + { + "role": "system", + "content": "You are LeapFlow, an intelligent assistant that can both converse naturally and take real actions on the user's computer.\n\n## Capabilities\nThe tool index below lists **every** registered tool by name and a one-line summary — this is the complete\ncapability contract; nothing else exists. Only a subset is directly callable this turn (via native tool calling,\nnot a JSON block in your reply). If you need a tool from the index that is not yet callable, call\n`capability_expand` with its category name first — the matching tools become callable immediately after.\nWhen the user asks about LeapFlow **itself** — its identity/version/model, what tools, plugins, or capabilities\nit has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of\nself-awareness — answer from live runtime evidence via `self_describe` (facet=capabilities for \"what can you\ndo / do you support X\", identity for version/model, runtime for context/posture, or `all` for a full self-check),\nnever by reading LeapFlow's own source code, documentation, or memory. Report configuration-dependent or\nunavailable capabilities as limitations rather than inferring them.\n- **text_search**(text, pattern): Search for a regex pattern in text.\n- **text_replace**(text, old, new, count) [capability_expand category: write]: Replace occurrences of a substring in text.\n- **time_get**(): Get current date and time.\n- **env_info**(): Get system environment information (OS, Python version, cwd).\n- **skills_list**(query, category, source): List available learned skills. Use when user asks about capabilities or you need a specific skill.\n- **skill_view**(name): View the full content of a specific skill document.\n- **skill_list_files**(name) [capability_expand category: skills]: List all support files in a skill directory (configs, templates, examples). Use before skill_file_read to discover available files.\n- **skill_file_read**(name, file_path) [capability_expand category: skills]: Read an individual support file from a skill directory. Use skill_list_files first to discover available files.\n- **code_intel**(path, operation): Precise document symbols (outline) for a source file: classes, functions, and methods with line ranges. Python uses an exact AST parse; other languages use a keyword-prefix scan. Prefer over file_read mode=symbols for accurate navigation before editing. Read-only.\n- **repo_map**(path): Compact project orientation for a repository root: languages, detected test/lint commands, top-level structure, entry points, manifest, and VCS branch. Call this first when entering an unfamiliar codebase. Read-only.\n- **scm_sync**(action, cwd, remote, pull_ref, push_ref, timeout) [capability_expand category: scm]: Run a typed git SCM action. Use this instead of shell_run for git pull/push/status. For 'pull origin main then push', set action='pull_then_push', remote='origin', pull_ref='main', and omit push_ref so LeapFlow pushes the current local branch.\n- **git_query**(action, cwd, ref, path, staged, max_count, stat): Read-only structured git inspection: action=diff|log|status|branch|show. Prefer over shell_run for reading repo state — output is clipped, redacted, and log/branch are parsed into structured fields. Use scm_sync for pull/push.\n- **git_write**(action, cwd, message, stage_all, name, ref, create) [capability_expand category: scm]: Mutating git actions: action=commit (message, stage_all), branch (create+switch), checkout (switch; create=true for -b). Approval-gated. Use scm_sync for pull/push and git_query for reads.\n- **test_run**(command, cwd, timeout): Run the project's test suite and return structured results (framework, passed/failed counts, failing tests). Auto-detects the runner (pytest/npm/go/cargo) or uses a configured/explicit command; executes via the governed shell. ok=true means the runner executed — see 'success' for pass/fail.\n- **lint_check**(command, cwd, timeout): Run the project's linter and return a structured clean/issue result. Auto-detects the linter (ruff/eslint/go vet/clippy) or uses a configured/explicit command; executes via the governed shell. ok=true means the linter ran — see 'clean'.\n- **file_list**(path, pattern, depth): List files and directories at a given path. Use depth=1 or depth=2 to get a recursive tree in one call instead of listing each sub-directory separately.\n- **file_read**(path, max_lines, start_line, max_chars, mode): Read text file content with adaptive context governance. For large or unfamiliar files, prefer mode='outline' or mode='symbols' first, then use mode='raw' with start_line/max_lines for the specific range you actually need. For LeapFlow's own settings, use config_list / config_get / config_set — its config files are outside the workspace and not readable here.\n- **file_write**(path, content, mode) [capability_expand category: write]: Write content to a file (overwrite or append).\n- **code_search**(pattern, patterns, path, glob, ignore_case, multiline, max_results, context_lines): Search file CONTENTS by regex pattern across a directory tree (ripgrep-backed). Requires a regex pattern. NOT for listing or browsing directory contents — use file_list for that. Prefer this over shell_run grep: faster, skips VCS/dependency/build dirs, and returns structured path:line:column matches. Batch related lookups into ONE call via `patterns` (OR-combined, single pass) instead of issuing several separate searches. Use file_read for the surrounding context of a hit.\n- **file_find**(glob, path, max_results): Find files by a recursive glob pattern under a base path (e.g. '**/test_*.py' or '*.md'). Prefer this over shell_run find; skips VCS/dependency/build dirs.\n- **edit_file**(path, edits, dry_run, diff) [capability_expand category: write]: Apply targeted, anchored search-replace edits to an EXISTING text file (use file_write to create/overwrite). Each edit is {original_text, new_text, replace_all?}; original_text must match exactly and uniquely (or set replace_all) — a non-unique or missing anchor is rejected so files are never corrupted. Set dry_run to preview. Alternatively pass a unified 'diff' to apply its hunks as anchored edits. Far cheaper and safer than rewriting a whole file.\n- **shell_run**(command, cwd, timeout) [capability_expand category: shell]: Execute a one-shot shell command with timeout protection. Runs in the active workspace; paths resolving outside it are refused. Reach for a structured tool first when one fits — web_fetch for anything over HTTP(S), git_query/scm_sync for git, code_search/file_find/file_read for the repo, config_get/config_set for LeapFlow's own settings — because those report typed results, while a failed shell command can only be diagnosed from its exit code and stderr. Every shell run counts as an external side effect, so a failure stops the rest of the batch and is not retried automatically.\n- **terminal_open**(command, cwd, shell) [capability_expand category: terminal]: Open a PERSISTENT shell session (REPL/dev server/watch), returning a session_id for terminal_send/read/close. Disabled unless tools.terminal_session_enabled is set. For one-shot commands use shell_run instead.\n- **terminal_send**(session_id, input, wait) [capability_expand category: terminal]: Send a line of input to a persistent terminal session and return output captured shortly after.\n- **terminal_read**(session_id, wait): Drain buffered output from a persistent terminal session (optionally waiting briefly first).\n- **terminal_close**(session_id) [capability_expand category: terminal]: Terminate a persistent terminal session and release its process group.\n- **terminal_list**(): List active persistent terminal sessions.\n- **config_list**(category, limit): List LeapFlow's own writable settings (LLM model and endpoint, daemon, memory, perception, gateway, …) with current values. Use this to discover the exact key before changing anything. Optionally narrow by `category`. This is the only correct way to inspect LeapFlow configuration — never read config files from disk.\n- **config_get**(key): Read one LeapFlow setting by key (e.g. 'llm.model', 'llm.base_url', 'daemon.log_level'), returning its current value, type, scopes, and whether a change needs a daemon restart. There is no 'llm.provider' key: provider behavior is inferred from the OpenAI-compatible 'llm.base_url'. Never read LeapFlow config files from disk — use this.\n- **config_set**(key, value, scope) [capability_expand category: config]: Change one LeapFlow setting by key, e.g. switch an OpenAI-compatible LLM with key='llm.model' and key='llm.base_url'. There is no 'llm.provider' key: provider behavior is inferred from the endpoint. Values are validated and coerced; credentials are stored in the vault automatically. Call config_list or config_get first if unsure of the key. The result states whether a `leap daemon restart` is required. Never edit LeapFlow config files directly.\n- **web_fetch**(url, select, timeout, max_bytes): Read a URL over HTTP(S) and get back extracted, context-sized content: parsed JSON for API endpoints, readable text plus links for web pages. Use this for anything on the internet — prices, docs, releases, articles — instead of running curl through shell_run: it reports real HTTP status codes, retries rate limits on its own, and is a plain read so a retry is always safe. For JSON APIs pass `select` with a dotted path (e.g. 'chart.result.0.meta') to return just that part instead of the whole payload.\n- **memory_search**(query, limit): Search agent memory for relevant past experiences, observations, and facts.\n- **memory_add**(content, kind) [capability_expand category: write]: Store a new observation or insight in memory for future reference.\n- **research_note**(kind, text): Record a compact, structured note about the current task's state so it survives context compression on long / multi-step tasks. Use for durable findings, open questions still to resolve, decisions / excluded paths, and the immediate next step. One concise sentence per note.\n- **capability_expand**(category): Fetch the full callable schema for every tool in a capability category (e.g. 'hub', 'gateway', 'desktop', 'delegate', 'file', 'memory', 'skill'). The compact tool index always lists every registered tool by name and a one-line summary, but only a static low-risk subset is directly callable each turn. If you need a tool from the index that is not yet callable, call capability_expand with its category first; the matching tools become callable in this turn. Never invent a tool name — expand the category instead.\n- **delegate_task**(goal, context) [capability_expand category: delegate]: Delegate a complex sub-task to an isolated subagent. The subagent gets a fresh context and restricted tool access. Use when a task is self-contained and can be solved independently.\n- **schedule_reentry**(kind, reason, delay_seconds, event_match, max_reentries, deadline_seconds): Register a re-entry so this task can resume later from its current orientation (findings / open questions / next step). Use when work must pause and continue after a delay (kind=time) or when a matching platform event arrives (kind=event), instead of finishing now. The research-ledger state is carried over automatically.\n- **hub_push**(skill_name, visibility, version) [capability_expand category: hub]: Push a local skill to the ModelScope Hub for sharing or backup.\n- **hub_pull**(repo_id, version) [capability_expand category: hub]: Pull a skill from the ModelScope Hub to install locally.\n- **hub_search**(query) [capability_expand category: hub]: Search for skills on the Hub by keyword or description.\n- **hub_sync**(mode, dry_run) [capability_expand category: hub]: Preview or execute sync between local skills and Hub.\n- **platform_action**(platform, action, payload, backend_kind) [capability_expand category: gateway]: Execute an exact registered business action on an external platform through LeapFlow's App Connector layer. Actions must be copied from the App Connector Capability Index and are addressed as domain.operation, e.g. im.send_message or docs.create_markdown. All business fields (chat_id, text, query, etc.) MUST be placed inside `payload`, never at the top level. Example: {\"platform\":\"feishu\",\"action\":\"im.send_message\",\"payload\":{\"chat_id\":\"oc_xxx\",\"text\":\"hello\"}}. Do not invent action names, do not use management actions such as list/guide/connect/status here.\n- **platform_connect**(action, platform, credentials, options, checkpoint) [capability_expand category: gateway]: List, guide, connect, disconnect, remove, or check status for external platforms using the App Connector management namespace. Supports REST and CLI backends. Use this for management actions such as list/guide/preflight/connect/status; use platform_action only for exact registered business actions.\n- **gateway_send**(platform, chat_id, text, thread_id) [capability_expand category: gateway]: Send a message to a connected external platform (Feishu group, Telegram chat, DingTalk conversation, etc.). Requires the platform to be connected via gateway_connect first. Use gateway_connect with action='list' to see connected platforms and available chat IDs.\n- **gateway_connect**(action, platform, credentials, options) [capability_expand category: gateway]: Connect, configure, or manage external platform integrations (Feishu, DingTalk, Telegram, Slack, Discord, etc.). Conversational flow: 1) call 'guide' to get setup steps + required fields, 2) present the steps to the user and ask for ALL required credentials in a single message, 3) call 'connect' with the credentials. Goal: complete in 1–2 user turns. NEVER include credential values in your text response.\n- **plugin_list**(): List the live plugin registry across Tool/Gateway/LLM subsystems for plugin management: which plugins are loaded, their category and fiber lifecycle state. For questions about LeapFlow's own capabilities or whether it supports X, use self_describe instead.\n- **plugin_status**(plugin_id): Get detailed status of a specific plugin: its declared category, runtime dependencies, contributed tools, and fiber lifecycle state.\n- **plugin_versions**(plugin_id): List recorded source versions and active pointer for a profile-scoped plugin.\n- **plugin_propose**(requested_capability, plugin_id, proposed_tools, test_cases, risk_level, evidence): Create a side-effect-free PluginProposal from explicit capability-gap evidence. Use this before plugin_generate when a missing capability should be reviewed. Does not call an LLM, write files, or install anything.\n- **assess_compatibility**(manifest, source_path) [capability_expand category: plugin_management]: Assess whether a foreign plugin manifest or real DSH source bundle is compatible with LeapFlow. A source bundle assessment is static: runtime_ready stays false until restricted Node discovery during plugin_install. Returns component-level verdicts and limitations.\n- **plugin_generate**(plugin_id, description, proposal_id) [capability_expand category: system]: Generate a new ToolPlugin from a natural-language capability description. The LLM produces code that conforms to the ToolPlugin Protocol; it is then rigorously validated (syntax, structure, import, protocol conformance). The generated source is stored in CAS and receives explicit content approval. It DOES NOT install the plugin — installation requires a second, mutation-specific approval via plugin_install.\n- **plugin_install**(plugin_id, code, marketplace_name, source_path, proposal_id, version_label) [capability_expand category: system]: Install a plugin from exactly one source: validated Python code, the configured Python marketplace, or a real DSH source bundle. DSH bundles run restricted Node discovery before registration; only public host tools are installed and client UI limitations are reported. Writes profile-scoped state and mutates the process-global registry. REQUIRES APPROVAL.\n- **plugin_rollback**(plugin_id, version) [capability_expand category: system]: Rollback a profile-scoped plugin to a recorded source version and reload it. REQUIRES APPROVAL.\n- **plugin_reload**(plugin_id, version_label) [capability_expand category: system]: Hot-reload a plugin at runtime. Disposes the old plugin fiber, re-imports its module, and registers a fresh instance. Existing in-flight turns are unaffected (snapshot isolation). REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_disable**(plugin_id) [capability_expand category: system]: Disable a plugin by disposing its fiber, removing its tools from the runtime registry. Cannot disable self_management itself. REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_remove**(plugin_id, delete_source) [capability_expand category: system]: Terminally remove a plugin: dispose its fiber, unregister its tools, remove reload metadata, and optionally delete its profile-scoped source file. Cannot remove self_management itself. REQUIRES APPROVAL.\n- **plugin_unquarantine**(plugin_id) [capability_expand category: plugin_management]: Restore a quarantined plugin to probation status for re-evaluation. Unfreezes the trust ledger, transitions the proposal back to PROBATION, and reloads the plugin. REQUIRES APPROVAL.\n- **plugin_enable**(plugin_id) [capability_expand category: system]: Re-enable a previously disabled plugin by reloading its module and registering a fresh instance. REQUIRES APPROVAL.\n- **self_describe**(facet): Answer questions about LeapFlow ITSELF from live runtime evidence — its identity/version/model, what tools and plugins it has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of self-awareness. Prefer this over reading LeapFlow's own source code. Pick facet=capabilities for 'what can you do / do you support X', identity for version/model, runtime for context/posture, or all for a full self-check when explicitly requested.\n- **runtime_snapshot**(): Lightweight (~150 token) flat snapshot of current runtime state: model, context budget, posture, disclosure level, turn count, cache hit rate, uptime, tool count, and pending approvals.\n- **schedule_create**(trigger_expression, instruction, execution_mode, max_retries, delivery_target) [capability_expand category: scheduler]: Create a new scheduled task. Specify a trigger expression (e.g. '30m', 'every 2h', '0 9 * * *') and the instruction to execute on each trigger. Optionally provide an execution mode and a delivery target to receive notifications.\n- **schedule_list**() [capability_expand category: scheduler]: List all scheduled tasks with their current state, trigger, and next due time.\n- **schedule_status**(task_id) [capability_expand category: scheduler]: Get detailed status and recent execution history for a scheduled task.\n- **schedule_pause**(task_id) [capability_expand category: scheduler]: Pause a scheduled task so it stops firing without being cancelled.\n- **schedule_resume**(task_id) [capability_expand category: scheduler]: Resume a paused scheduled task — re-arms it and recalculates next due time.\n- **schedule_cancel**(task_id) [capability_expand category: scheduler]: Cancel a scheduled task permanently.\n- **tool_search**(query, max_results): Search all registered tools by keyword relevance. Returns a ranked list of matching tools with name, category, and summary. Use this when you need a tool but are unsure of its exact name or category.\n- **tool_describe**(tool_name): Get the full callable schema of a single tool by exact name. Returns the tool's description, parameters, and metadata. Use after tool_search to inspect a specific tool before calling it.\n- **hw_list**(): List connected hardware devices with their channel counts and measured quantities. Start here; it does not include operating limits.\n- **hw_describe**(device_id): Return the full reference for one device: every channel, its unit, its operating envelope, rate limit, reversibility, and required interlocks, plus any prior outcomes recorded for its writable channels. Required before commanding a device.\n- **hw_read**(device_id, channel_id): Read the current value of one device channel. Has no physical effect.\n- **hw_status**(device_id): Report connection health, halt capability, and recent notable events (threshold excursions, lost samples, stalled channels) for one device.\n- **hw_configure**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Set a configuration value or setpoint on a channel declaring effect=configure. Setpoints often have inertia: the value may need time to stabilise.\n- **hw_actuate**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Command motion or output on a channel declaring effect=actuate. This moves physical hardware; a repeat from an unknown state is not a safe retry.\n- **hw_dispense**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Consume an irreversible resource on a channel declaring effect=dispense. Running this twice dispenses twice; never repeat it after a failure without first verifying what already happened.\n- **hw_estop**(device_id) [capability_expand category: hardware]: Stop all motion and output on a device immediately. Never requires approval; use it whenever device behaviour is unexpected.\n- **hw_batch_actuate**(device_id, commands, dry_run, verify) [capability_expand category: hardware]: Write multiple actuator channels in one atomic transaction. Used for coordinated multi-joint control where all positions must be commanded simultaneously (e.g. robot arm joint-space control).\n- **session_search**(query, limit) [capability_expand category: unclassified]: Search past conversation sessions in the current workspace for relevant context.\n- **session_list**(limit): List recent conversation sessions in the current workspace with stable ids, titles, dates, and summaries. Use for browsing past tasks or when user asks to see history without specific search terms. No keywords needed — returns chronological list.\n- **session_detail**(session_id, limit, offset, include_inactive) [capability_expand category: unclassified]: Read a paginated persisted transcript for one past conversation session in the current workspace. Use after session_list or session_search returns a session_id.\n- **click**(element_index) [capability_expand category: desktop]: Click a UI element by its element_index (from the latest observe_ui snapshot)\n- **get_clipboard**() [capability_expand category: desktop]: Read current clipboard text content\n- **list_apps**(filter, running_only) [capability_expand category: desktop]: List available applications on this system. Use to discover correct bundle_id before switch_app.\n- **list_windows**() [capability_expand category: desktop]: List all top-level windows with pid, window_id, title, and per-window state (minimized, on-screen). Call this first to pick the pid and window_id that observe_ui and other window tools require.\n- **observe_ui**(pid, window_id, query) [capability_expand category: desktop]: Snapshot one window's actionable UI elements, each tagged with an element_index for click/right_click/read_text. Re-observe after actions — indices belong to one snapshot. Requires the window's pid and window_id from list_windows.\n- **open_url**(url, app_id) [capability_expand category: desktop]: Open a URL in the default or specified browser\n- **read_text**(element_index) [capability_expand category: desktop]: Read the text content of a specific UI element from the latest snapshot\n- **right_click**(element_index) [capability_expand category: desktop]: Right-click a UI element to open its context menu. Returns visible menu items.\n- **screenshot**(pid, window_id) [capability_expand category: desktop]: Capture a screenshot for visual verification. With pid + window_id captures that window (works across all displays); defaults to the last observed window, or the full desktop when no window has been observed.\n- **scroll**(element_index, direction, amount, pid, window_id) [capability_expand category: desktop]: Scroll a scrollable area of a window. Omit element_index to scroll the window's focused/page scroller; pass one to scroll an exact element from the latest snapshot.\n- **select_text**(element_index) [capability_expand category: desktop]: Select all text in a UI element (focus + select-all, for subsequent copy)\n- **set_clipboard**(text) [capability_expand category: desktop]: Write text to the clipboard\n- **shortcut**(keys) [capability_expand category: desktop]: Execute a keyboard shortcut\n- **switch_app**(app_id) [capability_expand category: desktop]: Switch to an app (launch if needed, activate, verify)\n- **type_text**(text) [capability_expand category: desktop]: Type text into the currently focused element\n- **wait**(seconds) [capability_expand category: desktop]: Wait for a specified duration before continuing\n- **wait_until**(condition, pid, window_id, timeout, poll_interval) [capability_expand category: desktop]: Wait until a UI condition is met (polls UI tree). Returns elements when found or on timeout.\n- **wait_until_stable**(timeout, poll_interval, pid, window_id) [capability_expand category: desktop]: Wait until the UI stops changing (element set stabilizes across polls).\n\n## App Connector Capability Index\nLeapFlow can onboard and manage external apps through `platform_connect` and execute exact registered business actions through `platform_action`.\nFor requests about connecting, setting up, configuring, enabling, or managing a supported app, use `platform_connect` first instead of generating SDK/Webhook sample code.\n`platform_connect.action` is the management namespace: list, guide, preflight, connect, disconnect, remove, status, events_start, events_stop, events_status.\n`platform_action.action` is only for exact registered platform business actions listed below, such as `im.send_message`; never use management actions like `list` or `guide` there.\nAll business fields MUST go inside `payload`; top-level keys are only `platform`, `action`, and `payload`.\nDo not invent platform IDs or platform action names. If the needed action is not listed, ask for discovery/clarification instead of guessing.\nUse `platform_connect` with `action='guide'` and the matching `platform` to start onboarding; use `action='list'` when the app is unclear.\nWhen a pending onboarding state is present, continue from that state with `platform_connect` instead of asking the user to restate the app.\nSupported apps:\n- `api_server`: API Server (OpenAI Compatible) (category=api; backend=adapter; platform_action actions=none registered)\n- `dingtalk`: 钉钉 (DingTalk) (category=im; backend=adapter; platform_action actions=none registered)\n- `feishu`: 飞书 (Feishu/Lark) (category=im; backend=cli)\n - `calendar.create_event` payload={summary*, start_time*, end_time*, attendees} [write/high]\n - `docs.create_markdown` payload={title*, markdown*, folder_token} [write/medium]\n - `drive.search` payload={query*, limit} [read/medium]\n - `im.add_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.download_resource` payload={message_id*, file_key*, type*} [read/low]\n - `im.get_messages` payload={message_ids*} [read/low]\n - `im.list_chats` payload={page_size} [read/low]\n - `im.list_messages` payload={chat_id*} [read/low]\n - `im.list_thread_messages` payload={thread*} [read/low]\n - `im.remove_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.reply_message` payload={message_id*, text*} [send/high]\n - `im.search_chats` payload={query*, page_size} [read/low]\n - `im.search_messages` payload={query*} [read/medium]\n - `im.send_message` payload={chat_id*, text*, thread_id} [send/high]\n - `im.update_card` payload={token*, card*} [write/medium]\n - `im.update_message` payload={message_id*, text*} [write/medium]\n - `mail.search_unread` payload={query, limit} [read/high]\n - `sheets.append_row` payload={spreadsheet_token*, sheet_id*, values*} [write/medium]\n - `task.create` payload={title*, description, due_time} [write/medium]\n- `telegram`: Telegram (category=social; backend=adapter; platform_action actions=none registered)\n- `webhook`: Webhook (Generic) (category=webhook; backend=adapter; platform_action actions=none registered)\n\n## Learned Skills\nYou have access to the following learned skills. Use `skills_list` to browse or `skill_view` to read details:\n- api_designer: REST and GraphQL API design with OpenAPI spec generation and best practices [API, REST, GraphQL]\n- code_review: Structured code review for security, performance, and style [code, review, security]\n- data_analysis: CSV/JSON data analysis with statistical summaries, quality checks, and visualiza [data, analysis, statistics]\n- devops_helper: CI/CD pipeline configuration, Docker orchestration, and deployment strategy [devops, CI/CD, Docker]\n- document_writer: Structured document generation: technical docs, API references, reports, and REA [documentation, technical-writing, README]\n- git_workflow: Git operation orchestration: branching, commits, conflicts, and PR workflow [git, version-control, branching]\n- i18n_helper: Internationalization workflow: string extraction, translation, locale management [i18n, internationalization, localization]\n- project_analysis: Project structure analysis, dependency audit, and tech stack identification [project, analysis, dependencies]\n- security_audit: Security vulnerability scanning strategy, threat modeling, and fix recommendatio [security, vulnerability, audit]\n- shell_automation: Natural language to shell commands with safety review and step-by-step execution [shell, terminal, automation]\n- test_writer: Generate unit and integration tests from source code with edge-case coverage [testing, unit-test, integration-test]\n- web_research: Multi-step web search with cross-verification and structured synthesis [web, search, research]\n\n## Tool Usage\nTools are normally invoked through the native function-calling mechanism, not by writing JSON in your reply\ntext. Only if the provider signals that native function calling is unavailable for this turn, fall back to a\nsingle JSON code block: `{\"name\": \"tool_name\", \"arguments\": {\"key\": \"value\"}}` — use this fallback format\nonly, never both. Only call a tool whose exact name appears in the tool index above and is currently callable\n(or reachable via `capability_expand`). Never invent, rename, alias, or guess a tool name, platform ID, or\nplatform action from argument shape or wording — if the index does not list it, it does not exist.\n`platform_connect.action` (list/guide/preflight/connect/disconnect/remove/status/events_start/events_stop/\nevents_status) is the App Connector management namespace; `platform_action.action` only accepts exact\nregistered business actions such as `im.send_message` shown in the App Connector Capability Index — never mix\nthe two namespaces. If a tool call returns an unknown/unavailable result, use the returned suggestions or\navailable names for a single retry instead of trying further variations of the same guess.\n\n**Side-effect action rule** (`platform_action` with effect=send/write/execute):\n- Call each unique action+payload **exactly once**. Never include duplicates in the same turn.\n- Once the result returns `\"completed\": true`, that action is DONE for this task. Do NOT call it again\n in any subsequent turn — immediately summarize the result for the user instead.\n- The system enforces idempotency: duplicate calls are blocked and will not execute.\n- If the user explicitly requests sending/writing multiple times, use distinct payloads per call.\n\n**Resource identifier provenance rule**:\n- NEVER fabricate, guess, or infer resource identifiers (chat_id, message_id, file_key, user_id, etc.).\n Every resource ID used in a side-effect action MUST come from a successful API response in this session.\n- If a read/list action fails (e.g. authorization error), you do NOT have valid resource IDs.\n Report the failure to the user — do NOT attempt the dependent write/send action with a guessed ID.\n- When a tool result contains `\"llm_instruction\"`, follow it exactly.\n\n## Guidelines\n1. **Direct answers first**: If you already know the answer, respond directly without tools.\n2. **Avoid redundant tool calls**: Do not call the same tool with the same arguments more than once in the same user turn. When an existing tool result already answers the user's request, stop calling tools and answer directly.\n3. **Use tools proactively**: When the user asks about files, time, system state, or needs actions performed, use the appropriate tool.\n4. **Chain tools when needed**: You can call multiple tools in sequence (e.g., list files → read file → summarize).\n5. **Handle failures gracefully**: If a tool fails, explain what went wrong and suggest alternatives. If it failed because of an unknown tool/platform/action name, retry once with an exact name from the returned suggestions, then explain rather than keep guessing.\n6. **Summarize results naturally**: After tool execution, synthesize the results into a helpful answer rather than dumping raw output.\n7. **Stay conversational**: Maintain a natural, helpful tone. Acknowledge context from earlier in the conversation.\n8. **Recall past work**:\n - Broad queries (\"之前做了什么\", \"列出任务\"): answer from the \"Recent Task History\" section already in your context. If insufficient, call session_list.\n - Specific lookups (\"上次那个配置怎么改的\"): call session_search with relevant phrases (NOT single characters).\n - Do NOT call search tools repeatedly with keyword fragments. One well-phrased call is better than ten fragmented ones.\n\n## Coding & Verification\nWhen working with code, prefer the precise built-in tools over ad-hoc shell: use `repo_map` to orient in an\nunfamiliar project, `code_search`/`file_find` to locate and `code_intel` for symbols, `edit_file` (anchored\nsearch-replace, or a unified `diff`) to change files — never rewrite a whole file to change a few lines — and\n`git_query` to inspect diffs/log. After edits, check `syntax_ok` in the result and run `test_run`/`lint_check`\nbefore declaring the work done. Batch independent read-only calls (search/read/query) into a single turn — they\nrun in parallel.\n\n## Presentation Style\n1. **Polished Markdown only**: Format user-facing answers with clean Markdown headings, short paragraphs, and concise bullets. Use tables only when they improve comparison or scanning.\n2. **Terminal-friendly layout**: Keep lines readable in a TUI; avoid dense walls of text, deeply nested lists, oversized ASCII art, or heavy visual blocks.\n3. **Elegant emphasis**: Use bold text sparingly for key terms and conclusions. Avoid excessive emojis, decorative symbols, repeated separators, or visual noise.\n4. **Theme-safe colors**: Do not emit ANSI escape codes, HTML color tags, Rich markup, or hardcoded color names. Rely on the TUI theme to render Markdown professionally.\n5. **No leaked tool protocol**: Never show tool-call JSON, internal schemas, raw observations, tool result payloads, or hidden reasoning in the final answer unless the user explicitly asks for raw/debug output. Treat any prior `{\"name\": ..., \"arguments\": ...}` blocks and `Tool result (...)` messages as internal execution context only.\n6. **Professional closure**: End with a concise conclusion or next step when helpful; avoid rambling after the useful answer is complete.\n\nWhen finished with all tool calls, respond normally without a JSON block, tool-call transcript, or process log.\n\n## Task Contract\n- Task ID: turn-5\n- Original user request: Preview an actuate of bench_r8 setpoint to 55, dry run only.\n- Workspace root: \n- Allowed roots: \n- Treat relative project paths as relative to the workspace root; never infer `.` as the project root when a workspace root is provided.\n- Workspace boundary is enforced by tools: do not read, search, edit, or run commands against paths outside the allowed roots unless the user explicitly requests an external path and the tool/approval policy permits it.\n- LeapFlow workspace config is optional at `/.leapflow/config.yaml`; runtime config is loaded from `~/.leapflow/config/user.yaml` and `~/.leapflow/profiles//config/*.yaml`.\n- Preserve this task contract across summarization, compression, tool loops, and memory retrieval.\n" + }, + { + "role": "assistant", + "content": "Read the setpoint channel." + }, + { + "role": "user", + "content": "Actuate the bench_r8 setpoint to 60.\nConfigure the bench_r8 homed channel to true to complete homing." + }, + { + "role": "assistant", + "content": "[Called: hw_configure]\nHomed the device; the readiness gate is now satisfied." + }, + { + "role": "user", + "content": "Preview an actuate of bench_r8 setpoint to 55, dry run only.\nPreview an actuate of bench_r8 setpoint to 55, dry run only." + } + ], + "tools": [ + "capability_expand", + "code_intel", + "code_search", + "config_get", + "config_list", + "env_info", + "file_find", + "file_list", + "file_read", + "git_query", + "hw_actuate", + "hw_batch_actuate", + "hw_configure", + "hw_describe", + "hw_dispense", + "hw_estop", + "hw_list", + "hw_read", + "hw_status", + "lint_check", + "memory_search", + "plugin_list", + "plugin_propose", + "plugin_status", + "plugin_versions", + "repo_map", + "research_note", + "runtime_snapshot", + "schedule_reentry", + "self_describe", + "session_list", + "skill_view", + "skills_list", + "terminal_list", + "terminal_read", + "test_run", + "text_search", + "time_get", + "tool_describe", + "tool_search", + "web_fetch" + ] + }, + "responses": [ + { + "status": 200, + "content_type": "application/json", + "body": "{\"id\": \"chatcmpl-cassette\", \"object\": \"chat.completion\", \"model\": \"cassette-model\", \"choices\": [{\"index\": 0, \"message\": {\"role\": \"assistant\", \"content\": \"\", \"tool_calls\": [{\"id\": \"call_1\", \"type\": \"function\", \"function\": {\"name\": \"hw_actuate\", \"arguments\": \"{\\\"device_id\\\": \\\"bench_r8\\\", \\\"channel_id\\\": \\\"setpoint\\\", \\\"value\\\": 55.0, \\\"dry_run\\\": true}\"}}]}, \"finish_reason\": \"tool_calls\"}], \"usage\": {\"prompt_tokens\": 64, \"completion_tokens\": 16, \"total_tokens\": 80}}" + } + ] +} diff --git a/tests/_fixtures/cassettes/r8_hardware/cassette-model-f837185b141289a5.cassette.json b/tests/_fixtures/cassettes/r8_hardware/cassette-model-f837185b141289a5.cassette.json deleted file mode 100644 index 17c5fbe5..00000000 --- a/tests/_fixtures/cassettes/r8_hardware/cassette-model-f837185b141289a5.cassette.json +++ /dev/null @@ -1,79 +0,0 @@ -{ - "fingerprint": "f837185b141289a52372ba1d5dbb0fcf990a02ef296b0051931884f7c858bb25", - "note": "captured in seed mode", - "request": { - "model": "cassette-model", - "stream": false, - "messages": [ - { - "role": "system", - "content": "You are LeapFlow, an intelligent assistant that can both converse naturally and take real actions on the user's computer.\n\n## Capabilities\nThe tool index below lists **every** registered tool by name and a one-line summary — this is the complete\ncapability contract; nothing else exists. Only a subset is directly callable this turn (via native tool calling,\nnot a JSON block in your reply). If you need a tool from the index that is not yet callable, call\n`capability_expand` with its category name first — the matching tools become callable immediately after.\nWhen the user asks what LeapFlow itself supports, whether it supports plugins, or which runtime capabilities\nare available, use the live capability evidence exposed by `plugin_list` before making capability claims; report\nconfiguration-dependent or unavailable capabilities as limitations instead of inferring from documentation.\n- **text_search**(text, pattern): Search for a regex pattern in text.\n- **text_replace**(text, old, new, count) [capability_expand category: write]: Replace occurrences of a substring in text.\n- **time_get**(): Get current date and time.\n- **env_info**(): Get system environment information (OS, Python version, cwd).\n- **skills_list**(query, category, source): List available learned skills. Use when user asks about capabilities or you need a specific skill.\n- **skill_view**(name): View the full content of a specific skill document.\n- **code_intel**(path, operation): Precise document symbols (outline) for a source file: classes, functions, and methods with line ranges. Python uses an exact AST parse; other languages use a keyword-prefix scan. Prefer over file_read mode=symbols for accurate navigation before editing. Read-only.\n- **repo_map**(path): Compact project orientation for a repository root: languages, detected test/lint commands, top-level structure, entry points, manifest, and VCS branch. Call this first when entering an unfamiliar codebase. Read-only.\n- **scm_sync**(action, cwd, remote, pull_ref, push_ref, timeout) [capability_expand category: scm]: Run a typed git SCM action. Use this instead of shell_run for git pull/push/status. For 'pull origin main then push', set action='pull_then_push', remote='origin', pull_ref='main', and omit push_ref so LeapFlow pushes the current local branch.\n- **git_query**(action, cwd, ref, path, staged, max_count, stat): Read-only structured git inspection: action=diff|log|status|branch|show. Prefer over shell_run for reading repo state — output is clipped, redacted, and log/branch are parsed into structured fields. Use scm_sync for pull/push.\n- **git_write**(action, cwd, message, stage_all, name, ref, create) [capability_expand category: scm]: Mutating git actions: action=commit (message, stage_all), branch (create+switch), checkout (switch; create=true for -b). Approval-gated. Use scm_sync for pull/push and git_query for reads.\n- **test_run**(command, cwd, timeout): Run the project's test suite and return structured results (framework, passed/failed counts, failing tests). Auto-detects the runner (pytest/npm/go/cargo) or uses a configured/explicit command; executes via the governed shell. ok=true means the runner executed — see 'success' for pass/fail.\n- **lint_check**(command, cwd, timeout): Run the project's linter and return a structured clean/issue result. Auto-detects the linter (ruff/eslint/go vet/clippy) or uses a configured/explicit command; executes via the governed shell. ok=true means the linter ran — see 'clean'.\n- **file_list**(path, pattern, depth): List files and directories at a given path. Use depth=1 or depth=2 to get a recursive tree in one call instead of listing each sub-directory separately.\n- **file_read**(path, max_lines, start_line, max_chars, mode): Read text file content with adaptive context governance. For large or unfamiliar files, prefer mode='outline' or mode='symbols' first, then use mode='raw' with start_line/max_lines for the specific range you actually need. For LeapFlow's own settings, use config_list / config_get / config_set — its config files are outside the workspace and not readable here.\n- **file_write**(path, content, mode) [capability_expand category: write]: Write content to a file (overwrite or append).\n- **code_search**(pattern, patterns, path, glob, ignore_case, multiline, max_results, context_lines): Search file CONTENTS by regex pattern across a directory tree (ripgrep-backed). Requires a regex pattern. NOT for listing or browsing directory contents — use file_list for that. Prefer this over shell_run grep: faster, skips VCS/dependency/build dirs, and returns structured path:line:column matches. Batch related lookups into ONE call via `patterns` (OR-combined, single pass) instead of issuing several separate searches. Use file_read for the surrounding context of a hit.\n- **file_find**(glob, path, max_results): Find files by a recursive glob pattern under a base path (e.g. '**/test_*.py' or '*.md'). Prefer this over shell_run find; skips VCS/dependency/build dirs.\n- **edit_file**(path, edits, dry_run, diff) [capability_expand category: write]: Apply targeted, anchored search-replace edits to an EXISTING text file (use file_write to create/overwrite). Each edit is {original_text, new_text, replace_all?}; original_text must match exactly and uniquely (or set replace_all) — a non-unique or missing anchor is rejected so files are never corrupted. Set dry_run to preview. Alternatively pass a unified 'diff' to apply its hunks as anchored edits. Far cheaper and safer than rewriting a whole file.\n- **shell_run**(command, cwd, timeout) [capability_expand category: shell]: Execute a one-shot shell command with timeout protection. Runs in the active workspace; paths resolving outside it are refused. Reach for a structured tool first when one fits — web_fetch for anything over HTTP(S), git_query/scm_sync for git, code_search/file_find/file_read for the repo, config_get/config_set for LeapFlow's own settings — because those report typed results, while a failed shell command can only be diagnosed from its exit code and stderr. Every shell run counts as an external side effect, so a failure stops the rest of the batch and is not retried automatically.\n- **terminal_open**(command, cwd, shell) [capability_expand category: terminal]: Open a PERSISTENT shell session (REPL/dev server/watch), returning a session_id for terminal_send/read/close. Disabled unless tools.terminal_session_enabled is set. For one-shot commands use shell_run instead.\n- **terminal_send**(session_id, input, wait) [capability_expand category: terminal]: Send a line of input to a persistent terminal session and return output captured shortly after.\n- **terminal_read**(session_id, wait): Drain buffered output from a persistent terminal session (optionally waiting briefly first).\n- **terminal_close**(session_id) [capability_expand category: terminal]: Terminate a persistent terminal session and release its process group.\n- **terminal_list**(): List active persistent terminal sessions.\n- **config_list**(category, limit): List LeapFlow's own writable settings (LLM model and endpoint, daemon, memory, perception, gateway, …) with current values. Use this to discover the exact key before changing anything. Optionally narrow by `category`. This is the only correct way to inspect LeapFlow configuration — never read config files from disk.\n- **config_get**(key): Read one LeapFlow setting by key (e.g. 'llm.model', 'llm.base_url', 'daemon.log_level'), returning its current value, type, scopes, and whether a change needs a daemon restart. There is no 'llm.provider' key: provider behavior is inferred from the OpenAI-compatible 'llm.base_url'. Never read LeapFlow config files from disk — use this.\n- **config_set**(key, value, scope) [capability_expand category: config]: Change one LeapFlow setting by key, e.g. switch an OpenAI-compatible LLM with key='llm.model' and key='llm.base_url'. There is no 'llm.provider' key: provider behavior is inferred from the endpoint. Values are validated and coerced; credentials are stored in the vault automatically. Call config_list or config_get first if unsure of the key. The result states whether a `leap daemon restart` is required. Never edit LeapFlow config files directly.\n- **web_fetch**(url, select, timeout, max_bytes): Read a URL over HTTP(S) and get back extracted, context-sized content: parsed JSON for API endpoints, readable text plus links for web pages. Use this for anything on the internet — prices, docs, releases, articles — instead of running curl through shell_run: it reports real HTTP status codes, retries rate limits on its own, and is a plain read so a retry is always safe. For JSON APIs pass `select` with a dotted path (e.g. 'chart.result.0.meta') to return just that part instead of the whole payload.\n- **memory_search**(query, limit): Search agent memory for relevant past experiences, observations, and facts.\n- **memory_add**(content, kind) [capability_expand category: write]: Store a new observation or insight in memory for future reference.\n- **research_note**(kind, text): Record a compact, structured note about the current task's state so it survives context compression on long / multi-step tasks. Use for durable findings, open questions still to resolve, decisions / excluded paths, and the immediate next step. One concise sentence per note.\n- **capability_expand**(category): Fetch the full callable schema for every tool in a capability category (e.g. 'hub', 'gateway', 'desktop', 'delegate', 'file', 'memory', 'skill'). The compact tool index always lists every registered tool by name and a one-line summary, but only a static low-risk subset is directly callable each turn. If you need a tool from the index that is not yet callable, call capability_expand with its category first; the matching tools become callable in this turn. Never invent a tool name — expand the category instead.\n- **delegate_task**(goal, context) [capability_expand category: delegate]: Delegate a complex sub-task to an isolated subagent. The subagent gets a fresh context and restricted tool access. Use when a task is self-contained and can be solved independently.\n- **schedule_reentry**(kind, reason, delay_seconds, event_match, max_reentries, deadline_seconds): Register a re-entry so this task can resume later from its current orientation (findings / open questions / next step). Use when work must pause and continue after a delay (kind=time) or when a matching platform event arrives (kind=event), instead of finishing now. The research-ledger state is carried over automatically.\n- **hub_push**(skill_name, visibility, version) [capability_expand category: hub]: Push a local skill to the ModelScope Hub for sharing or backup.\n- **hub_pull**(repo_id, version) [capability_expand category: hub]: Pull a skill from the ModelScope Hub to install locally.\n- **hub_search**(query) [capability_expand category: hub]: Search for skills on the Hub by keyword or description.\n- **hub_sync**(mode, dry_run) [capability_expand category: hub]: Preview or execute sync between local skills and Hub.\n- **platform_action**(platform, action, payload, backend_kind) [capability_expand category: gateway]: Execute an exact registered business action on an external platform through LeapFlow's App Connector layer. Actions must be copied from the App Connector Capability Index and are addressed as domain.operation, e.g. im.send_message or docs.create_markdown. All business fields (chat_id, text, query, etc.) MUST be placed inside `payload`, never at the top level. Example: {\"platform\":\"feishu\",\"action\":\"im.send_message\",\"payload\":{\"chat_id\":\"oc_xxx\",\"text\":\"hello\"}}. Do not invent action names, do not use management actions such as list/guide/connect/status here.\n- **platform_connect**(action, platform, credentials, options, checkpoint) [capability_expand category: gateway]: List, guide, connect, disconnect, remove, or check status for external platforms using the App Connector management namespace. Supports REST and CLI backends. Use this for management actions such as list/guide/preflight/connect/status; use platform_action only for exact registered business actions.\n- **gateway_send**(platform, chat_id, text, thread_id) [capability_expand category: gateway]: Send a message to a connected external platform (Feishu group, Telegram chat, DingTalk conversation, etc.). Requires the platform to be connected via gateway_connect first. Use gateway_connect with action='list' to see connected platforms and available chat IDs.\n- **gateway_connect**(action, platform, credentials, options) [capability_expand category: gateway]: Connect, configure, or manage external platform integrations (Feishu, DingTalk, Telegram, Slack, Discord, etc.). Conversational flow: 1) call 'guide' to get setup steps + required fields, 2) present the steps to the user and ask for ALL required credentials in a single message, 3) call 'connect' with the credentials. Goal: complete in 1–2 user turns. NEVER include credential values in your text response.\n- **plugin_list**(): List the live plugin registry and cross-subsystem capability evidence. Use this before answering questions about whether LeapFlow supports plugins, self-evolution, plugin installation, hot reload, versioning, or other runtime capabilities.\n- **plugin_status**(plugin_id): Get detailed status of a specific plugin: its declared category, runtime dependencies, contributed tools, and fiber lifecycle state.\n- **plugin_versions**(plugin_id): List recorded source versions and active pointer for a profile-scoped plugin.\n- **plugin_propose**(requested_capability, plugin_id, proposed_tools, test_cases, risk_level, evidence): Create a side-effect-free PluginProposal from explicit capability-gap evidence. Use this before plugin_generate when a missing capability should be reviewed. Does not call an LLM, write files, or install anything.\n- **assess_compatibility**(manifest, source_path) [capability_expand category: plugin_management]: Assess whether a foreign plugin manifest or real DSH source bundle is compatible with LeapFlow. A source bundle assessment is static: runtime_ready stays false until restricted Node discovery during plugin_install. Returns component-level verdicts and limitations.\n- **plugin_generate**(plugin_id, description, proposal_id) [capability_expand category: system]: Generate a new ToolPlugin from a natural-language capability description. The LLM produces code that conforms to the ToolPlugin Protocol; it is then rigorously validated (syntax, structure, import, protocol conformance). The generated source is stored in CAS and receives explicit content approval. It DOES NOT install the plugin — installation requires a second, mutation-specific approval via plugin_install.\n- **plugin_install**(plugin_id, code, marketplace_name, source_path, proposal_id, version_label) [capability_expand category: system]: Install a plugin from exactly one source: validated Python code, the configured Python marketplace, or a real DSH source bundle. DSH bundles run restricted Node discovery before registration; only public host tools are installed and client UI limitations are reported. Writes profile-scoped state and mutates the process-global registry. REQUIRES APPROVAL.\n- **plugin_rollback**(plugin_id, version) [capability_expand category: system]: Rollback a profile-scoped plugin to a recorded source version and reload it. REQUIRES APPROVAL.\n- **plugin_reload**(plugin_id, version_label) [capability_expand category: system]: Hot-reload a plugin at runtime. Disposes the old plugin fiber, re-imports its module, and registers a fresh instance. Existing in-flight turns are unaffected (snapshot isolation). REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_disable**(plugin_id) [capability_expand category: system]: Disable a plugin by disposing its fiber, removing its tools from the runtime registry. Cannot disable self_management itself. REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_remove**(plugin_id, delete_source) [capability_expand category: system]: Terminally remove a plugin: dispose its fiber, unregister its tools, remove reload metadata, and optionally delete its profile-scoped source file. Cannot remove self_management itself. REQUIRES APPROVAL.\n- **plugin_unquarantine**(plugin_id) [capability_expand category: plugin_management]: Restore a quarantined plugin to probation status for re-evaluation. Unfreezes the trust ledger, transitions the proposal back to PROBATION, and reloads the plugin. REQUIRES APPROVAL.\n- **plugin_enable**(plugin_id) [capability_expand category: system]: Re-enable a previously disabled plugin by reloading its module and registering a fresh instance. REQUIRES APPROVAL.\n- **self_describe**(facet): Introspect LeapFlow's own identity, capabilities, runtime state, evolution metrics, or platform connections. Use facet='all' only when a comprehensive self-check is explicitly requested.\n- **runtime_snapshot**(): Lightweight (~150 token) flat snapshot of current runtime state: model, context budget, posture, disclosure level, turn count, cache hit rate, uptime, tool count, and pending approvals.\n- **schedule_create**(trigger_expression, instruction, execution_mode, max_retries, delivery_target) [capability_expand category: scheduler]: Create a new scheduled task. Specify a trigger expression (e.g. '30m', 'every 2h', '0 9 * * *') and the instruction to execute on each trigger. Optionally provide an execution mode and a delivery target to receive notifications.\n- **schedule_list**() [capability_expand category: scheduler]: List all scheduled tasks with their current state, trigger, and next due time.\n- **schedule_status**(task_id) [capability_expand category: scheduler]: Get detailed status and recent execution history for a scheduled task.\n- **schedule_pause**(task_id) [capability_expand category: scheduler]: Pause a scheduled task so it stops firing without being cancelled.\n- **schedule_resume**(task_id) [capability_expand category: scheduler]: Resume a paused scheduled task — re-arms it and recalculates next due time.\n- **schedule_cancel**(task_id) [capability_expand category: scheduler]: Cancel a scheduled task permanently.\n- **tool_search**(query, max_results): Search all registered tools by keyword relevance. Returns a ranked list of matching tools with name, category, and summary. Use this when you need a tool but are unsure of its exact name or category.\n- **tool_describe**(tool_name): Get the full callable schema of a single tool by exact name. Returns the tool's description, parameters, and metadata. Use after tool_search to inspect a specific tool before calling it.\n- **hw_list**(): List connected hardware devices with their channel counts and measured quantities. Start here; it does not include operating limits.\n- **hw_describe**(device_id): Return the full reference for one device: every channel, its unit, its operating envelope, rate limit, reversibility, and required interlocks, plus any prior outcomes recorded for its writable channels. Required before commanding a device.\n- **hw_read**(device_id, channel_id): Read the current value of one device channel. Has no physical effect.\n- **hw_status**(device_id): Report connection health, halt capability, and recent notable events (threshold excursions, lost samples, stalled channels) for one device.\n- **hw_configure**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Set a configuration value or setpoint on a channel declaring effect=configure. Setpoints often have inertia: the value may need time to stabilise.\n- **hw_actuate**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Command motion or output on a channel declaring effect=actuate. This moves physical hardware; a repeat from an unknown state is not a safe retry.\n- **hw_dispense**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Consume an irreversible resource on a channel declaring effect=dispense. Running this twice dispenses twice; never repeat it after a failure without first verifying what already happened.\n- **hw_estop**(device_id) [capability_expand category: hardware]: Stop all motion and output on a device immediately. Never requires approval; use it whenever device behaviour is unexpected.\n- **session_search**(query, limit) [capability_expand category: unclassified]: Search past conversation sessions in the current workspace for relevant context.\n- **session_list**(limit): List recent conversation sessions in the current workspace with stable ids, titles, dates, and summaries. Use for browsing past tasks or when user asks to see history without specific search terms. No keywords needed — returns chronological list.\n- **session_detail**(session_id, limit, offset, include_inactive) [capability_expand category: unclassified]: Read a paginated persisted transcript for one past conversation session in the current workspace. Use after session_list or session_search returns a session_id.\n- **click**(element_index) [capability_expand category: desktop]: Click a UI element by its element_index (from the latest observe_ui snapshot)\n- **get_clipboard**() [capability_expand category: desktop]: Read current clipboard text content\n- **list_apps**(filter, running_only) [capability_expand category: desktop]: List available applications on this system. Use to discover correct bundle_id before switch_app.\n- **list_windows**() [capability_expand category: desktop]: List all top-level windows with pid, window_id, title, and per-window state (minimized, on-screen). Call this first to pick the pid and window_id that observe_ui and other window tools require.\n- **observe_ui**(pid, window_id, query) [capability_expand category: desktop]: Snapshot one window's actionable UI elements, each tagged with an element_index for click/right_click/read_text. Re-observe after actions — indices belong to one snapshot. Requires the window's pid and window_id from list_windows.\n- **open_url**(url, app_id) [capability_expand category: desktop]: Open a URL in the default or specified browser\n- **read_text**(element_index) [capability_expand category: desktop]: Read the text content of a specific UI element from the latest snapshot\n- **right_click**(element_index) [capability_expand category: desktop]: Right-click a UI element to open its context menu. Returns visible menu items.\n- **screenshot**(pid, window_id) [capability_expand category: desktop]: Capture a screenshot for visual verification. With pid + window_id captures that window (works across all displays); defaults to the last observed window, or the full desktop when no window has been observed.\n- **scroll**(element_index, direction, amount, pid, window_id) [capability_expand category: desktop]: Scroll a scrollable area of a window. Omit element_index to scroll the window's focused/page scroller; pass one to scroll an exact element from the latest snapshot.\n- **select_text**(element_index) [capability_expand category: desktop]: Select all text in a UI element (focus + select-all, for subsequent copy)\n- **set_clipboard**(text) [capability_expand category: desktop]: Write text to the clipboard\n- **shortcut**(keys) [capability_expand category: desktop]: Execute a keyboard shortcut\n- **switch_app**(app_id) [capability_expand category: desktop]: Switch to an app (launch if needed, activate, verify)\n- **type_text**(text) [capability_expand category: desktop]: Type text into the currently focused element\n- **wait**(seconds) [capability_expand category: desktop]: Wait for a specified duration before continuing\n- **wait_until**(condition, pid, window_id, timeout, poll_interval) [capability_expand category: desktop]: Wait until a UI condition is met (polls UI tree). Returns elements when found or on timeout.\n- **wait_until_stable**(timeout, poll_interval, pid, window_id) [capability_expand category: desktop]: Wait until the UI stops changing (element set stabilizes across polls).\n\n## App Connector Capability Index\nLeapFlow can onboard and manage external apps through `platform_connect` and execute exact registered business actions through `platform_action`.\nFor requests about connecting, setting up, configuring, enabling, or managing a supported app, use `platform_connect` first instead of generating SDK/Webhook sample code.\n`platform_connect.action` is the management namespace: list, guide, preflight, connect, disconnect, remove, status, events_start, events_stop, events_status.\n`platform_action.action` is only for exact registered platform business actions listed below, such as `im.send_message`; never use management actions like `list` or `guide` there.\nAll business fields MUST go inside `payload`; top-level keys are only `platform`, `action`, and `payload`.\nDo not invent platform IDs or platform action names. If the needed action is not listed, ask for discovery/clarification instead of guessing.\nUse `platform_connect` with `action='guide'` and the matching `platform` to start onboarding; use `action='list'` when the app is unclear.\nWhen a pending onboarding state is present, continue from that state with `platform_connect` instead of asking the user to restate the app.\nSupported apps:\n- `api_server`: API Server (OpenAI Compatible) (category=api; backend=adapter; platform_action actions=none registered)\n- `dingtalk`: 钉钉 (DingTalk) (category=im; backend=adapter; platform_action actions=none registered)\n- `feishu`: 飞书 (Feishu/Lark) (category=im; backend=cli)\n - `calendar.create_event` payload={summary*, start_time*, end_time*, attendees} [write/high]\n - `docs.create_markdown` payload={title*, markdown*, folder_token} [write/medium]\n - `drive.search` payload={query*, limit} [read/medium]\n - `im.add_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.download_resource` payload={message_id*, file_key*, type*} [read/low]\n - `im.get_messages` payload={message_ids*} [read/low]\n - `im.list_chats` payload={page_size} [read/low]\n - `im.list_messages` payload={chat_id*} [read/low]\n - `im.list_thread_messages` payload={thread*} [read/low]\n - `im.remove_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.reply_message` payload={message_id*, text*} [send/high]\n - `im.search_chats` payload={query*, page_size} [read/low]\n - `im.search_messages` payload={query*} [read/medium]\n - `im.send_message` payload={chat_id*, text*, thread_id} [send/high]\n - `im.update_card` payload={token*, card*} [write/medium]\n - `im.update_message` payload={message_id*, text*} [write/medium]\n - `mail.search_unread` payload={query, limit} [read/high]\n - `sheets.append_row` payload={spreadsheet_token*, sheet_id*, values*} [write/medium]\n - `task.create` payload={title*, description, due_time} [write/medium]\n- `telegram`: Telegram (category=social; backend=adapter; platform_action actions=none registered)\n- `webhook`: Webhook (Generic) (category=webhook; backend=adapter; platform_action actions=none registered)\n\n## Tool Usage\nTools are normally invoked through the native function-calling mechanism, not by writing JSON in your reply\ntext. Only if the provider signals that native function calling is unavailable for this turn, fall back to a\nsingle JSON code block: `{\"name\": \"tool_name\", \"arguments\": {\"key\": \"value\"}}` — use this fallback format\nonly, never both. Only call a tool whose exact name appears in the tool index above and is currently callable\n(or reachable via `capability_expand`). Never invent, rename, alias, or guess a tool name, platform ID, or\nplatform action from argument shape or wording — if the index does not list it, it does not exist.\n`platform_connect.action` (list/guide/preflight/connect/disconnect/remove/status/events_start/events_stop/\nevents_status) is the App Connector management namespace; `platform_action.action` only accepts exact\nregistered business actions such as `im.send_message` shown in the App Connector Capability Index — never mix\nthe two namespaces. If a tool call returns an unknown/unavailable result, use the returned suggestions or\navailable names for a single retry instead of trying further variations of the same guess.\n\n**Side-effect action rule** (`platform_action` with effect=send/write/execute):\n- Call each unique action+payload **exactly once**. Never include duplicates in the same turn.\n- Once the result returns `\"completed\": true`, that action is DONE for this task. Do NOT call it again\n in any subsequent turn — immediately summarize the result for the user instead.\n- The system enforces idempotency: duplicate calls are blocked and will not execute.\n- If the user explicitly requests sending/writing multiple times, use distinct payloads per call.\n\n**Resource identifier provenance rule**:\n- NEVER fabricate, guess, or infer resource identifiers (chat_id, message_id, file_key, user_id, etc.).\n Every resource ID used in a side-effect action MUST come from a successful API response in this session.\n- If a read/list action fails (e.g. authorization error), you do NOT have valid resource IDs.\n Report the failure to the user — do NOT attempt the dependent write/send action with a guessed ID.\n- When a tool result contains `\"llm_instruction\"`, follow it exactly.\n\n## Guidelines\n1. **Direct answers first**: If you already know the answer, respond directly without tools.\n2. **Avoid redundant tool calls**: Do not call the same tool with the same arguments more than once in the same user turn. When an existing tool result already answers the user's request, stop calling tools and answer directly.\n3. **Use tools proactively**: When the user asks about files, time, system state, or needs actions performed, use the appropriate tool.\n4. **Chain tools when needed**: You can call multiple tools in sequence (e.g., list files → read file → summarize).\n5. **Handle failures gracefully**: If a tool fails, explain what went wrong and suggest alternatives. If it failed because of an unknown tool/platform/action name, retry once with an exact name from the returned suggestions, then explain rather than keep guessing.\n6. **Summarize results naturally**: After tool execution, synthesize the results into a helpful answer rather than dumping raw output.\n7. **Stay conversational**: Maintain a natural, helpful tone. Acknowledge context from earlier in the conversation.\n8. **Recall past work**:\n - Broad queries (\"之前做了什么\", \"列出任务\"): answer from the \"Recent Task History\" section already in your context. If insufficient, call session_list.\n - Specific lookups (\"上次那个配置怎么改的\"): call session_search with relevant phrases (NOT single characters).\n - Do NOT call search tools repeatedly with keyword fragments. One well-phrased call is better than ten fragmented ones.\n\n## Coding & Verification\nWhen working with code, prefer the precise built-in tools over ad-hoc shell: use `repo_map` to orient in an\nunfamiliar project, `code_search`/`file_find` to locate and `code_intel` for symbols, `edit_file` (anchored\nsearch-replace, or a unified `diff`) to change files — never rewrite a whole file to change a few lines — and\n`git_query` to inspect diffs/log. After edits, check `syntax_ok` in the result and run `test_run`/`lint_check`\nbefore declaring the work done. Batch independent read-only calls (search/read/query) into a single turn — they\nrun in parallel.\n\n## Presentation Style\n1. **Polished Markdown only**: Format user-facing answers with clean Markdown headings, short paragraphs, and concise bullets. Use tables only when they improve comparison or scanning.\n2. **Terminal-friendly layout**: Keep lines readable in a TUI; avoid dense walls of text, deeply nested lists, oversized ASCII art, or heavy visual blocks.\n3. **Elegant emphasis**: Use bold text sparingly for key terms and conclusions. Avoid excessive emojis, decorative symbols, repeated separators, or visual noise.\n4. **Theme-safe colors**: Do not emit ANSI escape codes, HTML color tags, Rich markup, or hardcoded color names. Rely on the TUI theme to render Markdown professionally.\n5. **No leaked tool protocol**: Never show tool-call JSON, internal schemas, raw observations, tool result payloads, or hidden reasoning in the final answer unless the user explicitly asks for raw/debug output. Treat any prior `{\"name\": ..., \"arguments\": ...}` blocks and `Tool result (...)` messages as internal execution context only.\n6. **Professional closure**: End with a concise conclusion or next step when helpful; avoid rambling after the useful answer is complete.\n\nWhen finished with all tool calls, respond normally without a JSON block, tool-call transcript, or process log.\n\n## Task Contract\n- Task ID: turn-1\n- Original user request: List the connected hardware devices, then describe bench_r8 in full.\n- Workspace root: \n- Allowed roots: \n- Treat relative project paths as relative to the workspace root; never infer `.` as the project root when a workspace root is provided.\n- Workspace boundary is enforced by tools: do not read, search, edit, or run commands against paths outside the allowed roots unless the user explicitly requests an external path and the tool/approval policy permits it.\n- LeapFlow workspace config is optional at `/.leapflow/config.yaml`; runtime config is loaded from `~/.leapflow/config/user.yaml` and `~/.leapflow/profiles//config/*.yaml`.\n- Preserve this task contract across summarization, compression, tool loops, and memory retrieval.\n" - }, - { - "role": "system", - "content": "\n## Recent Session Summary\n- [user] List the connected hardware devices, then describe bench_r8 in full.\n" - }, - { - "role": "user", - "content": "List the connected hardware devices, then describe bench_r8 in full.\nList the connected hardware devices, then describe bench_r8 in full." - }, - { - "role": "assistant", - "content": "", - "tool_calls": [ - "hw_list" - ] - }, - { - "role": "tool", - "content": "{\"ok\": true, \"devices\": [{\"device_id\": \"bench_r8\", \"display_name\": \"R8 Simulated Bench\", \"location\": \"journey-lab\", \"channels\": 3, \"writable\": 2, \"streaming\": 1, \"quantities\": [\"state.homed\", \"temperature\"], \"verified\": true, \"halt_supported\": true}], \"count\": 1, \"hint\": \"Call hw_describe(device_id) for channel limits before commanding a device.\", \"execution_id\": \"\", \"idempotency_key\": \"\", \"execution_policy\": \"read_only\", \"tool_call_id\": \"call_1\", \"execution_status\": \"completed\"}", - "tool_result": true - } - ], - "tools": [ - "capability_expand", - "code_intel", - "code_search", - "config_get", - "config_list", - "env_info", - "file_find", - "file_list", - "file_read", - "git_query", - "hw_describe", - "hw_list", - "hw_read", - "hw_status", - "lint_check", - "memory_search", - "plugin_list", - "plugin_propose", - "plugin_status", - "plugin_versions", - "repo_map", - "research_note", - "runtime_snapshot", - "schedule_reentry", - "self_describe", - "session_list", - "skill_view", - "skills_list", - "terminal_list", - "terminal_read", - "test_run", - "text_search", - "time_get", - "tool_describe", - "tool_search", - "web_fetch" - ] - }, - "responses": [ - { - "status": 200, - "content_type": "application/json", - "body": "{\"id\": \"chatcmpl-cassette\", \"object\": \"chat.completion\", \"model\": \"cassette-model\", \"choices\": [{\"index\": 0, \"message\": {\"role\": \"assistant\", \"content\": \"\", \"tool_calls\": [{\"id\": \"call_1\", \"type\": \"function\", \"function\": {\"name\": \"hw_describe\", \"arguments\": \"{\\\"device_id\\\": \\\"bench_r8\\\"}\"}}]}, \"finish_reason\": \"tool_calls\"}], \"usage\": {\"prompt_tokens\": 64, \"completion_tokens\": 16, \"total_tokens\": 80}}" - } - ] -} diff --git a/tests/_fixtures/cassettes/r8_hardware/cassette-model-ff9056562a13326a.cassette.json b/tests/_fixtures/cassettes/r8_hardware/cassette-model-ff9056562a13326a.cassette.json new file mode 100644 index 00000000..a063f3f9 --- /dev/null +++ b/tests/_fixtures/cassettes/r8_hardware/cassette-model-ff9056562a13326a.cassette.json @@ -0,0 +1,80 @@ +{ + "fingerprint": "ff9056562a13326a54b0ec73b68422054089499292e1c93864a6d0baf7798030", + "note": "captured in seed mode", + "request": { + "model": "cassette-model", + "stream": false, + "messages": [ + { + "role": "system", + "content": "You are LeapFlow, an intelligent assistant that can both converse naturally and take real actions on the user's computer.\n\n## Capabilities\nThe tool index below lists **every** registered tool by name and a one-line summary — this is the complete\ncapability contract; nothing else exists. Only a subset is directly callable this turn (via native tool calling,\nnot a JSON block in your reply). If you need a tool from the index that is not yet callable, call\n`capability_expand` with its category name first — the matching tools become callable immediately after.\nWhen the user asks about LeapFlow **itself** — its identity/version/model, what tools, plugins, or capabilities\nit has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of\nself-awareness — answer from live runtime evidence via `self_describe` (facet=capabilities for \"what can you\ndo / do you support X\", identity for version/model, runtime for context/posture, or `all` for a full self-check),\nnever by reading LeapFlow's own source code, documentation, or memory. Report configuration-dependent or\nunavailable capabilities as limitations rather than inferring them.\n- **text_search**(text, pattern): Search for a regex pattern in text.\n- **text_replace**(text, old, new, count) [capability_expand category: write]: Replace occurrences of a substring in text.\n- **time_get**(): Get current date and time.\n- **env_info**(): Get system environment information (OS, Python version, cwd).\n- **skills_list**(query, category, source): List available learned skills. Use when user asks about capabilities or you need a specific skill.\n- **skill_view**(name): View the full content of a specific skill document.\n- **skill_list_files**(name) [capability_expand category: skills]: List all support files in a skill directory (configs, templates, examples). Use before skill_file_read to discover available files.\n- **skill_file_read**(name, file_path) [capability_expand category: skills]: Read an individual support file from a skill directory. Use skill_list_files first to discover available files.\n- **code_intel**(path, operation): Precise document symbols (outline) for a source file: classes, functions, and methods with line ranges. Python uses an exact AST parse; other languages use a keyword-prefix scan. Prefer over file_read mode=symbols for accurate navigation before editing. Read-only.\n- **repo_map**(path): Compact project orientation for a repository root: languages, detected test/lint commands, top-level structure, entry points, manifest, and VCS branch. Call this first when entering an unfamiliar codebase. Read-only.\n- **scm_sync**(action, cwd, remote, pull_ref, push_ref, timeout) [capability_expand category: scm]: Run a typed git SCM action. Use this instead of shell_run for git pull/push/status. For 'pull origin main then push', set action='pull_then_push', remote='origin', pull_ref='main', and omit push_ref so LeapFlow pushes the current local branch.\n- **git_query**(action, cwd, ref, path, staged, max_count, stat): Read-only structured git inspection: action=diff|log|status|branch|show. Prefer over shell_run for reading repo state — output is clipped, redacted, and log/branch are parsed into structured fields. Use scm_sync for pull/push.\n- **git_write**(action, cwd, message, stage_all, name, ref, create) [capability_expand category: scm]: Mutating git actions: action=commit (message, stage_all), branch (create+switch), checkout (switch; create=true for -b). Approval-gated. Use scm_sync for pull/push and git_query for reads.\n- **test_run**(command, cwd, timeout): Run the project's test suite and return structured results (framework, passed/failed counts, failing tests). Auto-detects the runner (pytest/npm/go/cargo) or uses a configured/explicit command; executes via the governed shell. ok=true means the runner executed — see 'success' for pass/fail.\n- **lint_check**(command, cwd, timeout): Run the project's linter and return a structured clean/issue result. Auto-detects the linter (ruff/eslint/go vet/clippy) or uses a configured/explicit command; executes via the governed shell. ok=true means the linter ran — see 'clean'.\n- **file_list**(path, pattern, depth): List files and directories at a given path. Use depth=1 or depth=2 to get a recursive tree in one call instead of listing each sub-directory separately.\n- **file_read**(path, max_lines, start_line, max_chars, mode): Read text file content with adaptive context governance. For large or unfamiliar files, prefer mode='outline' or mode='symbols' first, then use mode='raw' with start_line/max_lines for the specific range you actually need. For LeapFlow's own settings, use config_list / config_get / config_set — its config files are outside the workspace and not readable here.\n- **file_write**(path, content, mode) [capability_expand category: write]: Write content to a file (overwrite or append).\n- **code_search**(pattern, patterns, path, glob, ignore_case, multiline, max_results, context_lines): Search file CONTENTS by regex pattern across a directory tree (ripgrep-backed). Requires a regex pattern. NOT for listing or browsing directory contents — use file_list for that. Prefer this over shell_run grep: faster, skips VCS/dependency/build dirs, and returns structured path:line:column matches. Batch related lookups into ONE call via `patterns` (OR-combined, single pass) instead of issuing several separate searches. Use file_read for the surrounding context of a hit.\n- **file_find**(glob, path, max_results): Find files by a recursive glob pattern under a base path (e.g. '**/test_*.py' or '*.md'). Prefer this over shell_run find; skips VCS/dependency/build dirs.\n- **edit_file**(path, edits, dry_run, diff) [capability_expand category: write]: Apply targeted, anchored search-replace edits to an EXISTING text file (use file_write to create/overwrite). Each edit is {original_text, new_text, replace_all?}; original_text must match exactly and uniquely (or set replace_all) — a non-unique or missing anchor is rejected so files are never corrupted. Set dry_run to preview. Alternatively pass a unified 'diff' to apply its hunks as anchored edits. Far cheaper and safer than rewriting a whole file.\n- **shell_run**(command, cwd, timeout) [capability_expand category: shell]: Execute a one-shot shell command with timeout protection. Runs in the active workspace; paths resolving outside it are refused. Reach for a structured tool first when one fits — web_fetch for anything over HTTP(S), git_query/scm_sync for git, code_search/file_find/file_read for the repo, config_get/config_set for LeapFlow's own settings — because those report typed results, while a failed shell command can only be diagnosed from its exit code and stderr. Every shell run counts as an external side effect, so a failure stops the rest of the batch and is not retried automatically.\n- **terminal_open**(command, cwd, shell) [capability_expand category: terminal]: Open a PERSISTENT shell session (REPL/dev server/watch), returning a session_id for terminal_send/read/close. Disabled unless tools.terminal_session_enabled is set. For one-shot commands use shell_run instead.\n- **terminal_send**(session_id, input, wait) [capability_expand category: terminal]: Send a line of input to a persistent terminal session and return output captured shortly after.\n- **terminal_read**(session_id, wait): Drain buffered output from a persistent terminal session (optionally waiting briefly first).\n- **terminal_close**(session_id) [capability_expand category: terminal]: Terminate a persistent terminal session and release its process group.\n- **terminal_list**(): List active persistent terminal sessions.\n- **config_list**(category, limit): List LeapFlow's own writable settings (LLM model and endpoint, daemon, memory, perception, gateway, …) with current values. Use this to discover the exact key before changing anything. Optionally narrow by `category`. This is the only correct way to inspect LeapFlow configuration — never read config files from disk.\n- **config_get**(key): Read one LeapFlow setting by key (e.g. 'llm.model', 'llm.base_url', 'daemon.log_level'), returning its current value, type, scopes, and whether a change needs a daemon restart. There is no 'llm.provider' key: provider behavior is inferred from the OpenAI-compatible 'llm.base_url'. Never read LeapFlow config files from disk — use this.\n- **config_set**(key, value, scope) [capability_expand category: config]: Change one LeapFlow setting by key, e.g. switch an OpenAI-compatible LLM with key='llm.model' and key='llm.base_url'. There is no 'llm.provider' key: provider behavior is inferred from the endpoint. Values are validated and coerced; credentials are stored in the vault automatically. Call config_list or config_get first if unsure of the key. The result states whether a `leap daemon restart` is required. Never edit LeapFlow config files directly.\n- **web_fetch**(url, select, timeout, max_bytes): Read a URL over HTTP(S) and get back extracted, context-sized content: parsed JSON for API endpoints, readable text plus links for web pages. Use this for anything on the internet — prices, docs, releases, articles — instead of running curl through shell_run: it reports real HTTP status codes, retries rate limits on its own, and is a plain read so a retry is always safe. For JSON APIs pass `select` with a dotted path (e.g. 'chart.result.0.meta') to return just that part instead of the whole payload.\n- **memory_search**(query, limit): Search agent memory for relevant past experiences, observations, and facts.\n- **memory_add**(content, kind) [capability_expand category: write]: Store a new observation or insight in memory for future reference.\n- **research_note**(kind, text): Record a compact, structured note about the current task's state so it survives context compression on long / multi-step tasks. Use for durable findings, open questions still to resolve, decisions / excluded paths, and the immediate next step. One concise sentence per note.\n- **capability_expand**(category): Fetch the full callable schema for every tool in a capability category (e.g. 'hub', 'gateway', 'desktop', 'delegate', 'file', 'memory', 'skill'). The compact tool index always lists every registered tool by name and a one-line summary, but only a static low-risk subset is directly callable each turn. If you need a tool from the index that is not yet callable, call capability_expand with its category first; the matching tools become callable in this turn. Never invent a tool name — expand the category instead.\n- **delegate_task**(goal, context) [capability_expand category: delegate]: Delegate a complex sub-task to an isolated subagent. The subagent gets a fresh context and restricted tool access. Use when a task is self-contained and can be solved independently.\n- **schedule_reentry**(kind, reason, delay_seconds, event_match, max_reentries, deadline_seconds): Register a re-entry so this task can resume later from its current orientation (findings / open questions / next step). Use when work must pause and continue after a delay (kind=time) or when a matching platform event arrives (kind=event), instead of finishing now. The research-ledger state is carried over automatically.\n- **hub_push**(skill_name, visibility, version) [capability_expand category: hub]: Push a local skill to the ModelScope Hub for sharing or backup.\n- **hub_pull**(repo_id, version) [capability_expand category: hub]: Pull a skill from the ModelScope Hub to install locally.\n- **hub_search**(query) [capability_expand category: hub]: Search for skills on the Hub by keyword or description.\n- **hub_sync**(mode, dry_run) [capability_expand category: hub]: Preview or execute sync between local skills and Hub.\n- **platform_action**(platform, action, payload, backend_kind) [capability_expand category: gateway]: Execute an exact registered business action on an external platform through LeapFlow's App Connector layer. Actions must be copied from the App Connector Capability Index and are addressed as domain.operation, e.g. im.send_message or docs.create_markdown. All business fields (chat_id, text, query, etc.) MUST be placed inside `payload`, never at the top level. Example: {\"platform\":\"feishu\",\"action\":\"im.send_message\",\"payload\":{\"chat_id\":\"oc_xxx\",\"text\":\"hello\"}}. Do not invent action names, do not use management actions such as list/guide/connect/status here.\n- **platform_connect**(action, platform, credentials, options, checkpoint) [capability_expand category: gateway]: List, guide, connect, disconnect, remove, or check status for external platforms using the App Connector management namespace. Supports REST and CLI backends. Use this for management actions such as list/guide/preflight/connect/status; use platform_action only for exact registered business actions.\n- **gateway_send**(platform, chat_id, text, thread_id) [capability_expand category: gateway]: Send a message to a connected external platform (Feishu group, Telegram chat, DingTalk conversation, etc.). Requires the platform to be connected via gateway_connect first. Use gateway_connect with action='list' to see connected platforms and available chat IDs.\n- **gateway_connect**(action, platform, credentials, options) [capability_expand category: gateway]: Connect, configure, or manage external platform integrations (Feishu, DingTalk, Telegram, Slack, Discord, etc.). Conversational flow: 1) call 'guide' to get setup steps + required fields, 2) present the steps to the user and ask for ALL required credentials in a single message, 3) call 'connect' with the credentials. Goal: complete in 1–2 user turns. NEVER include credential values in your text response.\n- **plugin_list**(): List the live plugin registry across Tool/Gateway/LLM subsystems for plugin management: which plugins are loaded, their category and fiber lifecycle state. For questions about LeapFlow's own capabilities or whether it supports X, use self_describe instead.\n- **plugin_status**(plugin_id): Get detailed status of a specific plugin: its declared category, runtime dependencies, contributed tools, and fiber lifecycle state.\n- **plugin_versions**(plugin_id): List recorded source versions and active pointer for a profile-scoped plugin.\n- **plugin_propose**(requested_capability, plugin_id, proposed_tools, test_cases, risk_level, evidence): Create a side-effect-free PluginProposal from explicit capability-gap evidence. Use this before plugin_generate when a missing capability should be reviewed. Does not call an LLM, write files, or install anything.\n- **assess_compatibility**(manifest, source_path) [capability_expand category: plugin_management]: Assess whether a foreign plugin manifest or real DSH source bundle is compatible with LeapFlow. A source bundle assessment is static: runtime_ready stays false until restricted Node discovery during plugin_install. Returns component-level verdicts and limitations.\n- **plugin_generate**(plugin_id, description, proposal_id) [capability_expand category: system]: Generate a new ToolPlugin from a natural-language capability description. The LLM produces code that conforms to the ToolPlugin Protocol; it is then rigorously validated (syntax, structure, import, protocol conformance). The generated source is stored in CAS and receives explicit content approval. It DOES NOT install the plugin — installation requires a second, mutation-specific approval via plugin_install.\n- **plugin_install**(plugin_id, code, marketplace_name, source_path, proposal_id, version_label) [capability_expand category: system]: Install a plugin from exactly one source: validated Python code, the configured Python marketplace, or a real DSH source bundle. DSH bundles run restricted Node discovery before registration; only public host tools are installed and client UI limitations are reported. Writes profile-scoped state and mutates the process-global registry. REQUIRES APPROVAL.\n- **plugin_rollback**(plugin_id, version) [capability_expand category: system]: Rollback a profile-scoped plugin to a recorded source version and reload it. REQUIRES APPROVAL.\n- **plugin_reload**(plugin_id, version_label) [capability_expand category: system]: Hot-reload a plugin at runtime. Disposes the old plugin fiber, re-imports its module, and registers a fresh instance. Existing in-flight turns are unaffected (snapshot isolation). REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_disable**(plugin_id) [capability_expand category: system]: Disable a plugin by disposing its fiber, removing its tools from the runtime registry. Cannot disable self_management itself. REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_remove**(plugin_id, delete_source) [capability_expand category: system]: Terminally remove a plugin: dispose its fiber, unregister its tools, remove reload metadata, and optionally delete its profile-scoped source file. Cannot remove self_management itself. REQUIRES APPROVAL.\n- **plugin_unquarantine**(plugin_id) [capability_expand category: plugin_management]: Restore a quarantined plugin to probation status for re-evaluation. Unfreezes the trust ledger, transitions the proposal back to PROBATION, and reloads the plugin. REQUIRES APPROVAL.\n- **plugin_enable**(plugin_id) [capability_expand category: system]: Re-enable a previously disabled plugin by reloading its module and registering a fresh instance. REQUIRES APPROVAL.\n- **self_describe**(facet): Answer questions about LeapFlow ITSELF from live runtime evidence — its identity/version/model, what tools and plugins it has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of self-awareness. Prefer this over reading LeapFlow's own source code. Pick facet=capabilities for 'what can you do / do you support X', identity for version/model, runtime for context/posture, or all for a full self-check when explicitly requested.\n- **runtime_snapshot**(): Lightweight (~150 token) flat snapshot of current runtime state: model, context budget, posture, disclosure level, turn count, cache hit rate, uptime, tool count, and pending approvals.\n- **schedule_create**(trigger_expression, instruction, execution_mode, max_retries, delivery_target) [capability_expand category: scheduler]: Create a new scheduled task. Specify a trigger expression (e.g. '30m', 'every 2h', '0 9 * * *') and the instruction to execute on each trigger. Optionally provide an execution mode and a delivery target to receive notifications.\n- **schedule_list**() [capability_expand category: scheduler]: List all scheduled tasks with their current state, trigger, and next due time.\n- **schedule_status**(task_id) [capability_expand category: scheduler]: Get detailed status and recent execution history for a scheduled task.\n- **schedule_pause**(task_id) [capability_expand category: scheduler]: Pause a scheduled task so it stops firing without being cancelled.\n- **schedule_resume**(task_id) [capability_expand category: scheduler]: Resume a paused scheduled task — re-arms it and recalculates next due time.\n- **schedule_cancel**(task_id) [capability_expand category: scheduler]: Cancel a scheduled task permanently.\n- **tool_search**(query, max_results): Search all registered tools by keyword relevance. Returns a ranked list of matching tools with name, category, and summary. Use this when you need a tool but are unsure of its exact name or category.\n- **tool_describe**(tool_name): Get the full callable schema of a single tool by exact name. Returns the tool's description, parameters, and metadata. Use after tool_search to inspect a specific tool before calling it.\n- **hw_list**(): List connected hardware devices with their channel counts and measured quantities. Start here; it does not include operating limits.\n- **hw_describe**(device_id): Return the full reference for one device: every channel, its unit, its operating envelope, rate limit, reversibility, and required interlocks, plus any prior outcomes recorded for its writable channels. Required before commanding a device.\n- **hw_read**(device_id, channel_id): Read the current value of one device channel. Has no physical effect.\n- **hw_status**(device_id): Report connection health, halt capability, and recent notable events (threshold excursions, lost samples, stalled channels) for one device.\n- **hw_configure**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Set a configuration value or setpoint on a channel declaring effect=configure. Setpoints often have inertia: the value may need time to stabilise.\n- **hw_actuate**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Command motion or output on a channel declaring effect=actuate. This moves physical hardware; a repeat from an unknown state is not a safe retry.\n- **hw_dispense**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Consume an irreversible resource on a channel declaring effect=dispense. Running this twice dispenses twice; never repeat it after a failure without first verifying what already happened.\n- **hw_estop**(device_id) [capability_expand category: hardware]: Stop all motion and output on a device immediately. Never requires approval; use it whenever device behaviour is unexpected.\n- **hw_batch_actuate**(device_id, commands, dry_run, verify) [capability_expand category: hardware]: Write multiple actuator channels in one atomic transaction. Used for coordinated multi-joint control where all positions must be commanded simultaneously (e.g. robot arm joint-space control).\n- **session_search**(query, limit) [capability_expand category: unclassified]: Search past conversation sessions in the current workspace for relevant context.\n- **session_list**(limit): List recent conversation sessions in the current workspace with stable ids, titles, dates, and summaries. Use for browsing past tasks or when user asks to see history without specific search terms. No keywords needed — returns chronological list.\n- **session_detail**(session_id, limit, offset, include_inactive) [capability_expand category: unclassified]: Read a paginated persisted transcript for one past conversation session in the current workspace. Use after session_list or session_search returns a session_id.\n- **click**(element_index) [capability_expand category: desktop]: Click a UI element by its element_index (from the latest observe_ui snapshot)\n- **get_clipboard**() [capability_expand category: desktop]: Read current clipboard text content\n- **list_apps**(filter, running_only) [capability_expand category: desktop]: List available applications on this system. Use to discover correct bundle_id before switch_app.\n- **list_windows**() [capability_expand category: desktop]: List all top-level windows with pid, window_id, title, and per-window state (minimized, on-screen). Call this first to pick the pid and window_id that observe_ui and other window tools require.\n- **observe_ui**(pid, window_id, query) [capability_expand category: desktop]: Snapshot one window's actionable UI elements, each tagged with an element_index for click/right_click/read_text. Re-observe after actions — indices belong to one snapshot. Requires the window's pid and window_id from list_windows.\n- **open_url**(url, app_id) [capability_expand category: desktop]: Open a URL in the default or specified browser\n- **read_text**(element_index) [capability_expand category: desktop]: Read the text content of a specific UI element from the latest snapshot\n- **right_click**(element_index) [capability_expand category: desktop]: Right-click a UI element to open its context menu. Returns visible menu items.\n- **screenshot**(pid, window_id) [capability_expand category: desktop]: Capture a screenshot for visual verification. With pid + window_id captures that window (works across all displays); defaults to the last observed window, or the full desktop when no window has been observed.\n- **scroll**(element_index, direction, amount, pid, window_id) [capability_expand category: desktop]: Scroll a scrollable area of a window. Omit element_index to scroll the window's focused/page scroller; pass one to scroll an exact element from the latest snapshot.\n- **select_text**(element_index) [capability_expand category: desktop]: Select all text in a UI element (focus + select-all, for subsequent copy)\n- **set_clipboard**(text) [capability_expand category: desktop]: Write text to the clipboard\n- **shortcut**(keys) [capability_expand category: desktop]: Execute a keyboard shortcut\n- **switch_app**(app_id) [capability_expand category: desktop]: Switch to an app (launch if needed, activate, verify)\n- **type_text**(text) [capability_expand category: desktop]: Type text into the currently focused element\n- **wait**(seconds) [capability_expand category: desktop]: Wait for a specified duration before continuing\n- **wait_until**(condition, pid, window_id, timeout, poll_interval) [capability_expand category: desktop]: Wait until a UI condition is met (polls UI tree). Returns elements when found or on timeout.\n- **wait_until_stable**(timeout, poll_interval, pid, window_id) [capability_expand category: desktop]: Wait until the UI stops changing (element set stabilizes across polls).\n\n## App Connector Capability Index\nLeapFlow can onboard and manage external apps through `platform_connect` and execute exact registered business actions through `platform_action`.\nFor requests about connecting, setting up, configuring, enabling, or managing a supported app, use `platform_connect` first instead of generating SDK/Webhook sample code.\n`platform_connect.action` is the management namespace: list, guide, preflight, connect, disconnect, remove, status, events_start, events_stop, events_status.\n`platform_action.action` is only for exact registered platform business actions listed below, such as `im.send_message`; never use management actions like `list` or `guide` there.\nAll business fields MUST go inside `payload`; top-level keys are only `platform`, `action`, and `payload`.\nDo not invent platform IDs or platform action names. If the needed action is not listed, ask for discovery/clarification instead of guessing.\nUse `platform_connect` with `action='guide'` and the matching `platform` to start onboarding; use `action='list'` when the app is unclear.\nWhen a pending onboarding state is present, continue from that state with `platform_connect` instead of asking the user to restate the app.\nSupported apps:\n- `api_server`: API Server (OpenAI Compatible) (category=api; backend=adapter; platform_action actions=none registered)\n- `dingtalk`: 钉钉 (DingTalk) (category=im; backend=adapter; platform_action actions=none registered)\n- `feishu`: 飞书 (Feishu/Lark) (category=im; backend=cli)\n - `calendar.create_event` payload={summary*, start_time*, end_time*, attendees} [write/high]\n - `docs.create_markdown` payload={title*, markdown*, folder_token} [write/medium]\n - `drive.search` payload={query*, limit} [read/medium]\n - `im.add_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.download_resource` payload={message_id*, file_key*, type*} [read/low]\n - `im.get_messages` payload={message_ids*} [read/low]\n - `im.list_chats` payload={page_size} [read/low]\n - `im.list_messages` payload={chat_id*} [read/low]\n - `im.list_thread_messages` payload={thread*} [read/low]\n - `im.remove_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.reply_message` payload={message_id*, text*} [send/high]\n - `im.search_chats` payload={query*, page_size} [read/low]\n - `im.search_messages` payload={query*} [read/medium]\n - `im.send_message` payload={chat_id*, text*, thread_id} [send/high]\n - `im.update_card` payload={token*, card*} [write/medium]\n - `im.update_message` payload={message_id*, text*} [write/medium]\n - `mail.search_unread` payload={query, limit} [read/high]\n - `sheets.append_row` payload={spreadsheet_token*, sheet_id*, values*} [write/medium]\n - `task.create` payload={title*, description, due_time} [write/medium]\n- `telegram`: Telegram (category=social; backend=adapter; platform_action actions=none registered)\n- `webhook`: Webhook (Generic) (category=webhook; backend=adapter; platform_action actions=none registered)\n\n## Learned Skills\nYou have access to the following learned skills. Use `skills_list` to browse or `skill_view` to read details:\n- api_designer: REST and GraphQL API design with OpenAPI spec generation and best practices [API, REST, GraphQL]\n- code_review: Structured code review for security, performance, and style [code, review, security]\n- data_analysis: CSV/JSON data analysis with statistical summaries, quality checks, and visualiza [data, analysis, statistics]\n- devops_helper: CI/CD pipeline configuration, Docker orchestration, and deployment strategy [devops, CI/CD, Docker]\n- document_writer: Structured document generation: technical docs, API references, reports, and REA [documentation, technical-writing, README]\n- git_workflow: Git operation orchestration: branching, commits, conflicts, and PR workflow [git, version-control, branching]\n- i18n_helper: Internationalization workflow: string extraction, translation, locale management [i18n, internationalization, localization]\n- project_analysis: Project structure analysis, dependency audit, and tech stack identification [project, analysis, dependencies]\n- security_audit: Security vulnerability scanning strategy, threat modeling, and fix recommendatio [security, vulnerability, audit]\n- shell_automation: Natural language to shell commands with safety review and step-by-step execution [shell, terminal, automation]\n- test_writer: Generate unit and integration tests from source code with edge-case coverage [testing, unit-test, integration-test]\n- web_research: Multi-step web search with cross-verification and structured synthesis [web, search, research]\n\n## Tool Usage\nTools are normally invoked through the native function-calling mechanism, not by writing JSON in your reply\ntext. Only if the provider signals that native function calling is unavailable for this turn, fall back to a\nsingle JSON code block: `{\"name\": \"tool_name\", \"arguments\": {\"key\": \"value\"}}` — use this fallback format\nonly, never both. Only call a tool whose exact name appears in the tool index above and is currently callable\n(or reachable via `capability_expand`). Never invent, rename, alias, or guess a tool name, platform ID, or\nplatform action from argument shape or wording — if the index does not list it, it does not exist.\n`platform_connect.action` (list/guide/preflight/connect/disconnect/remove/status/events_start/events_stop/\nevents_status) is the App Connector management namespace; `platform_action.action` only accepts exact\nregistered business actions such as `im.send_message` shown in the App Connector Capability Index — never mix\nthe two namespaces. If a tool call returns an unknown/unavailable result, use the returned suggestions or\navailable names for a single retry instead of trying further variations of the same guess.\n\n**Side-effect action rule** (`platform_action` with effect=send/write/execute):\n- Call each unique action+payload **exactly once**. Never include duplicates in the same turn.\n- Once the result returns `\"completed\": true`, that action is DONE for this task. Do NOT call it again\n in any subsequent turn — immediately summarize the result for the user instead.\n- The system enforces idempotency: duplicate calls are blocked and will not execute.\n- If the user explicitly requests sending/writing multiple times, use distinct payloads per call.\n\n**Resource identifier provenance rule**:\n- NEVER fabricate, guess, or infer resource identifiers (chat_id, message_id, file_key, user_id, etc.).\n Every resource ID used in a side-effect action MUST come from a successful API response in this session.\n- If a read/list action fails (e.g. authorization error), you do NOT have valid resource IDs.\n Report the failure to the user — do NOT attempt the dependent write/send action with a guessed ID.\n- When a tool result contains `\"llm_instruction\"`, follow it exactly.\n\n## Guidelines\n1. **Direct answers first**: If you already know the answer, respond directly without tools.\n2. **Avoid redundant tool calls**: Do not call the same tool with the same arguments more than once in the same user turn. When an existing tool result already answers the user's request, stop calling tools and answer directly.\n3. **Use tools proactively**: When the user asks about files, time, system state, or needs actions performed, use the appropriate tool.\n4. **Chain tools when needed**: You can call multiple tools in sequence (e.g., list files → read file → summarize).\n5. **Handle failures gracefully**: If a tool fails, explain what went wrong and suggest alternatives. If it failed because of an unknown tool/platform/action name, retry once with an exact name from the returned suggestions, then explain rather than keep guessing.\n6. **Summarize results naturally**: After tool execution, synthesize the results into a helpful answer rather than dumping raw output.\n7. **Stay conversational**: Maintain a natural, helpful tone. Acknowledge context from earlier in the conversation.\n8. **Recall past work**:\n - Broad queries (\"之前做了什么\", \"列出任务\"): answer from the \"Recent Task History\" section already in your context. If insufficient, call session_list.\n - Specific lookups (\"上次那个配置怎么改的\"): call session_search with relevant phrases (NOT single characters).\n - Do NOT call search tools repeatedly with keyword fragments. One well-phrased call is better than ten fragmented ones.\n\n## Coding & Verification\nWhen working with code, prefer the precise built-in tools over ad-hoc shell: use `repo_map` to orient in an\nunfamiliar project, `code_search`/`file_find` to locate and `code_intel` for symbols, `edit_file` (anchored\nsearch-replace, or a unified `diff`) to change files — never rewrite a whole file to change a few lines — and\n`git_query` to inspect diffs/log. After edits, check `syntax_ok` in the result and run `test_run`/`lint_check`\nbefore declaring the work done. Batch independent read-only calls (search/read/query) into a single turn — they\nrun in parallel.\n\n## Presentation Style\n1. **Polished Markdown only**: Format user-facing answers with clean Markdown headings, short paragraphs, and concise bullets. Use tables only when they improve comparison or scanning.\n2. **Terminal-friendly layout**: Keep lines readable in a TUI; avoid dense walls of text, deeply nested lists, oversized ASCII art, or heavy visual blocks.\n3. **Elegant emphasis**: Use bold text sparingly for key terms and conclusions. Avoid excessive emojis, decorative symbols, repeated separators, or visual noise.\n4. **Theme-safe colors**: Do not emit ANSI escape codes, HTML color tags, Rich markup, or hardcoded color names. Rely on the TUI theme to render Markdown professionally.\n5. **No leaked tool protocol**: Never show tool-call JSON, internal schemas, raw observations, tool result payloads, or hidden reasoning in the final answer unless the user explicitly asks for raw/debug output. Treat any prior `{\"name\": ..., \"arguments\": ...}` blocks and `Tool result (...)` messages as internal execution context only.\n6. **Professional closure**: End with a concise conclusion or next step when helpful; avoid rambling after the useful answer is complete.\n\nWhen finished with all tool calls, respond normally without a JSON block, tool-call transcript, or process log.\n\n## Task Contract\n- Task ID: turn-4\n- Original user request: Configure the bench_r8 homed channel to true to complete homing.\n- Workspace root: \n- Allowed roots: \n- Treat relative project paths as relative to the workspace root; never infer `.` as the project root when a workspace root is provided.\n- Workspace boundary is enforced by tools: do not read, search, edit, or run commands against paths outside the allowed roots unless the user explicitly requests an external path and the tool/approval policy permits it.\n- LeapFlow workspace config is optional at `/.leapflow/config.yaml`; runtime config is loaded from `~/.leapflow/config/user.yaml` and `~/.leapflow/profiles//config/*.yaml`.\n- Preserve this task contract across summarization, compression, tool loops, and memory retrieval.\n" + }, + { + "role": "assistant", + "content": "Discovered the simulated bench and read its channel limits." + }, + { + "role": "user", + "content": "Read the current value of the setpoint channel on bench_r8." + }, + { + "role": "assistant", + "content": "[Called: hw_read]\nRead the setpoint channel." + }, + { + "role": "user", + "content": "Actuate the bench_r8 setpoint to 60.\nConfigure the bench_r8 homed channel to true to complete homing.\nConfigure the bench_r8 homed channel to true to complete homing." + } + ], + "tools": [ + "capability_expand", + "code_intel", + "code_search", + "config_get", + "config_list", + "env_info", + "file_find", + "file_list", + "file_read", + "git_query", + "hw_actuate", + "hw_batch_actuate", + "hw_configure", + "hw_describe", + "hw_dispense", + "hw_estop", + "hw_list", + "hw_read", + "hw_status", + "lint_check", + "memory_search", + "plugin_list", + "plugin_propose", + "plugin_status", + "plugin_versions", + "repo_map", + "research_note", + "runtime_snapshot", + "schedule_reentry", + "self_describe", + "session_list", + "skill_view", + "skills_list", + "terminal_list", + "terminal_read", + "test_run", + "text_search", + "time_get", + "tool_describe", + "tool_search", + "web_fetch" + ] + }, + "responses": [ + { + "status": 200, + "content_type": "application/json", + "body": "{\"id\": \"chatcmpl-cassette\", \"object\": \"chat.completion\", \"model\": \"cassette-model\", \"choices\": [{\"index\": 0, \"message\": {\"role\": \"assistant\", \"content\": \"\", \"tool_calls\": [{\"id\": \"call_1\", \"type\": \"function\", \"function\": {\"name\": \"hw_configure\", \"arguments\": \"{\\\"device_id\\\": \\\"bench_r8\\\", \\\"channel_id\\\": \\\"homed\\\", \\\"value\\\": true}\"}}]}, \"finish_reason\": \"tool_calls\"}], \"usage\": {\"prompt_tokens\": 64, \"completion_tokens\": 16, \"total_tokens\": 80}}" + } + ] +} diff --git a/tests/test_architecture_contracts.py b/tests/test_architecture_contracts.py index 3bc2b21b..61bbebed 100644 --- a/tests/test_architecture_contracts.py +++ b/tests/test_architecture_contracts.py @@ -224,6 +224,10 @@ def test_hardware_transports_are_not_named_after_one_device() -> None: "mcp.py", "host.py", "media.py", + "robot_arm.py", + "robot_channels.py", + "ros2.py", + "vla_inference.py", } present = {p.name for p in (HARDWARE_DIR / "transports").glob("*.py")} unexpected = present - allowed diff --git a/tests/test_hardware_governance.py b/tests/test_hardware_governance.py index 5884b248..122c6c5d 100644 --- a/tests/test_hardware_governance.py +++ b/tests/test_hardware_governance.py @@ -1401,7 +1401,7 @@ def test_plugin_exposes_no_tools_until_a_registry_is_bound() -> None: assert plugin.plugin_id == "hardware_context" -def test_plugin_exposes_exactly_eight_tools_when_bound() -> None: +def test_plugin_exposes_exactly_nine_tools_when_bound() -> None: """Tool count is fixed regardless of how many devices exist.""" from leapflow.hardware.plugin import HardwareContextPlugin @@ -1415,6 +1415,7 @@ def test_plugin_exposes_exactly_eight_tools_when_bound() -> None: names = sorted(tool.name for tool in plugin.tools) assert names == [ "hw_actuate", + "hw_batch_actuate", "hw_configure", "hw_describe", "hw_dispense", diff --git a/tests/test_hardware_transport_contract.py b/tests/test_hardware_transport_contract.py index 14950a1e..9a3dc40e 100644 --- a/tests/test_hardware_transport_contract.py +++ b/tests/test_hardware_transport_contract.py @@ -230,6 +230,15 @@ async def call_tool(self, tool_name: str, arguments: dict[str, Any]) -> Any: # tests/test_hardware_media.py: read-only refusal with a provable NONE verdict, # frame metadata without bytes, and the FrameTransport capability check. "media", + # Depends on torch/leapflow.robot -- optional heavy ML packages that are not installed + # in the standard test environment. + "robot_arm", + # Depends on torch and gRPC for VLA model serving; exercised in dedicated + # integration tests when the runtime is available. + "vla_inference", + # Depends on rclpy (ROS2 Python client library), which is an optional + # heavy dependency not installed in the standard test environment. + "ros2", } ) diff --git a/tests/test_orchestrator.py b/tests/test_orchestrator.py new file mode 100644 index 00000000..5be3e401 --- /dev/null +++ b/tests/test_orchestrator.py @@ -0,0 +1,518 @@ +# Copyright (c) Alibaba, Inc. and its affiliates. +"""Tests for MultiDeviceOrchestrator: multi-device orchestration primitives. + +Covers SEQUENTIAL, PARALLEL, BARRIER modes, halt_all, affordance resolution, +and leader-follower teleoperation scenarios using mock transports. +""" + +from __future__ import annotations + +import asyncio +from dataclasses import dataclass +from typing import Any + +import pytest + +from leapflow.robot.orchestrator import ( + DeviceOperation, + ExecutionMode, + MultiDeviceOrchestrator, + OrchestrationStep, +) + + +# ════════════════════════════════════════════════════════════════ +# Mock transport and registry +# ════════════════════════════════════════════════════════════════ + + +@dataclass +class _MockReading: + """Minimal reading returned by mock transport.""" + + device_id: str = "" + channel_id: str = "" + value: Any = 0.0 + + +@dataclass +class _MockWriteOutcome: + """Minimal write outcome returned by mock transport.""" + + ok: bool = True + side_effect_state: str = "committed" + + +@dataclass +class _MockHaltStatus: + """Minimal halt status returned by mock transport.""" + + connected: bool = True + halt_supported: bool = True + + +class MockTransport: + """Simulates a HardwareTransport for orchestration tests. + + Tracks all reads, writes, and halts for assertion. ``fail_write`` + causes the next write to return ``ok=False``. + """ + + def __init__(self, device_id: str = "dev_a") -> None: + self.device_id = device_id + self.reads: list[str] = [] + self.writes: list[tuple[str, Any]] = [] + self.halted: bool = False + self.halt_count: int = 0 + self.fail_write: bool = False + self.values: dict[str, float] = {} + + async def read(self, channel_id: str) -> _MockReading: + self.reads.append(channel_id) + return _MockReading( + device_id=self.device_id, + channel_id=channel_id, + value=self.values.get(channel_id, 0.0), + ) + + async def write(self, channel_id: str, value: Any) -> _MockWriteOutcome: + self.writes.append((channel_id, value)) + if self.fail_write: + self.fail_write = False + return _MockWriteOutcome(ok=False, side_effect_state="unknown") + self.values[channel_id] = float(value) + return _MockWriteOutcome(ok=True, side_effect_state="committed") + + async def halt(self) -> _MockHaltStatus: + self.halted = True + self.halt_count += 1 + return _MockHaltStatus() + + +class MockRegistry: + """Simulates HardwareRegistry with pre-registered mock transports.""" + + def __init__(self, transports: dict[str, MockTransport] | None = None) -> None: + self._transports = transports or {} + + async def transport(self, device_id: str) -> MockTransport: + t = self._transports.get(device_id) + if t is None: + raise RuntimeError(f"unknown device: {device_id}") + return t + + async def read(self, device_id: str, channel_id: str) -> _MockReading: + t = await self.transport(device_id) + return await t.read(channel_id) + + +class MockCapabilityEntry: + """Minimal CapabilityEntry for affordance resolution tests.""" + + def __init__(self, device_id: str) -> None: + self.device_id = device_id + + +class MockCapabilityIndex: + """Simulates CapabilityIndex returning pre-set entries.""" + + def __init__(self, mapping: dict[str, list[MockCapabilityEntry]]) -> None: + self._mapping = mapping + + def resolve(self, affordance: str) -> list[MockCapabilityEntry]: + return list(self._mapping.get(affordance, [])) + + +class MockHardwareTools: + """Simulates HardwareTools.batch_actuate for approval-gated path.""" + + def __init__(self, fail_device: str = "") -> None: + self.calls: list[dict] = [] + self._fail_device = fail_device + + async def batch_actuate(self, params: dict) -> dict: + self.calls.append(params) + if params.get("device_id") == self._fail_device: + return {"ok": False, "error": "approval_denied"} + return {"ok": True, "side_effect_state": "committed"} + + +# ════════════════════════════════════════════════════════════════ +# 1. SEQUENTIAL mode +# ════════════════════════════════════════════════════════════════ + + +class TestSequentialExecution: + """SEQUENTIAL mode: one-at-a-time, stop on first failure.""" + + @pytest.fixture + def transports(self): + return { + "arm_a": MockTransport("arm_a"), + "arm_b": MockTransport("arm_b"), + } + + @pytest.fixture + def orchestrator(self, transports): + return MultiDeviceOrchestrator(MockRegistry(transports)) + + @pytest.mark.asyncio + async def test_sequential_all_succeed(self, orchestrator, transports): + """All operations execute in order when none fail.""" + step = OrchestrationStep( + mode=ExecutionMode.SEQUENTIAL, + operations=( + DeviceOperation("arm_a", (("joint1", 1.0),)), + DeviceOperation("arm_b", (("joint1", 2.0),)), + ), + label="seq_ok", + ) + result = await orchestrator.execute_step(step) + assert result.ok is True + assert len(result.step_results) == 2 + assert result.halted is False + assert transports["arm_a"].writes == [("joint1", 1.0)] + assert transports["arm_b"].writes == [("joint1", 2.0)] + + @pytest.mark.asyncio + async def test_sequential_stops_on_failure(self, orchestrator, transports): + """Stops at first failure; second operation is never executed.""" + transports["arm_a"].fail_write = True + step = OrchestrationStep( + mode=ExecutionMode.SEQUENTIAL, + operations=( + DeviceOperation("arm_a", (("joint1", 1.0),)), + DeviceOperation("arm_b", (("joint1", 2.0),)), + ), + ) + result = await orchestrator.execute_step(step) + assert result.ok is False + assert len(result.step_results) == 1 + assert transports["arm_b"].writes == [] + + +# ════════════════════════════════════════════════════════════════ +# 2. PARALLEL mode +# ════════════════════════════════════════════════════════════════ + + +class TestParallelExecution: + """PARALLEL mode: concurrent execution, halt_all on failure.""" + + @pytest.fixture + def transports(self): + return { + "arm_a": MockTransport("arm_a"), + "arm_b": MockTransport("arm_b"), + } + + @pytest.fixture + def orchestrator(self, transports): + return MultiDeviceOrchestrator(MockRegistry(transports)) + + @pytest.mark.asyncio + async def test_parallel_all_succeed(self, orchestrator, transports): + """Both devices written concurrently when none fail.""" + step = OrchestrationStep( + mode=ExecutionMode.PARALLEL, + operations=( + DeviceOperation("arm_a", (("joint1", 1.0),)), + DeviceOperation("arm_b", (("joint1", 2.0),)), + ), + ) + result = await orchestrator.execute_step(step) + assert result.ok is True + assert not result.halted + assert transports["arm_a"].writes == [("joint1", 1.0)] + assert transports["arm_b"].writes == [("joint1", 2.0)] + + @pytest.mark.asyncio + async def test_parallel_failure_triggers_halt_all(self, orchestrator, transports): + """A failure in one device triggers halt_all on ALL participating devices.""" + transports["arm_a"].fail_write = True + step = OrchestrationStep( + mode=ExecutionMode.PARALLEL, + operations=( + DeviceOperation("arm_a", (("joint1", 1.0),)), + DeviceOperation("arm_b", (("joint1", 2.0),)), + ), + ) + result = await orchestrator.execute_step(step) + assert result.ok is False + assert result.halted is True + # Both devices must have been halted. + assert transports["arm_a"].halted is True + assert transports["arm_b"].halted is True + + +# ════════════════════════════════════════════════════════════════ +# 3. BARRIER mode +# ════════════════════════════════════════════════════════════════ + + +class TestBarrierExecution: + """BARRIER mode: all devices reach target, or timeout → halt.""" + + @pytest.fixture + def transports(self): + return { + "arm_a": MockTransport("arm_a"), + "arm_b": MockTransport("arm_b"), + } + + @pytest.fixture + def orchestrator(self, transports): + return MultiDeviceOrchestrator(MockRegistry(transports)) + + @pytest.mark.asyncio + async def test_barrier_all_settle(self, orchestrator): + """Barrier succeeds when all devices reach their targets immediately.""" + step = OrchestrationStep( + mode=ExecutionMode.BARRIER, + operations=( + DeviceOperation("arm_a", (("joint1", 1.0),)), + DeviceOperation("arm_b", (("joint1", 2.0),)), + ), + barrier_timeout_s=2.0, + ) + result = await orchestrator.execute_step(step) + assert result.ok is True + assert not result.halted + + @pytest.mark.asyncio + async def test_barrier_timeout_triggers_halt(self, transports): + """Barrier timeout triggers halt_all when a device never reaches target.""" + # Override arm_b read to always return 0.0, simulating a device + # that never reaches the commanded 999.0 target. + original_read = transports["arm_b"].read + + async def _stuck_read(channel_id: str) -> _MockReading: + return _MockReading( + device_id="arm_b", channel_id=channel_id, value=0.0 + ) + + transports["arm_b"].read = _stuck_read + # Override registry.read for arm_b too. + registry = MockRegistry(transports) + orch = MultiDeviceOrchestrator(registry) + + step = OrchestrationStep( + mode=ExecutionMode.BARRIER, + operations=( + DeviceOperation("arm_a", (("joint1", 1.0),)), + DeviceOperation("arm_b", (("joint1", 999.0),)), + ), + barrier_timeout_s=0.15, + ) + result = await orch.execute_step(step) + assert result.ok is False + assert result.halted is True + assert transports["arm_a"].halted is True + assert transports["arm_b"].halted is True + + +# ════════════════════════════════════════════════════════════════ +# 4. halt_all +# ════════════════════════════════════════════════════════════════ + + +class TestHaltAll: + """halt_all: concurrent emergency stop of all devices.""" + + @pytest.mark.asyncio + async def test_halt_all_stops_all_devices(self): + """Every device receives a halt() call concurrently.""" + ta = MockTransport("arm_a") + tb = MockTransport("arm_b") + registry = MockRegistry({"arm_a": ta, "arm_b": tb}) + orch = MultiDeviceOrchestrator(registry) + + halt_map = await orch.halt_all(("arm_a", "arm_b")) + assert ta.halted is True + assert tb.halted is True + assert halt_map["arm_a"]["halted"] is True + assert halt_map["arm_b"]["halted"] is True + + @pytest.mark.asyncio + async def test_halt_all_deduplicates_ids(self): + """Duplicate device_ids are halted only once.""" + ta = MockTransport("arm_a") + registry = MockRegistry({"arm_a": ta}) + orch = MultiDeviceOrchestrator(registry) + + await orch.halt_all(("arm_a", "arm_a", "arm_a")) + assert ta.halt_count == 1 + + @pytest.mark.asyncio + async def test_halt_all_empty(self): + """Empty device list returns empty map.""" + orch = MultiDeviceOrchestrator(MockRegistry()) + result = await orch.halt_all(()) + assert result == {} + + @pytest.mark.asyncio + async def test_halt_all_handles_transport_error(self): + """halt_all does not raise when a device transport fails.""" + registry = MockRegistry({}) # No devices → transport() raises + orch = MultiDeviceOrchestrator(registry) + halt_map = await orch.halt_all(("missing_device",)) + assert halt_map["missing_device"]["halted"] is False + assert "error" in halt_map["missing_device"] + + +# ════════════════════════════════════════════════════════════════ +# 5. Affordance resolution +# ════════════════════════════════════════════════════════════════ + + +class TestResolveByAffordance: + """resolve_by_affordance routes to CapabilityIndex.""" + + def test_resolve_returns_device_ids(self): + index = MockCapabilityIndex({ + "grasp": [MockCapabilityEntry("arm_a"), MockCapabilityEntry("arm_b")], + }) + orch = MultiDeviceOrchestrator(MockRegistry(), capability_index=index) + result = orch.resolve_by_affordance("grasp") + assert result == ("arm_a", "arm_b") + + def test_resolve_unknown_affordance(self): + index = MockCapabilityIndex({}) + orch = MultiDeviceOrchestrator(MockRegistry(), capability_index=index) + assert orch.resolve_by_affordance("fly") == () + + def test_resolve_without_index(self): + orch = MultiDeviceOrchestrator(MockRegistry()) + assert orch.resolve_by_affordance("grasp") == () + + +# ════════════════════════════════════════════════════════════════ +# 6. Leader-follower scenario +# ════════════════════════════════════════════════════════════════ + + +class TestLeaderFollowerScenario: + """Leader reads state, follower writes — a teleoperation pattern.""" + + @pytest.mark.asyncio + async def test_leader_follower_teleop(self): + """Read from leader, write to follower in a two-step plan.""" + leader = MockTransport("leader") + leader.values["joint1"] = 1.57 + follower = MockTransport("follower") + registry = MockRegistry({"leader": leader, "follower": follower}) + orch = MultiDeviceOrchestrator(registry) + + # Step 1: read leader's state. + leader_reading = await registry.read("leader", "joint1") + target = leader_reading.value + + # Step 2: write to follower. + step = OrchestrationStep( + mode=ExecutionMode.SEQUENTIAL, + operations=( + DeviceOperation("follower", (("joint1", target),)), + ), + label="teleop_write", + ) + result = await orch.execute_step(step) + assert result.ok is True + assert follower.writes == [("joint1", 1.57)] + + +# ════════════════════════════════════════════════════════════════ +# 7. Approval-gated path (HardwareTools) +# ════════════════════════════════════════════════════════════════ + + +class TestApprovalGatedPath: + """Operations use batch_actuate when hardware_tools is provided.""" + + @pytest.mark.asyncio + async def test_tools_path_used_when_provided(self): + tools = MockHardwareTools() + registry = MockRegistry({"arm_a": MockTransport("arm_a")}) + orch = MultiDeviceOrchestrator(registry, hardware_tools=tools) + + step = OrchestrationStep( + mode=ExecutionMode.SEQUENTIAL, + operations=( + DeviceOperation("arm_a", (("joint1", 1.0),)), + ), + ) + result = await orch.execute_step(step) + assert result.ok is True + assert len(tools.calls) == 1 + assert tools.calls[0]["device_id"] == "arm_a" + + @pytest.mark.asyncio + async def test_tools_failure_propagates(self): + tools = MockHardwareTools(fail_device="arm_a") + registry = MockRegistry({"arm_a": MockTransport("arm_a")}) + orch = MultiDeviceOrchestrator(registry, hardware_tools=tools) + + step = OrchestrationStep( + mode=ExecutionMode.SEQUENTIAL, + operations=(DeviceOperation("arm_a", (("joint1", 1.0),)),), + ) + result = await orch.execute_step(step) + assert result.ok is False + + +# ════════════════════════════════════════════════════════════════ +# 8. execute_plan +# ════════════════════════════════════════════════════════════════ + + +class TestExecutePlan: + """Multi-step plan execution.""" + + @pytest.mark.asyncio + async def test_plan_sequential_steps(self): + ta = MockTransport("arm_a") + registry = MockRegistry({"arm_a": ta}) + orch = MultiDeviceOrchestrator(registry) + + plan = ( + OrchestrationStep( + mode=ExecutionMode.SEQUENTIAL, + operations=(DeviceOperation("arm_a", (("j1", 1.0),)),), + label="step1", + ), + OrchestrationStep( + mode=ExecutionMode.SEQUENTIAL, + operations=(DeviceOperation("arm_a", (("j2", 2.0),)),), + label="step2", + ), + ) + result = await orch.execute_plan(plan) + assert result.ok is True + assert ta.writes == [("j1", 1.0), ("j2", 2.0)] + + @pytest.mark.asyncio + async def test_plan_stops_on_failed_step(self): + ta = MockTransport("arm_a") + registry = MockRegistry({"arm_a": ta}) + orch = MultiDeviceOrchestrator(registry) + + plan = ( + OrchestrationStep( + mode=ExecutionMode.SEQUENTIAL, + operations=(DeviceOperation("arm_a", (("j1", 1.0),)),), + label="step1", + ), + OrchestrationStep( + mode=ExecutionMode.SEQUENTIAL, + operations=(DeviceOperation("missing", (("j1", 1.0),)),), + label="step2_fail", + ), + OrchestrationStep( + mode=ExecutionMode.SEQUENTIAL, + operations=(DeviceOperation("arm_a", (("j3", 3.0),)),), + label="step3_never", + ), + ) + result = await orch.execute_plan(plan) + assert result.ok is False + # Step 3 should never have executed. + assert ("j3", 3.0) not in ta.writes diff --git a/tests/test_physical_capability_resolver.py b/tests/test_physical_capability_resolver.py new file mode 100644 index 00000000..36f80b97 --- /dev/null +++ b/tests/test_physical_capability_resolver.py @@ -0,0 +1,342 @@ +# Copyright (c) Alibaba, Inc. and its affiliates. +"""Tests for the physical capability resolver's four-value adjudication. + +The resolver turns physical outcome observations into the same absorb / rebind +/ acquire / escalate verdict space the software evolution engine uses. These +tests exercise the four gap classifications, each verdict path, the evidence +threshold that refuses to adjudicate on too few samples, and the rebind +candidate lookup against a mock inference registry. +""" + +from __future__ import annotations + +from typing import Any + +import pytest + +from leapflow.domain.adaptation_verdict import ADAPTATION_ACTIONS +from leapflow.plugins.physical_capability_resolver import ( + PhysicalCapabilityGap, + PhysicalCapabilityResolver, + PhysicalGapType, +) + + +# ── Test doubles ── + + +class _FakeStrategy: + """Inference strategy that declares the affordances it can serve.""" + + def __init__(self, strategy_id: str, affordances: tuple[str, ...] = ()) -> None: + self.strategy_id = strategy_id + self.affordances = affordances + + +class _BareStrategy: + """Strategy that declares no affordances (exercises the id-substring path).""" + + def __init__(self, strategy_id: str) -> None: + self.strategy_id = strategy_id + + +class FakeInferenceRegistry: + """Minimal stand-in for InferenceStrategyRegistry.""" + + def __init__(self, strategies: tuple[Any, ...] = ()) -> None: + self._strategies = {s.strategy_id: s for s in strategies} + + def list_strategies(self) -> list[dict[str, Any]]: + return [{"strategy_id": sid} for sid in self._strategies] + + def get(self, strategy_id: str) -> Any: + return self._strategies.get(strategy_id) + + +class FakeEvidenceStore: + """Placeholder evidence store — the resolver holds but does not query it here.""" + + +def _precision_gap(**overrides: Any) -> PhysicalCapabilityGap: + kwargs: dict[str, Any] = { + "gap_type": PhysicalGapType.INSUFFICIENT_PRECISION.value, + "device_id": "robot.arm", + "affordance": "grasp", + "success_rate": 0.3, + "sample_count": 10, + } + kwargs.update(overrides) + return PhysicalCapabilityGap(**kwargs) + + +def _resolver(**overrides: Any) -> PhysicalCapabilityResolver: + kwargs: dict[str, Any] = { + "inference_registry": None, + "evidence_store": FakeEvidenceStore(), + "precision_threshold": 0.7, + "min_samples": 5, + } + kwargs.update(overrides) + return PhysicalCapabilityResolver(**kwargs) + + +# ── Gap detection ── + + +async def test_detect_insufficient_precision() -> None: + resolver = _resolver() + outcome = { + "devices": { + "robot.arm": { + "total_operations": 10, + "success_rate": 0.4, + "affordance": "grasp", + "recent_failures": ["position_deviation > tolerance"], + } + } + } + gaps = await resolver.detect_gaps(outcome) + assert len(gaps) == 1 + assert gaps[0].gap_type == PhysicalGapType.INSUFFICIENT_PRECISION.value + assert gaps[0].sample_count == 10 + assert gaps[0].success_rate == pytest.approx(0.4) + + +async def test_detect_no_gap_when_performing_well() -> None: + resolver = _resolver() + outcome = {"devices": {"robot.arm": {"total_operations": 10, "success_rate": 0.95}}} + assert await resolver.detect_gaps(outcome) == () + + +async def test_detect_missing_device() -> None: + resolver = _resolver() + outcome = {"devices": {"robot.arm": {"connected": False, "affordance": "grasp"}}} + gaps = await resolver.detect_gaps(outcome) + assert len(gaps) == 1 + assert gaps[0].gap_type == PhysicalGapType.MISSING_DEVICE.value + + +async def test_detect_hardware_degraded() -> None: + resolver = _resolver() + outcome = { + "devices": { + "robot.arm": {"health": "degraded", "total_operations": 4, "success_rate": 0.1} + } + } + gaps = await resolver.detect_gaps(outcome) + assert len(gaps) == 1 + assert gaps[0].gap_type == PhysicalGapType.HARDWARE_DEGRADED.value + + +async def test_detect_missing_skill_from_unsupported_affordance() -> None: + resolver = _resolver() + outcome = {"devices": {"robot.arm": {"unsupported_affordances": ["pour"]}}} + gaps = await resolver.detect_gaps(outcome) + assert len(gaps) == 1 + assert gaps[0].gap_type == PhysicalGapType.MISSING_SKILL.value + assert gaps[0].affordance == "pour" + + +async def test_detect_ignores_malformed_payload() -> None: + resolver = _resolver() + assert await resolver.detect_gaps({}) == () + assert await resolver.detect_gaps({"devices": "not-a-mapping"}) == () + + +# ── Four-value adjudication ── + + +async def test_adjudicate_absorb_on_small_deviation() -> None: + # 0.6 is only 0.1 below the 0.7 bar — within the absorb margin. + resolver = _resolver() + gap = _precision_gap(success_rate=0.6, sample_count=8) + verdict = await resolver.adjudicate(gap) + assert verdict is not None + assert verdict.action == "absorb" + assert verdict.knowledge # mandatory + assert verdict.rationale # mandatory rationale + assert not verdict.writes_code + + +async def test_adjudicate_rebind_when_alternative_strategy_exists() -> None: + registry = FakeInferenceRegistry( + (_FakeStrategy("diffusion_grasp_v2", affordances=("grasp",)),) + ) + resolver = _resolver(inference_registry=registry) + gap = _precision_gap(success_rate=0.2, sample_count=12) + verdict = await resolver.adjudicate(gap) + assert verdict is not None + assert verdict.action == "rebind" + assert verdict.target == "diffusion_grasp_v2" + assert verdict.knowledge and verdict.rationale + + +async def test_adjudicate_acquire_missing_skill_when_downloadable() -> None: + resolver = _resolver(acquire_probe=lambda gap: True) + gap = PhysicalCapabilityGap( + gap_type=PhysicalGapType.MISSING_SKILL.value, + device_id="robot.arm", + affordance="pour", + ) + verdict = await resolver.adjudicate(gap) + assert verdict is not None + assert verdict.action == "acquire" + assert verdict.writes_code + assert verdict.to_intent() is not None + + +async def test_adjudicate_escalate_missing_skill_when_not_downloadable() -> None: + resolver = _resolver(acquire_probe=lambda gap: False) + gap = PhysicalCapabilityGap( + gap_type=PhysicalGapType.MISSING_SKILL.value, + device_id="robot.arm", + affordance="pour", + ) + verdict = await resolver.adjudicate(gap) + assert verdict is not None + assert verdict.action == "escalate" + + +async def test_adjudicate_escalate_missing_device() -> None: + resolver = _resolver() + gap = PhysicalCapabilityGap( + gap_type=PhysicalGapType.MISSING_DEVICE.value, + device_id="robot.arm", + affordance="grasp", + ) + verdict = await resolver.adjudicate(gap) + assert verdict is not None + assert verdict.action == "escalate" + assert "connect" in verdict.target.lower() + + +async def test_adjudicate_escalate_hardware_degraded() -> None: + resolver = _resolver() + gap = PhysicalCapabilityGap( + gap_type=PhysicalGapType.HARDWARE_DEGRADED.value, + device_id="robot.arm", + affordance="grasp", + success_rate=0.1, + sample_count=6, + ) + verdict = await resolver.adjudicate(gap) + assert verdict is not None + assert verdict.action == "escalate" + assert "inspect" in verdict.target.lower() + + +async def test_adjudicate_escalate_teleop_when_nothing_cheaper_applies() -> None: + # Large deviation, no alternative strategy available → teleoperation. + resolver = _resolver(inference_registry=FakeInferenceRegistry()) + gap = _precision_gap(success_rate=0.2, sample_count=12) + verdict = await resolver.adjudicate(gap) + assert verdict is not None + assert verdict.action == "escalate" + assert "teleoperate" in verdict.target.lower() + + +async def test_all_verdict_actions_are_within_the_closed_space() -> None: + resolver = _resolver( + inference_registry=FakeInferenceRegistry( + (_FakeStrategy("grasp_alt", affordances=("grasp",)),) + ) + ) + gaps = ( + _precision_gap(success_rate=0.6, sample_count=8), # absorb + _precision_gap(success_rate=0.2, sample_count=8), # rebind + PhysicalCapabilityGap( + gap_type=PhysicalGapType.MISSING_DEVICE.value, device_id="robot.arm" + ), # escalate + ) + for gap in gaps: + verdict = await resolver.adjudicate(gap) + assert verdict is not None + assert verdict.action in ADAPTATION_ACTIONS + + +# ── Evidence threshold ── + + +async def test_precision_gap_below_min_samples_is_not_adjudicated() -> None: + resolver = _resolver(min_samples=5) + gap = _precision_gap(success_rate=0.2, sample_count=2) + assert await resolver.adjudicate(gap) is None + + +async def test_precision_gap_at_min_samples_is_adjudicated() -> None: + resolver = _resolver(min_samples=5) + gap = _precision_gap(success_rate=0.6, sample_count=5) + verdict = await resolver.adjudicate(gap) + assert verdict is not None + assert verdict.action == "absorb" + + +async def test_structural_gap_not_gated_by_sample_count() -> None: + # A missing device is a fact, not a statistic; zero samples must still escalate. + resolver = _resolver(min_samples=5) + gap = PhysicalCapabilityGap( + gap_type=PhysicalGapType.MISSING_DEVICE.value, + device_id="robot.arm", + sample_count=0, + ) + verdict = await resolver.adjudicate(gap) + assert verdict is not None + assert verdict.action == "escalate" + + +# ── Rebind candidate lookup ── + + +def test_find_rebind_candidate_matches_declared_affordance() -> None: + registry = FakeInferenceRegistry( + ( + _FakeStrategy("place_policy", affordances=("place",)), + _FakeStrategy("grasp_policy", affordances=("grasp", "pick")), + ) + ) + resolver = _resolver(inference_registry=registry) + assert resolver._find_rebind_candidate(_precision_gap()) == "grasp_policy" + + +def test_find_rebind_candidate_falls_back_to_id_substring() -> None: + registry = FakeInferenceRegistry((_BareStrategy("vla_grasp_baseline"),)) + resolver = _resolver(inference_registry=registry) + assert resolver._find_rebind_candidate(_precision_gap()) == "vla_grasp_baseline" + + +def test_find_rebind_candidate_none_without_registry() -> None: + resolver = _resolver(inference_registry=None) + assert resolver._find_rebind_candidate(_precision_gap()) is None + + +def test_find_rebind_candidate_none_when_no_match() -> None: + registry = FakeInferenceRegistry( + (_FakeStrategy("place_policy", affordances=("place",)),) + ) + resolver = _resolver(inference_registry=registry) + assert resolver._find_rebind_candidate(_precision_gap(affordance="grasp")) is None + + +# ── Capability naming ── + + +def test_capability_name_is_valid_dotted_form() -> None: + from leapflow.domain.evolution_intent import is_capability_name + + resolver = _resolver() + gap = _precision_gap(device_id="robot.arm", affordance="grasp") + name = resolver._capability(gap) + assert name == "physical.robot_arm.grasp" + assert is_capability_name(name) + + +def test_capability_name_without_affordance() -> None: + from leapflow.domain.evolution_intent import is_capability_name + + resolver = _resolver() + gap = PhysicalCapabilityGap( + gap_type=PhysicalGapType.MISSING_DEVICE.value, device_id="robot.arm" + ) + name = resolver._capability(gap) + assert name == "physical.robot_arm" + assert is_capability_name(name) diff --git a/tests/test_physical_episode.py b/tests/test_physical_episode.py new file mode 100644 index 00000000..e0b2bad8 --- /dev/null +++ b/tests/test_physical_episode.py @@ -0,0 +1,576 @@ +# Copyright (c) Alibaba, Inc. and its affiliates. +"""Tests for PhysicalEpisodeRecorder and EpisodeExporter.""" + +from __future__ import annotations + +import asyncio +import json +import time +from dataclasses import dataclass, field +from pathlib import Path +from typing import Any, Mapping +from unittest.mock import AsyncMock, MagicMock, patch + +import pytest + +from leapflow.learning.physical_episode import ( + EPISODE_SCHEMA_VERSION, + PhysicalEpisode, + PhysicalEpisodeRecorder, + make_episode_id, +) +from leapflow.robot.episode_export import EpisodeExporter +from leapflow.robot.trajectory import ( + PhysicalTrajectory, + PhysicalTrajectoryStep, + make_trajectory_id, +) + + +# --------------------------------------------------------------------------- +# Fixtures: mock registry +# --------------------------------------------------------------------------- + + +@dataclass +class _MockReading: + channel_id: str + value: float + + def to_dict(self) -> dict[str, Any]: + return {"channel_id": self.channel_id, "value": self.value} + + +@dataclass +class _MockBatch: + readings: tuple[_MockReading, ...] + + +@dataclass +class _MockChannel: + channel_id: str + is_readable: bool = True + + +@dataclass +class _MockContext: + channels: tuple[_MockChannel, ...] + + +class MockRegistry: + """Minimal mock HardwareRegistry for episode tests.""" + + def __init__(self, state: dict[str, float] | None = None) -> None: + self._state = state or {"joint_0": 0.5, "joint_1": 1.0} + self._call_count = 0 + + def context(self, device_id: str) -> _MockContext: + channels = tuple( + _MockChannel(channel_id=ch) for ch in self._state + ) + return _MockContext(channels=channels) + + async def read_batch(self, device_id: str, channels: tuple[str, ...]) -> _MockBatch: + self._call_count += 1 + readings = tuple( + _MockReading(channel_id=ch, value=self._state.get(ch, 0.0)) + for ch in channels + ) + return _MockBatch(readings=readings) + + def set_state(self, new_state: dict[str, float]) -> None: + self._state = dict(new_state) + + +def _make_step(seq: int = 0, source: str = "teleop") -> PhysicalTrajectoryStep: + """Create a test trajectory step.""" + return PhysicalTrajectoryStep( + timestamp=time.time(), + monotonic_at=time.monotonic(), + joint_positions={"joint_0": 0.1 * seq, "joint_1": 0.2 * seq}, + joint_velocities={"joint_0": 0.01, "joint_1": 0.02}, + gripper_state=0.5, + action_source=source, + sequence=seq, + ) + + +def _make_step_with_camera(seq: int = 0, camera_ref: str = "/tmp/f.png") -> PhysicalTrajectoryStep: + """Create a step with a camera frame reference.""" + return PhysicalTrajectoryStep( + timestamp=time.time(), + monotonic_at=time.monotonic(), + joint_positions={"joint_0": 0.1 * seq}, + camera_frames={"cam0": camera_ref}, + action_source="teleop", + sequence=seq, + ) + + +# --------------------------------------------------------------------------- +# PhysicalEpisode dataclass tests +# --------------------------------------------------------------------------- + + +class TestPhysicalEpisode: + """Test the PhysicalEpisode frozen dataclass.""" + + def test_basic_construction(self) -> None: + trajectory = PhysicalTrajectory( + trajectory_id="traj-1", + device_id="robot.arm0", + goal="pick", + steps=(_make_step(0), _make_step(1)), + started_at=100.0, + ended_at=110.0, + ) + ep = PhysicalEpisode( + episode_id="ep-1", + device_id="robot.arm0", + task="pick", + trajectory=trajectory, + initial_state={"joint_0": 0.0}, + final_state={"joint_0": 1.0}, + success=True, + started_at=100.0, + ended_at=110.0, + ) + assert ep.episode_id == "ep-1" + assert ep.success is True + assert ep.duration_s == pytest.approx(10.0) + assert ep.step_count == 2 + + def test_to_dict_roundtrip(self) -> None: + ep = PhysicalEpisode( + episode_id="ep-2", + device_id="robot.arm0", + task="place", + trajectory=None, + initial_state={"a": 1}, + final_state={"a": 2}, + evidence_ids=("ev-1", "ev-2"), + success=False, + ) + d = ep.to_dict() + assert d["episode_id"] == "ep-2" + assert d["evidence_ids"] == ["ev-1", "ev-2"] + assert d["success"] is False + assert d["trajectory"] is None + + def test_zero_duration_when_no_timestamps(self) -> None: + ep = PhysicalEpisode( + episode_id="ep-3", + device_id="d", + task="t", + trajectory=None, + initial_state={}, + final_state={}, + ) + assert ep.duration_s == 0.0 + assert ep.step_count == 0 + + +class TestMakeEpisodeId: + def test_unique(self) -> None: + ids = {make_episode_id() for _ in range(100)} + assert len(ids) == 100 + + def test_length(self) -> None: + assert len(make_episode_id()) == 16 + + +# --------------------------------------------------------------------------- +# PhysicalEpisodeRecorder lifecycle tests +# --------------------------------------------------------------------------- + + +class TestPhysicalEpisodeRecorder: + """Test the full episode lifecycle: begin → record → end.""" + + @pytest.fixture() + def registry(self) -> MockRegistry: + return MockRegistry() + + @pytest.mark.asyncio + async def test_basic_lifecycle(self, registry: MockRegistry) -> None: + recorder = PhysicalEpisodeRecorder(registry) + + eid = await recorder.begin_episode("robot.arm0", "pick_cube") + assert recorder.is_recording + assert isinstance(eid, str) and len(eid) == 16 + + recorder.record_step(_make_step(0)) + recorder.record_step(_make_step(1)) + recorder.record_evidence("ev-abc") + + episode = await recorder.end_episode() + assert not recorder.is_recording + assert episode.episode_id == eid + assert episode.device_id == "robot.arm0" + assert episode.task == "pick_cube" + assert episode.step_count == 2 + assert "ev-abc" in episode.evidence_ids + assert episode.success is False # default when no verdict + + @pytest.mark.asyncio + async def test_success_from_verdict(self, registry: MockRegistry) -> None: + recorder = PhysicalEpisodeRecorder(registry) + await recorder.begin_episode("d", "t") + recorder.record_step(_make_step(0)) + + verdict = MagicMock() + verdict.is_success = True + verdict.status = "success" + verdict.confidence = 0.95 + + episode = await recorder.end_episode(verdict) + assert episode.success is True + assert episode.verdict is verdict + + @pytest.mark.asyncio + async def test_failure_verdict(self, registry: MockRegistry) -> None: + recorder = PhysicalEpisodeRecorder(registry) + await recorder.begin_episode("d", "t") + + verdict = MagicMock() + verdict.is_success = False + verdict.status = "failure" + verdict.confidence = 0.9 + + episode = await recorder.end_episode(verdict) + assert episode.success is False + + @pytest.mark.asyncio + async def test_explicit_success_flag(self, registry: MockRegistry) -> None: + recorder = PhysicalEpisodeRecorder(registry) + await recorder.begin_episode("d", "t") + episode = await recorder.end_episode(success=True) + assert episode.success is True + + @pytest.mark.asyncio + async def test_double_begin_raises(self, registry: MockRegistry) -> None: + recorder = PhysicalEpisodeRecorder(registry) + await recorder.begin_episode("d", "t") + with pytest.raises(RuntimeError, match="already in progress"): + await recorder.begin_episode("d2", "t2") + + @pytest.mark.asyncio + async def test_end_without_begin_raises(self, registry: MockRegistry) -> None: + recorder = PhysicalEpisodeRecorder(registry) + with pytest.raises(RuntimeError, match="No episode in progress"): + await recorder.end_episode() + + @pytest.mark.asyncio + async def test_record_step_without_begin_raises(self, registry: MockRegistry) -> None: + recorder = PhysicalEpisodeRecorder(registry) + with pytest.raises(RuntimeError, match="No episode in progress"): + recorder.record_step(_make_step(0)) + + @pytest.mark.asyncio + async def test_record_evidence_without_begin_raises(self, registry: MockRegistry) -> None: + recorder = PhysicalEpisodeRecorder(registry) + with pytest.raises(RuntimeError, match="No episode in progress"): + recorder.record_evidence("ev-1") + + @pytest.mark.asyncio + async def test_initial_and_final_state_captured(self, registry: MockRegistry) -> None: + recorder = PhysicalEpisodeRecorder(registry) + await recorder.begin_episode("d", "t") + # Change state between begin and end. + registry.set_state({"joint_0": 9.9, "joint_1": 8.8}) + episode = await recorder.end_episode() + + assert episode.initial_state == {"joint_0": 0.5, "joint_1": 1.0} + assert episode.final_state == {"joint_0": 9.9, "joint_1": 8.8} + + @pytest.mark.asyncio + async def test_episodes_recorded_counter(self, registry: MockRegistry) -> None: + recorder = PhysicalEpisodeRecorder(registry) + assert recorder.episodes_recorded == 0 + + await recorder.begin_episode("d", "t") + await recorder.end_episode() + assert recorder.episodes_recorded == 1 + + await recorder.begin_episode("d", "t2") + await recorder.end_episode() + assert recorder.episodes_recorded == 2 + + +# --------------------------------------------------------------------------- +# DuckDB persistence tests +# --------------------------------------------------------------------------- + + +class TestEpisodePersistence: + """Test DuckDB write/query round-trip.""" + + @pytest.mark.asyncio + async def test_persist_and_query(self, tmp_path: Path) -> None: + registry = MockRegistry() + db_path = tmp_path / "episodes.duckdb" + recorder = PhysicalEpisodeRecorder(registry, db_path=db_path) + + await recorder.begin_episode("robot.arm0", "pick") + recorder.record_step(_make_step(0)) + await recorder.end_episode(success=True) + + await recorder.begin_episode("robot.arm0", "place") + recorder.record_step(_make_step(0)) + await recorder.end_episode(success=False) + + # Query all. + all_eps = await recorder.query_episodes(device_id="robot.arm0") + assert len(all_eps) == 2 + + # Query success only. + success_eps = await recorder.query_episodes( + device_id="robot.arm0", success_only=True, + ) + assert len(success_eps) == 1 + assert success_eps[0]["task"] == "pick" + + # Query by task. + task_eps = await recorder.query_episodes(task="place") + assert len(task_eps) == 1 + + recorder.close() + + @pytest.mark.asyncio + async def test_query_without_db(self) -> None: + registry = MockRegistry() + recorder = PhysicalEpisodeRecorder(registry) # no db_path + result = await recorder.query_episodes() + assert result == [] + + +# --------------------------------------------------------------------------- +# Evolution integration tests +# --------------------------------------------------------------------------- + + +class TestEvolutionFeed: + """Test best-effort evolution feed.""" + + @pytest.mark.asyncio + async def test_evolution_feed_success(self) -> None: + registry = MockRegistry() + mock_outbox = AsyncMock() + mock_recorder = MagicMock() + mock_recorder._outbox = mock_outbox + + recorder = PhysicalEpisodeRecorder( + registry, action_recorder=mock_recorder, + ) + await recorder.begin_episode("d", "t") + await recorder.end_episode(success=True) + + mock_outbox.publish.assert_called_once() + event = mock_outbox.publish.call_args[0][0] + assert event.payload["ok"] is True + assert event.payload["action_type"] == "physical_episode" + + @pytest.mark.asyncio + async def test_evolution_feed_failure_episode(self) -> None: + registry = MockRegistry() + mock_outbox = AsyncMock() + mock_recorder = MagicMock() + mock_recorder._outbox = mock_outbox + + recorder = PhysicalEpisodeRecorder( + registry, action_recorder=mock_recorder, + ) + await recorder.begin_episode("d", "t") + await recorder.end_episode(success=False) + + event = mock_outbox.publish.call_args[0][0] + assert event.payload["ok"] is False + + @pytest.mark.asyncio + async def test_evolution_feed_skipped_without_recorder(self) -> None: + registry = MockRegistry() + recorder = PhysicalEpisodeRecorder(registry) + await recorder.begin_episode("d", "t") + # Should not raise. + episode = await recorder.end_episode() + assert episode is not None + + @pytest.mark.asyncio + async def test_evolution_feed_tolerates_broken_outbox(self) -> None: + registry = MockRegistry() + mock_outbox = AsyncMock() + mock_outbox.publish.side_effect = RuntimeError("outbox broken") + mock_recorder = MagicMock() + mock_recorder._outbox = mock_outbox + + recorder = PhysicalEpisodeRecorder( + registry, action_recorder=mock_recorder, + ) + await recorder.begin_episode("d", "t") + # Should not raise despite broken outbox. + episode = await recorder.end_episode() + assert episode is not None + + +# --------------------------------------------------------------------------- +# EpisodeExporter tests +# --------------------------------------------------------------------------- + + +class TestEpisodeExporter: + """Test export to temporary directory.""" + + def _make_episode(self, steps: int = 3) -> PhysicalEpisode: + """Create a minimal episode for export tests.""" + step_list = tuple(_make_step(i) for i in range(steps)) + trajectory = PhysicalTrajectory( + trajectory_id="traj-x", + device_id="robot.arm0", + goal="export_test", + steps=step_list, + started_at=100.0, + ended_at=110.0, + ) + return PhysicalEpisode( + episode_id="ep-export", + device_id="robot.arm0", + task="export_test", + trajectory=trajectory, + initial_state={"joint_0": 0.0}, + final_state={"joint_0": 1.0}, + success=True, + started_at=100.0, + ended_at=110.0, + ) + + @pytest.mark.asyncio + async def test_export_episode_json_fallback(self, tmp_path: Path) -> None: + """Test export when pyarrow is NOT available (JSON fallback).""" + exporter = EpisodeExporter(tmp_path, fps=30.0) + episode = self._make_episode() + + with patch("leapflow.robot.episode_export._PYARROW_AVAILABLE", False): + result = await exporter.export_episode(episode) + + assert result["episode_id"] == "ep-export" + assert result["step_count"] == 3 + assert result["format"] == "json" + + # Check JSON data file exists. + data_file = tmp_path / "data" / "episode_ep-export.json" + assert data_file.exists() + data = json.loads(data_file.read_text()) + assert len(data) == 3 + + # Check info.json. + info_file = tmp_path / "meta" / "info.json" + assert info_file.exists() + info = json.loads(info_file.read_text()) + assert info["fps"] == 30.0 + + # Check episode manifest. + manifest = tmp_path / "meta" / "episodes.jsonl" + assert manifest.exists() + + @pytest.mark.asyncio + async def test_export_episode_parquet_when_available(self, tmp_path: Path) -> None: + """Test export with pyarrow available.""" + pytest.importorskip("pyarrow") + + exporter = EpisodeExporter(tmp_path, fps=15.0) + episode = self._make_episode(steps=5) + result = await exporter.export_episode(episode) + + assert result["format"] == "parquet" + parquet_file = tmp_path / "data" / "episode_ep-export.parquet" + assert parquet_file.exists() + + @pytest.mark.asyncio + async def test_export_trajectory_raw(self, tmp_path: Path) -> None: + """Test exporting a raw PhysicalTrajectory.""" + exporter = EpisodeExporter(tmp_path) + trajectory = PhysicalTrajectory( + trajectory_id="traj-raw", + device_id="robot.arm0", + goal="demo", + steps=(_make_step(0), _make_step(1)), + started_at=50.0, + ended_at=55.0, + ) + + with patch("leapflow.robot.episode_export._PYARROW_AVAILABLE", False): + result = await exporter.export_trajectory(trajectory, task="demo_task") + + assert result["episode_id"] == "traj-raw" + assert result["step_count"] == 2 + + @pytest.mark.asyncio + async def test_export_with_camera_frames_fallback(self, tmp_path: Path) -> None: + """Test camera frame export when opencv is unavailable.""" + steps = ( + _make_step_with_camera(0, "/tmp/frame0.png"), + _make_step_with_camera(1, "/tmp/frame1.png"), + ) + trajectory = PhysicalTrajectory( + trajectory_id="traj-cam", + device_id="d", + goal="cam_test", + steps=steps, + started_at=0.0, + ended_at=1.0, + ) + episode = PhysicalEpisode( + episode_id="ep-cam", + device_id="d", + task="cam_test", + trajectory=trajectory, + initial_state={}, + final_state={}, + ) + exporter = EpisodeExporter(tmp_path) + + with patch("leapflow.robot.episode_export._PYARROW_AVAILABLE", False), \ + patch("leapflow.robot.episode_export._CV2_AVAILABLE", False): + result = await exporter.export_episode(episode) + + # Check frame refs file was written. + refs_file = tmp_path / "videos" / "episode_ep-cam" / "cam0_refs.json" + assert refs_file.exists() + refs = json.loads(refs_file.read_text()) + assert len(refs) == 2 + + @pytest.mark.asyncio + async def test_episodes_exported_counter(self, tmp_path: Path) -> None: + exporter = EpisodeExporter(tmp_path) + assert exporter.episodes_exported == 0 + + with patch("leapflow.robot.episode_export._PYARROW_AVAILABLE", False): + await exporter.export_episode(self._make_episode(1)) + assert exporter.episodes_exported == 1 + + @pytest.mark.asyncio + async def test_empty_episode_export(self, tmp_path: Path) -> None: + """Export an episode with zero steps.""" + trajectory = PhysicalTrajectory( + trajectory_id="traj-empty", + device_id="d", + goal="nothing", + steps=(), + ) + episode = PhysicalEpisode( + episode_id="ep-empty", + device_id="d", + task="empty", + trajectory=trajectory, + initial_state={}, + final_state={}, + ) + exporter = EpisodeExporter(tmp_path) + + with patch("leapflow.robot.episode_export._PYARROW_AVAILABLE", False): + result = await exporter.export_episode(episode) + + assert result["step_count"] == 0 + data_file = tmp_path / "data" / "episode_ep-empty.json" + assert data_file.exists() + assert json.loads(data_file.read_text()) == [] diff --git a/tests/test_physical_skill_e2e.py b/tests/test_physical_skill_e2e.py new file mode 100644 index 00000000..6a6b92e4 --- /dev/null +++ b/tests/test_physical_skill_e2e.py @@ -0,0 +1,955 @@ +# Copyright (c) Alibaba, Inc. and its affiliates. +"""Phase 1 OODA+V end-to-end tests for PhysicalSkillPlugin. + +Exercises the complete Observe-Orient-Decide-Act-Verify cycle through +PhysicalSkillPlugin, VLAInferenceTransport, OperationVerifier, +PositionVerifier, GraspVerifier, and EvidenceStore -- all backed by +in-process mocks so no hardware or gRPC dependencies are needed. +""" + +from __future__ import annotations + +import asyncio +import time +from typing import Any + +import pytest + +from leapflow.hardware.context import ( + Channel, + ContextProvenance, + ContextSource, + Direction, + Envelope, + HardwareContext, + HardwareEffect, + Representation, + TransportRef, +) +from leapflow.hardware.evidence import EvidenceStore +from leapflow.hardware.transport import ( + BatchReading, + BatchWriteOutcome, + Reading, + SIDE_EFFECT_COMMITTED, + SIDE_EFFECT_NONE, + WriteOutcome, +) +from leapflow.hardware.verification import ( + EvidenceBundle, + OperationVerdict, + PositionVerifier, + VerdictStatus, + collect_evidence, +) +from leapflow.plugins.tool_plugins.physical_skill import PhysicalSkillPlugin +from leapflow.robot.inference.strategy import ( + ComputeBudget, + ComputeProfile, + InferenceResult, +) + +# Re-use the mock robot from the robot arm transport test suite. +from tests.test_robot_arm_transport import MockRobotArm + + +# ════════════════════════════════════════════════════════════════ +# Mock infrastructure +# ════════════════════════════════════════════════════════════════ + + +class MockPolicy: + """Simulates a VLA policy for testing without actual model.""" + + def __init__(self, action_dim: int = 4) -> None: + self._action_dim = action_dim + self._call_count = 0 + self._reset_count = 0 + + def select_action(self, batch: dict) -> list[float]: + """Return a deterministic action based on call count.""" + self._call_count += 1 + return [0.1 * self._call_count] * self._action_dim + + def reset(self) -> None: + self._reset_count += 1 + self._call_count = 0 + + +class MockStrategy: + """InferenceStrategy adapter around ``MockPolicy`` for tests. + + The plugin cache now holds ``InferenceStrategy`` instances, not raw + policies, so every direct ``plugin._policies[...] = ...`` injection + used by these tests goes through this thin adapter. It preserves the + call/reset counters on the wrapped ``MockPolicy`` so existing + assertions on ``mock_policy._call_count`` keep working. + """ + + def __init__(self, policy: Any, *, strategy_id: str = "mock") -> None: + self._policy = policy + self.strategy_id = strategy_id + + @property + def compute_profile(self) -> ComputeProfile: + return ComputeProfile( + latency_range_ms=(0.1, 5.0), + supports_chunking=True, + gpu_required=False, + ) + + async def infer( + self, + observation: dict, + *, + budget: ComputeBudget | None = None, + ) -> InferenceResult: + chunk = int(budget.chunk_size) if budget is not None else 1 + select = getattr(self._policy, "select_action", None) + if select is None: + raise RuntimeError("MockPolicy missing select_action()") + action = select(dict(observation)) + return InferenceResult( + action=action, + latency_ms=0.5, + confidence=1.0, + chunk_size=chunk, + metadata={"strategy": self.strategy_id}, + ) + + async def reset(self) -> None: + reset = getattr(self._policy, "reset", None) + if reset is not None: + reset() + + +class MockInferenceServer: + """Simulates a VLA inference server for testing without gRPC.""" + + def __init__(self) -> None: + self.infer_count = 0 + self.last_observation: dict | None = None + + async def infer(self, observation: dict) -> dict: + self.infer_count += 1 + self.last_observation = observation + return {"action": [0.1, 0.2, 0.3, 0.0]} + + +class _ApprovalResult: + """Minimal stand-in for the orchestrator's ApprovalResult.""" + + def __init__(self, approved: bool, denial_message: str = "") -> None: + self.approved = approved + self.denial_message = denial_message + + +class MockApprovalGate: + """Auto-approves or auto-rejects based on configuration. + + Exposes ``evaluate`` (the production orchestrator API the plugin calls) + and keeps ``request_approval`` for direct-call tests. Both record into + ``approval_requests`` so a test can assert the gate was consulted. + """ + + def __init__(self, auto_approve: bool = True) -> None: + self._auto_approve = auto_approve + self.approval_requests: list[Any] = [] + + async def request_approval(self, request: dict) -> bool: + self.approval_requests.append(request) + return self._auto_approve + + async def evaluate(self, descriptor: Any) -> _ApprovalResult: + self.approval_requests.append(descriptor) + return _ApprovalResult( + approved=self._auto_approve, + denial_message="" if self._auto_approve else "denied by mock gate", + ) + + +class MockTrustGate: + """Tracks trust level changes for verification.""" + + def __init__(self, initial_level: str = "UNTRUSTED") -> None: + self._level = initial_level + self._successes = 0 + self._failures = 0 + self.history: list[tuple[str, str, str]] = [] + + def record_success(self, device_id: str, channel_id: str) -> None: + self._successes += 1 + self.history.append(("success", device_id, channel_id)) + + def record_failure(self, device_id: str, channel_id: str) -> None: + self._failures += 1 + self.history.append(("failure", device_id, channel_id)) + + +class MockRegistry: + """Minimal hardware registry wiring MockRobotArm for PhysicalSkillPlugin. + + Presents channels as writable joints + a gripper, and supports + read/write via duck-typed device with context and transport. + """ + + def __init__( + self, + robot: MockRobotArm, + verifiers: list[Any] | None = None, + ) -> None: + self._robot = robot + self.verifiers: list[Any] = verifiers or [] + # Writable channels matching MockRobotArm's 3 joints + gripper. + self._writable_channels = [ + Channel( + channel_id="joint.shoulder.position", + direction=Direction.READWRITE.value, + representation=Representation.SCALAR.value, + effect=HardwareEffect.ACTUATE.value, + description="Shoulder joint position", + envelope=Envelope(declared=True, min_value=-3.14, max_value=3.14), + ), + Channel( + channel_id="joint.elbow.position", + direction=Direction.READWRITE.value, + representation=Representation.SCALAR.value, + effect=HardwareEffect.ACTUATE.value, + description="Elbow joint position", + envelope=Envelope(declared=True, min_value=-1.57, max_value=1.57), + ), + Channel( + channel_id="joint.wrist.position", + direction=Direction.READWRITE.value, + representation=Representation.SCALAR.value, + effect=HardwareEffect.ACTUATE.value, + description="Wrist joint position", + envelope=Envelope(declared=True, min_value=-3.14, max_value=3.14), + ), + Channel( + channel_id="gripper.grip", + direction=Direction.READWRITE.value, + representation=Representation.STATE.value, + effect=HardwareEffect.ACTUATE.value, + description="Gripper state", + envelope=Envelope(declared=True, min_value=0.0, max_value=1.0), + ), + ] + self._context = HardwareContext( + device_id="mock.arm", + display_name="Mock Arm", + transport=TransportRef(kind="mock", config={}), + channels=tuple(self._writable_channels), + halt_supported=True, + provenance=ContextProvenance(source=ContextSource.IMPORTED.value), + ) + self._device = _MockDevice(self._robot, self._context) + + def get_device(self, device_id: str) -> Any: + return self._device + + async def read(self, device_id: str, channel_id: str) -> Reading: + obs = self._robot.get_observation() + # Map channel id back to robot observation key. + key_map = { + "joint.shoulder.position": "shoulder.position", + "joint.elbow.position": "elbow.position", + "joint.wrist.position": "wrist.position", + "gripper.grip": "grip", + } + obs_key = key_map.get(channel_id, channel_id) + value = obs.get(obs_key, 0.0) + if hasattr(value, "item"): + value = float(value) + return Reading( + device_id=device_id, + channel_id=channel_id, + value=value, + ) + + async def write(self, device_id: str, channel_id: str, value: Any) -> WriteOutcome: + return WriteOutcome(ok=True, side_effect_state=SIDE_EFFECT_COMMITTED) + + +class _MockDevice: + """Quacks like a registered device: has .transport and .context.""" + + def __init__(self, robot: MockRobotArm, context: HardwareContext) -> None: + self._robot = robot + self.context = context + self.transport = _MockBatchTransport(robot, context) + + +class _MockBatchTransport: + """Minimal BatchTransport backed by MockRobotArm.""" + + def __init__(self, robot: MockRobotArm, context: HardwareContext) -> None: + self._robot = robot + self._context = context + + async def read_batch(self, channel_ids: tuple[str, ...]) -> BatchReading: + obs = self._robot.get_observation() + key_map = { + "joint.shoulder.position": "shoulder.position", + "joint.elbow.position": "elbow.position", + "joint.wrist.position": "wrist.position", + "gripper.grip": "grip", + } + readings = [] + for cid in channel_ids: + obs_key = key_map.get(cid, cid) + val = obs.get(obs_key, 0.0) + if hasattr(val, "item"): + val = float(val) + readings.append(Reading(device_id="mock.arm", channel_id=cid, value=val)) + return BatchReading(device_id="mock.arm", readings=tuple(readings)) + + async def write_batch( + self, commands: tuple[tuple[str, Any], ...], + ) -> BatchWriteOutcome: + action = [v for _, v in commands] + self._robot.send_action(action) + outcomes = tuple( + WriteOutcome(ok=True, side_effect_state=SIDE_EFFECT_COMMITTED) + for _ in commands + ) + return BatchWriteOutcome( + ok=True, + outcomes=outcomes, + side_effect_state=SIDE_EFFECT_COMMITTED, + ) + + +# ── Fixtures ── + + +@pytest.fixture +def mock_robot() -> MockRobotArm: + robot = MockRobotArm() + robot.connect() + return robot + + +@pytest.fixture +def verifier() -> PositionVerifier: + return PositionVerifier(default_tolerance=0.05) + + +@pytest.fixture +def registry(mock_robot: MockRobotArm, verifier: PositionVerifier) -> MockRegistry: + return MockRegistry(mock_robot, verifiers=[verifier]) + + +@pytest.fixture +def plugin(registry: MockRegistry) -> PhysicalSkillPlugin: + p = PhysicalSkillPlugin() + p.bind_runtime( + hardware_registry=registry, + hardware_approval_gate=MockApprovalGate(auto_approve=True), + hardware_trust_gate=MockTrustGate(), + session_id="test-session", + ) + return p + + +# ════════════════════════════════════════════════════════════════ +# 1. OODA+V complete cycle +# ════════════════════════════════════════════════════════════════ + + +class TestOODAVLoop: + """Test the complete Observe-Orient-Decide-Act-Verify cycle.""" + + @pytest.mark.asyncio + async def test_full_cycle_observe_infer_execute_verify( + self, plugin: PhysicalSkillPlugin, registry: MockRegistry, + ) -> None: + """Complete OODA+V: read observation -> policy inference -> + write action -> verify position -> success.""" + # Inject a mock local policy to bypass LeapRobot import. + mock_policy = MockPolicy(action_dim=4) + plugin._policies["mock_policy"] = MockStrategy(mock_policy) + + result = await plugin.policy_infer({ + "device_id": "mock.arm", + "policy": "mock_policy", + "task": "pick up cube", + "execute": True, + "verify": True, + }) + + assert result["ok"] is True + assert "action" in result + assert "execution" in result + assert result["execution"]["ok"] is True + assert mock_policy._call_count == 1 + + @pytest.mark.asyncio + async def test_cycle_with_verification_failure( + self, plugin: PhysicalSkillPlugin, mock_robot: MockRobotArm, + ) -> None: + """OODA+V where verification detects position error.""" + mock_policy = MockPolicy(action_dim=4) + plugin._policies["mock_policy"] = MockStrategy(mock_policy) + + # Make the robot report a very different position after writing, + # by forcing the robot to return positions far from what was commanded. + orig_send = mock_robot.send_action + + def _divergent_send(action: Any) -> None: + orig_send(action) + # Override all positions to a far-off value. + for key in mock_robot._positions: + mock_robot._positions[key] = 999.0 + + mock_robot.send_action = _divergent_send # type: ignore[assignment] + + result = await plugin.policy_infer({ + "device_id": "mock.arm", + "policy": "mock_policy", + "execute": True, + "verify": True, + }) + + assert result["ok"] is True + # Verification should report failure or inconclusive due to deviation. + if "verification" in result: + v = result["verification"] + assert v["status"] in ("failure", "inconclusive") + + @pytest.mark.asyncio + async def test_cycle_without_execution( + self, plugin: PhysicalSkillPlugin, mock_robot: MockRobotArm, + ) -> None: + """Inference only (execute=False): no physical effect.""" + mock_policy = MockPolicy(action_dim=4) + plugin._policies["mock_policy"] = MockStrategy(mock_policy) + sent_before = len(mock_robot._actions_sent) + + result = await plugin.policy_infer({ + "device_id": "mock.arm", + "policy": "mock_policy", + "execute": False, + }) + + assert result["ok"] is True + assert "action" in result + # No execution block and no new actions sent. + assert "execution" not in result or result.get("execution") is None + assert len(mock_robot._actions_sent) == sent_before + + @pytest.mark.asyncio + async def test_chunk_prediction( + self, plugin: PhysicalSkillPlugin, mock_robot: MockRobotArm, + ) -> None: + """Multi-step chunk: predict N actions, execute sequentially.""" + mock_policy = MockPolicy(action_dim=4) + plugin._policies["mock_policy"] = MockStrategy(mock_policy) + + result = await plugin.policy_infer({ + "device_id": "mock.arm", + "policy": "mock_policy", + "chunk_size": 3, + "execute": True, + }) + + assert result["ok"] is True + assert "action" in result + # Policy was called once (chunk prediction produces one action). + assert mock_policy._call_count == 1 + + +# ════════════════════════════════════════════════════════════════ +# 2. Policy management +# ════════════════════════════════════════════════════════════════ + + +class TestPolicyManagement: + """Policy loading, caching, and listing.""" + + @pytest.mark.asyncio + async def test_local_policy_loading(self, plugin: PhysicalSkillPlugin) -> None: + """Load a mock local policy and run inference.""" + mock_policy = MockPolicy(action_dim=4) + plugin._policies["my_policy"] = MockStrategy(mock_policy) + + result = await plugin.policy_infer({ + "device_id": "mock.arm", + "policy": "my_policy", + "execute": False, + }) + assert result["ok"] is True + assert mock_policy._call_count == 1 + + @pytest.mark.asyncio + async def test_remote_policy_path(self, plugin: PhysicalSkillPlugin) -> None: + """Remote policy path 'remote:host:port' resolves to a VLARemoteStrategy.""" + from leapflow.robot.inference.vla_remote import VLARemoteStrategy + + result = await plugin._load_policy("remote:localhost:50051") + assert isinstance(result, VLARemoteStrategy) + # Verify caching. + assert "remote:localhost:50051" in plugin._policies + # The strategy id carries the address so multiple endpoints can + # coexist in the registry without colliding on ``vla_remote``. + assert result.strategy_id == "vla_remote:localhost:50051" + + @pytest.mark.asyncio + async def test_policy_caching(self, plugin: PhysicalSkillPlugin) -> None: + """Same policy path reuses cached instance.""" + mock_policy = MockPolicy() + plugin._policies["cached_pol"] = MockStrategy(mock_policy) + + p1 = await plugin._load_policy("cached_pol") + p2 = await plugin._load_policy("cached_pol") + assert p1 is p2 + + @pytest.mark.asyncio + async def test_policy_list(self, plugin: PhysicalSkillPlugin) -> None: + """hw_policy_list returns loaded policies.""" + plugin._policies["local_a"] = MockStrategy(MockPolicy()) + plugin._policies["remote:gpu:5000"] = object() + + result = await plugin.policy_list({"source": "all"}) + assert result["ok"] is True + names = {p["name"] for p in result["policies"]} + assert "local_a" in names + assert "remote:gpu:5000" in names + + +# ════════════════════════════════════════════════════════════════ +# 3. Episode lifecycle +# ════════════════════════════════════════════════════════════════ + + +class TestEpisodeLifecycle: + """Episode start / stop / status management.""" + + @pytest.mark.asyncio + async def test_episode_start_stop(self, plugin: PhysicalSkillPlugin) -> None: + """Start and stop episode, verify summary stats.""" + mock_policy = MockPolicy() + plugin._policies["ep_pol"] = MockStrategy(mock_policy) + + start = await plugin.policy_episode({ + "action": "start", + "device_id": "mock.arm", + "policy": "ep_pol", + "task": "stack blocks", + }) + assert start["ok"] is True + assert start["action"] == "started" + + stop = await plugin.policy_episode({"action": "stop"}) + assert stop["ok"] is True + assert stop["action"] == "stopped" + assert "summary" in stop + assert stop["summary"]["steps"] == 0 + + @pytest.mark.asyncio + async def test_episode_status(self, plugin: PhysicalSkillPlugin) -> None: + """Query episode status during active episode.""" + plugin._policies["ep_pol"] = MockStrategy(MockPolicy()) + await plugin.policy_episode({ + "action": "start", + "device_id": "mock.arm", + "policy": "ep_pol", + }) + + status = await plugin.policy_episode({"action": "status"}) + assert status["ok"] is True + assert status["active"] is True + assert status["episode"]["device_id"] == "mock.arm" + + await plugin.policy_episode({"action": "stop"}) + + @pytest.mark.asyncio + async def test_infer_within_episode(self, plugin: PhysicalSkillPlugin) -> None: + """Inference during active episode tracks step count.""" + mock_policy = MockPolicy(action_dim=4) + plugin._policies["ep_pol"] = MockStrategy(mock_policy) + + await plugin.policy_episode({ + "action": "start", + "device_id": "mock.arm", + "policy": "ep_pol", + }) + + # Run two inferences. + for _ in range(2): + await plugin.policy_infer({ + "device_id": "mock.arm", + "policy": "ep_pol", + "execute": True, + }) + + stop = await plugin.policy_episode({"action": "stop"}) + assert stop["summary"]["steps"] == 2 + assert mock_policy._call_count == 2 + + +# ════════════════════════════════════════════════════════════════ +# 4. Trust progression +# ════════════════════════════════════════════════════════════════ + + +class TestTrustProgression: + """Trust gate interaction for success/failure recording.""" + + @pytest.mark.asyncio + async def test_trust_promotion_on_success(self) -> None: + """Multiple successful verifications record trust successes.""" + gate = MockTrustGate() + for _ in range(3): + gate.record_success("mock.arm", "joint.shoulder.position") + + assert gate._successes == 3 + assert len(gate.history) == 3 + assert all(h[0] == "success" for h in gate.history) + + @pytest.mark.asyncio + async def test_trust_demotion_on_failure(self) -> None: + """Verification failure triggers trust demotion.""" + gate = MockTrustGate() + gate.record_success("mock.arm", "joint.shoulder.position") + gate.record_failure("mock.arm", "joint.shoulder.position") + + assert gate._successes == 1 + assert gate._failures == 1 + assert gate.history[-1][0] == "failure" + + @pytest.mark.asyncio + async def test_irreversible_always_needs_approval(self) -> None: + """Approval gate is always invoked regardless of trust level.""" + gate = MockApprovalGate(auto_approve=True) + await gate.request_approval({"channel": "gripper.grip", "effect": "DISPENSE"}) + await gate.request_approval({"channel": "gripper.grip", "effect": "EMIT"}) + + assert len(gate.approval_requests) == 2 + + +# ════════════════════════════════════════════════════════════════ +# 5. Evidence store integration +# ════════════════════════════════════════════════════════════════ + + +class TestEvidenceIntegration: + """EvidenceStore persistence and analytics.""" + + @pytest.mark.asyncio + async def test_evidence_recorded_after_verification(self, tmp_path) -> None: + """EvidenceStore receives bundle after verified operation.""" + store = EvidenceStore(tmp_path / "evidence.duckdb") + try: + bundle = EvidenceBundle( + operation_id="op_001", + device_id="mock.arm", + channel_id="joint.shoulder.position", + timestamp=time.time(), + intended_value=1.5, + actual_outcome=None, + post_settle_readings=( + Reading( + device_id="mock.arm", + channel_id="joint.shoulder.position", + value=1.48, + ), + ), + ) + verdict = OperationVerdict( + status=VerdictStatus.SUCCESS.value, + confidence=0.95, + deviation=0.02, + detail="within tolerance", + ) + + op_id = await store.record(bundle, verdict) + assert op_id == "op_001" + assert store.records_written == 1 + + rows = await store.query(device_id="mock.arm") + assert len(rows) >= 1 + assert rows[0]["operation_id"] == "op_001" + assert rows[0]["verdict_status"] == "success" + finally: + store.close() + + @pytest.mark.asyncio + async def test_success_rate_query(self, tmp_path) -> None: + """success_rate returns correct statistics after operations.""" + store = EvidenceStore(tmp_path / "evidence.duckdb") + try: + now = time.time() + # Record 2 successes and 1 failure. + for i, status in enumerate(["success", "success", "failure"]): + bundle = EvidenceBundle( + operation_id=f"rate_{i:03d}", + device_id="mock.arm", + channel_id="joint.shoulder.position", + timestamp=now + i, + intended_value=1.0, + actual_outcome=None, + post_settle_readings=(), + ) + verdict = OperationVerdict( + status=status, + confidence=0.9, + ) + await store.record(bundle, verdict) + + stats = await store.success_rate("mock.arm") + assert stats["total"] == 3 + assert stats["success"] == 2 + assert stats["failure"] == 1 + assert stats["success_rate"] == pytest.approx(2 / 3) + finally: + store.close() + + +# ════════════════════════════════════════════════════════════════ +# 6. Error handling +# ════════════════════════════════════════════════════════════════ + + +class TestErrorHandling: + """Graceful failures without physical effects.""" + + @pytest.mark.asyncio + async def test_inference_timeout(self, plugin: PhysicalSkillPlugin) -> None: + """Policy inference timeout returns error, no physical effect.""" + + class SlowPolicy: + async def select_action(self, batch: dict) -> list: + await asyncio.sleep(10) + return [0.0] + + plugin._policies["slow"] = MockStrategy(SlowPolicy()) + # The plugin calls asyncio.to_thread(select) which is sync, + # but a truly slow policy would be caught by broader timeout. + # Here we test the error path with an exception. + class FailPolicy: + def select_action(self, batch: dict) -> None: + raise TimeoutError("inference timed out") + + plugin._policies["fail_timeout"] = MockStrategy(FailPolicy()) + result = await plugin.policy_infer({ + "device_id": "mock.arm", + "policy": "fail_timeout", + "execute": True, + }) + assert result["ok"] is False + assert "inference failed" in result["error"] + + @pytest.mark.asyncio + async def test_robot_disconnected( + self, plugin: PhysicalSkillPlugin, mock_robot: MockRobotArm, + ) -> None: + """Operation on disconnected robot returns clear error.""" + mock_robot.disconnect() + + class FailReadRegistry: + verifiers: list = [] + + def get_device(self, device_id: str) -> Any: + raise RuntimeError("device not connected") + + plugin.bind_runtime(hardware_registry=FailReadRegistry()) + plugin._policies["pol"] = MockStrategy(MockPolicy(action_dim=4)) + + result = await plugin.policy_infer({ + "device_id": "mock.arm", + "policy": "pol", + "execute": True, + }) + assert result["ok"] is False + assert "error" in result + + @pytest.mark.asyncio + async def test_missing_policy(self, plugin: PhysicalSkillPlugin) -> None: + """Non-existent policy path surfaces the load failure at inference time. + + With the strategy-registry delegation, ``_load_policy`` constructs + the strategy eagerly but the underlying model is loaded lazily + inside ``VLALocalStrategy.infer``; the missing checkpoint therefore + turns into an inference failure carrying the loader's own message. + """ + result = await plugin.policy_infer({ + "device_id": "mock.arm", + "policy": "nonexistent/path/to/model", + }) + assert result["ok"] is False + assert "inference failed" in result["error"] + + @pytest.mark.asyncio + async def test_missing_device_id(self, plugin: PhysicalSkillPlugin) -> None: + """Missing device_id is rejected immediately.""" + result = await plugin.policy_infer({ + "policy": "some_policy", + }) + assert result["ok"] is False + assert "device_id" in result["error"] + + @pytest.mark.asyncio + async def test_missing_policy_path(self, plugin: PhysicalSkillPlugin) -> None: + """Missing policy path is rejected immediately.""" + result = await plugin.policy_infer({ + "device_id": "mock.arm", + }) + assert result["ok"] is False + assert "policy" in result["error"] + + +# ════════════════════════════════════════════════════════════════ +# 7. Approval gate integration +# ════════════════════════════════════════════════════════════════ + + +class TestApprovalIntegration: + """Test that physical operations go through the approval gate.""" + + @pytest.mark.asyncio + async def test_approval_gate_receives_request(self, registry: MockRegistry) -> None: + """hw_policy_infer sends an approval request to the gate before execution.""" + gate = MockApprovalGate(auto_approve=True) + p = PhysicalSkillPlugin() + p.bind_runtime( + hardware_registry=registry, + hardware_approval_gate=gate, + hardware_trust_gate=MockTrustGate(), + session_id="test-session", + ) + p._policies["mock_policy"] = MockStrategy(MockPolicy(action_dim=4)) + + result = await p.policy_infer({ + "device_id": "mock.arm", + "policy": "mock_policy", + "execute": True, + }) + + assert result["ok"] is True + assert len(gate.approval_requests) == 1 + # The descriptor carries the policy provenance and the target device. + descriptor = gate.approval_requests[0] + assert descriptor.metadata["device_id"] == "mock.arm" + assert descriptor.metadata["source"] == "hw_policy_infer" + + @pytest.mark.asyncio + async def test_approval_rejected_blocks_execution( + self, registry: MockRegistry, mock_robot: MockRobotArm, + ) -> None: + """When the gate rejects, no write_batch reaches the robot.""" + gate = MockApprovalGate(auto_approve=False) + p = PhysicalSkillPlugin() + p.bind_runtime( + hardware_registry=registry, + hardware_approval_gate=gate, + hardware_trust_gate=MockTrustGate(), + session_id="test-session", + ) + p._policies["mock_policy"] = MockStrategy(MockPolicy(action_dim=4)) + sent_before = len(mock_robot._actions_sent) + + result = await p.policy_infer({ + "device_id": "mock.arm", + "policy": "mock_policy", + "execute": True, + }) + + assert len(gate.approval_requests) == 1 + # Execution was refused and nothing was written to the robot. + assert result["ok"] is False + assert result["execution"]["ok"] is False + assert "not approved" in result["execution"]["error"] + assert len(mock_robot._actions_sent) == sent_before + + @pytest.mark.asyncio + async def test_absent_gate_fails_closed( + self, registry: MockRegistry, mock_robot: MockRobotArm, + ) -> None: + """With no gate bound, a physical write is refused (fail-closed).""" + p = PhysicalSkillPlugin() + p.bind_runtime(hardware_registry=registry, session_id="test-session") + p._policies["mock_policy"] = MockStrategy(MockPolicy(action_dim=4)) + sent_before = len(mock_robot._actions_sent) + + result = await p.policy_infer({ + "device_id": "mock.arm", + "policy": "mock_policy", + "execute": True, + }) + + assert result["ok"] is False + assert result["execution"]["ok"] is False + assert len(mock_robot._actions_sent) == sent_before + + +# ════════════════════════════════════════════════════════════════ +# 8. Trust gate integration +# ════════════════════════════════════════════════════════════════ + + +class TestTrustIntegration: + """Test the trust gate receives verification results.""" + + @pytest.mark.asyncio + async def test_successful_verification_records_trust( + self, registry: MockRegistry, + ) -> None: + """After a successful verification, the trust gate records success.""" + trust = MockTrustGate() + p = PhysicalSkillPlugin() + p.bind_runtime( + hardware_registry=registry, + hardware_approval_gate=MockApprovalGate(auto_approve=True), + hardware_trust_gate=trust, + session_id="test-session", + ) + p._policies["mock_policy"] = MockStrategy(MockPolicy(action_dim=4)) + + result = await p.policy_infer({ + "device_id": "mock.arm", + "policy": "mock_policy", + "execute": True, + "verify": True, + }) + + assert result["ok"] is True + # The commanded position lands within tolerance -> success recorded. + assert any(h[0] == "success" for h in trust.history) + assert all(h[0] != "failure" for h in trust.history) + + @pytest.mark.asyncio + async def test_failed_verification_records_failure( + self, registry: MockRegistry, mock_robot: MockRobotArm, + ) -> None: + """After a failed verification, the trust gate records failure.""" + trust = MockTrustGate() + p = PhysicalSkillPlugin() + p.bind_runtime( + hardware_registry=registry, + hardware_approval_gate=MockApprovalGate(auto_approve=True), + hardware_trust_gate=trust, + session_id="test-session", + ) + p._policies["mock_policy"] = MockStrategy(MockPolicy(action_dim=4)) + + # Force the robot far from the commanded position so verification fails. + orig_send = mock_robot.send_action + + def _divergent_send(action: Any) -> None: + orig_send(action) + for key in mock_robot._positions: + mock_robot._positions[key] = 999.0 + + mock_robot.send_action = _divergent_send # type: ignore[assignment] + + result = await p.policy_infer({ + "device_id": "mock.arm", + "policy": "mock_policy", + "execute": True, + "verify": True, + }) + + assert result["ok"] is True + # A large deviation yields a failure verdict -> failure recorded. + assert any(h[0] == "failure" for h in trust.history) diff --git a/tests/test_physical_teleop.py b/tests/test_physical_teleop.py new file mode 100644 index 00000000..9afdd368 --- /dev/null +++ b/tests/test_physical_teleop.py @@ -0,0 +1,502 @@ +# Copyright (c) Alibaba, Inc. and its affiliates. +"""Tests for physical trajectory types and teleop bridge. + +Covers: +- PhysicalTrajectoryStep construction, serialization +- PhysicalTrajectory builder pattern, finalization, serialization +- LeaderFollowerBridge mock teleop loop +- KinestheticRecorder mock sampling +- Halt / stop behavior +- _parse_teach_start_args helper +""" + +from __future__ import annotations + +import asyncio +import time +from dataclasses import dataclass +from typing import Any +from unittest.mock import AsyncMock + +import pytest + +from leapflow.robot.trajectory import ( + PhysicalTrajectory, + PhysicalTrajectoryStep, + make_trajectory_id, +) +from leapflow.robot.teleop import ( + KinestheticRecorder, + LeaderFollowerBridge, + TeleopBridge, + get_physical_session, + register_physical_session, + remove_physical_session, +) + + +# --------------------------------------------------------------------------- +# Fixtures: mock hardware registry +# --------------------------------------------------------------------------- + +@dataclass +class _MockChannel: + channel_id: str + is_readable: bool = True + is_writable: bool = False + + +@dataclass +class _MockContext: + device_id: str + channels: tuple[_MockChannel, ...] = () + + +@dataclass +class _MockReading: + channel_id: str + value: float + + +@dataclass +class _MockBatchReading: + device_id: str + readings: tuple[_MockReading, ...] + + +class _MockTransport: + """Minimal transport that records writes and returns preset readings.""" + + kind = "mock" + + def __init__(self, positions: dict[str, float] | None = None) -> None: + self._positions = positions or {} + self.writes: list[tuple[str, Any]] = [] + + async def read(self, channel_id: str) -> _MockReading: + return _MockReading(channel_id=channel_id, value=self._positions.get(channel_id, 0.0)) + + async def write(self, channel_id: str, value: Any) -> None: + self.writes.append((channel_id, value)) + self._positions[channel_id] = float(value) + + +class MockRegistry: + """Minimal mock of HardwareRegistry for teleop/kinesthetic tests.""" + + def __init__( + self, + contexts: dict[str, _MockContext] | None = None, + positions: dict[str, dict[str, float]] | None = None, + ) -> None: + self._contexts = contexts or {} + self._transports: dict[str, _MockTransport] = {} + for device_id, pos in (positions or {}).items(): + self._transports[device_id] = _MockTransport(pos) + + def context(self, device_id: str) -> _MockContext | None: + return self._contexts.get(device_id) + + async def transport(self, device_id: str) -> _MockTransport: + if device_id not in self._transports: + self._transports[device_id] = _MockTransport() + return self._transports[device_id] + + async def read_batch( + self, device_id: str, channel_ids: tuple[str, ...] + ) -> _MockBatchReading: + transport = await self.transport(device_id) + readings = [] + for ch_id in channel_ids: + r = await transport.read(ch_id) + readings.append(r) + return _MockBatchReading(device_id=device_id, readings=tuple(readings)) + + def device_io(self, device_id: str) -> Any: + return _NoopLock() + + def device_io_batch(self, device_id: str) -> Any: + return _NoopLock() + + +class _NoopLock: + async def __aenter__(self) -> None: + return None + + async def __aexit__(self, *args: Any) -> None: + pass + + +# --------------------------------------------------------------------------- +# PhysicalTrajectoryStep tests +# --------------------------------------------------------------------------- + +class TestPhysicalTrajectoryStep: + def test_construction_minimal(self) -> None: + step = PhysicalTrajectoryStep( + timestamp=1000.0, + monotonic_at=500.0, + joint_positions={"j1": 0.5, "j2": 1.0}, + ) + assert step.timestamp == 1000.0 + assert step.monotonic_at == 500.0 + assert step.joint_positions == {"j1": 0.5, "j2": 1.0} + assert step.joint_velocities == {} + assert step.gripper_state is None + assert step.camera_frames == {} + assert step.sensor_readings == {} + assert step.action_source == "teleop" + assert step.sequence == 0 + + def test_construction_full(self) -> None: + step = PhysicalTrajectoryStep( + timestamp=1000.0, + monotonic_at=500.0, + joint_positions={"j1": 0.5}, + joint_velocities={"j1": 0.1}, + gripper_state=0.8, + camera_frames={"cam0": "/tmp/frame_001.jpg"}, + sensor_readings={"force": 3.14}, + action_source="kinesthetic", + sequence=42, + ) + assert step.gripper_state == 0.8 + assert step.camera_frames == {"cam0": "/tmp/frame_001.jpg"} + assert step.action_source == "kinesthetic" + assert step.sequence == 42 + + def test_to_dict_minimal(self) -> None: + step = PhysicalTrajectoryStep( + timestamp=100.0, + monotonic_at=50.0, + joint_positions={"j1": 1.0}, + ) + d = step.to_dict() + assert d["timestamp"] == 100.0 + assert d["joint_positions"] == {"j1": 1.0} + assert "joint_velocities" not in d # empty → omitted + assert "gripper_state" not in d # None → omitted + + def test_to_dict_full(self) -> None: + step = PhysicalTrajectoryStep( + timestamp=100.0, + monotonic_at=50.0, + joint_positions={"j1": 1.0}, + joint_velocities={"j1": 0.5}, + gripper_state=0.3, + camera_frames={"cam": "ref"}, + sensor_readings={"f": 1.0}, + ) + d = step.to_dict() + assert d["joint_velocities"] == {"j1": 0.5} + assert d["gripper_state"] == 0.3 + assert d["camera_frames"] == {"cam": "ref"} + assert d["sensor_readings"] == {"f": 1.0} + + def test_frozen(self) -> None: + step = PhysicalTrajectoryStep( + timestamp=1.0, monotonic_at=1.0, joint_positions={"j": 0.0} + ) + with pytest.raises(AttributeError): + step.timestamp = 2.0 # type: ignore[misc] + + +# --------------------------------------------------------------------------- +# PhysicalTrajectory tests +# --------------------------------------------------------------------------- + +class TestPhysicalTrajectory: + def test_empty_trajectory(self) -> None: + t = PhysicalTrajectory( + trajectory_id="abc", device_id="robot.follower", goal="pick up" + ) + assert t.sample_count == 0 + assert t.duration_s == 0.0 + assert t.steps == () + + def test_with_step(self) -> None: + t = PhysicalTrajectory( + trajectory_id="t1", device_id="dev", goal="test" + ) + step = PhysicalTrajectoryStep( + timestamp=1.0, monotonic_at=1.0, joint_positions={"j": 0.5} + ) + t2 = t.with_step(step) + assert t.sample_count == 0 # original unchanged + assert t2.sample_count == 1 + assert t2.steps[0] is step + + def test_finalized(self) -> None: + t = PhysicalTrajectory( + trajectory_id="t1", + device_id="dev", + goal="test", + started_at=1000.0, + ) + t2 = t.finalized(ended_at=1010.0) + assert t2.ended_at == 1010.0 + assert t2.duration_s == 10.0 + assert t.ended_at == 0.0 # original unchanged + + def test_finalized_default_time(self) -> None: + t = PhysicalTrajectory( + trajectory_id="t1", device_id="dev", goal="", started_at=1.0 + ) + before = time.time() + t2 = t.finalized() + after = time.time() + assert before <= t2.ended_at <= after + + def test_to_dict(self) -> None: + step = PhysicalTrajectoryStep( + timestamp=1.0, monotonic_at=1.0, joint_positions={"j": 0.1} + ) + t = PhysicalTrajectory( + trajectory_id="tid", + device_id="dev", + goal="g", + steps=(step,), + started_at=1.0, + ended_at=2.0, + metadata={"mode": "teleop"}, + ) + d = t.to_dict() + assert d["trajectory_id"] == "tid" + assert d["sample_count"] == 1 + assert d["duration_s"] == 1.0 + assert len(d["steps"]) == 1 + + def test_make_trajectory_id(self) -> None: + tid = make_trajectory_id() + assert len(tid) == 16 + assert tid != make_trajectory_id() # unique + + +# --------------------------------------------------------------------------- +# LeaderFollowerBridge tests +# --------------------------------------------------------------------------- + +class TestLeaderFollowerBridge: + @pytest.fixture + def registry(self) -> MockRegistry: + return MockRegistry( + contexts={ + "leader": _MockContext( + "leader", + (_MockChannel("j1"), _MockChannel("j2")), + ), + "follower": _MockContext( + "follower", + (_MockChannel("j1"), _MockChannel("j2")), + ), + }, + positions={ + "leader": {"j1": 1.0, "j2": 2.0}, + "follower": {"j1": 0.0, "j2": 0.0}, + }, + ) + + @pytest.mark.asyncio + async def test_start_stop(self, registry: MockRegistry) -> None: + bridge = LeaderFollowerBridge( + registry, + leader_device_id="leader", + follower_device_id="follower", + goal="test teleop", + control_rate_hz=100.0, # fast for test + ) + assert not bridge.is_active + await bridge.start() + assert bridge.is_active + + # Let it run a few cycles. + await asyncio.sleep(0.1) + + trajectory = await bridge.stop() + assert not bridge.is_active + assert trajectory.sample_count > 0 + assert trajectory.duration_s > 0.0 + assert trajectory.goal == "test teleop" + assert trajectory.device_id == "follower" + + # Check step contents. + step = trajectory.steps[0] + assert step.action_source == "teleop" + assert "j1" in step.joint_positions + + @pytest.mark.asyncio + async def test_protocol_compliance(self, registry: MockRegistry) -> None: + bridge = LeaderFollowerBridge( + registry, + leader_device_id="leader", + follower_device_id="follower", + ) + assert isinstance(bridge, TeleopBridge) + + @pytest.mark.asyncio + async def test_joint_map(self, registry: MockRegistry) -> None: + bridge = LeaderFollowerBridge( + registry, + leader_device_id="leader", + follower_device_id="follower", + joint_map={"j1": "motor_a", "j2": "motor_b"}, + control_rate_hz=100.0, + ) + await bridge.start() + await asyncio.sleep(0.05) + trajectory = await bridge.stop() + # The bridge should have written to the mapped channels. + transport = await registry.transport("follower") + written_channels = {ch for ch, _ in transport.writes} + assert "motor_a" in written_channels or "motor_b" in written_channels + + @pytest.mark.asyncio + async def test_double_start_noop(self, registry: MockRegistry) -> None: + bridge = LeaderFollowerBridge( + registry, + leader_device_id="leader", + follower_device_id="follower", + ) + await bridge.start() + await bridge.start() # should be idempotent + assert bridge.is_active + await bridge.stop() + + +# --------------------------------------------------------------------------- +# KinestheticRecorder tests +# --------------------------------------------------------------------------- + +class TestKinestheticRecorder: + @pytest.fixture + def registry(self) -> MockRegistry: + return MockRegistry( + contexts={ + "arm": _MockContext( + "arm", + (_MockChannel("j1"), _MockChannel("j2"), _MockChannel("j3")), + ), + }, + positions={"arm": {"j1": 0.1, "j2": 0.2, "j3": 0.3}}, + ) + + @pytest.mark.asyncio + async def test_record_and_stop(self, registry: MockRegistry) -> None: + recorder = KinestheticRecorder( + registry, + device_id="arm", + goal="grasp demo", + sample_rate_hz=100.0, + ) + assert not recorder.is_active + await recorder.start() + assert recorder.is_active + + await asyncio.sleep(0.1) + + trajectory = await recorder.stop() + assert not recorder.is_active + assert trajectory.sample_count > 0 + assert trajectory.duration_s > 0.0 + assert trajectory.goal == "grasp demo" + assert trajectory.device_id == "arm" + + step = trajectory.steps[0] + assert step.action_source == "kinesthetic" + assert "j1" in step.joint_positions + assert "j2" in step.joint_positions + + @pytest.mark.asyncio + async def test_sequence_increments(self, registry: MockRegistry) -> None: + recorder = KinestheticRecorder( + registry, + device_id="arm", + sample_rate_hz=200.0, + ) + await recorder.start() + await asyncio.sleep(0.1) + trajectory = await recorder.stop() + + sequences = [s.sequence for s in trajectory.steps] + assert sequences == list(range(len(sequences))) + + @pytest.mark.asyncio + async def test_double_start_noop(self, registry: MockRegistry) -> None: + recorder = KinestheticRecorder(registry, device_id="arm") + await recorder.start() + await recorder.start() # idempotent + assert recorder.is_active + await recorder.stop() + + +# --------------------------------------------------------------------------- +# Session tracking helpers +# --------------------------------------------------------------------------- + +class TestSessionTracking: + def test_register_get_remove(self) -> None: + bridge = AsyncMock() + register_physical_session("test_key", bridge) + assert get_physical_session("test_key") is bridge + removed = remove_physical_session("test_key") + assert removed is bridge + assert get_physical_session("test_key") is None + + def test_get_missing(self) -> None: + assert get_physical_session("nonexistent") is None + + def test_remove_missing(self) -> None: + assert remove_physical_session("nonexistent") is None + + +# --------------------------------------------------------------------------- +# _parse_teach_start_args tests +# --------------------------------------------------------------------------- + +class TestParseTeachStartArgs: + def test_empty(self) -> None: + from leapflow.cli.commands.slash_handlers import _parse_teach_start_args + + result = _parse_teach_start_args("") + assert result["mode"] == "gui" + assert result["goal"] == "" + + def test_goal_only(self) -> None: + from leapflow.cli.commands.slash_handlers import _parse_teach_start_args + + result = _parse_teach_start_args("pick up the cup") + assert result["mode"] == "gui" + assert result["goal"] == "pick up the cup" + + def test_teleop_mode(self) -> None: + from leapflow.cli.commands.slash_handlers import _parse_teach_start_args + + result = _parse_teach_start_args( + "--mode=teleop --leader=arm_leader --follower=arm_follower" + ) + assert result["mode"] == "teleop" + assert result["leader"] == "arm_leader" + assert result["follower"] == "arm_follower" + + def test_kinesthetic_mode(self) -> None: + from leapflow.cli.commands.slash_handlers import _parse_teach_start_args + + result = _parse_teach_start_args("--mode=kinesthetic --device=robot_arm") + assert result["mode"] == "kinesthetic" + assert result["device"] == "robot_arm" + + def test_mode_with_goal(self) -> None: + from leapflow.cli.commands.slash_handlers import _parse_teach_start_args + + result = _parse_teach_start_args( + "--mode=teleop --leader=l --follower=f pick up cup" + ) + assert result["mode"] == "teleop" + assert result["goal"] == "pick up cup" + + def test_space_separated_args(self) -> None: + from leapflow.cli.commands.slash_handlers import _parse_teach_start_args + + result = _parse_teach_start_args("--mode kinesthetic --device arm") + assert result["mode"] == "kinesthetic" + assert result["device"] == "arm" diff --git a/tests/test_robot_arm_transport.py b/tests/test_robot_arm_transport.py new file mode 100644 index 00000000..49ceebed --- /dev/null +++ b/tests/test_robot_arm_transport.py @@ -0,0 +1,612 @@ +# Adapted from LeRobot (https://github.com/huggingface/lerobot). +"""Robot arm transport conformance and integration tests. + +Verifies that RobotArmTransport satisfies all three protocols (HardwareTransport, +BatchTransport, FrameTransport), that the channel mapper produces correct +declarations, and that the context provider discovers robots from configuration. +""" + +from __future__ import annotations + +from typing import Any + +import numpy as np +import pytest + +from leapflow.hardware.context import ( + Channel, + ContextProvenance, + ContextSource, + Direction, + Envelope, + HardwareContext, + HardwareEffect, + PrivacyTier, + Representation, + TransportRef, +) +from leapflow.hardware.transport import ( + BatchTransport, + FrameTransport, + HardwareTransport, + Reading, + SIDE_EFFECT_COMMITTED, + SIDE_EFFECT_UNKNOWN, + TransportError, + TransportStatus, +) +from leapflow.hardware.transports.robot_arm import RobotArmTransport +from leapflow.hardware.transports.robot_channels import ( + RobotChannelMapper, + camera_channel, + gripper_channel, + infer_channel_type, + motor_channel_pair, +) +from leapflow.hardware.providers.robot_arm import RobotArmContextProvider + + +# ════════════════════════════════════════════════════════════════ +# Mock robot — no external SDK import +# ════════════════════════════════════════════════════════════════ + + +class MockRobotArm: + """Simulates a robot arm for testing without hardware. + + Features: + - 3 motors (shoulder, elbow, wrist) with position/velocity + - 1 camera (top) producing 640x480 images + - 1 gripper (binary) + - Tracks connect/disconnect state + - Records all sent actions + """ + + def __init__(self) -> None: + self.is_connected = False + self.is_calibrated = True + self._positions = {"shoulder": 0.0, "elbow": 0.5, "wrist": -0.3} + self._velocities = {"shoulder": 0.0, "elbow": 0.0, "wrist": 0.0} + self._gripper = 0.0 + self._actions_sent: list[Any] = [] + self._fail_next_action = False + + @property + def observation_features(self) -> dict[str, Any]: + """Robot-style observation feature dict.""" + return { + "shoulder.position": (1,), + "elbow.position": (1,), + "wrist.position": (1,), + "top": (480, 640, 3), + "grip": (1,), + } + + @property + def action_features(self) -> dict[str, Any]: + """Robot-style action feature dict.""" + return { + "shoulder.position": (1,), + "elbow.position": (1,), + "wrist.position": (1,), + "grip": (1,), + } + + def connect(self, calibrate: bool = True) -> None: + self.is_connected = True + + def disconnect(self) -> None: + self.is_connected = False + + def get_observation(self) -> dict[str, Any]: + """Return current state as a dict matching robot observation format.""" + return { + "shoulder.position": self._positions["shoulder"], + "elbow.position": self._positions["elbow"], + "wrist.position": self._positions["wrist"], + "shoulder.velocity": self._velocities["shoulder"], + "elbow.velocity": self._velocities["elbow"], + "wrist.velocity": self._velocities["wrist"], + "top": np.random.randint(0, 255, (480, 640, 3), dtype=np.uint8), + "grip": self._gripper, + } + + def send_action(self, action: Any) -> None: + """Record action and update positions.""" + if self._fail_next_action: + self._fail_next_action = False + raise RuntimeError("simulated hardware failure") + self._actions_sent.append(action) + if isinstance(action, (list, tuple)) and len(action) >= 3: + for i, key in enumerate(self._positions): + if i < len(action): + self._positions[key] = float(action[i]) + + +# ════════════════════════════════════════════════════════════════ +# Helpers +# ════════════════════════════════════════════════════════════════ + + +def _mock_context(channels: tuple[Channel, ...]) -> HardwareContext: + """Build a minimal HardwareContext for transport tests.""" + return HardwareContext( + device_id="robot.test_arm", + display_name="Test Arm", + transport=TransportRef(kind="robot_arm", config={}), + channels=channels, + halt_supported=True, + provenance=ContextProvenance(source=ContextSource.IMPORTED.value), + ) + + +# ════════════════════════════════════════════════════════════════ +# 1. Protocol conformance +# ════════════════════════════════════════════════════════════════ + + +class TestProtocolConformance: + """Verify RobotArmTransport satisfies all three protocols.""" + + def test_satisfies_hardware_transport(self): + """isinstance check against HardwareTransport.""" + t = RobotArmTransport(robot_factory=lambda: None) + assert isinstance(t, HardwareTransport) + + def test_satisfies_batch_transport(self): + """isinstance check against BatchTransport.""" + t = RobotArmTransport(robot_factory=lambda: None) + assert isinstance(t, BatchTransport) + + def test_satisfies_frame_transport(self): + """isinstance check against FrameTransport.""" + t = RobotArmTransport(robot_factory=lambda: None) + assert isinstance(t, FrameTransport) + + def test_kind_attribute(self): + """Transport declares kind='robot_arm'.""" + t = RobotArmTransport(robot_factory=lambda: None) + assert t.kind == "robot_arm" + + +# ════════════════════════════════════════════════════════════════ +# 2. Channel mapper +# ════════════════════════════════════════════════════════════════ + + +class TestRobotChannelMapper: + """Channel mapper produces correct HCP channel declarations.""" + + def test_motor_channels_from_features(self): + """Motor features produce position (READWRITE/ACTUATE) + velocity (READ) channels.""" + mapper = RobotChannelMapper() + channels = mapper.map_features( + {"shoulder.position": (1,)}, + {"shoulder.position": (1,)}, + ) + ids = {ch.channel_id for ch in channels} + assert "joint.shoulder.position" in ids + assert "joint.shoulder.velocity" in ids + pos = next(ch for ch in channels if ch.channel_id == "joint.shoulder.position") + vel = next(ch for ch in channels if ch.channel_id == "joint.shoulder.velocity") + assert pos.direction == Direction.READWRITE.value + assert pos.effect == HardwareEffect.ACTUATE.value + assert vel.direction == Direction.READ.value + + def test_camera_channel_from_features(self): + """Camera features produce FRAME/READ channels with ENVIRONMENT privacy.""" + mapper = RobotChannelMapper() + channels = mapper.map_features({"top": (480, 640, 3)}, {}) + cam = next(ch for ch in channels if "camera" in ch.channel_id) + assert cam.representation == Representation.FRAME.value + assert cam.direction == Direction.READ.value + assert cam.privacy == PrivacyTier.ENVIRONMENT.value + + def test_gripper_channel(self): + """Gripper features produce STATE/ACTUATE channels.""" + mapper = RobotChannelMapper() + channels = mapper.map_features({"grip": (1,)}, {"grip": (1,)}) + grip = next(ch for ch in channels if "gripper" in ch.channel_id) + assert grip.representation == Representation.STATE.value + assert grip.effect == HardwareEffect.ACTUATE.value + assert grip.direction == Direction.READWRITE.value + + def test_map_robot_duck_typing(self): + """map_robot() works via duck typing without external SDK import.""" + robot = MockRobotArm() + mapper = RobotChannelMapper() + channels = mapper.map_robot(robot) + assert len(channels) > 0 + ids = {ch.channel_id for ch in channels} + assert any("joint" in cid for cid in ids) + assert any("camera" in cid for cid in ids) + + def test_channel_descriptions_are_llm_readable(self): + """Every channel has a non-empty human-readable description.""" + robot = MockRobotArm() + mapper = RobotChannelMapper() + for ch in mapper.map_robot(robot): + assert ch.description, f"Channel {ch.channel_id} has no description" + + def test_envelope_limits_from_motor_config(self): + """Motor limits are reflected in channel Envelope min/max.""" + mapper = RobotChannelMapper() + channels = mapper.map_features( + {"shoulder.position": (1,)}, + {"shoulder.position": (1,)}, + motor_limits={"shoulder": (-3.14, 3.14)}, + ) + pos = next(ch for ch in channels if ch.channel_id == "joint.shoulder.position") + assert pos.envelope.declared is True + assert pos.envelope.min_value == pytest.approx(-3.14) + assert pos.envelope.max_value == pytest.approx(3.14) + + +# ════════════════════════════════════════════════════════════════ +# 3. Transport core +# ════════════════════════════════════════════════════════════════ + + +class TestRobotArmTransport: + """Core six-method transport tests.""" + + @pytest.fixture + def mock_robot(self): + return MockRobotArm() + + @pytest.fixture + def transport(self, mock_robot): + """Create a transport with MockRobotArm factory.""" + return RobotArmTransport(robot_factory=lambda: mock_robot) + + @pytest.fixture + def context(self, mock_robot): + """Create a HardwareContext matching the mock robot.""" + mapper = RobotChannelMapper() + return _mock_context(mapper.map_robot(mock_robot)) + + @pytest.mark.asyncio + async def test_open_connects_robot(self, transport, context, mock_robot): + """open() calls factory and connects the robot.""" + status = await transport.open(context) + assert status.connected is True + assert mock_robot.is_connected is True + + @pytest.mark.asyncio + async def test_close_disconnects(self, transport, context, mock_robot): + """close() disconnects the robot.""" + await transport.open(context) + status = await transport.close() + assert status.connected is False + assert mock_robot.is_connected is False + + @pytest.mark.asyncio + async def test_close_idempotent(self, transport, context): + """Calling close() twice does not raise.""" + await transport.open(context) + await transport.close() + status = await transport.close() + assert status.connected is False + + @pytest.mark.asyncio + async def test_read_motor_position(self, transport, context): + """Read a motor position channel returns a Reading with correct value.""" + await transport.open(context) + reading = await transport.read("joint.shoulder.position") + assert isinstance(reading, Reading) + assert reading.value == pytest.approx(0.0) + assert reading.channel_id == "joint.shoulder.position" + await transport.close() + + @pytest.mark.asyncio + async def test_read_motor_velocity(self, transport, context): + """Read a velocity channel returns a Reading.""" + await transport.open(context) + reading = await transport.read("joint.shoulder.velocity") + assert isinstance(reading, Reading) + assert reading.value == pytest.approx(0.0) + await transport.close() + + @pytest.mark.asyncio + async def test_read_unknown_channel_raises(self, transport, context): + """Reading a nonexistent channel raises TransportError.""" + await transport.open(context) + with pytest.raises(TransportError, match="unknown channel"): + await transport.read("nonexistent.channel") + await transport.close() + + @pytest.mark.asyncio + async def test_write_motor_position(self, transport, context, mock_robot): + """Write updates position and returns WriteOutcome with ok=True.""" + await transport.open(context) + outcome = await transport.write("joint.shoulder.position", 1.5) + assert outcome.ok is True + assert outcome.side_effect_state == SIDE_EFFECT_COMMITTED + assert len(mock_robot._actions_sent) >= 1 + await transport.close() + + @pytest.mark.asyncio + async def test_write_holds_other_channels(self, transport, context, mock_robot): + """Writing one channel preserves other channels at their current positions.""" + await transport.open(context) + # Read first to populate the observation cache / last_obs. + await transport.read("joint.shoulder.position") + # Write only shoulder; others should hold current values. + await transport.write("joint.shoulder.position", 2.0) + action = mock_robot._actions_sent[-1] + # Action may be a torch tensor or plain list depending on environment. + assert len(action) >= 3 # all writable channels + + @pytest.mark.asyncio + async def test_probe_returns_connected_status(self, transport, context): + """probe() returns connected=True when transport is open.""" + await transport.open(context) + status = await transport.probe() + assert status.connected is True + assert status.halt_supported is True + await transport.close() + + @pytest.mark.asyncio + async def test_halt_sends_zero_velocity(self, transport, context, mock_robot): + """halt() commands zero velocity/position to all actuators.""" + await transport.open(context) + status = await transport.halt() + assert status.halt_supported is True + assert len(mock_robot._actions_sent) >= 1 + await transport.close() + + @pytest.mark.asyncio + async def test_halt_without_open(self, transport): + """halt() before open returns without error.""" + status = await transport.halt() + assert status.connected is False + assert status.halt_supported is True + + +# ════════════════════════════════════════════════════════════════ +# 4. Batch transport +# ════════════════════════════════════════════════════════════════ + + +class TestBatchTransport: + """Vectorized read/write tests.""" + + @pytest.fixture + def mock_robot(self): + return MockRobotArm() + + @pytest.fixture + def transport(self, mock_robot): + return RobotArmTransport(robot_factory=lambda: mock_robot) + + @pytest.fixture + def context(self, mock_robot): + mapper = RobotChannelMapper() + return _mock_context(mapper.map_robot(mock_robot)) + + @pytest.mark.asyncio + async def test_read_batch_all_channels(self, transport, context): + """read_batch returns all requested channels in one BatchReading.""" + await transport.open(context) + batch = await transport.read_batch(( + "joint.shoulder.position", + "joint.elbow.position", + )) + assert len(batch.readings) == 2 + ids = {r.channel_id for r in batch.readings} + assert ids == {"joint.shoulder.position", "joint.elbow.position"} + await transport.close() + + @pytest.mark.asyncio + async def test_read_batch_shared_timestamp(self, transport, context): + """All readings in a batch share the same observed_at.""" + await transport.open(context) + batch = await transport.read_batch(( + "joint.shoulder.position", + "joint.elbow.position", + "joint.wrist.position", + )) + timestamps = {r.observed_at for r in batch.readings} + assert len(timestamps) == 1, "All batch readings must share one timestamp" + assert batch.observed_at == next(iter(timestamps)) + await transport.close() + + @pytest.mark.asyncio + async def test_write_batch_atomic(self, transport, context, mock_robot): + """write_batch sends one send_action() call with all values.""" + await transport.open(context) + outcome = await transport.write_batch(( + ("joint.shoulder.position", 1.0), + ("joint.elbow.position", 2.0), + )) + assert outcome.ok is True + assert outcome.side_effect_state == SIDE_EFFECT_COMMITTED + assert len(mock_robot._actions_sent) == 1 # one atomic call + await transport.close() + + @pytest.mark.asyncio + async def test_write_batch_partial_failure(self, transport, context, mock_robot): + """When robot.send_action fails, outcome reports side_effect=UNKNOWN.""" + await transport.open(context) + mock_robot._fail_next_action = True + outcome = await transport.write_batch(( + ("joint.shoulder.position", 1.0), + )) + assert outcome.ok is False + assert outcome.side_effect_state == SIDE_EFFECT_UNKNOWN + await transport.close() + + +# ════════════════════════════════════════════════════════════════ +# 5. Frame transport +# ════════════════════════════════════════════════════════════════ + + +class TestFrameTransport: + """Camera frame capture tests.""" + + @pytest.fixture + def mock_robot(self): + return MockRobotArm() + + @pytest.fixture + def transport(self, mock_robot): + return RobotArmTransport(robot_factory=lambda: mock_robot) + + @pytest.fixture + def context(self, mock_robot): + mapper = RobotChannelMapper() + return _mock_context(mapper.map_robot(mock_robot)) + + @pytest.mark.asyncio + async def test_read_frame_returns_jpeg(self, transport, context): + """read_frame returns JPEG-encoded bytes.""" + await transport.open(context) + frame = await transport.read_frame("camera.top") + assert frame.media_type == "image/jpeg" + assert len(frame.data) > 0 + assert frame.data[:2] == b"\xff\xd8" # JPEG magic bytes + await transport.close() + + @pytest.mark.asyncio + async def test_read_frame_dimensions(self, transport, context): + """read_frame reports correct width and height.""" + await transport.open(context) + frame = await transport.read_frame("camera.top") + assert frame.width == 640 + assert frame.height == 480 + await transport.close() + + @pytest.mark.asyncio + async def test_read_non_camera_channel_raises(self, transport, context): + """read_frame on a non-frame channel raises TransportError.""" + await transport.open(context) + with pytest.raises(TransportError, match="not a frame channel"): + await transport.read_frame("joint.shoulder.position") + await transport.close() + + +# ════════════════════════════════════════════════════════════════ +# 6. Context provider +# ════════════════════════════════════════════════════════════════ + + +class TestRobotArmContextProvider: + """Provider discovers robots from configuration.""" + + @pytest.fixture + def config(self) -> dict[str, Any]: + return { + "robot_type": "so100_follower", + "device_name": "test_arm", + "display_name": "Test Arm", + "cameras": {"top": {"index": 0, "width": 640, "height": 480, "fps": 30}}, + "motor_limits": { + "shoulder": [-3.14, 3.14], + "elbow": [-1.57, 1.57], + "wrist": [-3.14, 3.14], + }, + "sample_rate_hz": 50.0, + } + + def test_discover_returns_context(self, config): + """Provider returns one HardwareContext from config.""" + provider = RobotArmContextProvider(config) + contexts = provider.discover() + assert len(contexts) == 1 + assert isinstance(contexts[0], HardwareContext) + + def test_device_id_format(self, config): + """device_id is 'robot.{device_name}'.""" + ctx = RobotArmContextProvider(config).discover()[0] + assert ctx.device_id == "robot.test_arm" + + def test_transport_ref_kind(self, config): + """TransportRef.kind is 'robot_arm'.""" + ctx = RobotArmContextProvider(config).discover()[0] + assert ctx.transport.kind == "robot_arm" + + def test_provenance_is_imported(self, config): + """ContextProvenance.source is IMPORTED.""" + ctx = RobotArmContextProvider(config).discover()[0] + assert ctx.provenance.source == ContextSource.IMPORTED.value + + def test_halt_supported(self, config): + """Discovered context declares halt_supported=True.""" + ctx = RobotArmContextProvider(config).discover()[0] + assert ctx.halt_supported is True + + def test_infer_device_class_arm(self): + """so100_follower maps to robot_arm.""" + cfg = {"robot_type": "so100_follower", "motor_limits": {"j": [-3.14, 3.14]}} + ctx = RobotArmContextProvider(cfg).discover()[0] + assert ctx.device_class == "robot_arm" + + def test_infer_device_class_mobile(self): + """kiwi_bot maps to mobile_robot.""" + cfg = {"robot_type": "kiwi_bot", "motor_limits": {"wheel": [-10, 10]}} + ctx = RobotArmContextProvider(cfg).discover()[0] + assert ctx.device_class == "mobile_robot" + + def test_channels_include_motors_and_cameras(self, config): + """Discovered context includes motor and camera channels.""" + ctx = RobotArmContextProvider(config).discover()[0] + ids = {ch.channel_id for ch in ctx.channels} + assert any("joint" in cid for cid in ids) + assert any("camera" in cid for cid in ids) + + +# ════════════════════════════════════════════════════════════════ +# 7. Integration +# ════════════════════════════════════════════════════════════════ + + +class TestIntegration: + """End-to-end: Provider → Registry admission → Transport → read/write.""" + + @pytest.mark.asyncio + async def test_provider_to_transport_roundtrip(self): + """Discover robot, admit to registry, open transport, read and write.""" + # 1. Provider discovers from config. + config = { + "robot_type": "so100_follower", + "device_name": "integration_arm", + "motor_limits": { + "shoulder": [-3.14, 3.14], + "elbow": [-1.57, 1.57], + }, + "cameras": {"top": {"width": 640, "height": 480, "fps": 30}}, + "sample_rate_hz": 50.0, + } + provider = RobotArmContextProvider(config) + contexts = provider.discover() + assert len(contexts) == 1 + ctx = contexts[0] + assert ctx.device_id == "robot.integration_arm" + + # 2. Build transport backed by mock robot. + mock = MockRobotArm() + transport = RobotArmTransport(robot_factory=lambda: mock) + + # 3. Open. + status = await transport.open(ctx) + assert status.connected is True + + # 4. Read a motor channel from the discovered context. + motor_channels = [ + ch for ch in ctx.channels + if "joint" in ch.channel_id and ch.channel_id.endswith(".position") + ] + assert len(motor_channels) > 0 + reading = await transport.read(motor_channels[0].channel_id) + assert reading.device_id == ctx.device_id + + # 5. Write a writable motor channel. + writable = [ch for ch in motor_channels if ch.is_writable] + assert len(writable) > 0 + outcome = await transport.write(writable[0].channel_id, 1.0) + assert outcome.ok is True + + # 6. Close. + status = await transport.close() + assert status.connected is False diff --git a/tests/test_self_awareness.py b/tests/test_self_awareness.py index 232392cd..ad420f5b 100644 --- a/tests/test_self_awareness.py +++ b/tests/test_self_awareness.py @@ -1,32 +1,67 @@ # Copyright (c) Alibaba, Inc. and its affiliates. -"""Tests for the SelfAwarenessPlugin.""" +"""Tests for the SelfAwarenessPlugin (pull-based, session-scoped redesign).""" from __future__ import annotations -import time +import asyncio from types import SimpleNamespace from typing import Any from unittest.mock import MagicMock -from leapflow.plugins.tool_plugins.self_awareness import SelfAwarenessPlugin, _DAEMON_CACHE_TTL_S +from leapflow.plugins.tool_plugins.self_awareness import SelfAwarenessPlugin -# ── Stub factories ─────────────────────────────────────────────────────────── +# ── Helpers ────────────────────────────────────────────────────────────────── -def _stub_build_info( - version: str = "0.3.0", - commit: str = "abc123", - dirty_digest: str | None = None, - started_at: float = 0.0, -) -> SimpleNamespace: - return SimpleNamespace( - version=version, - commit=commit, - dirty_digest=dirty_digest, - pid=42, - started_at=started_at or time.time(), - ) +def _run(coro: Any) -> Any: + """Drive an async handler to completion in a sync test.""" + return asyncio.run(coro) + + +def _status(**overrides: Any) -> dict[str, Any]: + """A status snapshot shaped like ``RuntimeLeapService.status()``.""" + base: dict[str, Any] = { + "build": { + "version": "1.2.3", + "commit": "deadbeef", + "dirty_digest": "", + "pid": 42, + "started_at": 0.0, + }, + "model": "gpt-4o", + "llm_context_length": 1_000_000, + "pid": 1234, + "uptime_s": 780.0, + "context_budget_snapshot": { + "total_tokens": 112_000, + "context_length": 1_000_000, + "context_posture": "converging", + "disclosure_level": "EXPANDED", + }, + "context_used": 112_000, + "context_posture": "converging", + "session_turn_count": 13, + "cache_hit_rate": 62.3, + "evolution_performance": {"action_recorder": {"p50": 12.5}}, + "pending_approvals": 0, + "active_clients": 2, + "connected_clients": 1, + "host_backend": {"backend": "cua-driver", "started": True}, + "environment_sources": {"active": ["lark_event_source"], "dropped": 0}, + } + base.update(overrides) + return base + + +def _make_plugin(status: dict[str, Any] | None = None, provider: Any = None) -> SelfAwarenessPlugin: + """Create a plugin, optionally binding a status provider.""" + p = SelfAwarenessPlugin() + if provider is not None: + p.bind_runtime(runtime_status_provider=provider) + elif status is not None: + p.bind_runtime(runtime_status_provider=lambda _sid: status) + return p def _stub_registry( @@ -36,195 +71,146 @@ def _stub_registry( ) -> MagicMock: reg = MagicMock() reg.version = version - # all_metadata returns a list of stubs with x_leapflow - metas = [] - for i in range(tool_count): - meta = SimpleNamespace( + metas = [ + SimpleNamespace( name=f"tool_{i}", x_leapflow={"category": "system" if i % 2 == 0 else "code"}, + mutates_state=(i % 2 == 1), ) - metas.append(meta) + for i in range(tool_count) + ] reg.all_metadata = metas reg.plugins = {pid: MagicMock() for pid in plugin_ids} reg.conflicts = [] - reg.tool_handlers = {f"tool_{i}": lambda: None for i in range(tool_count)} + reg.tool_handlers = {f"tool_{i}": (lambda: None) for i in range(tool_count)} return reg -def _stub_engine( - context_snapshot: dict[str, Any] | None = None, - turn_count: int = 5, - cache_hit_rate: float | None = 62.3, - model: str = "deepseek-chat", - context_length: int = 1_000_000, -) -> SimpleNamespace: - snapshot = context_snapshot or { - "total_tokens": 112_000, - "context_length": context_length, - "context_posture": "converging", - "disclosure_level": "EXPANDED", - } - summary_ns = SimpleNamespace(cache_hit_rate=cache_hit_rate) - tracker = SimpleNamespace(summary=lambda: summary_ns) - settings = SimpleNamespace(llm_model=model, llm_context_length=context_length) - return SimpleNamespace( - context_budget_snapshot=lambda: snapshot, - _session_turn_count=turn_count, - _usage_tracker=tracker, - _settings=settings, - ) - - -def _stub_daemon_status( - model: str = "deepseek-chat", - uptime_s: float = 780.0, - pending_approvals: int = 0, - context_used: int = 112_000, - llm_context_length: int = 1_000_000, -) -> dict[str, Any]: - return { - "pid": 1234, - "model": model, - "uptime_s": uptime_s, - "llm_context_length": llm_context_length, - "context_used": context_used, - "pending_approvals": pending_approvals, - "active_clients": 2, - "connected_clients": 1, - "host_backend": {"backend": "cua-driver", "started": True}, - "environment_sources": {"active": ["lark_event_source"], "dropped": 0}, - "evolution_performance": {"action_recorder": {"p50": 12.5}}, - "context_posture": "converging", - } - - -def _make_plugin( - daemon_status: dict[str, Any] | None = None, - registry: Any = None, - engine: Any = None, - build_info: Any = None, -) -> SelfAwarenessPlugin: - """Create a plugin with stub dependencies injected.""" - p = SelfAwarenessPlugin() - if build_info is not None: - p.bind_runtime(build_info=build_info) - if registry is not None: - p.bind_runtime(registry=registry) - if engine is not None: - p.bind_runtime(engine=engine) - if daemon_status is not None: - p.inject_daemon_cache(daemon_status) - return p - - -# ── Facet tests ────────────────────────────────────────────────────────────── +# ── Facet: identity ────────────────────────────────────────────────────────── class TestFacetIdentity: - def test_identity_with_all_deps(self) -> None: - p = _make_plugin( - build_info=_stub_build_info(version="1.2.3", commit="deadbeef"), - daemon_status=_stub_daemon_status(model="gpt-4o"), - ) - result = p._handle_self_describe(facet="identity") + def test_identity_from_status(self) -> None: + p = _make_plugin(status=_status(model="gpt-4o")) + result = _run(p._handle_self_describe(facet="identity")) assert result["available"] is True assert result["version"] == "1.2.3" assert result["commit"] == "deadbeef" assert result["model"] == "gpt-4o" assert "uptime" in result - def test_identity_without_build_info(self) -> None: - p = _make_plugin(daemon_status=_stub_daemon_status()) - result = p._handle_self_describe(facet="identity") + def test_identity_without_status_uses_captured_build(self) -> None: + # No provider: identity still resolves from the captured build info and + # live settings (pull), never going dark. + p = _make_plugin() + result = _run(p._handle_self_describe(facet="identity")) assert result["available"] is True - assert result["version"] == "unknown" + assert isinstance(result["version"], str) and result["version"] + assert "model" in result - def test_identity_without_daemon(self) -> None: - engine = _stub_engine(model="claude-3") - p = _make_plugin(build_info=_stub_build_info(), engine=engine) - result = p._handle_self_describe(facet="identity") - assert result["available"] is True - assert result["model"] == "claude-3" + +# ── Facet: capabilities (live global registry) ─────────────────────────────── class TestFacetCapabilities: - def test_capabilities_with_registry(self) -> None: - reg = _stub_registry(tool_count=6, plugin_ids=("a", "b", "c")) - p = _make_plugin(registry=reg) - result = p._handle_self_describe(facet="capabilities") + def test_capabilities_from_live_registry(self) -> None: + # capabilities pulls from the process-global registry, so it works with + # no injection at all — the failure mode that left it dark before. + p = _make_plugin() + result = _run(p._handle_self_describe(facet="capabilities")) assert result["available"] is True - assert result["tool_count"] == 6 - assert result["plugin_count"] == 3 + assert result["tool_count"] >= 1 + assert result["plugin_count"] >= 1 + assert "self_management" in result["plugin_ids"] + assert result["supports_plugins"] is True assert "tools_by_category" in result + assert "mutating_tool_count" in result + # Skills are part of the single self-cognition surface. + assert "skills" in result and isinstance(result["skills"], dict) - def test_capabilities_without_registry(self) -> None: - p = _make_plugin() - result = p._handle_self_describe(facet="capabilities") - assert result["available"] is False - assert "registry" in result["reason"] + def test_capabilities_includes_skill_inventory(self, tmp_path) -> None: + """The capabilities facet reports skills from the discovery subsystem, so + self_describe alone answers 'what skills do you have' — no separate call.""" + import leapflow.skills.discovery as disc + from leapflow.skills.index import SkillIndex + + saved_index, saved_reg = disc._skill_index, disc._skill_registry + try: + disc._skill_index = SkillIndex(tmp_path) + disc._skill_registry = None + result = _run(_make_plugin()._handle_self_describe(facet="capabilities")) + skills = result["skills"] + assert skills["available"] is True + assert skills["count"] >= 12 + assert "web_research" in skills["names"] + finally: + disc._skill_index, disc._skill_registry = saved_index, saved_reg + + +# ── Facet: runtime ─────────────────────────────────────────────────────────── class TestFacetRuntime: - def test_runtime_with_engine(self) -> None: - engine = _stub_engine(turn_count=13, cache_hit_rate=62.3) - p = _make_plugin(engine=engine) - result = p._handle_self_describe(facet="runtime") + def test_runtime_from_status(self) -> None: + p = _make_plugin(status=_status()) + result = _run(p._handle_self_describe(facet="runtime")) assert result["available"] is True assert result["context_used"] == 112_000 assert result["session_turn_count"] == 13 assert result["cache_hit_rate"] == "62.3%" assert result["posture"] == "converging" + assert result["disclosure_level"] == "EXPANDED" - def test_runtime_without_engine(self) -> None: + def test_runtime_without_status_degrades(self) -> None: p = _make_plugin() - result = p._handle_self_describe(facet="runtime") + result = _run(p._handle_self_describe(facet="runtime")) assert result["available"] is False - assert "engine" in result["reason"] + assert "daemon" in result["reason"] + + +# ── Facet: evolution ───────────────────────────────────────────────────────── class TestFacetEvolution: - def test_evolution_with_daemon(self) -> None: - p = _make_plugin(daemon_status=_stub_daemon_status(pending_approvals=2)) - result = p._handle_self_describe(facet="evolution") + def test_evolution_from_status(self) -> None: + p = _make_plugin(status=_status(pending_approvals=2)) + result = _run(p._handle_self_describe(facet="evolution")) assert result["available"] is True assert result["pending_approvals"] == 2 assert "performance" in result - def test_evolution_without_daemon(self) -> None: + def test_evolution_without_status_degrades(self) -> None: p = _make_plugin() - result = p._handle_self_describe(facet="evolution") + result = _run(p._handle_self_describe(facet="evolution")) assert result["available"] is False +# ── Facet: platform ────────────────────────────────────────────────────────── + + class TestFacetPlatform: - def test_platform_with_daemon(self) -> None: - p = _make_plugin(daemon_status=_stub_daemon_status()) - result = p._handle_self_describe(facet="platform") + def test_platform_from_status(self) -> None: + p = _make_plugin(status=_status()) + result = _run(p._handle_self_describe(facet="platform")) assert result["available"] is True assert result["active_clients"] == 2 assert result["host_backend"]["backend"] == "cua-driver" assert result["environment_sources"]["active"] == ["lark_event_source"] - def test_platform_without_daemon(self) -> None: + def test_platform_without_status_degrades(self) -> None: p = _make_plugin() - result = p._handle_self_describe(facet="platform") + result = _run(p._handle_self_describe(facet="platform")) assert result["available"] is False +# ── Facet: all ─────────────────────────────────────────────────────────────── + + class TestFacetAll: def test_all_merges_sub_reports(self) -> None: - p = _make_plugin( - build_info=_stub_build_info(), - registry=_stub_registry(), - engine=_stub_engine(), - daemon_status=_stub_daemon_status(), - ) - result = p._handle_self_describe(facet="all") - assert "identity" in result - assert "capabilities" in result - assert "runtime" in result - assert "evolution" in result - assert "platform" in result + p = _make_plugin(status=_status()) + result = _run(p._handle_self_describe(facet="all")) + assert set(result) == {"identity", "capabilities", "runtime", "evolution", "platform"} assert result["identity"]["available"] is True assert result["capabilities"]["available"] is True assert result["runtime"]["available"] is True @@ -233,110 +219,121 @@ def test_all_merges_sub_reports(self) -> None: class TestInvalidFacet: def test_unknown_facet_returns_error(self) -> None: p = _make_plugin() - result = p._handle_self_describe(facet="nonexistent") + result = _run(p._handle_self_describe(facet="nonexistent")) assert "error" in result +# ── Async provider + session scoping ───────────────────────────────────────── + + +class TestStatusProvider: + def test_async_provider_is_awaited(self) -> None: + async def provider(_sid: str) -> dict[str, Any]: + return _status(model="async-model") + + p = _make_plugin(provider=provider) + result = _run(p._handle_self_describe(facet="identity")) + assert result["model"] == "async-model" + + def test_provider_receives_active_session_id(self) -> None: + """The provider must be keyed by the active turn's session id so the + daemon resolves the caller's own session, never a shared one.""" + from leapflow.tools.execution_context import ( + ToolExecutionContext, + reset_tool_context, + set_tool_context, + ) + + seen: list[str] = [] + + def provider(session_id: str) -> dict[str, Any]: + seen.append(session_id) + return _status() + + p = _make_plugin(provider=provider) + token = set_tool_context( + ToolExecutionContext.from_strings(workspace_root=".", session_id="sess-123") + ) + try: + _run(p._handle_self_describe(facet="runtime")) + finally: + reset_tool_context(token) + assert seen == ["sess-123"] + + def test_provider_failure_degrades(self) -> None: + def provider(_sid: str) -> dict[str, Any]: + raise RuntimeError("status boom") + + p = _make_plugin(provider=provider) + result = _run(p._handle_self_describe(facet="platform")) + assert result["available"] is False + + # ── Registry version gate ──────────────────────────────────────────────────── class TestRegistryVersionGate: def test_cache_hit_on_same_version(self) -> None: reg = _stub_registry(tool_count=3, version=5) - p = _make_plugin(registry=reg) + p = SelfAwarenessPlugin() + p._registry_or_none = lambda: reg # type: ignore[method-assign] - # First call populates cache - r1 = p._handle_self_describe(facet="capabilities") + r1 = _run(p._handle_self_describe(facet="capabilities")) assert r1["tool_count"] == 3 - # Mutate registry data but keep same version - reg.all_metadata = [SimpleNamespace(name="x", x_leapflow={"category": "new"})] + # Mutate registry data but keep the same version → stale cache returned. + reg.all_metadata = [SimpleNamespace(name="x", x_leapflow={"category": "new"}, mutates_state=False)] reg.plugins = {"only": MagicMock()} - # Second call returns cached (stale) data - r2 = p._handle_self_describe(facet="capabilities") - assert r2["tool_count"] == 3 # Still cached - assert r2["plugin_count"] == 2 # Still cached + r2 = _run(p._handle_self_describe(facet="capabilities")) + assert r2["tool_count"] == 3 + assert r2["plugin_count"] == 2 def test_cache_invalidated_on_version_bump(self) -> None: reg = _stub_registry(tool_count=3, version=5) - p = _make_plugin(registry=reg) + p = SelfAwarenessPlugin() + p._registry_or_none = lambda: reg # type: ignore[method-assign] - r1 = p._handle_self_describe(facet="capabilities") + r1 = _run(p._handle_self_describe(facet="capabilities")) assert r1["tool_count"] == 3 - # Bump version AND change data reg.version = 6 - reg.all_metadata = [SimpleNamespace(name="x", x_leapflow={"category": "new"})] + reg.all_metadata = [SimpleNamespace(name="x", x_leapflow={"category": "new"}, mutates_state=False)] reg.plugins = {"only": MagicMock()} reg.conflicts = [] - r2 = p._handle_self_describe(facet="capabilities") - assert r2["tool_count"] == 1 # Rebuilt + r2 = _run(p._handle_self_describe(facet="capabilities")) + assert r2["tool_count"] == 1 assert r2["plugin_count"] == 1 -# ── Daemon TTL cache ───────────────────────────────────────────────────────── - - -class TestDaemonTTLCache: - def test_within_ttl_returns_cached(self) -> None: - p = _make_plugin(daemon_status=_stub_daemon_status(model="model-v1")) - # Reading daemon cache should return the injected data within TTL - cache = p._get_daemon_cache() - assert cache.get("model") == "model-v1" - - def test_expired_ttl_attempts_refresh(self) -> None: - p = _make_plugin(daemon_status=_stub_daemon_status(model="old-model")) - # Force cache timestamp to be expired - p._daemon_cache_ts = time.monotonic() - _DAEMON_CACHE_TTL_S - 10 - - # Without a real daemon client, refresh returns empty (no async loop) - # The cache is cleared because no daemon_client is bound - cache = p._get_daemon_cache() - assert isinstance(cache, dict) - - def test_inject_resets_timestamp(self) -> None: - p = _make_plugin() - p.inject_daemon_cache(_stub_daemon_status(model="fresh")) - cache = p._get_daemon_cache() - assert cache.get("model") == "fresh" - - # ── runtime_snapshot ───────────────────────────────────────────────────────── class TestRuntimeSnapshot: def test_returns_expected_keys(self) -> None: - p = _make_plugin( - daemon_status=_stub_daemon_status(), - engine=_stub_engine(), - registry=_stub_registry(), - ) - result = p._handle_runtime_snapshot() - expected_keys = { + p = _make_plugin(status=_status()) + result = _run(p._handle_runtime_snapshot()) + assert set(result.keys()) == { "model", "context", "posture", "disclosure", "turn", "cache_hit_rate", "uptime", "tools_available", "pending_approvals", } - assert set(result.keys()) == expected_keys def test_context_format(self) -> None: - p = _make_plugin( - daemon_status=_stub_daemon_status(), - engine=_stub_engine(), - ) - result = p._handle_runtime_snapshot() + p = _make_plugin(status=_status()) + result = _run(p._handle_runtime_snapshot()) assert "112K" in result["context"] assert "1M" in result["context"] assert "11%" in result["context"] - def test_snapshot_without_deps(self) -> None: + def test_snapshot_without_provider(self) -> None: p = _make_plugin() - result = p._handle_runtime_snapshot() - assert result["model"] == "" + result = _run(p._handle_runtime_snapshot()) assert result["context"] == "unknown" - assert result["tools_available"] == 0 + # tools_available pulls from the live global registry, not a bound dep. + assert result["tools_available"] >= 1 + assert result["turn"] == 0 # ── PCD metadata ───────────────────────────────────────────────────────────── @@ -344,25 +341,18 @@ def test_snapshot_without_deps(self) -> None: class TestPCDMetadata: def test_self_describe_schema(self) -> None: - p = SelfAwarenessPlugin() - tools = p.tools - describe_tool = next(t for t in tools if t.name == "self_describe") + describe_tool = next(t for t in SelfAwarenessPlugin().tools if t.name == "self_describe") assert describe_tool.x_leapflow["category"] == "system" assert describe_tool.x_leapflow["risk_level"] == "read_only" def test_runtime_snapshot_schema(self) -> None: - p = SelfAwarenessPlugin() - tools = p.tools - snapshot_tool = next(t for t in tools if t.name == "runtime_snapshot") + snapshot_tool = next(t for t in SelfAwarenessPlugin().tools if t.name == "runtime_snapshot") assert snapshot_tool.x_leapflow["category"] == "system" assert snapshot_tool.x_leapflow["risk_level"] == "read_only" def test_openai_schema_includes_x_leapflow(self) -> None: - p = SelfAwarenessPlugin() - tools = p.tools - for tool in tools: - schema = tool.to_openai_schema() - x = schema["function"].get("x_leapflow", {}) + for tool in SelfAwarenessPlugin().tools: + x = tool.to_openai_schema()["function"].get("x_leapflow", {}) assert x.get("category") == "system" assert x.get("risk_level") == "read_only" @@ -372,29 +362,19 @@ def test_openai_schema_includes_x_leapflow(self) -> None: class TestPluginProtocol: def test_plugin_id(self) -> None: - p = SelfAwarenessPlugin() - assert p.plugin_id == "self_awareness" + assert SelfAwarenessPlugin().plugin_id == "self_awareness" def test_category(self) -> None: - p = SelfAwarenessPlugin() - assert p.category == "system" + assert SelfAwarenessPlugin().category == "system" def test_dependencies(self) -> None: - p = SelfAwarenessPlugin() - assert "daemon_client" in p.dependencies - - def test_bind_runtime_engine_weakref(self) -> None: - """Engine should be stored as a weak reference.""" - p = SelfAwarenessPlugin() - engine = _stub_engine() - p.bind_runtime(engine=engine) - resolved = p._resolve_engine() - assert resolved is engine + assert "runtime_status_provider" in SelfAwarenessPlugin().dependencies - def test_bind_runtime_none_engine(self) -> None: + def test_bind_runtime_status_provider(self) -> None: p = SelfAwarenessPlugin() - p.bind_runtime(engine=None) - assert p._resolve_engine() is None + fn = lambda _sid: _status() # noqa: E731 + p.bind_runtime(runtime_status_provider=fn) + assert p._status_provider is fn def test_module_level_plugin_exists(self) -> None: from leapflow.plugins.tool_plugins.self_awareness import plugin diff --git a/tests/test_self_management.py b/tests/test_self_management.py index 0ceccc1e..c8b93339 100644 --- a/tests/test_self_management.py +++ b/tests/test_self_management.py @@ -170,22 +170,18 @@ async def test_plugin_list_subsystem_field(self, self_mgmt_plugin: Any) -> None: assert result["plugin_count"] == len(result["plugins"]) @pytest.mark.asyncio - async def test_plugin_list_includes_live_capability_report( + async def test_plugin_list_is_management_only_no_capability_report( self, self_mgmt_plugin: Any ) -> None: - """plugin_list is the live evidence source for self-capability answers.""" + """plugin_list is a plugin-management view; self-capability evidence now + lives on self_describe, so the redundant capability_report is gone.""" result = await self_mgmt_plugin._plugin_list_handler() assert result["ok"] is True - report = result["capability_report"] - assert report["source"] == "live_runtime_registry" - assert report["registry"]["tool_count"] >= len(report["plugins_supported"]["evidence_tools"]) - assert report["plugins_supported"]["supported"] is True - assert "plugin_list" in report["plugins_supported"]["evidence_tools"] - assert report["plugins_supported"]["hot_reload"] is True - assert report["plugins_supported"]["versioning"] is True - assert "approval_gate_bound" in report["runtime_dependencies"] - assert report["answering_guidance"] + assert "capability_report" not in result + # The management view still lists the live registry. + assert result["plugin_count"] == len(result["plugins"]) + assert any(p["plugin_id"] == "self_management" for p in result["plugins"]) class TestPluginStatusIntrospection: diff --git a/tests/test_skill_curator.py b/tests/test_skill_curator.py index 4dabfa5f..5371084b 100644 --- a/tests/test_skill_curator.py +++ b/tests/test_skill_curator.py @@ -14,7 +14,7 @@ SkillCurationEntry, SkillCurator, ) -from leapflow.skills.index import SkillEntry, SkillIndex +from leapflow.skills.index import SkillEntry, SkillIndex, _BUILTIN_SKILLS_DIR # ── In-memory store (implements SkillCurationStore protocol) ── @@ -374,6 +374,44 @@ def test_include_archived_overrides_filter(self, tmp_path) -> None: names = [e.name for e in result] assert "archived-skill" in names + def test_fresh_scan_discovers_all_builtin_skills(self, tmp_path) -> None: + """A fresh skills_dir must surface every bundled builtin SKILL.md.""" + builtin = sorted(p.parent.name for p in _BUILTIN_SKILLS_DIR.glob("*/SKILL.md")) + assert len(builtin) >= 1, "expected bundled builtin skills to exist" + index = SkillIndex(tmp_path) + names = {e.name for e in index.get_entries()} + assert set(builtin).issubset(names) + + def test_legacy_empty_snapshot_is_rejected_and_self_heals(self, tmp_path) -> None: + """Regression: a pre-existing bare-list '[]' snapshot must NOT mask the + bundled builtin skills — the exact defect that reported only 3 skills.""" + import json + + (tmp_path / ".skills_index.json").write_text("[]", encoding="utf-8") + index = SkillIndex(tmp_path) + names = {e.name for e in index.get_entries()} + assert "web_research" in names and "code_review" in names + # The snapshot is rewritten in the new signed format. + rewritten = json.loads((tmp_path / ".skills_index.json").read_text()) + assert isinstance(rewritten, dict) + assert rewritten.get("schema") == 2 and rewritten.get("signature") + assert len(rewritten.get("entries", [])) == len(names) + + def test_signed_snapshot_is_reused_then_busted_by_new_skill(self, tmp_path) -> None: + """A matching signature reuses L2; adding a skill changes the signature.""" + base = {e.name for e in SkillIndex(tmp_path).get_entries()} + # Second instance reuses the signed snapshot (same source signature). + assert {e.name for e in SkillIndex(tmp_path).get_entries()} == base + # Add a user skill → signature drift → rescan picks it up. + skill = tmp_path / "custom_skill" + skill.mkdir() + (skill / "SKILL.md").write_text( + "---\nname: custom_skill\ndescription: d\n---\n", encoding="utf-8" + ) + after = {e.name for e in SkillIndex(tmp_path).get_entries()} + assert "custom_skill" in after + assert base.issubset(after) + # ── DuckDB persistence tests ── diff --git a/tests/test_tool_handler_invocation.py b/tests/test_tool_handler_invocation.py index fb1ebf6b..45dc2cc2 100644 --- a/tests/test_tool_handler_invocation.py +++ b/tests/test_tool_handler_invocation.py @@ -146,7 +146,7 @@ async def test_engine_executes_plugin_list_with_empty_native_arguments() -> None ) assert result["ok"] is True - assert result["capability_report"]["registry"]["tool_count"] >= 1 + assert result["plugin_count"] >= 1 assert any(plugin["plugin_id"] == "self_management" for plugin in result["plugins"]) From 8e9f53c06ceae56867a355dbf9e943f7a916d544 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8F=AD=E6=89=AC?= Date: Thu, 8 Oct 2026 17:34:34 +0800 Subject: [PATCH 2/4] feat(leaprobot): production-level embodied intelligence stack Phase 0: HCP bridge + BatchTransport + high-speed sampling Phase 1: VLA inference + PhysicalSkillPlugin + operation verification Phase 1.5: Runtime safety/trust/degradation enforcement + interface fixes Phase 2: HighFrequencyControlBus + RT control loop (PID/Impedance/PolicyChain) Phase 3: System-1/1.5/2 control hierarchy + TaskDecomposer Phase 4: Fleet deployment management (FleetManager + MCP bridge) Phase 5: LHP IR Gateway (LLM-Hardware intermediate representation) LeapRobot core: absorbed inference-side source (zero lerobot dependency) Brand: unified LeapRobot naming, leapflow[robot] extras hc.v1: declarative hardware descriptors with kinematics/capability/safety/trust/degradation Includes: - 12 HCP hardware tools (hw_read/write/actuate/batch_actuate/halt/control_start/stop/status/orchestrate/fleet) - 3 inference strategies (VLA local/remote, position control) - Physical teach/learn extension (/teach --mode=teleop/kinesthetic) - Episode recording + Parquet/MP4 export - Physical capability resolver (four-value adjudication) - 3 PCD levels for hardware context (minimal/task_relevant/rich_context) Tests: 5000+ passed, 0 regressions --- src/leapflow/cli/commands/slash_handlers.py | 54 ++ src/leapflow/hardware/__init__.py | 77 ++ src/leapflow/hardware/capability_router.py | 144 ++- src/leapflow/hardware/control_bus.py | 852 +++++++++++++++++ src/leapflow/hardware/control_hierarchy.py | 843 ++++++++++++++++ src/leapflow/hardware/fleet.py | 844 ++++++++++++++++ src/leapflow/hardware/lhp_gateway.py | 899 ++++++++++++++++++ src/leapflow/hardware/plugin.py | 23 +- src/leapflow/hardware/realtime.py | 773 +++++++++++++++ src/leapflow/hardware/registry.py | 199 ++++ src/leapflow/hardware/stream.py | 222 ++++- src/leapflow/hardware/tools.py | 343 ++++++- src/leapflow/hardware/trust.py | 137 ++- .../plugins/tool_plugins/physical_skill.py | 130 ++- src/leapflow/robot/base.py | 90 +- src/leapflow/robot/drivers/feetech.py | 49 + src/leapflow/robot/teleop.py | 152 ++- ...sette-model-201dfd2f16183356.cassette.json | 87 ++ ...sette-model-287f6667ad601f31.cassette.json | 99 ++ ...sette-model-33a143a132a46955.cassette.json | 83 ++ ...sette-model-443b72e43e2f10a0.cassette.json | 79 ++ ...sette-model-6ad26da28a760244.cassette.json | 95 ++ ...sette-model-8743f8121b93d7cd.cassette.json | 91 ++ ...sette-model-8a3bb4f6cbc14b09.cassette.json | 87 ++ ...sette-model-8efd290c8b71b947.cassette.json | 67 ++ ...sette-model-8f49d5320835bb67.cassette.json | 87 ++ ...sette-model-cc58cb0047244391.cassette.json | 99 ++ ...sette-model-d4c470c1a70774ce.cassette.json | 87 ++ ...sette-model-e823daf134b9698b.cassette.json | 99 ++ tests/test_hardware_governance.py | 5 +- tests/test_hardware_safety_policy.py | 452 +++++++++ tests/test_realtime_control.py | 559 +++++++++++ 32 files changed, 7876 insertions(+), 31 deletions(-) create mode 100644 src/leapflow/hardware/control_bus.py create mode 100644 src/leapflow/hardware/control_hierarchy.py create mode 100644 src/leapflow/hardware/fleet.py create mode 100644 src/leapflow/hardware/lhp_gateway.py create mode 100644 src/leapflow/hardware/realtime.py create mode 100644 tests/_fixtures/cassettes/r8_hardware/cassette-model-201dfd2f16183356.cassette.json create mode 100644 tests/_fixtures/cassettes/r8_hardware/cassette-model-287f6667ad601f31.cassette.json create mode 100644 tests/_fixtures/cassettes/r8_hardware/cassette-model-33a143a132a46955.cassette.json create mode 100644 tests/_fixtures/cassettes/r8_hardware/cassette-model-443b72e43e2f10a0.cassette.json create mode 100644 tests/_fixtures/cassettes/r8_hardware/cassette-model-6ad26da28a760244.cassette.json create mode 100644 tests/_fixtures/cassettes/r8_hardware/cassette-model-8743f8121b93d7cd.cassette.json create mode 100644 tests/_fixtures/cassettes/r8_hardware/cassette-model-8a3bb4f6cbc14b09.cassette.json create mode 100644 tests/_fixtures/cassettes/r8_hardware/cassette-model-8efd290c8b71b947.cassette.json create mode 100644 tests/_fixtures/cassettes/r8_hardware/cassette-model-8f49d5320835bb67.cassette.json create mode 100644 tests/_fixtures/cassettes/r8_hardware/cassette-model-cc58cb0047244391.cassette.json create mode 100644 tests/_fixtures/cassettes/r8_hardware/cassette-model-d4c470c1a70774ce.cassette.json create mode 100644 tests/_fixtures/cassettes/r8_hardware/cassette-model-e823daf134b9698b.cassette.json create mode 100644 tests/test_hardware_safety_policy.py create mode 100644 tests/test_realtime_control.py diff --git a/src/leapflow/cli/commands/slash_handlers.py b/src/leapflow/cli/commands/slash_handlers.py index fcc5b1dc..9bf0c36e 100644 --- a/src/leapflow/cli/commands/slash_handlers.py +++ b/src/leapflow/cli/commands/slash_handlers.py @@ -3381,11 +3381,36 @@ async def _start_physical_teach( "ok": False, "message": "Teleop mode requires --leader= --follower=.", } + + # Build HardwareTools so the bridge writes through the governed path + # (SafetyPolicy, envelope, approval, trust) rather than direct transport. + hardware_tools = None + try: + from leapflow.hardware.tools import HardwareTools + + hardware_tools = HardwareTools( + registry, + gate=getattr(ctx, "hardware_approval_gate", None) + or getattr(ctx, "_hardware_approval_gate", None), + session_id=getattr(ctx, "session_id", "") or "", + hardware_trust_gate=getattr(ctx, "hardware_trust_gate", None) + or getattr(ctx, "_hardware_trust_gate", None), + ) + except Exception as exc: # noqa: BLE001 - degrade to direct writes + import logging as _log + + _log.getLogger(__name__).warning( + "Could not build HardwareTools for teleop; writes will bypass " + "SafetyPolicy and approval gates: %s", + exc, + ) + bridge = LeaderFollowerBridge( registry, leader_device_id=leader, follower_device_id=follower, goal=goal, + hardware_tools=hardware_tools, ) else: # kinesthetic device = parsed.get("device", "") @@ -3419,16 +3444,45 @@ async def _stop_physical_teach(ctx: "Context") -> dict[str, Any] | None: trajectory = await bridge.stop() remove_physical_session(session_key) + # Optional episode export: when a robot.episode_export_dir is configured, + # persist the trajectory as a training episode so offline learning + # pipelines can consume it. + export_info: dict[str, Any] | None = None + try: + settings = getattr(ctx, "settings", None) + export_dir = ( + getattr(settings, "get", lambda *a: None)("robot.episode_export_dir") + if settings is not None + else None + ) + if export_dir: + from leapflow.robot.episode_export import EpisodeExporter + + exporter = EpisodeExporter(export_dir) + goal = getattr(trajectory, "goal", "") or "" + export_info = await exporter.export_trajectory(trajectory, task=goal) + except Exception as exc: # noqa: BLE001 - export failure must not block teach stop + import logging + + logging.getLogger(__name__).warning( + "Episode export failed (non-fatal): %s", exc, exc_info=True, + ) + msg = ( f"Physical recording stopped — {trajectory.sample_count} samples, " f"{trajectory.duration_s:.1f}s" ) + if export_info: + msg += f"\nEpisode exported: {export_info.get('format', '?')} " + msg += f"({export_info.get('step_count', 0)} steps)" + return { "ok": True, "message": msg, "sample_count": trajectory.sample_count, "duration": trajectory.duration_s, "trajectory_id": trajectory.trajectory_id, + "export": export_info, "session_mode": "idle", } diff --git a/src/leapflow/hardware/__init__.py b/src/leapflow/hardware/__init__.py index 78962bfe..d1da8160 100644 --- a/src/leapflow/hardware/__init__.py +++ b/src/leapflow/hardware/__init__.py @@ -73,6 +73,47 @@ VerificationMode, collect_evidence, ) +from leapflow.hardware.control_bus import ( + ControlBusConfig, + ControlBusStats, + ControlCommand, + ControlPolicy, + ControlState, + HighFrequencyControlBus, +) +from leapflow.hardware.realtime import ( + ControlTelemetryEvent, + ImpedanceController, + PIDJointController, + PolicyChain, + RealtimeControlLoop, + TrajectoryTracker, +) +from leapflow.hardware.fleet import ( + FleetError, + FleetManager, + FleetNode, + FleetSessionRouter, + FleetTopology, + build_fleet_tools, +) +from leapflow.hardware.control_hierarchy import ( + ControlHierarchy, + ControlLayer, + EscalationEvent, + LayerState, + SubtaskGoal, + TaskDecomposer, +) +from leapflow.hardware.lhp_gateway import ( + DeviceSnapshot, + HardwareContextDelta, + HardwareContextSnapshot, + LHPGateway, + LLMPlanSegment, + PCDLevel, + PlanStep, +) __all__ = [ "HC_VERSION", @@ -85,6 +126,12 @@ "Channel", "ContextProvenance", "ContextSource", + # control_bus.py + "ControlBusConfig", + "ControlBusStats", + "ControlCommand", + "ControlPolicy", + "ControlState", "Direction", "Envelope", "EvidenceBundle", @@ -97,6 +144,7 @@ "HardwareRegistry", "HardwareSettings", "HardwareTransport", + "HighFrequencyControlBus", "Interlock", "LoadReport", "OperationVerdict", @@ -114,4 +162,33 @@ "VerificationMode", "WriteOutcome", "collect_evidence", + # realtime.py + "ControlTelemetryEvent", + "ImpedanceController", + "PIDJointController", + "PolicyChain", + "RealtimeControlLoop", + "TrajectoryTracker", + # control_hierarchy.py + "ControlHierarchy", + "ControlLayer", + "EscalationEvent", + "LayerState", + "SubtaskGoal", + "TaskDecomposer", + # lhp_gateway.py + "DeviceSnapshot", + "HardwareContextDelta", + "HardwareContextSnapshot", + "LHPGateway", + "LLMPlanSegment", + "PCDLevel", + "PlanStep", + # fleet.py + "FleetError", + "FleetManager", + "FleetNode", + "FleetSessionRouter", + "FleetTopology", + "build_fleet_tools", ] diff --git a/src/leapflow/hardware/capability_router.py b/src/leapflow/hardware/capability_router.py index 6596904e..59bf2608 100644 --- a/src/leapflow/hardware/capability_router.py +++ b/src/leapflow/hardware/capability_router.py @@ -534,6 +534,9 @@ class CapabilityRouterPlugin: Tool generation is lazy: tools are generated on first access to ``self.tools`` and regenerated when the registry changes. + + Also exposes ``hw_orchestrate``, which delegates coordinated + multi-device operations to :class:`MultiDeviceOrchestrator`. """ plugin_id = "capability_router" @@ -545,6 +548,7 @@ def __init__(self) -> None: self._gate: Any = None self._skill_plugin: Any = None self._tools: list[ToolMetadata] | None = None + self._orchestrator: Any = None def bind_runtime(self, **deps: Any) -> None: """Receive the registry, the approval gate, and an optional skill plugin. @@ -569,10 +573,145 @@ def bind_runtime(self, **deps: Any) -> None: changed = True if changed: self._tools = None + self._orchestrator = None + + # -- Orchestrator tool handler ------------------------------------------ + + def _get_orchestrator(self) -> Any: + """Lazily construct a :class:`MultiDeviceOrchestrator`.""" + if self._orchestrator is None and self._registry is not None: + from leapflow.robot.orchestrator import MultiDeviceOrchestrator + from leapflow.hardware.tools import HardwareTools + + hw_tools = HardwareTools(self._registry, gate=self._gate) + index = CapabilityIndex(self._registry) + self._orchestrator = MultiDeviceOrchestrator( + self._registry, + hardware_tools=hw_tools, + capability_index=index, + ) + return self._orchestrator + + async def _hw_orchestrate_handler(self, params: dict) -> dict: + """Execute coordinated operations across multiple devices.""" + from leapflow.robot.orchestrator import ( + DeviceOperation, + OrchestrationStep, + ) + + orchestrator = self._get_orchestrator() + if orchestrator is None: + return {"ok": False, "error": "Hardware subsystem not available."} + + mode = str(params.get("mode", "sequential")) + raw_ops = params.get("operations") or [] + if not raw_ops: + return {"ok": False, "error": "No operations provided."} + + operations: list[DeviceOperation] = [] + for idx, raw_op in enumerate(raw_ops): + device_id = str(raw_op.get("device_id", "")) + if not device_id: + return { + "ok": False, + "error": f"Operation [{idx}]: device_id is required.", + } + raw_cmds = raw_op.get("commands") or [] + commands = tuple( + (str(c.get("channel_id", "")), c.get("value")) + for c in raw_cmds + ) + operations.append(DeviceOperation( + device_id=device_id, + commands=commands, + verify=bool(raw_op.get("verify", False)), + )) + + step = OrchestrationStep( + mode=mode, + operations=tuple(operations), + barrier_timeout_s=float(params.get("barrier_timeout_s", 10.0)), + label=str(params.get("label", "")), + ) + result = await orchestrator.execute_step(step) + return { + "ok": result.ok, + "halted": result.halted, + "halt_reason": result.halt_reason, + "elapsed_s": round(result.elapsed_s, 3), + "step_results": list(result.step_results), + } + + def _orchestrate_tool(self) -> ToolMetadata: + """Build the ``hw_orchestrate`` ToolMetadata.""" + return ToolMetadata( + name="hw_orchestrate", + description=( + "Execute coordinated operations across multiple hardware devices. " + "Supports sequential (one-at-a-time), parallel (concurrent), and " + "barrier (synchronised) execution modes. Any failure in a parallel " + "group triggers halt_all on every participating device for safety." + ), + parameters_schema={ + "type": "object", + "properties": { + "mode": { + "type": "string", + "enum": ["sequential", "parallel", "barrier"], + "description": "Execution coordination mode.", + }, + "operations": { + "type": "array", + "items": { + "type": "object", + "properties": { + "device_id": {"type": "string"}, + "commands": { + "type": "array", + "items": { + "type": "object", + "properties": { + "channel_id": {"type": "string"}, + "value": {"type": "number"}, + }, + "required": ["channel_id", "value"], + }, + }, + "verify": {"type": "boolean"}, + }, + "required": ["device_id", "commands"], + }, + "description": "List of per-device operations to coordinate.", + }, + "barrier_timeout_s": { + "type": "number", + "description": "Timeout for barrier mode (default 10s).", + }, + "label": { + "type": "string", + "description": "Optional human-readable label for the step.", + }, + }, + "required": ["mode", "operations"], + }, + handler=self._hw_orchestrate_handler, + x_leapflow={ + "category": "hardware", + "risk_level": "high", + "requires_approval": True, + "effect_scope": "external", + "idempotency_scope": "session", + "mutates_state": True, + "schema_cost": "medium", + }, + mutates_state=True, + execution_policy="serial", + provides_capabilities=("hw.orchestrate",), + ) @property def tools(self) -> list[ToolMetadata]: - """Return generated affordance tools, empty if no device has capabilities.""" + """Return generated affordance tools plus hw_orchestrate.""" if self._registry is None: return [] if self._tools is None: @@ -585,6 +724,9 @@ def tools(self) -> list[ToolMetadata]: ) generator = CapabilityToolGenerator(index, executor) self._tools = generator.generate() + # Always include the multi-device orchestration tool when + # the hardware subsystem is active. + self._tools.append(self._orchestrate_tool()) return list(self._tools) diff --git a/src/leapflow/hardware/control_bus.py b/src/leapflow/hardware/control_bus.py new file mode 100644 index 00000000..bb8606e1 --- /dev/null +++ b/src/leapflow/hardware/control_bus.py @@ -0,0 +1,852 @@ +# Copyright (c) Alibaba, Inc. and its affiliates. +"""High-frequency control bus: deterministic real-time control independent of asyncio. + +The EventBus and HardwareStreamSource run inside the asyncio event loop, which +shares its thread with LLM inference, UI rendering, and RPC handling. A 10ms +LLM token callback blocks the entire loop, making sub-20ms control cycles +unreliable. + +This module provides a dedicated-thread control bus that: + +1. Runs in its own OS thread with elevated priority (SCHED_FIFO on Linux) +2. Uses monotonic clock + busy-wait hybrid for sub-ms cycle accuracy +3. Executes a ControlPolicy each cycle: read sensors → compute → write actuators +4. Checks SafetyPolicy every cycle — violation triggers immediate halt +5. Monitors DegradationPolicy — comm loss triggers configured fallback +6. Provides an asyncio bridge for the Agent layer to start/stop/monitor + +The bus does NOT replace EventBus or StreamSource — it coexists: + +- EventBus: interaction signals (UI events, approvals, findings) +- StreamSource: observation telemetry (readings → events → dashboard) +- ControlBus: closed-loop servo control (read → compute → write at fixed Hz) +""" + +from __future__ import annotations + +import asyncio +import ctypes +import logging +import os +import platform +import threading +import time +from dataclasses import dataclass, field +from typing import Any, Mapping, Protocol, runtime_checkable + +logger = logging.getLogger(__name__) + + +# --------------------------------------------------------------------------- +# Core types +# --------------------------------------------------------------------------- + + +@runtime_checkable +class ControlPolicy(Protocol): + """Computes actuator commands from sensor readings each control cycle. + + Implementations range from simple PID to VLA neural network inference. + Must complete within the cycle budget (1/frequency seconds) — the bus + will log and optionally halt if the policy consistently overruns. + """ + + @property + def policy_id(self) -> str: + """Stable identifier for this policy instance.""" + ... + + def compute(self, state: "ControlState") -> "ControlCommand": + """Synchronous computation — called from the RT thread. + + For neural network policies that need GPU, run inference in a + separate thread/process and return the latest cached result here. + """ + ... + + def reset(self) -> None: + """Reset internal state (e.g. PID integrators) to initial values.""" + ... + + +@dataclass(frozen=True) +class ControlState: + """Sensor readings for one control cycle.""" + + timestamp: float + """Monotonic clock instant at which the readings were captured.""" + + joint_positions: Mapping[str, float] + """channel_id → position value for every readable joint channel.""" + + joint_velocities: Mapping[str, float] + """channel_id → velocity value (may be empty if not declared).""" + + sensor_readings: Mapping[str, Any] + """channel_id → arbitrary sensor value for non-joint channels.""" + + cycle_number: int + """Zero-based index of this cycle since the bus was started.""" + + cycle_dt_s: float + """Actual elapsed time since the previous cycle's read, in seconds.""" + + +@dataclass(frozen=True) +class ControlCommand: + """Actuator commands for one control cycle.""" + + joint_commands: Mapping[str, float] + """channel_id → commanded value for every writable joint channel.""" + + gripper_command: float | None = None + """Optional gripper setpoint (None = no change).""" + + halt: bool = False + """When True the policy requests an emergency stop.""" + + +@dataclass(frozen=True) +class ControlBusConfig: + """Configuration for the high-frequency control bus.""" + + frequency_hz: float = 100.0 + """Target control frequency in Hz.""" + + max_overrun_ratio: float = 1.5 + """Halt if a single cycle takes longer than ratio * period.""" + + consecutive_overrun_limit: int = 5 + """Halt after N consecutive overruns.""" + + use_busy_wait: bool = True + """Busy-wait the last millisecond for sub-ms accuracy (higher CPU).""" + + priority: str = "realtime" + """Thread scheduling class: ``realtime`` (SCHED_FIFO), ``high``, ``normal``.""" + + safety_check_interval: int = 1 + """Check SafetyPolicy every N cycles (1 = every cycle).""" + + comm_loss_timeout_s: float = 0.5 + """Seconds of consecutive read failures before triggering comm-loss halt.""" + + +@dataclass +class ControlBusStats: + """Runtime statistics for the control bus (mutable — updated each cycle).""" + + cycles: int = 0 + overruns: int = 0 + consecutive_overruns: int = 0 + max_jitter_ms: float = 0.0 + mean_jitter_ms: float = 0.0 + mean_cycle_ms: float = 0.0 + safety_violations: int = 0 + degradation_halts: int = 0 + policy_errors: int = 0 + started_at: float = 0.0 + stopped_at: float = 0.0 + + # Internal accumulators — not part of the public dict. + _jitter_sum: float = field(default=0.0, repr=False) + _cycle_sum: float = field(default=0.0, repr=False) + + def to_dict(self) -> dict[str, Any]: + """Return the public subset as a plain dict.""" + return { + "cycles": self.cycles, + "overruns": self.overruns, + "consecutive_overruns": self.consecutive_overruns, + "max_jitter_ms": round(self.max_jitter_ms, 4), + "mean_jitter_ms": round(self.mean_jitter_ms, 4), + "mean_cycle_ms": round(self.mean_cycle_ms, 4), + "safety_violations": self.safety_violations, + "degradation_halts": self.degradation_halts, + "policy_errors": self.policy_errors, + "started_at": self.started_at, + "stopped_at": self.stopped_at, + } + + +# --------------------------------------------------------------------------- +# HighFrequencyControlBus +# --------------------------------------------------------------------------- + + +class HighFrequencyControlBus: + """Dedicated-thread control bus for real-time servo control. + + Lifecycle:: + + bus = HighFrequencyControlBus(registry) + bus.start(device_id, policy) # spawns RT thread + ... # bus.latest_state is updated each cycle + bus.stop() # graceful halt + join + bus.emergency_stop() # immediate halt, no waiting + + Thread model: + + - **Control thread**: reads sensors, calls ``policy.compute()``, writes + actuators — all synchronous from the thread's own event loop. + - **Asyncio bridge**: ``async_start`` / ``async_stop`` wrap the synchronous + methods via ``asyncio.to_thread`` so the agent layer can drive the bus + from its own event loop. + - **State sharing**: ``latest_state`` is updated via simple attribute + assignment, which is atomic under CPython's GIL. + """ + + def __init__( + self, + registry: Any, + *, + config: ControlBusConfig | None = None, + safety_checker: Any = None, + ) -> None: + self._registry = registry + self._config = config or ControlBusConfig() + self._safety_checker = safety_checker + + self._thread: threading.Thread | None = None + self._running = threading.Event() + self._stop_requested = threading.Event() + + self._device_id: str = "" + self._policy: ControlPolicy | None = None + self._latest_state: ControlState | None = None # GIL-atomic + self._stats = ControlBusStats() + + # Pre-resolved channel lists — populated in start(). + self._read_channel_ids: tuple[str, ...] = () + self._write_channel_ids: tuple[str, ...] = () + + # -- Public interface -------------------------------------------------- + + def start(self, device_id: str, policy: ControlPolicy) -> None: + """Start the control loop in a dedicated thread. + + Synchronous — can be called from async via ``asyncio.to_thread`` + or directly from synchronous code. + + Raises ``RuntimeError`` if the bus is already running. + """ + if self._running.is_set(): + raise RuntimeError( + "HighFrequencyControlBus is already running; " + "call stop() before starting again" + ) + self._device_id = device_id + self._policy = policy + self._latest_state = None + self._stats = ControlBusStats(started_at=time.monotonic()) + self._stop_requested.clear() + + # Resolve channel topology from the registry before spawning the + # thread so a misconfigured device fails loudly here. + context = self._registry.context(device_id) + if context is None: + raise ValueError(f"unknown device {device_id!r}") + self._read_channel_ids = tuple( + ch.channel_id for ch in context.channels if ch.is_readable + ) + self._write_channel_ids = tuple( + ch.channel_id for ch in context.channels if ch.is_writable + ) + if not self._read_channel_ids: + raise ValueError( + f"device {device_id!r} has no readable channels for control" + ) + + self._thread = threading.Thread( + target=self._control_thread_main, + name=f"control-bus-{device_id}", + daemon=True, + ) + self._thread.start() + # Wait until the thread signals that it is running (transport opened). + if not self._running.wait(timeout=10.0): + raise RuntimeError( + f"control thread for {device_id!r} did not start within 10s" + ) + + def stop(self, timeout_s: float = 2.0) -> None: + """Request graceful stop: finishes current cycle, halts device, joins thread.""" + if not self._running.is_set(): + return + self._stop_requested.set() + thread = self._thread + if thread is not None: + thread.join(timeout=timeout_s) + if thread.is_alive(): + logger.warning( + "Control thread for %s did not join within %.1fs", + self._device_id, + timeout_s, + ) + self._thread = None + + def emergency_stop(self) -> None: + """Immediate halt: sets stop flag, calls transport.halt() from calling thread. + + Does not wait for the control thread to finish — the halt path is + lock-free by design (see ``HardwareTransport.halt`` docstring). + """ + self._stop_requested.set() + if not self._device_id: + return + try: + # Use the lock-free synchronous accessor — the transport was + # opened before the control thread started, so it is guaranteed + # to be present while the bus is alive. A short-lived event + # loop is still needed for the async ``halt()`` call, but that + # call does not touch the registry’s ``asyncio.Lock``. + transport = self._registry.get_open_transport(self._device_id) + loop = asyncio.new_event_loop() + try: + loop.run_until_complete(transport.halt()) + finally: + loop.close() + except Exception as exc: # noqa: BLE001 — emergency path must not raise + logger.error( + "Emergency halt failed for %s: %s", + self._device_id, + exc, + exc_info=True, + ) + + @property + def is_running(self) -> bool: + """Return whether the control thread is active.""" + return self._running.is_set() + + @property + def latest_state(self) -> ControlState | None: + """Latest sensor state from the control thread. Lock-free (GIL atomic).""" + return self._latest_state + + @property + def stats(self) -> ControlBusStats: + """Runtime statistics snapshot.""" + return self._stats + + # -- Asyncio bridge ---------------------------------------------------- + + async def async_start(self, device_id: str, policy: ControlPolicy) -> None: + """Async wrapper: ensure transport is open, then start the control loop. + + The transport must be opened in the main event loop *before* the RT + thread starts, because ``registry.transport()`` uses an + ``asyncio.Lock`` that cannot be shared across event loops. The RT + thread then retrieves the already-open transport via the lock-free + ``get_open_transport()``. + """ + # Pre-open the transport in the caller's (main) event loop. + await self._registry.transport(device_id) + await asyncio.to_thread(self.start, device_id, policy) + + async def async_stop(self) -> None: + """Async wrapper: stop the control loop.""" + await asyncio.to_thread(self.stop) + + # -- Control thread internals ------------------------------------------ + + def _control_thread_main(self) -> None: + """Main function of the dedicated control thread. + + 1. Set thread priority (SCHED_FIFO on Linux, high priority on macOS) + 2. Open transport via a thread-local event loop + 3. Loop at target frequency: + a. Read sensors (transport.read_batch — synchronous wrapper) + b. Build ControlState + c. Call policy.compute(state) → ControlCommand + d. Safety check (every N cycles per config) + e. Write actuators (transport.write_batch — synchronous wrapper) + f. Update stats (jitter, overrun count) + g. Wait for next cycle (busy-wait or sleep hybrid) + 4. On stop: halt device, close transport + """ + self._set_thread_priority() + loop = asyncio.new_event_loop() + config = self._config + period_s = 1.0 / config.frequency_hz + stats = self._stats + device_id = self._device_id + policy = self._policy + assert policy is not None # ensured by start() + + try: + # Retrieve the already-opened transport. The transport must have + # been opened in the main event loop (via ``async_start`` or an + # explicit ``await registry.transport(device_id)``) before the RT + # thread starts. Using ``get_open_transport()`` avoids crossing + # event loop boundaries with the registry's ``asyncio.Lock``. + transport = self._registry.get_open_transport(device_id) + except Exception as exc: # noqa: BLE001 + logger.error( + "Control bus failed to obtain transport for %s: %s", + device_id, + exc, + exc_info=True, + ) + return + finally: + # Signal the starting thread regardless of success/failure. + self._running.set() + + consecutive_read_failures = 0 + comm_loss_start: float | None = None + prev_read_mono: float = time.monotonic() + next_cycle_at = time.monotonic() + + try: + while not self._stop_requested.is_set(): + cycle_start = time.monotonic() + + # (a) Read sensors ---------------------------------------- + try: + batch = self._sync_read_batch(loop, transport) + consecutive_read_failures = 0 + comm_loss_start = None + except Exception as exc: # noqa: BLE001 + consecutive_read_failures += 1 + if consecutive_read_failures == 1: + logger.warning( + "Control bus read failed on %s: %s", + device_id, + exc, + ) + comm_loss_start = time.monotonic() + # DegradationPolicy: comm-loss check. + if ( + comm_loss_start is not None + and (time.monotonic() - comm_loss_start) + > config.comm_loss_timeout_s + ): + logger.error( + "Control bus comm loss exceeded %.2fs on %s — halting", + config.comm_loss_timeout_s, + device_id, + ) + stats.degradation_halts += 1 + self._sync_halt(loop, transport) + break + # Back-off briefly and retry. + self._precise_wait( + time.monotonic() + min(period_s, 0.01) + ) + next_cycle_at = time.monotonic() + period_s + continue + + # (b) Build ControlState ---------------------------------- + now = time.monotonic() + dt = now - prev_read_mono + prev_read_mono = now + + positions: dict[str, float] = {} + velocities: dict[str, float] = {} + sensors: dict[str, Any] = {} + for reading in batch.readings: + ch = reading.channel_id + val = reading.value + qty = reading.quantity + if qty in ("position", "angle", "joint_position"): + positions[ch] = float(val) if val is not None else 0.0 + elif qty in ("velocity", "angular_velocity", "joint_velocity"): + velocities[ch] = float(val) if val is not None else 0.0 + else: + sensors[ch] = val + + state = ControlState( + timestamp=now, + joint_positions=positions, + joint_velocities=velocities, + sensor_readings=sensors, + cycle_number=stats.cycles, + cycle_dt_s=dt, + ) + # GIL-atomic publish. + self._latest_state = state + + # (c) Compute command ------------------------------------- + try: + command = policy.compute(state) + except Exception as exc: # noqa: BLE001 + stats.policy_errors += 1 + logger.warning( + "Control policy %s error on cycle %d: %s", + policy.policy_id, + stats.cycles, + exc, + ) + # Skip write but keep the loop alive — a transient + # policy failure should not halt the device. + self._update_cycle_stats(stats, cycle_start, period_s) + next_cycle_at += period_s + self._precise_wait(next_cycle_at) + continue + + # Policy-requested halt. + if command.halt: + logger.warning( + "Control policy %s requested halt on cycle %d", + policy.policy_id, + stats.cycles, + ) + self._sync_halt(loop, transport) + break + + # (d) Safety check ---------------------------------------- + if ( + config.safety_check_interval > 0 + and stats.cycles % config.safety_check_interval == 0 + ): + safe = self._check_safety(state, command) + if not safe: + stats.safety_violations += 1 + logger.error( + "Safety violation on cycle %d — halting %s", + stats.cycles, + device_id, + ) + self._sync_halt(loop, transport) + break + + # (e) Write actuators ------------------------------------- + if command.joint_commands: + try: + self._sync_write_batch(loop, transport, command) + except Exception as exc: # noqa: BLE001 + logger.warning( + "Control bus write failed on %s cycle %d: %s", + device_id, + stats.cycles, + exc, + ) + # A write failure is serious but not necessarily + # fatal — the next cycle will attempt again. A + # persistent failure is caught by the read-side + # comm-loss path. + + # (f) Update stats ---------------------------------------- + self._update_cycle_stats( + stats, cycle_start, period_s, config.max_overrun_ratio + ) + + # Check for consecutive overrun halt. + if ( + stats.consecutive_overruns + >= config.consecutive_overrun_limit + ): + logger.error( + "Control bus hit %d consecutive overruns on %s — halting", + stats.consecutive_overruns, + device_id, + ) + self._sync_halt(loop, transport) + break + + # (g) Wait for next cycle --------------------------------- + next_cycle_at += period_s + self._precise_wait(next_cycle_at) + + except Exception as exc: # noqa: BLE001 — thread must not die silently + logger.error( + "Control thread unexpected error on %s: %s", + device_id, + exc, + exc_info=True, + ) + finally: + # Ensure the device is halted on any exit path. + try: + self._sync_halt(loop, transport) + except Exception: # noqa: BLE001 + pass + loop.close() + stats.stopped_at = time.monotonic() + self._running.clear() + logger.info( + "Control bus stopped for %s after %d cycles " + "(overruns=%d, safety=%d, policy_errors=%d)", + device_id, + stats.cycles, + stats.overruns, + stats.safety_violations, + stats.policy_errors, + ) + + # -- Thread priority --------------------------------------------------- + + def _set_thread_priority(self) -> None: + """Elevate thread priority for real-time control. + + Linux: ``SCHED_FIFO`` with priority 50 (requires ``CAP_SYS_NICE`` + or root). macOS: ``pthread_setschedparam`` with ``SCHED_RR``. + Fallback: log a warning and continue at normal priority. + """ + target = self._config.priority + if target == "normal": + return + + system = platform.system() + try: + if system == "Linux": + self._set_priority_linux(target) + elif system == "Darwin": + self._set_priority_darwin(target) + else: + logger.info( + "Thread priority elevation not supported on %s; " + "continuing at normal priority", + system, + ) + except Exception as exc: # noqa: BLE001 + logger.warning( + "Failed to elevate thread priority to %r: %s — " + "continuing at normal priority (consider running with " + "CAP_SYS_NICE on Linux or as root)", + target, + exc, + ) + + @staticmethod + def _set_priority_linux(target: str) -> None: + """Set SCHED_FIFO or elevated nice on Linux.""" + SCHED_FIFO = 1 # noqa: N806 + + if target == "realtime": + # sched_setscheduler via ctypes — avoids the os.sched_* wrappers + # which are unavailable on some minimal Python builds. + libc = ctypes.CDLL("libc.so.6", use_errno=True) + + class _SchedParam(ctypes.Structure): + _fields_ = [("sched_priority", ctypes.c_int)] + + param = _SchedParam(sched_priority=50) + ret = libc.sched_setscheduler(0, SCHED_FIFO, ctypes.byref(param)) + if ret != 0: + errno = ctypes.get_errno() + raise OSError(errno, os.strerror(errno)) + logger.info("Control thread set to SCHED_FIFO priority 50") + elif target == "high": + os.nice(-10) + logger.info("Control thread nice set to -10") + + @staticmethod + def _set_priority_darwin(target: str) -> None: + """Elevate thread priority on macOS via pthread.""" + import ctypes.util + + libpthread_path = ctypes.util.find_library("pthread") + if not libpthread_path: + logger.info("pthread library not found on macOS; skipping priority") + return + + libpthread = ctypes.CDLL(libpthread_path, use_errno=True) + SCHED_RR = 2 # noqa: N806 + thread_self = libpthread.pthread_self() + + class _SchedParam(ctypes.Structure): + _fields_ = [("sched_priority", ctypes.c_int)] + + prio = 47 if target == "realtime" else 31 + param = _SchedParam(sched_priority=prio) + ret = libpthread.pthread_setschedparam( + thread_self, SCHED_RR, ctypes.byref(param) + ) + if ret != 0: + errno = ctypes.get_errno() + raise OSError(errno, os.strerror(errno)) + logger.info("Control thread set to SCHED_RR priority %d (macOS)", prio) + + # -- Synchronous transport wrappers ------------------------------------ + + def _sync_read_batch( + self, loop: asyncio.AbstractEventLoop, transport: Any + ) -> Any: + """Synchronous read_batch wrapper for the control thread. + + Uses the thread-local event loop created in ``_control_thread_main``. + The transport's ``read_batch`` is async, so we run it with + ``loop.run_until_complete``. Falling back to sequential reads when + the transport does not support ``BatchTransport``. + """ + from leapflow.hardware.transport import BatchReading, BatchTransport, Reading + + if isinstance(transport, BatchTransport): + return loop.run_until_complete( + transport.read_batch(self._read_channel_ids) + ) + # Sequential fallback — still under one loop.run_until_complete to + # keep the overhead per-cycle rather than per-channel. + async def _seq_read() -> BatchReading: + readings: list[Reading] = [] + for ch_id in self._read_channel_ids: + readings.append(await transport.read(ch_id)) + return BatchReading( + device_id=self._device_id, + readings=tuple(readings), + ) + + return loop.run_until_complete(_seq_read()) + + def _sync_write_batch( + self, + loop: asyncio.AbstractEventLoop, + transport: Any, + command: ControlCommand, + ) -> None: + """Synchronous write_batch wrapper for the control thread.""" + from leapflow.hardware.transport import BatchTransport + + commands_tuple = tuple( + (ch_id, val) + for ch_id, val in command.joint_commands.items() + if ch_id in self._write_channel_ids + ) + if not commands_tuple: + return + + if isinstance(transport, BatchTransport): + loop.run_until_complete(transport.write_batch(commands_tuple)) + else: + # Sequential fallback. + async def _seq_write() -> None: + for ch_id, val in commands_tuple: + await transport.write(ch_id, val) + + loop.run_until_complete(_seq_write()) + + @staticmethod + def _sync_halt(loop: asyncio.AbstractEventLoop, transport: Any) -> None: + """Synchronous halt wrapper — called on every exit path.""" + try: + loop.run_until_complete(transport.halt()) + except Exception as exc: # noqa: BLE001 + logger.warning("Control bus halt failed: %s", exc, exc_info=True) + + # -- Precise timing ---------------------------------------------------- + + def _precise_wait(self, target_time: float) -> None: + """Wait until *target_time* (monotonic) with sub-ms accuracy. + + Strategy when ``use_busy_wait`` is enabled: + + - If remaining > 2ms: ``time.sleep(remaining - 1ms)`` + - Busy-wait the last 1ms for precision + + When disabled: plain ``time.sleep`` (lower CPU, higher jitter). + """ + remaining = target_time - time.monotonic() + if remaining <= 0: + return + + if not self._config.use_busy_wait: + if remaining > 0: + time.sleep(remaining) + return + + # Sleep the bulk, busy-wait the tail. + _BUSY_THRESHOLD = 0.001 # 1ms + if remaining > _BUSY_THRESHOLD + 0.001: + time.sleep(remaining - _BUSY_THRESHOLD) + + # Busy-wait for the remaining sub-ms. + while time.monotonic() < target_time: + pass + + # -- Safety checks ----------------------------------------------------- + + def _check_safety( + self, state: ControlState, command: ControlCommand + ) -> bool: + """Check SafetyPolicy constraints on the computed command. + + Returns True if safe. On violation: logs and returns False. + The caller halts instead of writing. + """ + checker = self._safety_checker + if checker is None: + return True + + context = self._registry.context(self._device_id) + if context is None or context.safety is None: + return True + + # Check each commanded joint against the device-level safety policy. + for ch_id, val in command.joint_commands.items(): + channel = context.channel(ch_id) + if channel is None: + continue + allowed, reason = checker(context, channel, val) + if not allowed: + logger.error( + "Safety violation on %s.%s: %s (value=%s)", + self._device_id, + ch_id, + reason, + val, + ) + return False + + # Check velocity limits against state readings. + if context.safety.max_velocity_rad_s > 0: + for ch_id, vel in state.joint_velocities.items(): + if abs(vel) > context.safety.max_velocity_rad_s: + logger.error( + "Safety: observed velocity %.3f rad/s on %s.%s " + "exceeds limit %.3f rad/s", + abs(vel), + self._device_id, + ch_id, + context.safety.max_velocity_rad_s, + ) + return False + + return True + + # -- Stats helpers ----------------------------------------------------- + + @staticmethod + def _update_cycle_stats( + stats: ControlBusStats, + cycle_start: float, + period_s: float, + overrun_ratio: float = 1.5, + ) -> None: + """Update timing statistics after one complete cycle.""" + cycle_elapsed = time.monotonic() - cycle_start + cycle_ms = cycle_elapsed * 1000.0 + jitter_ms = abs(cycle_elapsed - period_s) * 1000.0 + + stats.cycles += 1 + stats._cycle_sum += cycle_ms + stats._jitter_sum += jitter_ms + stats.mean_cycle_ms = stats._cycle_sum / stats.cycles + stats.mean_jitter_ms = stats._jitter_sum / stats.cycles + if jitter_ms > stats.max_jitter_ms: + stats.max_jitter_ms = jitter_ms + + # Overrun detection: cycle took longer than the tolerance-adjusted + # period. A cycle that exceeds ``overrun_ratio * period_s`` is a + # hard overrun; one that merely exceeds the period is a soft overrun. + # Both increment the consecutive counter so the bus can halt after + # a sustained sequence of missed deadlines. + if cycle_elapsed > period_s * overrun_ratio: + stats.overruns += 1 + stats.consecutive_overruns += 1 + elif cycle_elapsed > period_s: + stats.overruns += 1 + stats.consecutive_overruns += 1 + else: + stats.consecutive_overruns = 0 + + +__all__ = [ + "ControlBusConfig", + "ControlBusStats", + "ControlCommand", + "ControlPolicy", + "ControlState", + "HighFrequencyControlBus", +] diff --git a/src/leapflow/hardware/control_hierarchy.py b/src/leapflow/hardware/control_hierarchy.py new file mode 100644 index 00000000..5b0bd21b --- /dev/null +++ b/src/leapflow/hardware/control_hierarchy.py @@ -0,0 +1,843 @@ +# Copyright (c) Alibaba, Inc. and its affiliates. +"""Control hierarchy: System-1/1.5/2 layered control orchestration. + +Three control layers with distinct frequencies and responsibilities: + +- System-2 (LLM, 0.2-1 Hz): High-level task planning and reasoning. + Produces a sequence of subtask goals from natural language instructions. + Runs in the AgentEngine's async loop. + +- System-1.5 (VLA/Policy, 5-30 Hz): Learned or scripted policy execution. + Maps observations + goals to coarse action sequences (action chunks). + Runs via InferenceStrategy in async or dedicated thread. + +- System-1 (Servo, 100-1000 Hz): Closed-loop PID/impedance control. + Refines System-1.5 outputs into smooth actuator commands. + Runs in HighFrequencyControlBus dedicated thread. + +This module manages the interfaces between layers: goal passing, +state reporting, frequency budgeting, and escalation on failure. +""" + +from __future__ import annotations + +import asyncio +import logging +import time +import uuid +from dataclasses import dataclass +from enum import Enum +from typing import Any, Callable, Mapping + +logger = logging.getLogger(__name__) + + +# --------------------------------------------------------------------------- +# Core types +# --------------------------------------------------------------------------- + + +class ControlLayer(str, Enum): + """Identifies one of the three control layers.""" + + SYSTEM_2 = "system_2" # LLM planning + SYSTEM_1_5 = "system_1_5" # Policy inference + SYSTEM_1 = "system_1" # Servo control + + +@dataclass(frozen=True) +class LayerState: + """Runtime state of one control layer.""" + + layer: str # ControlLayer value + status: str # "idle", "running", "error", "escalated" + frequency_hz: float # actual measured frequency + target_frequency_hz: float + cycle_count: int = 0 + last_error: str = "" + compute_budget_used_ms: float = 0.0 + + +@dataclass(frozen=True) +class SubtaskGoal: + """A goal passed from System-2 to System-1.5. + + The LLM decomposes a natural language task into a sequence of these + typed goals, each consumable by a specific InferenceStrategy. + """ + + goal_id: str + description: str # natural language for context + goal_type: str # "reach_position", "grasp", "place", "follow_trajectory", etc. + target: Mapping[str, Any] # goal-type-specific parameters + strategy_hint: str = "" # preferred InferenceStrategy id + timeout_s: float = 30.0 + verify: bool = True + + +@dataclass(frozen=True) +class EscalationEvent: + """Signals that a lower layer needs higher-layer intervention. + + System-1 detects servo error -> escalate to System-1.5 for replanning. + System-1.5 detects policy failure -> escalate to System-2 for task replanning. + """ + + source_layer: str + target_layer: str + reason: str + detail: str + device_id: str = "" + timestamp: float = 0.0 + + +# --------------------------------------------------------------------------- +# Goal execution verdicts +# --------------------------------------------------------------------------- + + +_VERDICT_OK = "ok" +_VERDICT_TIMEOUT = "timeout" +_VERDICT_ERROR = "error" +_VERDICT_ESCALATED = "escalated" + + +# --------------------------------------------------------------------------- +# ControlHierarchy +# --------------------------------------------------------------------------- + + +class ControlHierarchy: + """Orchestrates the three-layer control hierarchy for one device. + + Manages the lifecycle of all three layers, routes goals downward + and escalations upward. + + Dependency injection keeps this module free of reverse imports into + ``leapflow.engine``: all heavy objects arrive as constructor arguments. + """ + + def __init__( + self, + device_id: str, + *, + registry: Any, + inference_registry: Any = None, # InferenceStrategyRegistry + control_bus: Any = None, # HighFrequencyControlBus + realtime_loop: Any = None, # RealtimeControlLoop + event_bus: Any = None, + ) -> None: + self._device_id = device_id + self._registry = registry + self._inference_registry = inference_registry + self._control_bus = control_bus + self._realtime_loop = realtime_loop + self._event_bus = event_bus + + # Per-layer mutable state — only touched from the async orchestrator. + self._layer_states: dict[str, LayerState] = { + ControlLayer.SYSTEM_2.value: LayerState( + layer=ControlLayer.SYSTEM_2.value, + status="idle", + frequency_hz=0.0, + target_frequency_hz=1.0, + ), + ControlLayer.SYSTEM_1_5.value: LayerState( + layer=ControlLayer.SYSTEM_1_5.value, + status="idle", + frequency_hz=0.0, + target_frequency_hz=10.0, + ), + ControlLayer.SYSTEM_1.value: LayerState( + layer=ControlLayer.SYSTEM_1.value, + status="idle", + frequency_hz=0.0, + target_frequency_hz=500.0, + ), + } + + self._goal_queue: list[SubtaskGoal] = [] + self._escalation_handlers: dict[str, Callable[..., Any]] = {} + self._active_goal: SubtaskGoal | None = None + self._shutdown_requested = False + + # -- System-2 interface (called by PhysicalSkillPlugin / AgentEngine) -- + + async def execute_task( + self, goals: tuple[SubtaskGoal, ...] + ) -> dict[str, Any]: + """Execute a sequence of subtask goals through the hierarchy. + + For each goal: + 1. Select InferenceStrategy from inference_registry (System-1.5) + 2. Start RealtimeControlLoop with PolicyChain (System-1.5 -> 1) + 3. Monitor execution until goal achieved or timeout + 4. On failure: escalate or move to next goal + + Returns a summary dict with per-goal verdicts. + """ + self._update_layer( + ControlLayer.SYSTEM_2.value, status="running", + ) + + results: list[dict[str, Any]] = [] + completed = 0 + failed = 0 + + for goal in goals: + if self._shutdown_requested: + results.append({ + "goal_id": goal.goal_id, + "verdict": "aborted", + "reason": "hierarchy shutdown requested", + }) + failed += 1 + continue + + result = await self.execute_single_goal(goal) + results.append(result) + if result.get("verdict") == _VERDICT_OK: + completed += 1 + else: + failed += 1 + # Escalate to System-2 on failure so the caller can replan. + if result.get("verdict") in (_VERDICT_ERROR, _VERDICT_TIMEOUT): + self._emit_escalation(EscalationEvent( + source_layer=ControlLayer.SYSTEM_1_5.value, + target_layer=ControlLayer.SYSTEM_2.value, + reason=result.get("verdict", "unknown"), + detail=result.get("error", ""), + device_id=self._device_id, + timestamp=time.monotonic(), + )) + + self._update_layer(ControlLayer.SYSTEM_2.value, status="idle") + + return { + "device_id": self._device_id, + "total": len(goals), + "completed": completed, + "failed": failed, + "results": results, + } + + async def execute_single_goal( + self, goal: SubtaskGoal + ) -> dict[str, Any]: + """Execute one subtask goal. Returns result with verdict.""" + self._active_goal = goal + self._goal_queue = [g for g in self._goal_queue if g.goal_id != goal.goal_id] + + start_t = time.monotonic() + self._update_layer( + ControlLayer.SYSTEM_1_5.value, status="running", + ) + + try: + result = await self._execute_with_system_1_5(goal) + except asyncio.TimeoutError: + elapsed = time.monotonic() - start_t + self._update_layer( + ControlLayer.SYSTEM_1_5.value, + status="error", + last_error=f"timeout after {elapsed:.1f}s", + ) + result = { + "goal_id": goal.goal_id, + "verdict": _VERDICT_TIMEOUT, + "error": f"goal timed out after {elapsed:.1f}s", + "elapsed_s": round(elapsed, 3), + } + except Exception as exc: # noqa: BLE001 + elapsed = time.monotonic() - start_t + self._update_layer( + ControlLayer.SYSTEM_1_5.value, + status="error", + last_error=str(exc), + ) + result = { + "goal_id": goal.goal_id, + "verdict": _VERDICT_ERROR, + "error": str(exc), + "elapsed_s": round(elapsed, 3), + } + finally: + self._active_goal = None + self._update_layer( + ControlLayer.SYSTEM_1_5.value, status="idle", + ) + + self._emit_telemetry("goal.completed", result) + return result + + # -- Escalation handling -- + + def on_escalation(self, event: EscalationEvent) -> None: + """Handle an escalation from a lower layer. + + System-1 -> System-1.5: replan action chunk + System-1.5 -> System-2: request LLM replanning (emit event for AgentEngine) + """ + logger.warning( + "Escalation %s -> %s: %s (%s)", + event.source_layer, + event.target_layer, + event.reason, + event.detail, + ) + + handler = self._escalation_handlers.get(event.target_layer) + if handler is not None: + try: + handler(event) + except Exception as exc: # noqa: BLE001 + logger.error( + "Escalation handler for %s failed: %s", + event.target_layer, + exc, + exc_info=True, + ) + + # Publish to EventBus for cross-module visibility. + self._emit_telemetry("escalation", { + "source_layer": event.source_layer, + "target_layer": event.target_layer, + "reason": event.reason, + "device_id": event.device_id, + }) + + def register_escalation_handler( + self, target_layer: str, handler: Callable[..., Any] + ) -> None: + """Register a handler for escalations targeting a specific layer.""" + self._escalation_handlers[target_layer] = handler + + # -- State and monitoring -- + + def layer_state(self, layer: str) -> LayerState: + """Return the current state of a single layer.""" + state = self._layer_states.get(layer) + if state is None: + raise KeyError(f"unknown control layer: {layer!r}") + return state + + def all_layer_states(self) -> dict[str, LayerState]: + """Return a snapshot of all layer states.""" + return dict(self._layer_states) + + async def status(self) -> dict[str, Any]: + """Complete hierarchy status for tools/dashboard.""" + layers: dict[str, dict[str, Any]] = {} + for key, state in self._layer_states.items(): + layers[key] = { + "status": state.status, + "frequency_hz": state.frequency_hz, + "target_frequency_hz": state.target_frequency_hz, + "cycle_count": state.cycle_count, + "last_error": state.last_error, + "compute_budget_used_ms": state.compute_budget_used_ms, + } + + active_goal: dict[str, Any] | None = None + if self._active_goal is not None: + active_goal = { + "goal_id": self._active_goal.goal_id, + "goal_type": self._active_goal.goal_type, + "description": self._active_goal.description, + } + + bus_running = False + bus_stats: dict[str, Any] | None = None + if self._control_bus is not None: + bus_running = self._control_bus.is_running + bus_stats = self._control_bus.stats.to_dict() + + return { + "device_id": self._device_id, + "layers": layers, + "active_goal": active_goal, + "queued_goals": len(self._goal_queue), + "control_bus_running": bus_running, + "control_bus_stats": bus_stats, + } + + # -- Lifecycle -- + + async def shutdown(self) -> None: + """Gracefully stop all layers (bottom-up: System-1 first).""" + self._shutdown_requested = True + + # 1. System-1: stop servo control bus. + if self._control_bus is not None and self._control_bus.is_running: + try: + await self._control_bus.async_stop() + except Exception as exc: # noqa: BLE001 + logger.warning( + "System-1 shutdown error on %s: %s", + self._device_id, exc, exc_info=True, + ) + self._update_layer(ControlLayer.SYSTEM_1.value, status="idle") + + # 2. System-1.5: stop realtime loop (if running independently). + if self._realtime_loop is not None: + try: + await self._realtime_loop.stop() + except Exception as exc: # noqa: BLE001 + logger.warning( + "System-1.5 shutdown error on %s: %s", + self._device_id, exc, exc_info=True, + ) + self._update_layer(ControlLayer.SYSTEM_1_5.value, status="idle") + + # 3. System-2: clear goal queue and mark idle. + self._goal_queue.clear() + self._active_goal = None + self._update_layer(ControlLayer.SYSTEM_2.value, status="idle") + + logger.info( + "Control hierarchy shutdown complete for device %s", + self._device_id, + ) + + # -- Internal: goal execution -- + + async def _execute_with_system_1_5( + self, goal: SubtaskGoal + ) -> dict[str, Any]: + """Select strategy, run inference, execute through control stack.""" + start_t = time.monotonic() + + # 1. Select inference strategy. + strategy = self._select_strategy(goal) + if strategy is None: + return { + "goal_id": goal.goal_id, + "verdict": _VERDICT_ERROR, + "error": ( + f"no inference strategy found for goal " + f"{goal.goal_type!r} (hint={goal.strategy_hint!r})" + ), + "elapsed_s": 0.0, + } + + # 2. Read observation. + observation = await self._read_observation() + + # Enrich observation with goal information so the policy has context. + observation["_goal_type"] = goal.goal_type + observation["_goal_target"] = dict(goal.target) + observation["_goal_description"] = goal.description + + # 3. Run System-1.5 inference. + self._update_layer( + ControlLayer.SYSTEM_1_5.value, status="running", + ) + inference_start = time.monotonic() + + from leapflow.robot.inference.strategy import ComputeBudget + + budget = ComputeBudget( + max_latency_ms=goal.timeout_s * 1000.0 * 0.5, # half of goal timeout + ) + result = await strategy.infer(observation, budget=budget) + + inference_ms = (time.monotonic() - inference_start) * 1000.0 + self._update_layer( + ControlLayer.SYSTEM_1_5.value, + compute_budget_used_ms=inference_ms, + ) + + # 4. Execute action through System-1. + action = result.action + if action is None: + return { + "goal_id": goal.goal_id, + "verdict": _VERDICT_ERROR, + "error": "inference returned null action", + "elapsed_s": round(time.monotonic() - start_t, 3), + } + + execution_result = await self._execute_through_system_1( + goal, action, + ) + + elapsed = time.monotonic() - start_t + verdict = _VERDICT_OK if execution_result.get("ok") else _VERDICT_ERROR + + return { + "goal_id": goal.goal_id, + "goal_type": goal.goal_type, + "verdict": verdict, + "elapsed_s": round(elapsed, 3), + "inference_ms": round(inference_ms, 3), + "confidence": float(result.confidence), + "strategy_id": getattr(strategy, "strategy_id", ""), + "execution": execution_result, + } + + async def _execute_through_system_1( + self, goal: SubtaskGoal, action: Any + ) -> dict[str, Any]: + """Execute an action through the System-1 servo layer. + + If a RealtimeControlLoop and ControlBus are available, builds a + PolicyChain (System-1.5 action -> System-1 PID) for smooth execution. + Falls back to direct registry writes when the RT stack is unavailable. + """ + self._update_layer(ControlLayer.SYSTEM_1.value, status="running") + + try: + # Attempt RT execution path. + if self._realtime_loop is not None and self._control_bus is not None: + return await self._execute_via_rt_loop(goal, action) + + # Fallback: write action directly via registry. + return await self._execute_direct_write(action) + except Exception as exc: # noqa: BLE001 + self._update_layer( + ControlLayer.SYSTEM_1.value, + status="error", + last_error=str(exc), + ) + # Escalate System-1 error to System-1.5. + self._emit_escalation(EscalationEvent( + source_layer=ControlLayer.SYSTEM_1.value, + target_layer=ControlLayer.SYSTEM_1_5.value, + reason="servo_error", + detail=str(exc), + device_id=self._device_id, + timestamp=time.monotonic(), + )) + return {"ok": False, "error": str(exc)} + finally: + if self._layer_states[ControlLayer.SYSTEM_1.value].status == "running": + self._update_layer( + ControlLayer.SYSTEM_1.value, status="idle", + ) + + async def _execute_via_rt_loop( + self, goal: SubtaskGoal, action: Any + ) -> dict[str, Any]: + """Execute action through the RealtimeControlLoop stack.""" + assert self._realtime_loop is not None + + # Convert action to joint commands mapping. + commands = self._action_to_joint_targets(action) + if not commands: + return {"ok": False, "error": "could not interpret action as joint targets"} + + joint_ids = tuple(commands.keys()) + + # Choose inner controller based on goal type. + inner_controller = "impedance" if goal.goal_type in ( + "grasp", "insert", "polish", "compliant_contact", + ) else "pid" + + session_id = await self._realtime_loop.start_pid( + self._device_id, + joint_ids, + commands, + ) if inner_controller == "pid" else await self._realtime_loop.start_impedance( + self._device_id, + joint_ids, + commands, + ) + + # Wait for convergence or timeout. + deadline = time.monotonic() + min(goal.timeout_s, 30.0) + while time.monotonic() < deadline: + status = await self._realtime_loop.status() + if status.get("status") != "running": + break + if status.get("is_complete"): + break + await asyncio.sleep(0.05) + + result = await self._realtime_loop.stop() + self._update_layer( + ControlLayer.SYSTEM_1.value, + status="idle", + cycle_count=result.get("stats", {}).get("cycles", 0), + ) + return { + "ok": True, + "session_id": session_id, + "execution_mode": f"rt_{inner_controller}", + "stats": result.get("stats", {}), + } + + async def _execute_direct_write( + self, action: Any + ) -> dict[str, Any]: + """Fallback: write action directly to hardware registry.""" + commands = self._action_to_joint_targets(action) + if not commands: + return {"ok": False, "error": "could not interpret action as joint targets"} + + from leapflow.hardware.transport import BatchTransport + + transport = await self._registry.transport(self._device_id) + if isinstance(transport, BatchTransport): + write_tuples = tuple(commands.items()) + outcome = await transport.write_batch(write_tuples) + return { + "ok": outcome.ok, + "channels_written": len(write_tuples), + "execution_mode": "direct_batch", + } + + # Sequential fallback. + written = 0 + for ch_id, val in commands.items(): + try: + await transport.write(ch_id, val) + written += 1 + except Exception as exc: # noqa: BLE001 + logger.debug( + "Direct write failed for %s.%s: %s", + self._device_id, ch_id, exc, + ) + return { + "ok": written == len(commands), + "channels_written": written, + "channels_total": len(commands), + "execution_mode": "direct_sequential", + } + + # -- Internal: strategy selection -- + + def _select_strategy(self, goal: SubtaskGoal) -> Any: + """Select an InferenceStrategy for a goal. + + Precedence: + 1. Explicit strategy_hint in the goal + 2. Auto-select from the inference registry based on budget + """ + if self._inference_registry is None: + return None + + # 1. Try explicit hint. + if goal.strategy_hint: + strategy = self._inference_registry.get(goal.strategy_hint) + if strategy is not None: + return strategy + logger.warning( + "Strategy hint %r not found in registry; falling back to auto-select", + goal.strategy_hint, + ) + + # 2. Auto-select: let the registry pick the best fit. + from leapflow.robot.inference.strategy import ComputeBudget + + budget = ComputeBudget( + max_latency_ms=min(goal.timeout_s * 1000.0 * 0.5, 500.0), + ) + return self._inference_registry.select(budget=budget) + + # -- Internal: observation reading -- + + async def _read_observation(self) -> dict[str, Any]: + """Read current device observation through the registry.""" + context = self._registry.context(self._device_id) + if context is None: + return {} + + channels = [ + ch.channel_id + for ch in context.channels + if ch.is_readable and getattr(ch, "representation", "") != "frame" + ] + + from leapflow.hardware.transport import BatchTransport + + transport = await self._registry.transport(self._device_id) + if isinstance(transport, BatchTransport) and channels: + batch = await transport.read_batch(tuple(channels)) + return {r.channel_id: r.value for r in batch.readings} + + obs: dict[str, Any] = {} + for cid in channels: + try: + reading = await self._registry.read(self._device_id, cid) + obs[cid] = reading.value + except Exception: # noqa: BLE001 + pass + return obs + + # -- Internal: helpers -- + + @staticmethod + def _action_to_joint_targets(action: Any) -> dict[str, float]: + """Convert an action value to a {channel_id: float} mapping.""" + if isinstance(action, dict): + return {str(k): float(v) for k, v in action.items()} + if hasattr(action, "tolist"): + # numpy/torch tensor: index-based channel ids. + vals = action.tolist() + if isinstance(vals, list): + return {str(i): float(v) for i, v in enumerate(vals)} + if isinstance(action, (list, tuple)): + return {str(i): float(v) for i, v in enumerate(action)} + return {} + + def _update_layer( + self, + layer: str, + *, + status: str | None = None, + frequency_hz: float | None = None, + cycle_count: int | None = None, + last_error: str | None = None, + compute_budget_used_ms: float | None = None, + ) -> None: + """Update one layer's state immutably.""" + current = self._layer_states.get(layer) + if current is None: + return + self._layer_states[layer] = LayerState( + layer=current.layer, + status=status if status is not None else current.status, + frequency_hz=( + frequency_hz if frequency_hz is not None else current.frequency_hz + ), + target_frequency_hz=current.target_frequency_hz, + cycle_count=( + cycle_count if cycle_count is not None else current.cycle_count + ), + last_error=( + last_error if last_error is not None else current.last_error + ), + compute_budget_used_ms=( + compute_budget_used_ms + if compute_budget_used_ms is not None + else current.compute_budget_used_ms + ), + ) + + def _emit_escalation(self, event: EscalationEvent) -> None: + """Route an escalation through registered handlers and EventBus.""" + self.on_escalation(event) + + def _emit_telemetry(self, topic: str, payload: Any) -> None: + """Best-effort telemetry publish to EventBus.""" + if self._event_bus is None: + return + try: + self._event_bus.emit(f"control.hierarchy.{topic}", payload) + except Exception: # noqa: BLE001 — telemetry must not break control + pass + + +# --------------------------------------------------------------------------- +# TaskDecomposer — System-2 -> System-1.5 bridge +# --------------------------------------------------------------------------- + + +class TaskDecomposer: + """Decomposes high-level task descriptions into SubtaskGoal sequences. + + This is a lightweight bridge -- the actual decomposition happens in + the LLM (System-2 via AgentEngine). This class provides the + structured representation that the hierarchy can consume. + + Not LLM-powered itself: it is a type converter that structures + the LLM's already-decomposed output into typed SubtaskGoals. + """ + + def parse_plan( + self, plan: Mapping[str, Any] + ) -> tuple[SubtaskGoal, ...]: + """Parse a structured plan (from LLM tool output) into goals. + + Expected plan format:: + + { + "steps": [ + { + "type": "reach_position", + "target": {"joint.shoulder": 1.0, ...}, + "description": "move to above the cup", + "strategy_hint": "vla_local:pick_place", + "timeout_s": 10.0, + "verify": true + }, + {"type": "grasp", "target": {"gripper": "close"}}, + {"type": "reach_position", "target": {...}}, + {"type": "place", "target": {"gripper": "open"}}, + ] + } + + Missing optional fields receive sensible defaults. + """ + steps = plan.get("steps") + if not steps or not isinstance(steps, (list, tuple)): + return () + + goals: list[SubtaskGoal] = [] + for idx, step in enumerate(steps): + if not isinstance(step, Mapping): + logger.warning( + "TaskDecomposer: skipping non-mapping step at index %d", idx, + ) + continue + + goal_type = str(step.get("type", "unknown")) + target = step.get("target", {}) + if not isinstance(target, Mapping): + target = {} + + goals.append(SubtaskGoal( + goal_id=step.get("goal_id", f"goal_{idx}_{uuid.uuid4().hex[:6]}"), + description=str(step.get("description", goal_type)), + goal_type=goal_type, + target=target, + strategy_hint=str(step.get("strategy_hint", "")), + timeout_s=float(step.get("timeout_s", 30.0)), + verify=bool(step.get("verify", True)), + )) + + return tuple(goals) + + def from_action_chunks( + self, + chunks: list[Mapping[str, float]], + *, + chunk_duration_s: float = 0.5, + ) -> tuple[SubtaskGoal, ...]: + """Convert raw action chunks from a VLA policy into trajectory goals. + + Each chunk becomes a ``follow_trajectory`` goal whose target carries + the joint-value mapping and the chunk's duration. This is useful + when a VLA policy returns a sequence of action chunks that should be + executed sequentially through the control hierarchy. + """ + if not chunks: + return () + + goals: list[SubtaskGoal] = [] + for idx, chunk in enumerate(chunks): + goals.append(SubtaskGoal( + goal_id=f"chunk_{idx}_{uuid.uuid4().hex[:6]}", + description=f"action chunk {idx}/{len(chunks)}", + goal_type="follow_trajectory", + target={ + "joint_targets": dict(chunk), + "duration_s": chunk_duration_s, + "chunk_index": idx, + "total_chunks": len(chunks), + }, + timeout_s=chunk_duration_s * 3.0, # generous timeout + verify=idx == len(chunks) - 1, # verify only last chunk + )) + + return tuple(goals) + + +__all__ = [ + "ControlHierarchy", + "ControlLayer", + "EscalationEvent", + "LayerState", + "SubtaskGoal", + "TaskDecomposer", +] diff --git a/src/leapflow/hardware/fleet.py b/src/leapflow/hardware/fleet.py new file mode 100644 index 00000000..bcd7e95d --- /dev/null +++ b/src/leapflow/hardware/fleet.py @@ -0,0 +1,844 @@ +# Copyright (c) Alibaba, Inc. and its affiliates. +"""Fleet deployment management: multi-node robot coordination. + +Extends LeapFlow's single-machine hardware stack to a fleet of robots +distributed across multiple edge nodes. The central leapd manages the +fleet topology, routes commands to the correct node via MCP Transport, +and aggregates trust/evidence/experience across all nodes. + +Three deployment scenarios (from research note 10): +1. Dual-machine (Mac + edge board): Mac runs LLM/console, board runs RT control +2. Standalone edge + remote LLM: edge runs independently, LLM via API +3. Mac-only: USB-connected devices, no fleet needed + +Fleet management adds: +- Node discovery and health monitoring +- Device-to-node routing (device_id → MCP endpoint) +- Cross-node trust aggregation +- Cross-node experience/evidence aggregation +- Fleet-level emergency stop (halt all nodes) +- Hot-plug: nodes joining/leaving the fleet + +Design invariants: + +- Fleet uses MCP Transport for bridging — no new RPC protocol. +- Remote devices register in the local HardwareRegistry as MCP transport + kind, making them transparent to the agent layer above. +- ``halt_all`` is concurrent (``asyncio.gather``), fire-and-forget. +- Heartbeat is best-effort and never blocks data-plane operations. +- Trust aggregation takes the conservative value (lowest trust level). +- Nodes that go offline have their devices marked degraded automatically. +- No dependency on ``leapflow.engine``. +""" + +from __future__ import annotations + +import asyncio +import logging +import time +from dataclasses import dataclass, field +from typing import Any, Mapping + +logger = logging.getLogger(__name__) + +# --------------------------------------------------------------------------- +# Core types +# --------------------------------------------------------------------------- + +_NODE_STATUS_ONLINE = "online" +_NODE_STATUS_OFFLINE = "offline" +_NODE_STATUS_DEGRADED = "degraded" +_NODE_STATUS_UNKNOWN = "unknown" + +_VALID_STATUSES = frozenset({ + _NODE_STATUS_ONLINE, + _NODE_STATUS_OFFLINE, + _NODE_STATUS_DEGRADED, + _NODE_STATUS_UNKNOWN, +}) + + +@dataclass(frozen=True) +class FleetNode: + """One edge node in the fleet. + + Each node represents a physical machine (e.g. a Jetson board) that + hosts one or more hardware devices. The ``mcp_endpoint`` is the + address of the MCP server on that node, through which all device + commands are routed. + + ``device_ids`` is a snapshot taken at discovery time. Devices may + appear or disappear as the node's own HardwareRegistry reconciles; + the fleet manager re-discovers periodically via heartbeat. + """ + + node_id: str + display_name: str + mcp_endpoint: str # "http://host:port" or "stdio:command" + device_ids: tuple[str, ...] = () + status: str = _NODE_STATUS_UNKNOWN + last_heartbeat: float = 0.0 + metadata: Mapping[str, Any] = field(default_factory=dict) + + def with_status(self, status: str, *, heartbeat: float = 0.0) -> FleetNode: + """Return a copy with an updated status and optional heartbeat.""" + return FleetNode( + node_id=self.node_id, + display_name=self.display_name, + mcp_endpoint=self.mcp_endpoint, + device_ids=self.device_ids, + status=status if status in _VALID_STATUSES else _NODE_STATUS_UNKNOWN, + last_heartbeat=heartbeat or self.last_heartbeat, + metadata=self.metadata, + ) + + def with_devices(self, device_ids: tuple[str, ...]) -> FleetNode: + """Return a copy with an updated device list.""" + return FleetNode( + node_id=self.node_id, + display_name=self.display_name, + mcp_endpoint=self.mcp_endpoint, + device_ids=device_ids, + status=self.status, + last_heartbeat=self.last_heartbeat, + metadata=self.metadata, + ) + + +@dataclass(frozen=True) +class FleetTopology: + """Complete fleet topology snapshot. + + An immutable view of the fleet at a point in time — safe to pass + across boundaries and serialize for dashboards. + """ + + nodes: tuple[FleetNode, ...] = () + total_devices: int = 0 + online_nodes: int = 0 + timestamp: float = 0.0 + + def to_dict(self) -> dict[str, Any]: + """Serialize for JSON / dashboard consumption.""" + return { + "nodes": [ + { + "node_id": n.node_id, + "display_name": n.display_name, + "mcp_endpoint": n.mcp_endpoint, + "device_ids": list(n.device_ids), + "status": n.status, + "last_heartbeat": n.last_heartbeat, + "metadata": dict(n.metadata), + } + for n in self.nodes + ], + "total_devices": self.total_devices, + "online_nodes": self.online_nodes, + "timestamp": self.timestamp, + } + + +# --------------------------------------------------------------------------- +# FleetManager +# --------------------------------------------------------------------------- + + +class FleetManager: + """Manages a fleet of distributed robot nodes. + + Coordinates with the local ``HardwareRegistry`` to present remote + devices as if they were local — commands are transparently routed + through MCP Transport to the correct edge node. + """ + + def __init__( + self, + registry: Any, # local HardwareRegistry + *, + heartbeat_interval_s: float = 10.0, + node_timeout_s: float = 30.0, + ) -> None: + self._registry = registry + self._nodes: dict[str, FleetNode] = {} + self._device_to_node: dict[str, str] = {} # device_id → node_id + self._heartbeat_task: asyncio.Task[None] | None = None + self._heartbeat_interval = max(1.0, float(heartbeat_interval_s)) + self._node_timeout = max(1.0, float(node_timeout_s)) + self._stopped = asyncio.Event() + + # -- Node management ------------------------------------------------- + + async def register_node(self, node: FleetNode) -> FleetNode: + """Register an edge node and discover its devices. + + 1. Probe the node's MCP endpoint for availability. + 2. Discover devices via MCP tools (equivalent to ``hw_list``). + 3. Update the device→node routing table. + 4. Register each remote device in the local registry (if a + reconciliation hook is available — otherwise the caller is + responsible for adding the MCP-backed device declarations). + + Returns the node with updated device list and status. + Raises ``FleetError`` if the endpoint is unreachable. + """ + if not node.node_id: + raise FleetError("node_id must be non-empty") + if not node.mcp_endpoint: + raise FleetError(f"node {node.node_id!r} has no mcp_endpoint") + + # Probe the endpoint. + reachable = await self._probe_endpoint(node.mcp_endpoint) + if not reachable: + raise FleetError( + f"node {node.node_id!r} at {node.mcp_endpoint!r} is unreachable" + ) + + # Discover devices on this node. + device_ids = await self._discover_devices(node.mcp_endpoint) + registered = node.with_devices(device_ids).with_status( + _NODE_STATUS_ONLINE, heartbeat=time.monotonic() + ) + + # Update internal state. + self._nodes[registered.node_id] = registered + for did in registered.device_ids: + self._device_to_node[did] = registered.node_id + + logger.info( + "Fleet: registered node %r (%s) with %d device(s): %s", + registered.node_id, + registered.mcp_endpoint, + len(registered.device_ids), + ", ".join(registered.device_ids) or "(none)", + ) + return registered + + async def unregister_node(self, node_id: str) -> None: + """Remove a node and all its devices from the fleet. + + Halts all devices on the node first — best-effort, failures are + logged but do not prevent removal. + """ + node = self._nodes.get(node_id) + if node is None: + logger.debug("Fleet: unregister_node(%r) — not found, no-op", node_id) + return + + # Halt all devices on the node (best-effort). + for did in node.device_ids: + try: + await self._halt_device(did) + except Exception as exc: # noqa: BLE001 + logger.warning( + "Fleet: halt device %r on unregister failed: %s", + did, exc, exc_info=True, + ) + + # Remove routing entries. + for did in node.device_ids: + self._device_to_node.pop(did, None) + del self._nodes[node_id] + + logger.info("Fleet: unregistered node %r", node_id) + + async def discover_nodes( + self, endpoints: tuple[str, ...] + ) -> tuple[FleetNode, ...]: + """Probe multiple endpoints for fleet nodes. + + Returns discovered nodes (not yet registered). Unreachable + endpoints are silently skipped. + """ + discovered: list[FleetNode] = [] + + async def _probe_one(endpoint: str) -> FleetNode | None: + reachable = await self._probe_endpoint(endpoint) + if not reachable: + return None + device_ids = await self._discover_devices(endpoint) + # Derive a node_id from the endpoint for convenience. + node_id = _endpoint_to_node_id(endpoint) + return FleetNode( + node_id=node_id, + display_name=endpoint, + mcp_endpoint=endpoint, + device_ids=device_ids, + status=_NODE_STATUS_ONLINE, + last_heartbeat=time.monotonic(), + ) + + results = await asyncio.gather( + *(_probe_one(ep) for ep in endpoints), + return_exceptions=True, + ) + for item in results: + if isinstance(item, FleetNode): + discovered.append(item) + elif isinstance(item, Exception): + logger.debug("Fleet: discover probe failed: %s", item) + return tuple(discovered) + + # -- Routing --------------------------------------------------------- + + def route_device(self, device_id: str) -> str | None: + """Return the node_id managing this device, or None if local.""" + return self._device_to_node.get(device_id) + + def is_remote(self, device_id: str) -> bool: + """Return True if the device is managed by a remote node.""" + return device_id in self._device_to_node + + # -- Health monitoring ----------------------------------------------- + + async def start_heartbeat(self) -> None: + """Start periodic heartbeat monitoring of all nodes. + + Idempotent — calling twice is a no-op. + """ + if self._heartbeat_task is not None: + return + self._stopped.clear() + self._heartbeat_task = asyncio.create_task( + self._heartbeat_loop(), name="fleet-heartbeat" + ) + + async def stop_heartbeat(self) -> None: + """Stop heartbeat monitoring. Idempotent.""" + self._stopped.set() + task = self._heartbeat_task + self._heartbeat_task = None + if task is None: + return + task.cancel() + try: + await asyncio.wait_for(task, timeout=5.0) + except (asyncio.CancelledError, asyncio.TimeoutError): + pass + except Exception: # noqa: BLE001 — teardown must not propagate + logger.warning("Fleet heartbeat stop raised", exc_info=True) + + async def _heartbeat_loop(self) -> None: + """Ping each node periodically. Mark offline nodes after timeout. + + Best-effort: a failed probe logs but does not block the next + cycle. Nodes that exceed ``node_timeout_s`` since their last + successful heartbeat are marked offline and their devices are + marked degraded in the routing table. + """ + while not self._stopped.is_set(): + for node_id in list(self._nodes): + node = self._nodes.get(node_id) + if node is None: + continue + try: + reachable = await self._probe_endpoint(node.mcp_endpoint) + except Exception: # noqa: BLE001 + reachable = False + + now = time.monotonic() + if reachable: + self._nodes[node_id] = node.with_status( + _NODE_STATUS_ONLINE, heartbeat=now + ) + else: + elapsed = now - node.last_heartbeat + if elapsed > self._node_timeout: + if node.status != _NODE_STATUS_OFFLINE: + logger.warning( + "Fleet: node %r offline (%.1fs since last heartbeat)", + node_id, elapsed, + ) + self._nodes[node_id] = node.with_status( + _NODE_STATUS_OFFLINE + ) + elif node.status == _NODE_STATUS_ONLINE: + self._nodes[node_id] = node.with_status( + _NODE_STATUS_DEGRADED + ) + + try: + await asyncio.wait_for( + self._stopped.wait(), timeout=self._heartbeat_interval + ) + break # stopped + except asyncio.TimeoutError: + pass + + async def node_health(self, node_id: str) -> dict[str, Any]: + """Return health details for one node. + + Probes the node synchronously and returns a diagnostic dict. + """ + node = self._nodes.get(node_id) + if node is None: + return {"node_id": node_id, "error": "unknown node"} + + reachable = await self._probe_endpoint(node.mcp_endpoint) + now = time.monotonic() + if reachable: + self._nodes[node_id] = node.with_status( + _NODE_STATUS_ONLINE, heartbeat=now + ) + return { + "node_id": node_id, + "display_name": node.display_name, + "mcp_endpoint": node.mcp_endpoint, + "status": _NODE_STATUS_ONLINE if reachable else node.status, + "reachable": reachable, + "device_count": len(node.device_ids), + "device_ids": list(node.device_ids), + "last_heartbeat": node.last_heartbeat, + "seconds_since_heartbeat": now - node.last_heartbeat if node.last_heartbeat else -1, + } + + # -- Fleet-level operations ------------------------------------------ + + async def halt_all(self) -> dict[str, Any]: + """Emergency stop ALL devices on ALL nodes. + + Sends halt concurrently to every node. This is the fleet-level + safety backstop — called when something goes wrong and we need + to stop everything immediately. + + ``halt`` is lock-free (HCP requirement) so this does not wait + behind in-flight I/O. Individual failures are logged but never + prevent the remaining halts from proceeding. + + Returns a per-node halt status map. + """ + if not self._nodes: + return {} + + async def _halt_node(node: FleetNode) -> tuple[str, dict[str, Any]]: + results: dict[str, Any] = {"device_ids": list(node.device_ids)} + successes = 0 + failures = 0 + for did in node.device_ids: + try: + await self._halt_device(did) + successes += 1 + except Exception as exc: # noqa: BLE001 + failures += 1 + logger.warning( + "Fleet halt_all: device %r on node %r failed: %s", + did, node.node_id, exc, + ) + results["successes"] = successes + results["failures"] = failures + results["halted"] = failures == 0 + return node.node_id, results + + raw = await asyncio.gather( + *(_halt_node(n) for n in self._nodes.values()), + return_exceptions=True, + ) + halt_map: dict[str, Any] = {} + for item in raw: + if isinstance(item, Exception): + logger.warning("Fleet halt_all: gather exception: %s", item) + continue + nid, status = item + halt_map[nid] = status + return halt_map + + def topology(self) -> FleetTopology: + """Return current fleet topology snapshot.""" + nodes = tuple(sorted(self._nodes.values(), key=lambda n: n.node_id)) + total_devices = sum(len(n.device_ids) for n in nodes) + online_nodes = sum(1 for n in nodes if n.status == _NODE_STATUS_ONLINE) + return FleetTopology( + nodes=nodes, + total_devices=total_devices, + online_nodes=online_nodes, + timestamp=time.time(), + ) + + # -- Aggregation ----------------------------------------------------- + + async def aggregate_trust(self) -> dict[str, Any]: + """Collect and merge trust states from all nodes. + + Each node has its own ``HardwareTrustGate``; this aggregates + them into a fleet-wide view. The merge strategy is conservative: + take the lowest trust level across nodes for each device/channel + pair. This ensures a device whose trust degraded on one node + does not appear trusted fleet-wide. + + Returns a dict keyed by ``"device_id:channel_id"`` with the + aggregated trust level name and per-node breakdown. + """ + # Function-local import: trust module is optional at this layer. + from leapflow.hardware.trust import HardwareTrustGate, HardwareTrustLevel + + fleet_trust: dict[str, dict[str, Any]] = {} + + # Collect from the local registry's trust gate if available. + trust_gate: HardwareTrustGate | None = getattr( + self._registry, "_trust_gate", None + ) + if trust_gate is not None: + for record in trust_gate.all_records(): + key = f"{record.device_id}:{record.channel_id}" + fleet_trust[key] = { + "level": record.level.name, + "level_value": int(record.level), + "frozen": record.frozen, + "sources": {"local": record.level.name}, + } + + # For remote nodes, we query via MCP if their trust endpoint is + # exposed. For now this is a best-effort aggregation from the + # local view — remote trust queries require a fleet-trust MCP tool + # on each node, which is a future extension. + + return { + "strategy": "conservative_merge", + "description": ( + "Lowest trust level across all nodes for each (device, channel) pair" + ), + "trust": fleet_trust, + "node_count": len(self._nodes), + "timestamp": time.time(), + } + + async def aggregate_evidence(self, *, since: float = 0.0) -> list[dict[str, Any]]: + """Collect recent evidence from all nodes' EvidenceStores. + + Queries the local registry's evidence store. Remote node + evidence requires a fleet-evidence MCP tool on each node, + which is a future extension. For now, returns local evidence. + """ + from leapflow.hardware.evidence import EvidenceStore + + store: EvidenceStore | None = getattr(self._registry, "_evidence_store", None) + if store is None: + # Try reading_store as a fallback attribute name. + store = getattr(self._registry, "reading_store", None) + + if store is None or not hasattr(store, "query"): + return [] + + try: + records = await store.query(since=since if since > 0 else None, limit=200) + return records + except Exception as exc: # noqa: BLE001 + logger.debug("Fleet evidence aggregation failed: %s", exc) + return [] + + # -- Internal helpers ------------------------------------------------ + + async def _probe_endpoint(self, endpoint: str) -> bool: + """Probe an MCP endpoint for liveness. + + Returns True if the endpoint responds to a basic health check. + This is a lightweight connectivity test, not a full capability + discovery — it must complete quickly so the heartbeat loop stays + responsive. + """ + try: + # Use the MCP client provider if available. + from leapflow.hardware.transports.mcp import _CLIENT_PROVIDER + + if _CLIENT_PROVIDER is not None: + client = _CLIENT_PROVIDER() + if client is not None and hasattr(client, "call_tool"): + # Attempt a lightweight call — probe_tool or list_tools. + if hasattr(client, "list_tools"): + raw = client.list_tools() + if asyncio.iscoroutine(raw): + await asyncio.wait_for(raw, timeout=5.0) + return True + # Fallback: treat endpoint as reachable if it parses. + return bool(endpoint) + except asyncio.TimeoutError: + return False + except Exception: # noqa: BLE001 + return False + + async def _discover_devices(self, endpoint: str) -> tuple[str, ...]: + """Discover devices on a node by querying its MCP server. + + Calls the equivalent of ``hw_list`` via MCP. Returns an empty + tuple if discovery fails. + """ + try: + from leapflow.hardware.transports.mcp import _CLIENT_PROVIDER + + if _CLIENT_PROVIDER is not None: + client = _CLIENT_PROVIDER() + if client is not None and hasattr(client, "call_tool"): + response = await client.call_tool("hw_list", {}) + if isinstance(response, Mapping): + devices = response.get("devices", ()) + if isinstance(devices, (list, tuple)): + return tuple( + str(d.get("device_id", d) if isinstance(d, dict) else d) + for d in devices + if d + ) + except Exception as exc: # noqa: BLE001 + logger.debug( + "Fleet: device discovery on %s failed: %s", endpoint, exc + ) + return () + + async def _halt_device(self, device_id: str) -> None: + """Halt a single device through the local registry's transport. + + Lock-free: ``transport.halt()`` does not acquire the I/O lock + (HCP requirement), so this preempts in-flight operations. + """ + try: + transport = await self._registry.transport(device_id) + await transport.halt() + except Exception as exc: # noqa: BLE001 + logger.debug("Fleet: halt device %r failed: %s", device_id, exc) + raise + + async def close(self) -> None: + """Shut down the fleet manager. + + Stops heartbeat, then unregisters all nodes (which halts their + devices). Never raises. + """ + await self.stop_heartbeat() + for node_id in list(self._nodes): + try: + await self.unregister_node(node_id) + except Exception: # noqa: BLE001 — teardown must not propagate + logger.warning( + "Fleet: unregister node %r during close failed", + node_id, exc_info=True, + ) + + +# --------------------------------------------------------------------------- +# FleetSessionRouter +# --------------------------------------------------------------------------- + + +class FleetSessionRouter: + """Routes session operations to the correct fleet node. + + When an Agent targets a device managed by a remote node, + the router transparently forwards the operation through + the MCP Transport connection to that node. + + The router is a thin delegation layer — it resolves the node, + then delegates to the local registry's transport (which is already + an MCP transport pointing at the correct node). + """ + + def __init__(self, fleet_manager: FleetManager) -> None: + self._fleet = fleet_manager + + async def route_write( + self, device_id: str, channel_id: str, value: Any + ) -> Any: + """Route a write operation to the correct node. + + If the device is remote, the write goes through the MCP + transport already registered in the local registry. If local, + it goes through the local transport directly. + """ + registry = self._fleet._registry + transport = await registry.transport(device_id) + return await transport.write(channel_id, value) + + async def route_read( + self, device_id: str, channel_id: str + ) -> Any: + """Route a read operation to the correct node. + + Same delegation pattern as ``route_write``: the transport + handles the MCP bridge transparently. + """ + registry = self._fleet._registry + transport = await registry.transport(device_id) + return await transport.read(channel_id) + + async def route_halt(self, device_id: str) -> Any: + """Route a halt to the correct node. + + Lock-free requirement preserved: ``transport.halt()`` does + not acquire the device I/O lock. + """ + registry = self._fleet._registry + transport = await registry.transport(device_id) + return await transport.halt() + + +# --------------------------------------------------------------------------- +# Fleet tools (LLM-callable) +# --------------------------------------------------------------------------- + + +def build_fleet_tools(fleet_manager: FleetManager) -> list[Any]: + """Generate ToolMetadata for fleet management tools. + + Three tools are exposed: + + - ``hw_fleet_status`` — show fleet topology, node health, device count + - ``hw_fleet_discover`` — probe endpoints for new nodes + - ``hw_fleet_halt_all`` — emergency stop all fleet devices + """ + from leapflow.plugins.protocol import ToolMetadata + + _FLEET_READ_META = { + "category": "hardware", + "risk_level": "none", + } + _FLEET_WRITE_META = { + "category": "hardware", + "risk_level": "external", + } + + async def _fleet_status(**kwargs: Any) -> dict[str, Any]: + """Return fleet topology and per-node health summary.""" + topo = fleet_manager.topology() + result = topo.to_dict() + # Enrich with per-node health for online nodes. + for node_dict in result.get("nodes", ()): + nid = node_dict.get("node_id", "") + if nid: + health = await fleet_manager.node_health(nid) + node_dict["health"] = health + return result + + async def _fleet_discover(**kwargs: Any) -> dict[str, Any]: + """Probe endpoints for new fleet nodes.""" + raw = kwargs.get("endpoints", "") + if isinstance(raw, str): + endpoints = tuple( + ep.strip() for ep in raw.split(",") if ep.strip() + ) + elif isinstance(raw, (list, tuple)): + endpoints = tuple(str(ep) for ep in raw) + else: + return {"error": "endpoints must be a comma-separated string or list"} + + if not endpoints: + return {"error": "no endpoints provided"} + + nodes = await fleet_manager.discover_nodes(endpoints) + return { + "discovered": [ + { + "node_id": n.node_id, + "display_name": n.display_name, + "mcp_endpoint": n.mcp_endpoint, + "device_ids": list(n.device_ids), + "status": n.status, + } + for n in nodes + ], + "total": len(nodes), + } + + async def _fleet_halt_all(**kwargs: Any) -> dict[str, Any]: + """Emergency stop ALL fleet devices on ALL nodes.""" + halt_map = await fleet_manager.halt_all() + return { + "halt_map": halt_map, + "node_count": len(halt_map), + "message": "Fleet-level emergency stop issued to all nodes", + } + + return [ + ToolMetadata( + name="hw_fleet_status", + description=( + "Show fleet topology: all nodes, their connection status, " + "device count, and per-node health. Use to understand the " + "distributed robot fleet before issuing commands." + ), + parameters_schema={"type": "object", "properties": {}}, + handler=_fleet_status, + x_leapflow=dict(_FLEET_READ_META), + provides_capabilities=("hw.fleet.status",), + ), + ToolMetadata( + name="hw_fleet_discover", + description=( + "Probe MCP endpoints for new fleet nodes. Supply a " + "comma-separated list of endpoints to check. Returns " + "discovered nodes that can be registered." + ), + parameters_schema={ + "type": "object", + "properties": { + "endpoints": { + "type": "string", + "description": ( + "Comma-separated MCP endpoints to probe, " + "e.g. 'http://192.168.1.50:9000,http://192.168.1.51:9000'" + ), + }, + }, + "required": ["endpoints"], + }, + handler=_fleet_discover, + x_leapflow=dict(_FLEET_READ_META), + provides_capabilities=("hw.fleet.discover",), + ), + ToolMetadata( + name="hw_fleet_halt_all", + description=( + "Emergency stop ALL devices on ALL fleet nodes immediately. " + "Use when any fleet-wide unsafe condition is observed. " + "Sends halt concurrently to every node; does not wait for " + "individual node responses." + ), + parameters_schema={"type": "object", "properties": {}}, + handler=_fleet_halt_all, + x_leapflow=dict(_FLEET_WRITE_META), + mutates_state=True, + provides_capabilities=("hw.fleet.halt_all",), + ), + ] + + +# --------------------------------------------------------------------------- +# FleetError +# --------------------------------------------------------------------------- + + +class FleetError(Exception): + """Raised when a fleet operation fails. + + Structured like ``TransportError`` but fleet-scoped: it carries a + ``failure_code`` for programmatic handling by the recovery layer. + """ + + def __init__(self, message: str, *, failure_code: str = "fleet_error") -> None: + super().__init__(message) + self.failure_code = failure_code + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + + +def _endpoint_to_node_id(endpoint: str) -> str: + """Derive a stable node_id from an endpoint string. + + Strips protocol, replaces non-alphanumeric chars with underscores, + and lowercases for consistency with device_id conventions. + """ + cleaned = endpoint.lower() + for prefix in ("http://", "https://", "stdio:"): + if cleaned.startswith(prefix): + cleaned = cleaned[len(prefix):] + return "".join(c if c.isalnum() else "_" for c in cleaned).strip("_") + + +__all__ = [ + "FleetError", + "FleetManager", + "FleetNode", + "FleetSessionRouter", + "FleetTopology", + "build_fleet_tools", +] diff --git a/src/leapflow/hardware/lhp_gateway.py b/src/leapflow/hardware/lhp_gateway.py new file mode 100644 index 00000000..e93fd2a0 --- /dev/null +++ b/src/leapflow/hardware/lhp_gateway.py @@ -0,0 +1,899 @@ +# Copyright (c) Alibaba, Inc. and its affiliates. +"""LHP Gateway: LLM-Hardware Protocol intermediate representation. + +The bridge between the LLM's semantic reasoning (System-2) and the +physical hardware stack. Two directions: + +UPLINK (Hardware -> LLM): + Raw device state -> structured context snapshots at three PCD levels. + The LLM never sees raw sensor readings; it sees a curated, resolution- + appropriate summary that fits its context budget. + +DOWNLINK (LLM -> Hardware): + Structured plan output -> typed SubtaskGoal sequences. + The LLM never emits raw joint commands; it produces semantic goals + that the ControlHierarchy decomposes into servo-level actions. + +PCD integration: the uplink respects Progressive Context Disclosure -- + a simple positioning task gets Level 0 (device list + basic state), + while a complex manipulation gets Level 2 (full kinematics + scene + graph + force readings). +""" + +from __future__ import annotations + +import logging +import time +import uuid +from dataclasses import dataclass, field +from enum import Enum +from typing import Any, Mapping + +logger = logging.getLogger(__name__) + +# --------------------------------------------------------------------------- +# Token estimation constant +# --------------------------------------------------------------------------- +_CHARS_PER_TOKEN = 4 + +# Snapshot freshness threshold for synchronous prompt_context(). +_SNAPSHOT_FRESHNESS_S = 5.0 + +# Complexity keywords used by recommend_level() heuristics. +_COMPLEX_KEYWORDS = frozenset({ + "assemble", "assembly", "insert", "thread", "screw", "solder", + "calibrate", "align", "complex", "multi-step", "coordinated", + "bimanual", "dual-arm", "force-controlled", "compliant", +}) +_SIMPLE_KEYWORDS = frozenset({ + "move", "go", "position", "home", "park", "stop", "halt", "wait", + "status", "check", "read", "observe", "look", "scan", +}) + + +# =================================================================== +# PCD Level +# =================================================================== + + +class PCDLevel(str, Enum): + """Progressive Context Disclosure level for hardware context.""" + + MINIMAL = "minimal" # Level 0: device names, connection status + TASK_RELEVANT = "task_relevant" # Level 1: + affordances, joint positions, health + RICH_CONTEXT = "rich_context" # Level 2: + kinematics, safety, full readings, frames + + +# =================================================================== +# Uplink IR: Hardware -> LLM +# =================================================================== + + +@dataclass(frozen=True) +class DeviceSnapshot: + """One device's state at a specific PCD level. + + Fields are progressively populated according to the requested level: + - Level 0 (minimal): device_id, device_class, display_name, status, connected, halt_supported + - Level 1 (task_relevant): + affordances, joint_positions, gripper_state, health + - Level 2 (rich_context): + kinematics, safety_limits, full_readings, trust_level, degradation_status + """ + + device_id: str + device_class: str + display_name: str + status: str # "connected", "disconnected", "degraded" + + # Level 0 (always present) + connected: bool = True + halt_supported: bool = True + + # Level 1 (task_relevant+) + affordances: tuple[str, ...] = () + joint_positions: Mapping[str, float] = field(default_factory=dict) + gripper_state: float | None = None + health: str = "" + + # Level 2 (rich_context) + kinematics: Mapping[str, Any] = field(default_factory=dict) + safety_limits: Mapping[str, Any] = field(default_factory=dict) + full_readings: Mapping[str, Any] = field(default_factory=dict) + trust_level: str = "" + degradation_status: str = "" + + def to_dict(self) -> dict[str, Any]: + """Serialize to a plain dict.""" + result: dict[str, Any] = { + "device_id": self.device_id, + "device_class": self.device_class, + "display_name": self.display_name, + "status": self.status, + "connected": self.connected, + "halt_supported": self.halt_supported, + } + if self.affordances: + result["affordances"] = list(self.affordances) + if self.joint_positions: + result["joint_positions"] = dict(self.joint_positions) + if self.gripper_state is not None: + result["gripper_state"] = self.gripper_state + if self.health: + result["health"] = self.health + if self.kinematics: + result["kinematics"] = dict(self.kinematics) + if self.safety_limits: + result["safety_limits"] = dict(self.safety_limits) + if self.full_readings: + result["full_readings"] = dict(self.full_readings) + if self.trust_level: + result["trust_level"] = self.trust_level + if self.degradation_status: + result["degradation_status"] = self.degradation_status + return result + + +@dataclass(frozen=True) +class HardwareContextSnapshot: + """Structured hardware context for LLM consumption. + + Not a raw dump -- a curated summary at the requested PCD level. + Designed to be injected into the system prompt or a tool result + without overwhelming the context window. + """ + + level: str # PCDLevel value + devices: tuple[DeviceSnapshot, ...] + capability_summary: Mapping[str, Any] + timestamp: float + token_estimate: int # rough estimate of tokens this will consume + + def to_prompt_text(self) -> str: + """Render as LLM-readable text for prompt injection. + + Structured Markdown format designed for LLM comprehension. + """ + lines: list[str] = [] + lines.append("## Hardware Context") + lines.append("") + level_label = { + PCDLevel.MINIMAL.value: "Minimal (L0)", + PCDLevel.TASK_RELEVANT.value: "Task-Relevant (L1)", + PCDLevel.RICH_CONTEXT.value: "Rich (L2)", + }.get(self.level, self.level) + lines.append(f"**Detail level**: {level_label} ") + lines.append(f"**Devices**: {len(self.devices)} ") + + cap = self.capability_summary + if cap: + affordances = cap.get("available_affordances", []) + if affordances: + lines.append(f"**Affordances**: {', '.join(str(a) for a in affordances)} ") + + lines.append("") + + for dev in self.devices: + lines.append(f"### {dev.display_name or dev.device_id}") + lines.append(f"- **Class**: {dev.device_class or 'unknown'}") + lines.append(f"- **Status**: {dev.status}") + if not dev.connected: + lines.append("- **⚠ DISCONNECTED**") + + # Level 1+ details + if dev.affordances: + lines.append(f"- **Can do**: {', '.join(dev.affordances)}") + if dev.joint_positions: + pos_str = ", ".join( + f"{k}={v:.3f}" for k, v in dev.joint_positions.items() + ) + lines.append(f"- **Joints**: {pos_str}") + if dev.gripper_state is not None: + lines.append(f"- **Gripper**: {dev.gripper_state:.2f}") + if dev.health: + lines.append(f"- **Health**: {dev.health}") + + # Level 2 details + if dev.kinematics: + dof = dev.kinematics.get("dof", "?") + chain = dev.kinematics.get("chain_type", "?") + lines.append(f"- **Kinematics**: {dof}-DOF {chain}") + if dev.safety_limits: + lines.append(f"- **Safety**: {_compact_safety(dev.safety_limits)}") + if dev.trust_level: + lines.append(f"- **Trust**: {dev.trust_level}") + if dev.degradation_status: + lines.append(f"- **Degradation**: {dev.degradation_status}") + if dev.full_readings: + lines.append("- **Readings**:") + for ch_id, reading in dev.full_readings.items(): + if isinstance(reading, dict): + val = reading.get("latest", reading.get("value", "?")) + unit = reading.get("unit", "") + quality = reading.get("quality", "") + q_tag = f" [{quality}]" if quality and quality != "ok" else "" + lines.append(f" - {ch_id}: {val} {unit}{q_tag}") + else: + lines.append(f" - {ch_id}: {reading}") + + lines.append("") + + return "\n".join(lines) + + def to_dict(self) -> dict[str, Any]: + """Serialize to a plain dict.""" + return { + "level": self.level, + "devices": [d.to_dict() for d in self.devices], + "capability_summary": dict(self.capability_summary), + "timestamp": self.timestamp, + "token_estimate": self.token_estimate, + } + + +@dataclass(frozen=True) +class HardwareContextDelta: + """Changes since the last snapshot, for incremental context updates.""" + + previous_timestamp: float + current_timestamp: float + devices_added: tuple[str, ...] = () + devices_removed: tuple[str, ...] = () + state_changes: tuple[Mapping[str, Any], ...] = () # per-device diffs + is_structural: bool = False # True if devices or affordances changed + + def to_prompt_text(self) -> str: + """Render delta as concise LLM-readable text.""" + lines: list[str] = [] + elapsed = self.current_timestamp - self.previous_timestamp + lines.append(f"## Hardware Delta (Δ{elapsed:.1f}s)") + lines.append("") + + if self.devices_added: + lines.append(f"**Added**: {', '.join(self.devices_added)}") + if self.devices_removed: + lines.append(f"**Removed**: {', '.join(self.devices_removed)}") + if self.is_structural: + lines.append("**⚠ Structural change** (devices or affordances changed)") + + for change in self.state_changes: + dev_id = change.get("device_id", "?") + lines.append(f"- **{dev_id}**: ", ) + for key, val in change.items(): + if key != "device_id": + lines.append(f" - {key}: {val}") + + if not (self.devices_added or self.devices_removed or self.state_changes): + lines.append("No changes detected.") + + return "\n".join(lines) + + def to_dict(self) -> dict[str, Any]: + """Serialize to a plain dict.""" + return { + "previous_timestamp": self.previous_timestamp, + "current_timestamp": self.current_timestamp, + "devices_added": list(self.devices_added), + "devices_removed": list(self.devices_removed), + "state_changes": [dict(c) for c in self.state_changes], + "is_structural": self.is_structural, + } + + +# =================================================================== +# Downlink IR: LLM -> Hardware +# =================================================================== + + +@dataclass(frozen=True) +class PlanStep: + """One step in an LLM plan.""" + + step_type: str # "reach_position", "grasp", "place", "wait", "verify", etc. + target: Mapping[str, Any] + device_id: str = "" # empty = auto-select from CapabilityIndex + description: str = "" + verify: bool = True + timeout_s: float = 30.0 + + +@dataclass(frozen=True) +class LLMPlanSegment: + """Structured plan output from LLM, ready for hardware execution. + + The LLM produces this (typically as a tool call result or structured + output), and the gateway converts it to SubtaskGoal sequences for + the ControlHierarchy. + """ + + plan_id: str + task_description: str + steps: tuple[PlanStep, ...] + constraints: Mapping[str, Any] = field(default_factory=dict) + timeout_s: float = 120.0 + + +# =================================================================== +# LHPGateway +# =================================================================== + + +class LHPGateway: + """Bidirectional LLM-Hardware Protocol gateway. + + Uplink: hardware state -> LLM context (at requested PCD level) + Downlink: LLM plan -> SubtaskGoal sequence -> ControlHierarchy + + Dependencies: + - registry: HardwareRegistry + - capability_index: CapabilityIndex (optional) + - environment_source: PhysicalEnvironmentSource (optional, for snapshot reuse) + - control_hierarchy: ControlHierarchy (optional, for plan execution) + """ + + def __init__( + self, + registry: Any, + *, + capability_index: Any = None, + environment_source: Any = None, + control_hierarchy: Any = None, + ) -> None: + self._registry = registry + self._capability_index = capability_index + self._environment_source = environment_source + self._control_hierarchy = control_hierarchy + self._last_snapshot: HardwareContextSnapshot | None = None + + # ------------------------------------------------------------------ + # Uplink: Hardware -> LLM + # ------------------------------------------------------------------ + + async def snapshot( + self, level: str = PCDLevel.TASK_RELEVANT.value + ) -> HardwareContextSnapshot: + """Build a hardware context snapshot at the requested PCD level. + + Level 0 (minimal): ~50 tokens per device + Level 1 (task_relevant): ~150 tokens per device + Level 2 (rich_context): ~400 tokens per device + """ + # Reuse PhysicalEnvironmentSource data when available to avoid + # a duplicate registry scan. + env_data: dict[str, Any] | None = None + if self._environment_source is not None: + try: + env_data = await self._environment_source.take_snapshot() + except Exception: # noqa: BLE001 – fallback to direct scan + logger.debug("Environment source snapshot failed, using direct scan", exc_info=True) + + device_snapshots: list[DeviceSnapshot] = [] + all_affordances: set[str] = set() + + for context in self._registry.contexts(): + env_device = _find_env_device(env_data, context.device_id) if env_data else None + ds = await self._build_device_snapshot(context, level, env_device=env_device) + device_snapshots.append(ds) + all_affordances.update(ds.affordances) + + # Merge capability_index affordances. + if self._capability_index is not None: + try: + all_affordances.update(self._capability_index.all_affordances()) + except Exception: # noqa: BLE001 + pass + + cap_summary: dict[str, Any] = { + "total_affordances": len(all_affordances), + "available_affordances": sorted(all_affordances), + "device_count": len(device_snapshots), + } + + snap = HardwareContextSnapshot( + level=level, + devices=tuple(device_snapshots), + capability_summary=cap_summary, + timestamp=time.time(), + token_estimate=0, + ) + # Replace with computed token estimate (frozen, so rebuild). + token_est = self._estimate_tokens(snap) + snap = HardwareContextSnapshot( + level=snap.level, + devices=snap.devices, + capability_summary=snap.capability_summary, + timestamp=snap.timestamp, + token_estimate=token_est, + ) + self._last_snapshot = snap + return snap + + async def delta(self) -> HardwareContextDelta | None: + """Compute delta since last snapshot. None if nothing changed.""" + if self._last_snapshot is None: + return None + + previous = self._last_snapshot + current = await self.snapshot(previous.level) + + prev_ids = {d.device_id for d in previous.devices} + curr_ids = {d.device_id for d in current.devices} + + added = tuple(sorted(curr_ids - prev_ids)) + removed = tuple(sorted(prev_ids - curr_ids)) + + # Compute per-device state changes for devices present in both. + prev_map = {d.device_id: d for d in previous.devices} + curr_map = {d.device_id: d for d in current.devices} + changes: list[dict[str, Any]] = [] + + for did in sorted(prev_ids & curr_ids): + pd, cd = prev_map[did], curr_map[did] + diff: dict[str, Any] = {"device_id": did} + changed = False + if pd.status != cd.status: + diff["status"] = f"{pd.status} → {cd.status}" + changed = True + if pd.health != cd.health and cd.health: + diff["health"] = f"{pd.health} → {cd.health}" + changed = True + if pd.affordances != cd.affordances: + diff["affordances_before"] = list(pd.affordances) + diff["affordances_after"] = list(cd.affordances) + changed = True + if pd.connected != cd.connected: + diff["connected"] = cd.connected + changed = True + if changed: + changes.append(diff) + + is_structural = bool(added or removed) or any( + "affordances_before" in c for c in changes + ) + + if not (added or removed or changes): + return None + + return HardwareContextDelta( + previous_timestamp=previous.timestamp, + current_timestamp=current.timestamp, + devices_added=added, + devices_removed=removed, + state_changes=tuple(changes), + is_structural=is_structural, + ) + + def prompt_context(self, level: str = PCDLevel.TASK_RELEVANT.value) -> str: + """Synchronous: return the latest snapshot as prompt-injectable text. + + Called by PromptAssembler during prompt construction. + Uses cached snapshot if fresh enough (< 5s old). + """ + snap = self._last_snapshot + if snap is not None and snap.level == level: + age = time.time() - snap.timestamp + if age < _SNAPSHOT_FRESHNESS_S: + return snap.to_prompt_text() + + # No fresh snapshot available; return a minimal fallback from cache + # or an empty string. The caller should have called snapshot() first + # in the async setup path. + if snap is not None: + return snap.to_prompt_text() + return "" + + # ------------------------------------------------------------------ + # Downlink: LLM -> Hardware + # ------------------------------------------------------------------ + + async def execute_plan(self, plan: LLMPlanSegment) -> dict[str, Any]: + """Convert LLM plan to SubtaskGoals and execute via ControlHierarchy. + + 1. Parse PlanSteps -> SubtaskGoals + 2. Resolve device_id for each step (via CapabilityIndex if auto) + 3. Delegate to ControlHierarchy.execute_task() + 4. Return aggregated results + """ + if self._control_hierarchy is None: + return { + "ok": False, + "error": "no_control_hierarchy", + "plan_id": plan.plan_id, + "detail": "ControlHierarchy not configured; cannot execute hardware plans.", + } + + goals = self.plan_to_goals(plan) + if not goals: + return { + "ok": False, + "error": "empty_plan", + "plan_id": plan.plan_id, + "detail": "Plan produced no executable goals.", + } + + try: + result = await self._control_hierarchy.execute_task(goals) + except Exception as exc: # noqa: BLE001 – surface as structured failure + logger.warning( + "LHP plan execution failed for %s: %s", + plan.plan_id, + exc, + exc_info=True, + ) + return { + "ok": False, + "error": "execution_failed", + "plan_id": plan.plan_id, + "detail": str(exc), + } + + result["plan_id"] = plan.plan_id + result["ok"] = result.get("failed", 1) == 0 + return result + + def parse_plan(self, raw: Mapping[str, Any]) -> LLMPlanSegment: + """Parse a raw dict (from LLM tool output) into a typed plan. + + Tolerant of missing fields: each absent key uses a sensible default + so malformed LLM output does not crash the pipeline. + """ + plan_id = str(raw.get("plan_id") or f"plan_{uuid.uuid4().hex[:8]}") + task_desc = str(raw.get("task_description") or raw.get("description") or "") + timeout = _safe_float(raw.get("timeout_s"), 120.0) + constraints = raw.get("constraints") or {} + if not isinstance(constraints, Mapping): + constraints = {} + + raw_steps = raw.get("steps") or [] + if not isinstance(raw_steps, (list, tuple)): + raw_steps = [] + + steps: list[PlanStep] = [] + for idx, rs in enumerate(raw_steps): + if not isinstance(rs, Mapping): + logger.debug("LHP parse_plan: skipping non-mapping step at index %d", idx) + continue + step_type = str(rs.get("step_type") or rs.get("type") or "unknown") + target = rs.get("target") or {} + if not isinstance(target, Mapping): + target = {} + steps.append(PlanStep( + step_type=step_type, + target=target, + device_id=str(rs.get("device_id") or ""), + description=str(rs.get("description") or step_type), + verify=bool(rs.get("verify", True)), + timeout_s=_safe_float(rs.get("timeout_s"), 30.0), + )) + + return LLMPlanSegment( + plan_id=plan_id, + task_description=task_desc, + steps=tuple(steps), + constraints=constraints, + timeout_s=timeout, + ) + + def plan_to_goals(self, plan: LLMPlanSegment) -> tuple[Any, ...]: + """Convert plan steps to SubtaskGoal sequence. + + Bridges PlanStep -> SubtaskGoal, resolving device routing + and strategy hints. + """ + from leapflow.hardware.control_hierarchy import SubtaskGoal + + goals: list[SubtaskGoal] = [] + for idx, step in enumerate(plan.steps): + device_id = step.device_id + # Auto-resolve device via CapabilityIndex when not specified. + if not device_id and self._capability_index is not None: + device_id = self._resolve_device_for_step(step) + + goal_id = f"{plan.plan_id}_step{idx}_{uuid.uuid4().hex[:6]}" + strategy_hint = self._infer_strategy_hint(step) + + goals.append(SubtaskGoal( + goal_id=goal_id, + description=step.description or step.step_type, + goal_type=step.step_type, + target=dict(step.target), + strategy_hint=strategy_hint, + timeout_s=step.timeout_s, + verify=step.verify, + )) + + return tuple(goals) + + # ------------------------------------------------------------------ + # PCD Level Selection + # ------------------------------------------------------------------ + + def recommend_level(self, task_description: str = "") -> str: + """Suggest PCD level based on task complexity. + + Simple tasks ("move to position") -> Level 0 + Standard manipulation ("pick up the cup") -> Level 1 + Complex tasks ("assemble the parts") -> Level 2 + + Uses keyword heuristics, not LLM inference. + """ + if not task_description: + return PCDLevel.TASK_RELEVANT.value + + lower = task_description.lower() + words = set(lower.split()) + + # Check complex keywords first (higher priority). + if words & _COMPLEX_KEYWORDS or any(kw in lower for kw in _COMPLEX_KEYWORDS): + return PCDLevel.RICH_CONTEXT.value + + # Check simple keywords. + if words & _SIMPLE_KEYWORDS and not (words & _COMPLEX_KEYWORDS): + return PCDLevel.MINIMAL.value + + # Default for standard manipulation tasks. + return PCDLevel.TASK_RELEVANT.value + + # ------------------------------------------------------------------ + # Internal: device snapshot building + # ------------------------------------------------------------------ + + async def _build_device_snapshot( + self, + context: Any, + level: str, + *, + env_device: dict[str, Any] | None = None, + ) -> DeviceSnapshot: + """Build one device's snapshot at the requested level. + + Reads from the HardwareContext declaration and optionally merges + live data from the PhysicalEnvironmentSource snapshot. + """ + device_id = context.device_id + device_class = getattr(context, "device_class", "") or "" + display_name = getattr(context, "display_name", "") or device_id + + # Determine connection status. + connected = True + health_str = "" + if env_device is not None: + connected = bool(env_device.get("connected", True)) + health_str = str(env_device.get("health", "ok")) + + if not connected: + status = "disconnected" + elif health_str in ("degraded", "stale", "unreachable"): + status = "degraded" + else: + status = "connected" + + halt_supported = bool(getattr(context, "halt_supported", False)) + + # Level 0: basic info only. + if level == PCDLevel.MINIMAL.value: + return DeviceSnapshot( + device_id=device_id, + device_class=device_class, + display_name=display_name, + status=status, + connected=connected, + halt_supported=halt_supported, + ) + + # Level 1+: affordances, joint positions, gripper state, health. + affordances: tuple[str, ...] = () + cap = getattr(context, "capabilities", None) + if cap is not None: + affordances = tuple(getattr(cap, "affordances", ()) or ()) + + # Collect joint positions from channel summaries. + joint_positions: dict[str, float] = {} + gripper_state: float | None = None + if env_device is not None: + raw_channels = env_device.get("channels") or {} + if isinstance(raw_channels, dict): + for ch_id, ch_data in raw_channels.items(): + if isinstance(ch_data, dict) and ch_data.get("value") is not None: + try: + val = float(ch_data["value"]) + except (TypeError, ValueError): + continue + if "gripper" in ch_id.lower(): + gripper_state = val + else: + joint_positions[ch_id] = val + else: + # Try reading from registry's channel summaries. + for ch in context.channels: + if not ch.is_readable or ch.is_media: + continue + summary = self._channel_summary(device_id, ch.channel_id) + if summary and summary.get("samples", 0) > 0: + val = summary.get("latest") + if val is not None: + try: + fval = float(val) + except (TypeError, ValueError): + continue + if "gripper" in ch.channel_id.lower(): + gripper_state = fval + else: + joint_positions[ch.channel_id] = fval + + if level == PCDLevel.TASK_RELEVANT.value: + return DeviceSnapshot( + device_id=device_id, + device_class=device_class, + display_name=display_name, + status=status, + connected=connected, + halt_supported=halt_supported, + affordances=affordances, + joint_positions=joint_positions, + gripper_state=gripper_state, + health=health_str or "ok", + ) + + # Level 2: full kinematics, safety, readings, trust, degradation. + kin_dict: Mapping[str, Any] = {} + kin = getattr(context, "kinematics", None) + if kin is not None: + kin_dict = kin.to_dict() if hasattr(kin, "to_dict") else { + "dof": getattr(kin, "dof", 0), + "chain_type": getattr(kin, "chain_type", ""), + } + + safety_dict: Mapping[str, Any] = {} + safety = getattr(context, "safety", None) + if safety is not None: + safety_dict = safety.to_dict() if hasattr(safety, "to_dict") else {} + + # Full channel readings. + full_readings: dict[str, Any] = {} + for ch in context.channels: + if not ch.is_readable or ch.is_media: + continue + summary = self._channel_summary(device_id, ch.channel_id) + if summary and summary.get("samples", 0) > 0: + full_readings[ch.channel_id] = summary + elif env_device is not None: + raw_channels = env_device.get("channels") or {} + if isinstance(raw_channels, dict) and ch.channel_id in raw_channels: + full_readings[ch.channel_id] = raw_channels[ch.channel_id] + + trust_level = "" + trust_cfg = getattr(context, "trust_config", None) + if trust_cfg is not None: + trust_level = getattr(trust_cfg, "initial_level", "") or "" + + degradation_status = "" + degrad = getattr(context, "degradation", None) + if degrad is not None: + degradation_status = getattr(degrad, "comm_loss_policy", "") or "" + + return DeviceSnapshot( + device_id=device_id, + device_class=device_class, + display_name=display_name, + status=status, + connected=connected, + halt_supported=halt_supported, + affordances=affordances, + joint_positions=joint_positions, + gripper_state=gripper_state, + health=health_str or "ok", + kinematics=kin_dict, + safety_limits=safety_dict, + full_readings=full_readings, + trust_level=trust_level, + degradation_status=degradation_status, + ) + + def _channel_summary( + self, device_id: str, channel_id: str + ) -> dict[str, Any] | None: + """Get channel reading summary from the registry's stream sources.""" + try: + return self._registry.channel_summary(device_id, channel_id) + except Exception: # noqa: BLE001 – observation must not fail + return None + + def _estimate_tokens(self, snapshot: HardwareContextSnapshot) -> int: + """Rough token count estimate for context budget planning. + + Uses ~4 characters per token as a conservative estimate. + """ + text = snapshot.to_prompt_text() + return max(1, len(text) // _CHARS_PER_TOKEN) + + # ------------------------------------------------------------------ + # Internal: downlink helpers + # ------------------------------------------------------------------ + + def _resolve_device_for_step(self, step: PlanStep) -> str: + """Auto-select a device for a plan step via CapabilityIndex. + + Maps step_type to an affordance and picks the sole provider or + the first one when multiple are available. + """ + if self._capability_index is None: + return "" + + # The step_type often maps directly to an affordance name. + affordance = step.step_type + entries = self._capability_index.resolve(affordance) + if entries and len(entries) == 1: + return entries[0].device_id + + # If multiple, return the first (caller can refine). + if entries: + return entries[0].device_id + + return "" + + @staticmethod + def _infer_strategy_hint(step: PlanStep) -> str: + """Infer a strategy hint from the step type. + + Maps well-known step types to InferenceStrategy identifiers + that the ControlHierarchy can consume. + """ + _STRATEGY_MAP: dict[str, str] = { + "reach_position": "position_control", + "grasp": "grasp_policy", + "place": "place_policy", + "follow_trajectory": "trajectory_tracking", + "insert": "compliant_insertion", + "pour": "pour_policy", + "push": "push_policy", + } + return _STRATEGY_MAP.get(step.step_type, "") + + +# =================================================================== +# Module-private helpers +# =================================================================== + + +def _find_env_device( + env_data: dict[str, Any] | None, device_id: str +) -> dict[str, Any] | None: + """Find a device dict inside a PhysicalEnvironmentSource snapshot.""" + if env_data is None: + return None + for dev in env_data.get("devices", ()): + if isinstance(dev, dict) and dev.get("device_id") == device_id: + return dev + return None + + +def _safe_float(value: Any, default: float) -> float: + """Coerce a value to float, returning *default* on failure.""" + if value is None: + return default + try: + return float(value) + except (TypeError, ValueError): + return default + + +def _compact_safety(limits: Mapping[str, Any]) -> str: + """Render safety limits as a compact one-line string.""" + parts: list[str] = [] + if limits.get("max_velocity_rad_s"): + parts.append(f"vel≤{limits['max_velocity_rad_s']}rad/s") + if limits.get("max_force_n"): + parts.append(f"force≤{limits['max_force_n']}N") + if limits.get("emergency_decel_s"): + parts.append(f"e-stop={limits['emergency_decel_s']}s") + return ", ".join(parts) if parts else "default" + + +__all__ = [ + "DeviceSnapshot", + "HardwareContextDelta", + "HardwareContextSnapshot", + "LHPGateway", + "LLMPlanSegment", + "PCDLevel", + "PlanStep", +] diff --git a/src/leapflow/hardware/plugin.py b/src/leapflow/hardware/plugin.py index 1a43f050..9506884e 100644 --- a/src/leapflow/hardware/plugin.py +++ b/src/leapflow/hardware/plugin.py @@ -27,7 +27,7 @@ class HardwareContextPlugin: - """Exposes admitted hardware devices through the eight generic tools.""" + """Exposes admitted hardware devices through the twelve generic tools.""" def __init__(self) -> None: self._registry: Any = None @@ -96,8 +96,29 @@ def bind_runtime(self, **deps: Any) -> None: if self._registry is None: return + self._register_trust_configs() self._register_teardown() + def _register_trust_configs(self) -> None: + """Register per-device TrustConfig with the trust gate after admission. + + Idempotent: ``register_device`` is a no-op for an already-known device. + Errors are swallowed per device so one bad config does not prevent the + rest from being registered. + """ + gate = self._hardware_trust_gate + if gate is None or not hasattr(gate, "register_device"): + return + for ctx in self._registry.contexts(): + try: + gate.register_device(ctx.device_id, ctx.trust_config) + except Exception as exc: # noqa: BLE001 - registration must not block startup + logger.warning( + "Could not register trust config for %s: %s", + ctx.device_id, + exc, + ) + def _register_teardown(self) -> None: """Close device connections when the owning scope unwinds. diff --git a/src/leapflow/hardware/realtime.py b/src/leapflow/hardware/realtime.py new file mode 100644 index 00000000..c0ca4527 --- /dev/null +++ b/src/leapflow/hardware/realtime.py @@ -0,0 +1,773 @@ +# Copyright (c) Alibaba, Inc. and its affiliates. +"""Realtime control loop: higher-level orchestration on top of ControlBus. + +While :class:`HighFrequencyControlBus` provides the raw cycle machinery (thread, +timing, transport I/O), this module adds: + +1. Built-in control policies: PID joint controller, impedance controller +2. Policy chaining: System-1.5 neural policy → System-1 servo refiner +3. Trajectory tracking: follow a pre-planned joint trajectory at Hz rate +4. Watchdog: detect and recover from policy computation overruns +5. Telemetry: publish cycle stats to EventBus for LeapBoard display + +This is the "System-1" layer in the System-1/1.5/2 hierarchy: + +- System-2 (LLM, 1–5 s): high-level planning and reasoning +- System-1.5 (VLA, 5–30 Hz): learned policy inference +- System-1 (this module, 100–1000 Hz): servo-level closed-loop control +""" + +from __future__ import annotations + +import logging +import math +import threading +import time +import uuid +from dataclasses import dataclass, field +from typing import Any, Mapping + +from leapflow.hardware.control_bus import ( + ControlBusConfig, + ControlCommand, + ControlPolicy, + ControlState, + HighFrequencyControlBus, +) + +logger = logging.getLogger(__name__) + + +# --------------------------------------------------------------------------- +# Built-in control policies +# --------------------------------------------------------------------------- + + +@dataclass +class _PIDState: + """Per-joint PID accumulator.""" + + integral: float = 0.0 + prev_error: float = 0.0 + + +class PIDJointController: + """PID controller for joint position tracking. + + Implements :class:`ControlPolicy` Protocol. One PID loop per joint, + configurable gains. Suitable for precise positioning tasks and as the + System-1 refiner under a System-1.5 neural policy. + """ + + def __init__( + self, + joint_ids: tuple[str, ...], + *, + kp: float = 10.0, + ki: float = 0.1, + kd: float = 1.0, + max_output: float = 6.28, + output_mode: str = "velocity", + ) -> None: + if output_mode not in ("velocity", "position"): + raise ValueError(f"output_mode must be 'velocity' or 'position', got {output_mode!r}") + self._joint_ids = joint_ids + self._kp = kp + self._ki = ki + self._kd = kd + self._max_output = max_output + self._output_mode = output_mode + + # Thread-safe target: written from async world, read from RT thread. + self._lock = threading.Lock() + self._targets: dict[str, float] = {jid: 0.0 for jid in joint_ids} + + # Per-joint PID state (only accessed from the RT thread). + self._pid: dict[str, _PIDState] = {jid: _PIDState() for jid in joint_ids} + + @property + def policy_id(self) -> str: + return "pid_joint" + + def set_target(self, targets: Mapping[str, float]) -> None: + """Set target positions for each joint. Thread-safe.""" + with self._lock: + for jid, val in targets.items(): + if jid in self._targets: + self._targets[jid] = val + + def compute(self, state: ControlState) -> ControlCommand: + """PID computation for each joint.""" + with self._lock: + targets = dict(self._targets) + + dt = state.cycle_dt_s + if dt <= 0: + dt = 1e-4 # guard against first-cycle zero dt + + commands: dict[str, float] = {} + for jid in self._joint_ids: + target = targets.get(jid, 0.0) + current = state.joint_positions.get(jid, 0.0) + error = target - current + + pid = self._pid[jid] + pid.integral += error * dt + derivative = (error - pid.prev_error) / dt if dt > 0 else 0.0 + pid.prev_error = error + + output = self._kp * error + self._ki * pid.integral + self._kd * derivative + + # Clamp output. + output = max(-self._max_output, min(self._max_output, output)) + + if self._output_mode == "velocity": + commands[jid] = output + else: + # Position mode: command = current + clamped delta. + commands[jid] = current + output + + return ControlCommand(joint_commands=commands) + + def reset(self) -> None: + """Reset integral accumulators.""" + for pid in self._pid.values(): + pid.integral = 0.0 + pid.prev_error = 0.0 + + +class ImpedanceController: + """Impedance controller for compliant manipulation. + + Implements :class:`ControlPolicy` Protocol. Models the robot end-effector + as a virtual mass-spring-damper system:: + + F = M * a_desired + D * (v - v_ref) + K * (x - x_ref) + + Useful for tasks requiring force control (insertion, polishing) and for + safe human-robot interaction. + """ + + def __init__( + self, + joint_ids: tuple[str, ...], + *, + stiffness: float = 100.0, + damping: float = 10.0, + inertia: float = 1.0, + ) -> None: + if inertia <= 0: + raise ValueError(f"inertia must be positive, got {inertia}") + self._joint_ids = joint_ids + self._stiffness = stiffness + self._damping = damping + self._inertia = inertia + + # Thread-safe reference: written from async world, read from RT thread. + self._lock = threading.Lock() + self._ref_pos: dict[str, float] = {jid: 0.0 for jid in joint_ids} + self._ref_vel: dict[str, float] = {jid: 0.0 for jid in joint_ids} + + @property + def policy_id(self) -> str: + return "impedance" + + def set_reference( + self, + position: Mapping[str, float], + velocity: Mapping[str, float] | None = None, + ) -> None: + """Set reference trajectory point. Thread-safe.""" + with self._lock: + for jid, val in position.items(): + if jid in self._ref_pos: + self._ref_pos[jid] = val + if velocity is not None: + for jid, val in velocity.items(): + if jid in self._ref_vel: + self._ref_vel[jid] = val + + def compute(self, state: ControlState) -> ControlCommand: + """Impedance control: compute torque from spring-damper model.""" + with self._lock: + ref_pos = dict(self._ref_pos) + ref_vel = dict(self._ref_vel) + + commands: dict[str, float] = {} + for jid in self._joint_ids: + x = state.joint_positions.get(jid, 0.0) + v = state.joint_velocities.get(jid, 0.0) + x_ref = ref_pos.get(jid, 0.0) + v_ref = ref_vel.get(jid, 0.0) + + # F = K * (x_ref - x) + D * (v_ref - v) + # a_desired = F / M → this becomes the velocity-change command + force = self._stiffness * (x_ref - x) + self._damping * (v_ref - v) + accel = force / self._inertia + # Integrate acceleration over one dt to get velocity command. + dt = state.cycle_dt_s if state.cycle_dt_s > 0 else 1e-4 + commands[jid] = v + accel * dt + + return ControlCommand(joint_commands=commands) + + def reset(self) -> None: + """Reset reference to zeros.""" + with self._lock: + for jid in self._ref_pos: + self._ref_pos[jid] = 0.0 + self._ref_vel[jid] = 0.0 + + +# --------------------------------------------------------------------------- +# Trajectory tracker +# --------------------------------------------------------------------------- + + +def _lerp(a: float, b: float, t: float) -> float: + """Linear interpolation between *a* and *b* at parameter *t* ∈ [0, 1].""" + return a + (b - a) * t + + +class TrajectoryTracker: + """Follows a pre-planned joint trajectory using PID or impedance control. + + Wraps a :class:`ControlPolicy` (typically :class:`PIDJointController`) and + feeds it target positions from a time-indexed trajectory at each cycle. + + Implements :class:`ControlPolicy` Protocol so it can be directly passed to + :class:`HighFrequencyControlBus`. + """ + + def __init__( + self, + inner_policy: ControlPolicy, + trajectory: tuple[tuple[float, Mapping[str, float]], ...], + *, + loop_mode: str = "once", + ) -> None: + if not trajectory: + raise ValueError("trajectory must contain at least one waypoint") + if loop_mode not in ("once", "loop", "hold_final"): + raise ValueError( + f"loop_mode must be 'once', 'loop', or 'hold_final', got {loop_mode!r}" + ) + self._inner = inner_policy + self._trajectory = trajectory + self._loop_mode = loop_mode + self._duration = trajectory[-1][0] - trajectory[0][0] if len(trajectory) > 1 else 0.0 + + # Runtime state — only touched from the RT thread. + self._start_time: float | None = None + self._complete = False + + @property + def policy_id(self) -> str: + return f"trajectory_tracker:{self._inner.policy_id}" + + def compute(self, state: ControlState) -> ControlCommand: + """Interpolate trajectory at current time, set as target for inner policy.""" + if self._start_time is None: + self._start_time = state.timestamp + + elapsed = state.timestamp - self._start_time + traj = self._trajectory + + if self._duration <= 0: + # Single waypoint — just hold it. + targets = dict(traj[0][1]) + self._set_inner_target(targets) + self._complete = True + return self._inner.compute(state) + + # Effective elapsed after loop-mode handling. + effective = elapsed + if self._loop_mode == "loop" and self._duration > 0: + effective = elapsed % self._duration + traj[0][0] + elif self._loop_mode == "once": + if elapsed >= self._duration: + self._complete = True + # Return halt once trajectory is complete. + return ControlCommand(joint_commands={}, halt=True) + effective = elapsed + traj[0][0] + elif self._loop_mode == "hold_final": + if elapsed >= self._duration: + self._complete = True + effective = traj[-1][0] + else: + effective = elapsed + traj[0][0] + + # Find the bracketing waypoints and interpolate. + targets = self._interpolate(effective, traj) + self._set_inner_target(targets) + return self._inner.compute(state) + + def _set_inner_target(self, targets: dict[str, float]) -> None: + """Feed interpolated targets to the inner policy.""" + if hasattr(self._inner, "set_target"): + self._inner.set_target(targets) # type: ignore[attr-defined] + elif hasattr(self._inner, "set_reference"): + self._inner.set_reference(targets) # type: ignore[attr-defined] + + @staticmethod + def _interpolate( + t: float, + traj: tuple[tuple[float, Mapping[str, float]], ...], + ) -> dict[str, float]: + """Linear interpolation between the two nearest waypoints.""" + # Before first waypoint. + if t <= traj[0][0]: + return dict(traj[0][1]) + # After last waypoint. + if t >= traj[-1][0]: + return dict(traj[-1][1]) + + # Binary search for the bracket. + lo, hi = 0, len(traj) - 1 + while hi - lo > 1: + mid = (lo + hi) // 2 + if traj[mid][0] <= t: + lo = mid + else: + hi = mid + + t0, pos0 = traj[lo] + t1, pos1 = traj[hi] + dt = t1 - t0 + alpha = (t - t0) / dt if dt > 0 else 0.0 + + result: dict[str, float] = {} + all_keys = set(pos0.keys()) | set(pos1.keys()) + for k in all_keys: + v0 = pos0.get(k, 0.0) + v1 = pos1.get(k, 0.0) + result[k] = _lerp(v0, v1, alpha) + return result + + @property + def progress(self) -> float: + """0.0 to 1.0, fraction of trajectory completed.""" + if self._start_time is None or self._duration <= 0: + return 0.0 + elapsed = time.monotonic() - self._start_time + return min(1.0, max(0.0, elapsed / self._duration)) + + @property + def is_complete(self) -> bool: + """Whether the trajectory has reached its end.""" + return self._complete + + def reset(self) -> None: + """Reset tracker and inner policy state.""" + self._start_time = None + self._complete = False + self._inner.reset() + + +# --------------------------------------------------------------------------- +# Policy chain (System-1.5 → System-1) +# --------------------------------------------------------------------------- + + +class PolicyChain: + """Chains a high-level policy with a low-level servo refiner. + + The outer policy (e.g. VLA at 10 Hz) produces coarse commands. The inner + policy (e.g. PID at 500 Hz) refines them into smooth servo-level control, + interpolating between the outer policy's sparse updates. + + Implements :class:`ControlPolicy` Protocol. + """ + + def __init__( + self, + outer: ControlPolicy, + inner: ControlPolicy, + *, + outer_rate_hz: float = 10.0, + ) -> None: + if outer_rate_hz <= 0: + raise ValueError(f"outer_rate_hz must be positive, got {outer_rate_hz}") + self._outer = outer + self._inner = inner + self._outer_rate_hz = outer_rate_hz + + # Cycle bookkeeping — only accessed from the RT thread. + self._cycles_since_outer: int = 0 + self._outer_period_cycles: int = 1 # computed on first call + self._first_call = True + self._last_outer_command: ControlCommand | None = None + + @property + def policy_id(self) -> str: + return f"chain:{self._outer.policy_id}+{self._inner.policy_id}" + + def compute(self, state: ControlState) -> ControlCommand: + """Every N cycles call outer for new target; every cycle call inner for servo.""" + if self._first_call: + bus_hz = 1.0 / state.cycle_dt_s if state.cycle_dt_s > 0 else 100.0 + self._outer_period_cycles = max(1, round(bus_hz / self._outer_rate_hz)) + self._first_call = False + self._cycles_since_outer = self._outer_period_cycles # force first outer call + + self._cycles_since_outer += 1 + if self._cycles_since_outer >= self._outer_period_cycles: + self._cycles_since_outer = 0 + outer_cmd = self._outer.compute(state) + self._last_outer_command = outer_cmd + if outer_cmd.halt: + return outer_cmd + # Feed outer targets to inner policy. + if hasattr(self._inner, "set_target"): + self._inner.set_target(outer_cmd.joint_commands) # type: ignore[attr-defined] + elif hasattr(self._inner, "set_reference"): + self._inner.set_reference(outer_cmd.joint_commands) # type: ignore[attr-defined] + + return self._inner.compute(state) + + def reset(self) -> None: + """Reset both outer and inner policies.""" + self._outer.reset() + self._inner.reset() + self._cycles_since_outer = 0 + self._first_call = True + self._last_outer_command = None + + +# --------------------------------------------------------------------------- +# Telemetry event +# --------------------------------------------------------------------------- + + +@dataclass(frozen=True) +class ControlTelemetryEvent: + """Published to EventBus at configurable intervals for LeapBoard display.""" + + session_id: str + device_id: str + policy_id: str + cycles: int + mean_cycle_ms: float + max_jitter_ms: float + overruns: int + is_running: bool + progress: float | None = None # only for trajectory sessions + + +# --------------------------------------------------------------------------- +# RealtimeControlLoop — high-level orchestrator +# --------------------------------------------------------------------------- + + +@dataclass +class _SessionInfo: + """Internal bookkeeping for an active control session.""" + + session_id: str + device_id: str + policy: ControlPolicy + started_at: float = field(default_factory=time.monotonic) + stopped_at: float | None = None + config: ControlBusConfig | None = None + + +class RealtimeControlLoop: + """High-level orchestrator for real-time control sessions. + + Wraps :class:`HighFrequencyControlBus` with: + + - Named control sessions (start → run → stop lifecycle) + - Built-in policy selection (PID, impedance, trajectory, chain) + - Telemetry publishing to EventBus + - Graceful transition between policies (ramp down → switch → ramp up) + """ + + def __init__( + self, + bus: HighFrequencyControlBus, + *, + event_bus: Any = None, + telemetry_interval_s: float = 1.0, + ) -> None: + self._bus = bus + self._event_bus = event_bus + self._telemetry_interval_s = telemetry_interval_s + + self._session: _SessionInfo | None = None + self._telemetry_timer: threading.Timer | None = None + self._lock = threading.Lock() + + # -- PID session ------------------------------------------------------- + + async def start_pid( + self, + device_id: str, + joint_ids: tuple[str, ...], + targets: Mapping[str, float], + *, + config: ControlBusConfig | None = None, + kp: float = 10.0, + ki: float = 0.1, + kd: float = 1.0, + max_output: float = 6.28, + output_mode: str = "velocity", + ) -> str: + """Start PID position tracking. Returns session_id.""" + policy = PIDJointController( + joint_ids, kp=kp, ki=ki, kd=kd, + max_output=max_output, output_mode=output_mode, + ) + policy.set_target(targets) + return await self._start_session(device_id, policy, config) + + # -- Impedance session ------------------------------------------------- + + async def start_impedance( + self, + device_id: str, + joint_ids: tuple[str, ...], + position: Mapping[str, float], + *, + config: ControlBusConfig | None = None, + velocity: Mapping[str, float] | None = None, + stiffness: float = 100.0, + damping: float = 10.0, + inertia: float = 1.0, + ) -> str: + """Start impedance control. Returns session_id.""" + policy = ImpedanceController( + joint_ids, stiffness=stiffness, damping=damping, inertia=inertia, + ) + policy.set_reference(position, velocity) + return await self._start_session(device_id, policy, config) + + # -- Trajectory session ------------------------------------------------ + + async def start_trajectory( + self, + device_id: str, + trajectory: tuple[tuple[float, Mapping[str, float]], ...], + *, + controller: str = "pid", + config: ControlBusConfig | None = None, + loop_mode: str = "once", + **controller_kwargs: Any, + ) -> str: + """Follow a joint trajectory. Returns session_id.""" + # Infer joint_ids from the first waypoint. + joint_ids = tuple(trajectory[0][1].keys()) + + inner: ControlPolicy + if controller == "pid": + inner = PIDJointController(joint_ids, **controller_kwargs) + elif controller == "impedance": + inner = ImpedanceController(joint_ids, **controller_kwargs) + else: + raise ValueError(f"Unknown controller type: {controller!r}") + + policy = TrajectoryTracker(inner, trajectory, loop_mode=loop_mode) + return await self._start_session(device_id, policy, config) + + # -- Policy-chain session ---------------------------------------------- + + async def start_policy_chain( + self, + device_id: str, + outer_policy: ControlPolicy, + *, + inner_controller: str = "pid", + outer_rate_hz: float = 10.0, + config: ControlBusConfig | None = None, + joint_ids: tuple[str, ...] | None = None, + **inner_kwargs: Any, + ) -> str: + """Start a System-1.5 → System-1 policy chain. Returns session_id.""" + if joint_ids is None: + raise ValueError("joint_ids required for policy chain inner controller") + + inner: ControlPolicy + if inner_controller == "pid": + inner = PIDJointController(joint_ids, **inner_kwargs) + elif inner_controller == "impedance": + inner = ImpedanceController(joint_ids, **inner_kwargs) + else: + raise ValueError(f"Unknown inner_controller type: {inner_controller!r}") + + policy = PolicyChain(outer_policy, inner, outer_rate_hz=outer_rate_hz) + return await self._start_session(device_id, policy, config) + + # -- Lifecycle --------------------------------------------------------- + + async def stop(self) -> dict[str, Any]: + """Stop the current control session gracefully.""" + with self._lock: + session = self._session + if session is None: + return {"status": "no_session"} + + self._stop_telemetry() + await self._bus.async_stop() + + with self._lock: + session.stopped_at = time.monotonic() + stats = self._bus.stats.to_dict() + result = { + "status": "stopped", + "session_id": session.session_id, + "device_id": session.device_id, + "policy_id": session.policy.policy_id, + "duration_s": round( + (session.stopped_at - session.started_at), 3 + ), + "stats": stats, + } + self._session = None + return result + + async def status(self) -> dict[str, Any]: + """Return current session status and statistics.""" + with self._lock: + session = self._session + if session is None: + return {"status": "idle"} + + stats = self._bus.stats.to_dict() + result: dict[str, Any] = { + "status": "running" if self._bus.is_running else "stopped", + "session_id": session.session_id, + "device_id": session.device_id, + "policy_id": session.policy.policy_id, + "uptime_s": round(time.monotonic() - session.started_at, 3), + "stats": stats, + } + # Include trajectory progress if applicable. + if isinstance(session.policy, TrajectoryTracker): + result["progress"] = round(session.policy.progress, 4) + result["is_complete"] = session.policy.is_complete + return result + + async def update_target(self, targets: Mapping[str, float]) -> None: + """Update PID/impedance targets without stopping the loop.""" + with self._lock: + session = self._session + if session is None: + raise RuntimeError("No active control session") + + policy = session.policy + # Unwrap TrajectoryTracker — target updates go to the inner policy. + if isinstance(policy, TrajectoryTracker): + policy = policy._inner + + if isinstance(policy, PIDJointController): + policy.set_target(targets) + elif isinstance(policy, ImpedanceController): + policy.set_reference(targets) + elif isinstance(policy, PolicyChain): + # Update the inner policy of the chain. + inner = policy._inner + if isinstance(inner, PIDJointController): + inner.set_target(targets) + elif isinstance(inner, ImpedanceController): + inner.set_reference(targets) + else: + raise TypeError( + f"Cannot update targets on policy {policy.policy_id!r}" + ) + + # -- Internal helpers -------------------------------------------------- + + async def _start_session( + self, + device_id: str, + policy: ControlPolicy, + config: ControlBusConfig | None, + ) -> str: + """Common session start logic.""" + # Stop any running session first. + if self._bus.is_running: + await self.stop() + + session_id = uuid.uuid4().hex[:12] + session = _SessionInfo( + session_id=session_id, + device_id=device_id, + policy=policy, + config=config, + ) + with self._lock: + self._session = session + + # Apply custom config to bus if provided. + if config is not None: + self._bus._config = config + + await self._bus.async_start(device_id, policy) + self._start_telemetry(session) + + logger.info( + "Realtime control session %s started: device=%s policy=%s", + session_id, + device_id, + policy.policy_id, + ) + return session_id + + def _start_telemetry(self, session: _SessionInfo) -> None: + """Start periodic telemetry publishing.""" + if self._event_bus is None or self._telemetry_interval_s <= 0: + return + + def _publish() -> None: + if not self._bus.is_running: + return + stats = self._bus.stats + progress = None + if isinstance(session.policy, TrajectoryTracker): + progress = session.policy.progress + + event = ControlTelemetryEvent( + session_id=session.session_id, + device_id=session.device_id, + policy_id=session.policy.policy_id, + cycles=stats.cycles, + mean_cycle_ms=round(stats.mean_cycle_ms, 4), + max_jitter_ms=round(stats.max_jitter_ms, 4), + overruns=stats.overruns, + is_running=self._bus.is_running, + progress=progress, + ) + try: + self._event_bus.emit("control.telemetry", event) + except Exception: # noqa: BLE001 — telemetry must not break control + pass + + # Reschedule. + if self._bus.is_running: + t = threading.Timer(self._telemetry_interval_s, _publish) + t.daemon = True + with self._lock: + self._telemetry_timer = t + t.start() + + timer = threading.Timer(self._telemetry_interval_s, _publish) + timer.daemon = True + with self._lock: + self._telemetry_timer = timer + timer.start() + + def _stop_telemetry(self) -> None: + """Cancel the telemetry timer.""" + with self._lock: + timer = self._telemetry_timer + self._telemetry_timer = None + if timer is not None: + timer.cancel() + + +__all__ = [ + "ControlTelemetryEvent", + "ImpedanceController", + "PIDJointController", + "PolicyChain", + "RealtimeControlLoop", + "TrajectoryTracker", +] diff --git a/src/leapflow/hardware/registry.py b/src/leapflow/hardware/registry.py index b70ffe1a..566800bf 100644 --- a/src/leapflow/hardware/registry.py +++ b/src/leapflow/hardware/registry.py @@ -24,7 +24,9 @@ from leapflow.hardware.context import ( SUPPORTED_HC_VERSIONS, + Channel, HardwareContext, + as_numeric, ) from leapflow.hardware.providers import ( HardwareContextProvider, @@ -323,6 +325,7 @@ def __init__( # with a schedule, and hw_status only ever shows a recent tail. self._recent_events: Deque[Any] = deque(maxlen=200) self._event_emitter: Any = None + self._control_buses: dict[str, Any] = {} # ── Loading ── @@ -743,6 +746,24 @@ async def read_frame(self, device_id: str, channel_id: str, **kwargs: Any) -> An ) return await transport.read_frame(channel_id, **kwargs) + def get_open_transport(self, device_id: str) -> HardwareTransport: + """Return an already-opened transport for the device. + + Unlike ``transport()`` which may open on first call (async + Lock), + this method only returns transports that are already open. Raises + ``TransportError`` if the device has no open transport. + + Safe to call from any thread — no asyncio primitives involved. + """ + existing = self._transports.get(device_id) + if existing is None: + raise TransportError( + f"No open transport for device {device_id!r}. " + "Call transport() from the main event loop first.", + failure_code="transport_not_open", + ) + return existing + async def transport(self, device_id: str) -> HardwareTransport: """Return the open transport for *device_id*, opening it on first use. @@ -1120,6 +1141,23 @@ def recent_events(self, device_id: str = "", limit: int = 10) -> tuple[Any, ...] # ── Describe-before-write bookkeeping ── + def is_device_degraded(self, device_id: str) -> bool: + """Return True if any stream source for this device has triggered degradation. + + Checked by the write path so a device whose sampling loop has detected + communication loss or sustained sensor failure cannot be commanded until + the condition clears. The flag is set by ``HardwareStreamSource`` / + ``BatchStreamCoordinator`` and cleared automatically on recovery. + """ + for source in self._stream_sources or (): + if getattr(source, "degradation_triggered", False): + # Match the source to the device. Individual sources store + # the context on ``_context``; batch coordinators do the same. + ctx = getattr(source, "_context", None) + if ctx is not None and getattr(ctx, "device_id", None) == device_id: + return True + return False + def mark_described(self, session_id: str, device_id: str) -> None: self._described.add((str(session_id), str(device_id))) @@ -1188,6 +1226,84 @@ async def read_batch(self, device_id: str, channel_ids: tuple[str, ...]) -> Any: readings=tuple(readings), ) + # ── Safety policy enforcement ── + + def check_safety_policy( + self, + context: HardwareContext, + channel: Channel, + value: Any, + *, + rate: float | None = None, + ) -> tuple[bool, str]: + """Check whether a write violates the device's declared SafetyPolicy. + + Returns ``(allowed, reason)``. When not allowed, *reason* explains + which constraint was violated. + + Checks performed: + + 1. **max_velocity_rad_s** -- if the channel quantity is angular velocity + (or velocity) and the absolute commanded value exceeds the limit. + 2. **max_force_n** -- if the channel quantity is force (or torque) and + the absolute commanded value exceeds the limit. + 3. **collision_zones** -- reserved for future kinematics integration; + skipped today because evaluating Cartesian zones requires a forward + kinematics solver that is not yet available. + 4. **require_safety_interlock** -- when set, every interlock on the + context must be satisfied. The per-channel interlock check already + covers per-envelope interlocks; this flag adds a device-level + blanket requirement. Actual evaluation is deferred to the existing + ``_failed_interlocks`` path in the tools layer, so here we only + verify the flag is honoured. + + When ``SafetyPolicy`` is ``None`` (hc.v0 device) the check passes -- + backward compatible by design. + """ + safety = context.safety + if safety is None: + return True, "" + + # 1. Global velocity limit + if ( + safety.max_velocity_rad_s > 0 + and channel.quantity in ("angular_velocity", "velocity") + ): + num = as_numeric(value) + if num is not None and abs(num) > safety.max_velocity_rad_s: + return ( + False, + f"velocity {abs(num):.3f} rad/s exceeds device limit " + f"{safety.max_velocity_rad_s:.3f} rad/s", + ) + + # 2. Global force limit + if safety.max_force_n > 0 and channel.quantity in ("force", "torque"): + num = as_numeric(value) + if num is not None and abs(num) > safety.max_force_n: + return ( + False, + f"force {abs(num):.3f} N exceeds device limit " + f"{safety.max_force_n:.3f} N", + ) + + # 3. Collision zones -- requires forward kinematics (not yet available). + # Placeholder for future integration. + + # 4. Device-level interlock blanket -- the flag is checked, but the + # actual interlock evaluation lives in the tools layer where the + # transport is available for reading channel values. When the flag + # is set and the context has no interlocks at all, the device is + # misconfigured: refuse rather than silently pass. + if safety.require_safety_interlock and not context.interlocks: + return ( + False, + "require_safety_interlock is set but the device declares no " + "interlocks; add interlock declarations or disable the flag", + ) + + return True, "" + # ── Rate-limit baseline ── def record_command(self, device_id: str, channel_id: str, value: float) -> None: @@ -1208,6 +1324,78 @@ def last_command(self, device_id: str, channel_id: str) -> tuple[float, float] | """Return ``(value, monotonic_timestamp)`` of the last accepted command.""" return self._last_command.get((str(device_id), str(channel_id))) + # ── Control loop management ── + + def create_control_bus( + self, device_id: str, config: Any = None + ) -> Any: + """Create a :class:`HighFrequencyControlBus` for a device. + + Caches per device — one bus per device at a time. Returns the + existing bus if already created and still alive. + """ + from leapflow.hardware.control_bus import ControlBusConfig, HighFrequencyControlBus + + existing = self._control_buses.get(device_id) + if existing is not None: + return existing + + bus_config = config if isinstance(config, ControlBusConfig) else ControlBusConfig() + bus = HighFrequencyControlBus( + self, config=bus_config, safety_checker=self.check_safety_policy, + ) + self._control_buses[device_id] = bus + return bus + + def start_control_loop( + self, + device_id: str, + policy: Any, + *, + config: Any = None, + ) -> None: + """Start a real-time control loop on a device. + + Creates the bus if needed, then starts it with the given policy. + Raises ``RuntimeError`` if the device already has a running loop. + """ + bus = self.create_control_bus(device_id, config) + if bus.is_running: + raise RuntimeError( + f"device {device_id!r} already has a running control loop; " + "stop it before starting another" + ) + bus.start(device_id, policy) + logger.info("Control loop started on %s", device_id) + + def stop_control_loop(self, device_id: str) -> dict[str, Any]: + """Stop the control loop on a device. Returns final stats. + + Returns ``{"status": "no_loop"}`` if no loop was running. + """ + bus = self._control_buses.get(device_id) + if bus is None or not bus.is_running: + return {"status": "no_loop", "device_id": device_id} + bus.stop() + stats = bus.stats.to_dict() + return { + "status": "stopped", + "device_id": device_id, + "stats": stats, + } + + def control_loop_status(self, device_id: str) -> dict[str, Any] | None: + """Return control loop status, or None if no loop exists.""" + bus = self._control_buses.get(device_id) + if bus is None: + return None + stats = bus.stats.to_dict() + return { + "running": bus.is_running, + "device_id": device_id, + "stats": stats, + } + # ── Teardown ── async def drop_transport(self, device_id: str) -> None: @@ -1248,6 +1436,17 @@ async def close_all(self) -> None: down on shutdown rather than at interpreter exit. """ await self.stop_streams() + # Stop any running control loops before closing transports. + for device_id, bus in list(self._control_buses.items()): + try: + if bus.is_running: + bus.stop() + except Exception as exc: # noqa: BLE001 - teardown must not propagate + logger.warning( + "Control bus stop failed for %r during close_all: %s", + device_id, exc, exc_info=True, + ) + self._control_buses.clear() if self._preview_broker is not None: await self._preview_broker.close() store = self._reading_store diff --git a/src/leapflow/hardware/stream.py b/src/leapflow/hardware/stream.py index f3484062..48fc8c22 100644 --- a/src/leapflow/hardware/stream.py +++ b/src/leapflow/hardware/stream.py @@ -31,7 +31,7 @@ from dataclasses import dataclass from typing import Any, Callable, Deque, Iterable, Iterator -from leapflow.hardware.context import Channel, HardwareContext, Quality, as_numeric +from leapflow.hardware.context import Channel, DegradationPolicy, HardwareContext, Quality, as_numeric from leapflow.hardware.transport import Reading logger = logging.getLogger(__name__) @@ -403,6 +403,7 @@ def __init__( event_sink: EventSink | None = None, reading_store: Any = None, alert_policy: Any = None, + degradation_policy: DegradationPolicy | None = None, ) -> None: self._registry = registry self._context = context @@ -412,6 +413,7 @@ def __init__( self._event_sink = event_sink self._store = reading_store self._alert_policy = alert_policy + self._degradation_policy = degradation_policy self._task: asyncio.Task[None] | None = None self._stopping = asyncio.Event() self._last_emitted: dict[str, float] = {} @@ -419,6 +421,10 @@ def __init__( self._samples = 0 self._skipped_slots = 0 self._started_monotonic: float | None = None + self._comm_loss_start: float | None = None + self._degradation_triggered: bool = False + self._sensor_loss_streak: int = 0 + self._sensor_loss_triggered: bool = False @property def source_id(self) -> str: @@ -481,6 +487,10 @@ async def _run(self, emit: Any) -> None: logger.warning( "Hardware stream %s read failed: %s", self.source_id, exc, exc_info=True ) + # Mark the start of a communication loss window. + self._comm_loss_start = time.monotonic() + # DegradationPolicy: comm_loss enforcement. + await self._maybe_trigger_comm_loss() self._dispatch(self._detector.check_stale(), emit) await self._sleep(min(interval * (2**consecutive_failures), 30.0)) # Backoff deliberately abandons the old cadence; resuming the prior @@ -488,10 +498,16 @@ async def _run(self, emit: Any) -> None: next_at = time.monotonic() continue + # Recovery: clear comm-loss state. + if consecutive_failures > 0 or self._comm_loss_start is not None: + self._comm_loss_start = None + self._degradation_triggered = False consecutive_failures = 0 self._samples += 1 lost = self.ring.record(reading) await self._persist(reading, lost=lost) + # Sensor quality tracking for degradation policy. + await self._track_sensor_quality(reading) self._dispatch(self._detector.observe(reading, lost=lost), emit) next_at += interval @@ -507,6 +523,114 @@ async def _run(self, emit: Any) -> None: delay = max(0.0, next_at - time.monotonic()) await self._sleep(delay) + async def _maybe_trigger_comm_loss(self) -> None: + """Evaluate DegradationPolicy.comm_loss_policy during a failure streak. + + Triggers at most once per contiguous loss streak; the flag is cleared on + the next successful read. No-op when no policy is declared or when the + loss window has not yet exceeded ``max_comm_loss_s``. + """ + policy = self._degradation_policy + if policy is None or self._degradation_triggered: + return + if self._comm_loss_start is None: + return + elapsed = time.monotonic() - self._comm_loss_start + if elapsed <= policy.max_comm_loss_s: + return + self._degradation_triggered = True + await self._execute_degradation(policy.comm_loss_policy, reason="comm_loss") + + async def _track_sensor_quality(self, reading: Reading) -> None: + """Evaluate DegradationPolicy.sensor_loss_policy on quality streaks. + + Non-OK readings (STALE/SATURATED/SUSPECT) accumulate a streak; a run of + three triggers the declared ``sensor_loss_policy`` once. An OK reading + resets both the streak and the triggered flag so a future loss can fire. + """ + policy = self._degradation_policy + if reading.quality == Quality.OK.value: + self._sensor_loss_streak = 0 + self._sensor_loss_triggered = False + return + self._sensor_loss_streak += 1 + if policy is None or self._sensor_loss_triggered: + return + # Three consecutive degraded samples mirrors the detector's own threshold + # for QUALITY_DEGRADED: a lone suspect sample is noise, a run of them is + # a fault worth acting on. + if self._sensor_loss_streak < 3: + return + self._sensor_loss_triggered = True + await self._execute_degradation(policy.sensor_loss_policy, reason="sensor_loss") + + async def _execute_degradation(self, action: str, *, reason: str) -> None: + """Carry out the declared degradation action against the transport. + + ``halt`` -> stop every writable channel immediately via the + transport's lock-free halt path. + ``hold_position`` -> stop issuing new commands; the device retains its + last commanded position. The runtime signal is a + log line; command gating is enforced by callers + reading :attr:`degradation_triggered`. + ``safe_return`` -> currently logged and treated as ``hold_position`` + until a per-device home position is declared. + """ + device_id = self._context.device_id + try: + if action == "halt": + transport = await self._registry.transport(device_id) + # halt() is lock-free by design: an emergency stop must preempt + # any device_io lock the sampling loop is holding. + await transport.halt() + logger.warning( + "DegradationPolicy halted device %s (%s)", + device_id, + reason, + ) + elif action == "hold_position": + logger.warning( + "DegradationPolicy holding position on device %s (%s)", + device_id, + reason, + ) + elif action == "safe_return": + logger.warning( + "DegradationPolicy safe_return requested on device %s (%s); " + "no home position declared, holding position", + device_id, + reason, + ) + elif action == "continue_blind": + logger.warning( + "DegradationPolicy continue_blind on device %s (%s)", + device_id, + reason, + ) + else: + logger.warning( + "DegradationPolicy unknown action %r on device %s (%s)", + action, + device_id, + reason, + ) + except Exception as exc: # noqa: BLE001 - degradation must not stop sampling + logger.warning( + "DegradationPolicy execution failed for %s: %s", + device_id, + exc, + exc_info=True, + ) + + @property + def degradation_triggered(self) -> bool: + """Return True while a declared degradation action is in effect. + + Callers that gate commands (e.g. ``hold_position``) can read this to + decide whether to issue new writes without blocking the sampling loop. + """ + return self._degradation_triggered or self._sensor_loss_triggered + async def _persist(self, reading: Reading, *, lost: int) -> None: """Buffer one sample and, when a window closes, write it off the sampling path. @@ -648,6 +772,7 @@ def __init__( event_sink: EventSink | None = None, reading_store: Any = None, alert_policy: Any = None, + degradation_policy: DegradationPolicy | None = None, ) -> None: self._registry = registry self._context = context @@ -662,6 +787,7 @@ def __init__( self._event_sink = event_sink self._store = reading_store self._alert_policy = alert_policy + self._degradation_policy = degradation_policy self._task: asyncio.Task[None] | None = None self._stopping = asyncio.Event() self._last_emitted: dict[str, float] = {} @@ -669,6 +795,10 @@ def __init__( self._samples = 0 self._skipped_slots = 0 self._started_monotonic: float | None = None + self._comm_loss_start: float | None = None + self._degradation_triggered: bool = False + self._sensor_loss_streak: int = 0 + self._sensor_loss_triggered: bool = False @property def source_id(self) -> str: @@ -724,6 +854,9 @@ async def _run(self, emit: Any) -> None: logger.warning( "Batch stream %s read failed: %s", self.source_id, exc, exc_info=True ) + self._comm_loss_start = time.monotonic() + # DegradationPolicy: comm_loss enforcement. + await self._maybe_trigger_comm_loss() # Check staleness on all channels during failures. for detector in self._detectors.values(): self._dispatch(detector.check_stale(), emit) @@ -731,6 +864,10 @@ async def _run(self, emit: Any) -> None: next_at = time.monotonic() continue + # Recovery: clear comm-loss state. + if consecutive_failures > 0 or self._comm_loss_start is not None: + self._comm_loss_start = None + self._degradation_triggered = False consecutive_failures = 0 self._samples += 1 @@ -742,6 +879,8 @@ async def _run(self, emit: Any) -> None: continue lost = ring.record(reading) await self._persist(reading, lost=lost) + # Sensor quality tracking for degradation policy. + await self._track_sensor_quality(reading) detector = self._detectors.get(ch_id) if detector is not None: self._dispatch(detector.observe(reading, lost=lost), emit) @@ -755,6 +894,85 @@ async def _run(self, emit: Any) -> None: delay = max(0.0, next_at - time.monotonic()) await self._sleep(delay) + async def _maybe_trigger_comm_loss(self) -> None: + """Evaluate DegradationPolicy.comm_loss_policy during a failure streak.""" + policy = self._degradation_policy + if policy is None or self._degradation_triggered: + return + if self._comm_loss_start is None: + return + elapsed = time.monotonic() - self._comm_loss_start + if elapsed <= policy.max_comm_loss_s: + return + self._degradation_triggered = True + await self._execute_degradation(policy.comm_loss_policy, reason="comm_loss") + + async def _track_sensor_quality(self, reading: Reading) -> None: + """Evaluate DegradationPolicy.sensor_loss_policy on quality streaks.""" + policy = self._degradation_policy + if reading.quality == Quality.OK.value: + self._sensor_loss_streak = 0 + self._sensor_loss_triggered = False + return + self._sensor_loss_streak += 1 + if policy is None or self._sensor_loss_triggered: + return + if self._sensor_loss_streak < 3: + return + self._sensor_loss_triggered = True + await self._execute_degradation(policy.sensor_loss_policy, reason="sensor_loss") + + async def _execute_degradation(self, action: str, *, reason: str) -> None: + """Carry out the declared degradation action, mirroring HardwareStreamSource.""" + device_id = self._context.device_id + try: + if action == "halt": + transport = await self._registry.transport(device_id) + await transport.halt() + logger.warning( + "DegradationPolicy halted device %s (%s)", + device_id, + reason, + ) + elif action == "hold_position": + logger.warning( + "DegradationPolicy holding position on device %s (%s)", + device_id, + reason, + ) + elif action == "safe_return": + logger.warning( + "DegradationPolicy safe_return requested on device %s (%s); " + "no home position declared, holding position", + device_id, + reason, + ) + elif action == "continue_blind": + logger.warning( + "DegradationPolicy continue_blind on device %s (%s)", + device_id, + reason, + ) + else: + logger.warning( + "DegradationPolicy unknown action %r on device %s (%s)", + action, + device_id, + reason, + ) + except Exception as exc: # noqa: BLE001 - degradation must not stop sampling + logger.warning( + "DegradationPolicy execution failed for %s: %s", + device_id, + exc, + exc_info=True, + ) + + @property + def degradation_triggered(self) -> bool: + """Return True while a declared degradation action is in effect.""" + return self._degradation_triggered or self._sensor_loss_triggered + async def _persist(self, reading: Reading, *, lost: int) -> None: """Buffer one sample for persistence, mirroring HardwareStreamSource._persist.""" store = self._store @@ -889,6 +1107,7 @@ def build_batch_coordinators( event_sink=event_sink, reading_store=reading_store, alert_policy=alert_policy, + degradation_policy=context.degradation, ) ) return tuple(coordinators) @@ -934,6 +1153,7 @@ def build_stream_sources( event_sink=event_sink, reading_store=reading_store, alert_policy=alert_policy, + degradation_policy=context.degradation, ) ) return tuple(sources) diff --git a/src/leapflow/hardware/tools.py b/src/leapflow/hardware/tools.py index 8953c7fc..16c4a40d 100644 --- a/src/leapflow/hardware/tools.py +++ b/src/leapflow/hardware/tools.py @@ -1,7 +1,8 @@ # Copyright (c) Alibaba, Inc. and its affiliates. """The eight hardware tools, derived from admitted contexts. -The count is fixed regardless of how many devices exist. A rig of seven programs +The count is fixed regardless of how many devices exist (nine generic tools plus +three real-time control tools). A rig of seven programs with six channels each would be 40+ schemas if tools were generated per channel, which would swamp the tool index; instead the index stays constant and the per-channel limits arrive through ``hw_describe`` when a model actually intends to @@ -384,12 +385,26 @@ async def hw_estop(self, device_id: str = "", **_: Any) -> dict[str, Any]: outcome="ok" if status.halt_supported else "unsupported", identity=self._session_id, ) - return { + # When the device declares an emergency deceleration time, report it so + # the caller knows how long the device needs to reach zero velocity. + # The transport may not enforce the wait itself (some drivers issue a + # halt command and return immediately), so surfacing the value here + # lets the agent or the operator decide whether to wait before + # re-commanding the device. + result: dict[str, Any] = { "ok": status.halt_supported, "device_id": device_id, "halted": status.halt_supported, "status": status.to_dict(), } + safety = context.safety + if safety is not None and safety.emergency_decel_s > 0: + result["emergency_decel_s"] = safety.emergency_decel_s + result["note"] = ( + f"Device declares {safety.emergency_decel_s:.3f}s emergency " + "deceleration time; wait at least that long before re-commanding." + ) + return result # ── Batch write ── @@ -504,6 +519,31 @@ async def batch_actuate(self, params: dict) -> dict: } resolved.append((channel, value)) + # ── SafetyPolicy (device-level, once per command) ── + for idx, (channel, value) in enumerate(resolved): + safety_ok, safety_reason = self._check_safety(context, channel, value) + if not safety_ok: + return { + "ok": False, + "device_id": device_id, + "channel_id": channel.channel_id, + "error": ( + f"Command [{idx}]: safety policy violation: {safety_reason}" + ), + "failure_code": "safety_policy_violation", + "side_effect_state": SIDE_EFFECT_NONE, + } + + # ── DegradationPolicy (device-level, once) ── + if self._registry.is_device_degraded(device_id): + return self._refusal( + device_id, + "", + "device_degraded", + "Device is in degraded state (communication or sensor loss). " + "Commands blocked until recovery.", + ) + # ── Reachability (once for the device) ── unreachable = await self._unreachable(device_id, resolved[0][0]) if unreachable is not None: @@ -797,6 +837,164 @@ def _worst_side_effect(states: list[str]) -> str: # ── Write path ── + # ── Real-time control tools ── + + async def control_start(self, **params: Any) -> dict[str, Any]: + """Start a real-time control loop on a device. + + Constructs the appropriate :class:`ControlPolicy` based on *mode* and + delegates to ``registry.start_control_loop``. The bus is created once + per device and cached by the registry. + """ + device_id = str(params.get("device_id") or "") + mode = str(params.get("mode") or "") + targets = params.get("targets") or {} + frequency_hz = float(params.get("frequency_hz") or 100) + trajectory = params.get("trajectory") or [] + policy_path = str(params.get("policy") or "") + gains = params.get("gains") or {} + + context = self._registry.context(device_id) + if context is None: + return self._unknown_device(device_id) + + from leapflow.hardware.control_bus import ControlBusConfig + from leapflow.hardware.realtime import ( + ImpedanceController, + PIDJointController, + TrajectoryTracker, + ) + + joint_ids = tuple( + ch.channel_id for ch in context.channels if ch.is_writable + ) + if not joint_ids: + return self._refusal( + device_id, "", "no_writable_channels", + f"Device {device_id!r} has no writable channels for control.", + ) + + bus_config = ControlBusConfig(frequency_hz=frequency_hz) + + kp = float(gains.get("kp", 10.0)) + ki = float(gains.get("ki", 0.1)) + kd = float(gains.get("kd", 1.0)) + stiffness = float(gains.get("stiffness", 100.0)) + damping = float(gains.get("damping", 10.0)) + + policy: Any + try: + if mode == "pid": + policy = PIDJointController(joint_ids, kp=kp, ki=ki, kd=kd) + if targets: + policy.set_target(targets) + elif mode == "impedance": + policy = ImpedanceController( + joint_ids, stiffness=stiffness, damping=damping, + ) + if targets: + policy.set_reference(targets) + elif mode == "trajectory": + if not trajectory: + return self._refusal( + device_id, "", "missing_trajectory", + "trajectory parameter is required for trajectory mode.", + ) + inner = PIDJointController(joint_ids, kp=kp, ki=ki, kd=kd) + waypoints = tuple( + (float(wp.get("t", i)), dict(wp.get("positions", {}))) + for i, wp in enumerate(trajectory) + ) + policy = TrajectoryTracker(inner, waypoints, loop_mode="hold_final") + elif mode == "policy_chain": + if not policy_path: + return self._refusal( + device_id, "", "missing_policy", + "policy parameter is required for policy_chain mode.", + ) + # Defer to the caller for the outer policy; return a descriptor + # that can be chained later. + return { + "ok": False, + "error": ( + "policy_chain mode requires an external policy; " + "use hw_policy_infer with real-time execution instead." + ), + } + else: + return self._refusal( + device_id, "", "unknown_mode", + f"Unknown control mode {mode!r}; " + "supported: pid, impedance, trajectory, policy_chain.", + ) + except (ValueError, TypeError) as exc: + return { + "ok": False, + "device_id": device_id, + "error": f"Failed to create control policy: {exc}", + } + + try: + # Ensure the transport is opened in the main event loop *before* + # the RT control thread starts. The thread uses the lock-free + # ``get_open_transport()`` to avoid crossing event-loop boundaries + # with the registry's asyncio.Lock (H1 fix). + await self._registry.transport(device_id) + self._registry.start_control_loop(device_id, policy, config=bus_config) + except RuntimeError as exc: + return {"ok": False, "device_id": device_id, "error": str(exc)} + + self._audit.record( + action="control_start", + device=device_id, + value=mode, + outcome="ok", + identity=self._session_id, + ) + return { + "ok": True, + "device_id": device_id, + "mode": mode, + "frequency_hz": frequency_hz, + "policy_id": getattr(policy, "policy_id", mode), + } + + async def control_stop(self, **params: Any) -> dict[str, Any]: + """Stop the real-time control loop on a device.""" + device_id = str(params.get("device_id") or "") + context = self._registry.context(device_id) + if context is None: + return self._unknown_device(device_id) + + result = self._registry.stop_control_loop(device_id) + self._audit.record( + action="control_stop", + device=device_id, + outcome=result.get("status", "unknown"), + identity=self._session_id, + ) + result["ok"] = True + return result + + async def control_status(self, **params: Any) -> dict[str, Any]: + """Return the status of a running control loop.""" + device_id = str(params.get("device_id") or "") + context = self._registry.context(device_id) + if context is None: + return self._unknown_device(device_id) + + status = self._registry.control_loop_status(device_id) + if status is None: + return { + "ok": True, + "device_id": device_id, + "status": "no_loop", + } + status["ok"] = True + return status + + # ── Write path (single-channel) ── + async def _write(self, tool_name: str, params: Mapping[str, Any]) -> dict[str, Any]: """Validate, gate, and execute one physical write. @@ -874,6 +1072,38 @@ async def _write(self, tool_name: str, params: Mapping[str, Any]) -> dict[str, A "side_effect_state": SIDE_EFFECT_NONE, } + # SafetyPolicy is a device-level constraint layer above the per-channel + # envelope. It is evaluated after the envelope (which governs per-channel + # bounds) and before reachability/approval, because a command that violates + # the safety policy must never reach the transport, and prompting for a + # command the safety layer will refuse teaches people to click through + # prompts. When no SafetyPolicy is declared (hc.v0 device), the check + # passes -- backward compatible. + safety_ok, safety_reason = self._check_safety(context, channel, value) + if not safety_ok: + return { + "ok": False, + "device_id": device_id, + "channel_id": channel_id, + "error": f"Safety policy violation: {safety_reason}", + "failure_code": "safety_policy_violation", + "side_effect_state": SIDE_EFFECT_NONE, + } + + # DegradationPolicy: a device whose stream source has entered a degraded + # state (communication loss or sustained sensor failure) must not accept + # new commands. The flag is set by the sampling loop and cleared + # automatically on recovery. Checked after the safety policy because a + # safety violation is a harder failure; degradation is transient. + if self._registry.is_device_degraded(device_id): + return self._refusal( + device_id, + channel_id, + "device_degraded", + "Device is in degraded state (communication or sensor loss). " + "Commands blocked until recovery.", + ) + # Reachability precedes both interlocks and consent. Before interlocks, # because an interlock cannot be evaluated on a device that cannot be reached # and reporting "interlock unevaluable" would name the wrong root cause -- @@ -1096,6 +1326,20 @@ def _rate_wait_s(self, device_id: str, channel: Channel, value: Any) -> float: elapsed_s=time.monotonic() - previous_ts, ) + def _check_safety( + self, + context: HardwareContext, + channel: Channel, + value: Any, + ) -> tuple[bool, str]: + """Delegate to the registry's device-level safety policy check. + + A thin wrapper so the tools layer does not duplicate the constraint + logic. Returns ``(allowed, reason)``; ``allowed=True`` when no + ``SafetyPolicy`` is declared (hc.v0 backward compatibility). + """ + return self._registry.check_safety_policy(context, channel, value) + async def _unreachable(self, device_id: str, channel: Channel) -> dict[str, Any] | None: """Return a refusal when the device cannot be reached, else None. @@ -1598,7 +1842,7 @@ def _tool_for_effect(effect: str) -> str: def build_hardware_tools(tools: HardwareTools) -> list[ToolMetadata]: - """Return the nine tool definitions bound to *tools*.""" + """Return the twelve tool definitions bound to *tools*.""" return [ ToolMetadata( name="hw_list", @@ -1762,6 +2006,99 @@ def build_hardware_tools(tools: HardwareTools) -> list[ToolMetadata]: "provides_capabilities": ["hardware_batch_control"], }, ), + # ── Real-time control tools ── + ToolMetadata( + name="hw_control_start", + description=( + "Start a real-time control loop on a robot device. " + "Supports PID position tracking, impedance control, " + "trajectory following, and neural policy chains." + ), + parameters_schema={ + "type": "object", + "properties": { + "device_id": {"type": "string", "description": "Target device"}, + "mode": { + "type": "string", + "enum": ["pid", "impedance", "trajectory", "policy_chain"], + "description": "Control mode", + }, + "targets": { + "type": "object", + "description": "Joint target positions (for pid/impedance)", + }, + "frequency_hz": { + "type": "number", + "description": "Control frequency (default 100)", + }, + "trajectory": { + "type": "array", + "description": "Time-indexed joint trajectory (for trajectory mode)", + }, + "policy": { + "type": "string", + "description": "Policy path (for policy_chain mode)", + }, + "gains": { + "type": "object", + "description": "PID gains {kp, ki, kd} or impedance {stiffness, damping}", + }, + }, + "required": ["device_id", "mode"], + }, + handler=tools.control_start, + x_leapflow={ + "category": "hardware", + "risk_level": "high", + "mutates_state": True, + "execution_policy": "serial", + }, + mutates_state=True, + provides_capabilities=("hw.control_start",), + ), + ToolMetadata( + name="hw_control_stop", + description=( + "Stop the real-time control loop on a device. " + "Gracefully halts the device and returns session statistics." + ), + parameters_schema={ + "type": "object", + "properties": { + "device_id": {"type": "string", "description": "Target device"}, + }, + "required": ["device_id"], + }, + handler=tools.control_stop, + x_leapflow={ + "category": "hardware", + "risk_level": "medium", + "mutates_state": True, + }, + mutates_state=True, + provides_capabilities=("hw.control_stop",), + ), + ToolMetadata( + name="hw_control_status", + description=( + "Get the status and statistics of a running control loop. " + "Returns frequency, jitter, overruns, and policy state." + ), + parameters_schema={ + "type": "object", + "properties": { + "device_id": {"type": "string", "description": "Target device"}, + }, + "required": ["device_id"], + }, + handler=tools.control_status, + x_leapflow={ + "category": "hardware", + "risk_level": "low", + "mutates_state": False, + }, + provides_capabilities=("hw.control_status",), + ), ] diff --git a/src/leapflow/hardware/trust.py b/src/leapflow/hardware/trust.py index 1a81becb..19961cb9 100644 --- a/src/leapflow/hardware/trust.py +++ b/src/leapflow/hardware/trust.py @@ -48,7 +48,6 @@ _DEMOTE_AFTER = 2 """Consecutive failures to demote one level.""" - class HardwareTrustLevel(IntEnum): """Trust gradient for a (device, channel) pair.""" @@ -58,6 +57,26 @@ class HardwareTrustLevel(IntEnum): PRODUCTION = 3 +_LEVEL_NAMES: Dict[str, int] = { + "UNTRUSTED": 0, + "CANDIDATE": 1, + "VERIFIED": 2, + "PRODUCTION": 3, +} + + +@dataclass(frozen=True) +class _DeviceThresholds: + """Resolved thresholds for one device, considering its TrustConfig.""" + + initial_level: HardwareTrustLevel + candidate_at: int + verified_at: int + production_at: int + demote_after: int + approval_override: str # "always", "never", or "" + + @dataclass(frozen=True) class TrustRecord: """Snapshot of trust state for one (device, channel) pair.""" @@ -80,6 +99,10 @@ class HardwareTrustGate: ``allow_permanent`` is True only for reversible channels at VERIFIED+, matching the platform rule that ``allow_permanent=True`` is reserved for actions whose effect can be undone. + + Per-device ``TrustConfig`` (from hc.v1 declarations) can override the + global defaults. Call ``register_device`` before the first success/failure + for that device, or the global defaults apply. """ def __init__( @@ -100,6 +123,80 @@ def __init__( self._consecutive_fail: Dict[Tuple[str, str], int] = {} self._levels: Dict[Tuple[str, str], HardwareTrustLevel] = {} self._frozen: set[Tuple[str, str]] = set() + self._device_thresholds: Dict[str, _DeviceThresholds] = {} + + # ── Device registration ── + + def register_device( + self, + device_id: str, + trust_config: Any = None, + ) -> None: + """Register per-device trust thresholds from a ``TrustConfig``. + + Must be called before the first success/failure for the device. + When *trust_config* is ``None`` the global defaults are used. + Idempotent: a second call for the same device is a no-op. + """ + if device_id in self._device_thresholds: + return + thresholds = self._resolve_thresholds(trust_config) + self._device_thresholds[device_id] = thresholds + + def _resolve_thresholds(self, trust_config: Any) -> _DeviceThresholds: + """Return effective thresholds, considering *trust_config*.""" + if trust_config is None: + return _DeviceThresholds( + initial_level=HardwareTrustLevel.UNTRUSTED, + candidate_at=self._candidate_at, + verified_at=self._verified_at, + production_at=self._production_at, + demote_after=self._demote_after, + approval_override="", + ) + # Resolve initial level from the config string. + raw_level = getattr(trust_config, "initial_level", "") or "" + level_int = _LEVEL_NAMES.get(raw_level.upper(), 0) + initial = HardwareTrustLevel(level_int) + + # Resolve promotion thresholds. The tuple carries at most 3 values: + # (candidate_at, verified_at, production_at). + promo = getattr(trust_config, "promotion_thresholds", ()) or () + candidate_at = max(1, int(promo[0])) if len(promo) > 0 else self._candidate_at + verified_at = max(1, int(promo[1])) if len(promo) > 1 else self._verified_at + production_at = max(1, int(promo[2])) if len(promo) > 2 else self._production_at + + raw_demote = getattr(trust_config, "demotion_threshold", 0) or 0 + demote_after = max(1, int(raw_demote)) if raw_demote else self._demote_after + + approval_override = str(getattr(trust_config, "approval_override", "") or "") + + return _DeviceThresholds( + initial_level=initial, + candidate_at=candidate_at, + verified_at=verified_at, + production_at=production_at, + demote_after=demote_after, + approval_override=approval_override, + ) + + def _thresholds_for(self, device_id: str) -> _DeviceThresholds: + """Return thresholds for *device_id*, using global defaults as fallback.""" + th = self._device_thresholds.get(device_id) + if th is not None: + return th + return _DeviceThresholds( + initial_level=HardwareTrustLevel.UNTRUSTED, + candidate_at=self._candidate_at, + verified_at=self._verified_at, + production_at=self._production_at, + demote_after=self._demote_after, + approval_override="", + ) + + def _initial_level_for(self, device_id: str) -> HardwareTrustLevel: + """Return the configured initial level for *device_id*.""" + return self._thresholds_for(device_id).initial_level # ── Query ── @@ -108,7 +205,7 @@ def level(self, device_id: str, channel_id: str) -> HardwareTrustLevel: key = (device_id, channel_id) if key in self._frozen: return HardwareTrustLevel.UNTRUSTED - return self._levels.get(key, HardwareTrustLevel.UNTRUSTED) + return self._levels.get(key, self._initial_level_for(device_id)) def may_skip_approval( self, @@ -122,7 +219,16 @@ def may_skip_approval( Only reversible channels at VERIFIED or above qualify. Irreversible channels *always* require approval — the cost of being wrong once cannot be recovered. + + Per-device ``approval_override`` takes precedence: + - ``"always"`` forces approval regardless of trust level. + - ``"never"`` skips approval for reversible channels regardless of level. """ + th = self._thresholds_for(device_id) + if th.approval_override == "always": + return False + if th.approval_override == "never" and reversible: + return True if not reversible: return False return self.level(device_id, channel_id) >= HardwareTrustLevel.VERIFIED @@ -150,6 +256,11 @@ def record_success(self, device_id: str, channel_id: str) -> None: key = (device_id, channel_id) if key in self._frozen: return + # Ensure the initial level is seeded on the first mutation. + if key not in self._levels: + init = self._initial_level_for(device_id) + if init != HardwareTrustLevel.UNTRUSTED: + self._levels[key] = init self._consecutive_ok[key] = self._consecutive_ok.get(key, 0) + 1 self._consecutive_fail[key] = 0 self._maybe_promote(key) @@ -177,9 +288,15 @@ def record_failure( return if key in self._frozen: return + # Ensure the initial level is seeded on the first mutation. + if key not in self._levels: + init = self._initial_level_for(device_id) + if init != HardwareTrustLevel.UNTRUSTED: + self._levels[key] = init self._consecutive_fail[key] = self._consecutive_fail.get(key, 0) + 1 self._consecutive_ok[key] = 0 - if self._consecutive_fail[key] >= self._demote_after: + th = self._thresholds_for(device_id) + if self._consecutive_fail[key] >= th.demote_after: self._demote(key) self._sync_plugin_trust(key, success=False) @@ -209,17 +326,20 @@ def all_records(self) -> list[TrustRecord]: # ── Internal ── def _maybe_promote(self, key: Tuple[str, str]) -> None: + device_id = key[0] + th = self._thresholds_for(device_id) streak = self._consecutive_ok.get(key, 0) - current = self._levels.get(key, HardwareTrustLevel.UNTRUSTED) - if current < HardwareTrustLevel.PRODUCTION and streak >= self._production_at: + current = self._levels.get(key, self._initial_level_for(device_id)) + if current < HardwareTrustLevel.PRODUCTION and streak >= th.production_at: self._levels[key] = HardwareTrustLevel.PRODUCTION - elif current < HardwareTrustLevel.VERIFIED and streak >= self._verified_at: + elif current < HardwareTrustLevel.VERIFIED and streak >= th.verified_at: self._levels[key] = HardwareTrustLevel.VERIFIED - elif current < HardwareTrustLevel.CANDIDATE and streak >= self._candidate_at: + elif current < HardwareTrustLevel.CANDIDATE and streak >= th.candidate_at: self._levels[key] = HardwareTrustLevel.CANDIDATE def _demote(self, key: Tuple[str, str]) -> None: - current = self._levels.get(key, HardwareTrustLevel.UNTRUSTED) + device_id = key[0] + current = self._levels.get(key, self._initial_level_for(device_id)) if current > HardwareTrustLevel.UNTRUSTED: self._levels[key] = HardwareTrustLevel(current - 1) self._consecutive_fail[key] = 0 @@ -255,4 +375,5 @@ def _key_for(device_id: str, channel_id: str) -> Tuple[str, str]: "HardwareTrustGate", "HardwareTrustLevel", "TrustRecord", + "_DeviceThresholds", ] diff --git a/src/leapflow/plugins/tool_plugins/physical_skill.py b/src/leapflow/plugins/tool_plugins/physical_skill.py index 0e8a1986..e89a830f 100644 --- a/src/leapflow/plugins/tool_plugins/physical_skill.py +++ b/src/leapflow/plugins/tool_plugins/physical_skill.py @@ -429,10 +429,37 @@ async def _execute_action( *, verify: bool = False, ) -> dict[str, Any]: - """Write action vector to robot and optionally verify.""" + """Write action vector to robot and optionally verify. + + When the action contains multiple steps (chunked inference) and a + hardware registry with RT control support is available, the chunk + is dispatched through a :class:`RealtimeControlLoop` for smooth, + high-frequency execution. Falls back to sequential batch writes + when the RT path is unavailable or fails. + """ if self._registry is None: raise RuntimeError("hardware registry is not bound") + # Multi-step chunk: attempt RT control loop execution first. + action_sequence = self._extract_action_sequence(action) + if ( + len(action_sequence) > 1 + and hasattr(self._registry, "create_control_bus") + ): + try: + result = await self._execute_chunk_via_rt( + device_id, action_sequence, + ) + if result is not None: + return result + except Exception: # noqa: BLE001 - fallback to batch path + logger.debug( + "RT control loop execution failed for %s; " + "falling back to sequential batch writes", + device_id, + exc_info=True, + ) + device = await self._resolve_device(device_id) transport = getattr(device, "transport", None) if transport is None: @@ -462,16 +489,19 @@ async def _execute_action( "channels_written": len(commands), } else: - # Fallback: sequential writes. + # Fallback: sequential writes through the transport directly. + # Uses device_io lock for serialisation, matching the governed path. written = 0 last_error = "" for cid, val in commands: try: - w = await self._registry.write(device_id, cid, val) + async with self._registry.device_io(device_id): + t = await self._registry.transport(device_id) + w = await t.write(cid, val) if w.ok: written += 1 else: - last_error = w.error + last_error = getattr(w, "error", "") or getattr(w, "failure_code", "unknown") except Exception as exc: last_error = str(exc) result = { @@ -490,6 +520,98 @@ async def _execute_action( return result + @staticmethod + def _extract_action_sequence(action: Any) -> list[Any]: + """Extract a sequence of action steps from a policy output. + + A chunked VLA policy may return a list/tuple of action vectors or a + 2-D array (steps x joints). A single-step action is returned as a + one-element list so the caller can branch on ``len``. + """ + if isinstance(action, (list, tuple)) and action: + # Already a sequence of steps (list of dicts or list of arrays). + first = action[0] + if isinstance(first, (dict, list, tuple)): + return list(action) + # Could be a flat 1-D vector: treat as single step. + return [action] + if hasattr(action, "ndim") and action.ndim == 2: + # 2-D numpy/torch tensor: each row is one step. + return [action[i] for i in range(action.shape[0])] + return [action] + + async def _execute_chunk_via_rt( + self, + device_id: str, + action_sequence: list[Any], + ) -> dict[str, Any] | None: + """Execute an action chunk through a RealtimeControlLoop. + + Builds a trajectory from the action sequence and runs it through + PID tracking on the control bus. Returns a result dict on success, + or None to signal the caller should fall back to the batch path. + """ + import asyncio as _asyncio + + from leapflow.hardware.control_bus import ControlBusConfig + from leapflow.hardware.realtime import RealtimeControlLoop + + bus = self._registry.create_control_bus(device_id) + loop = RealtimeControlLoop(bus) + + # Build waypoints from the action sequence. Each step is spaced + # at ``dt`` seconds, derived from the bus config frequency. + config: ControlBusConfig = bus._config + dt = 1.0 / config.frequency_hz + waypoints: list[tuple[float, dict[str, float]]] = [] + for idx, step in enumerate(action_sequence): + t = idx * dt + if isinstance(step, dict): + positions = {k: float(v) for k, v in step.items()} + elif hasattr(step, "tolist"): + vals = step.tolist() + positions = {str(i): float(v) for i, v in enumerate(vals)} + elif isinstance(step, (list, tuple)): + positions = {str(i): float(v) for i, v in enumerate(step)} + else: + return None # cannot interpret; fall back + waypoints.append((t, positions)) + + if not waypoints: + return None + + session_id = await loop.start_trajectory( + device_id, + tuple(waypoints), + controller="pid", + loop_mode="hold_final", + ) + + # Wait for trajectory completion with a generous timeout. + timeout = len(action_sequence) * dt * 3.0 + 2.0 + deadline = _asyncio.get_event_loop().time() + timeout + while True: + status = await loop.status() + if status.get("is_complete") or status.get("status") != "running": + break + if _asyncio.get_event_loop().time() > deadline: + logger.warning( + "RT chunk execution timed out after %.1fs on %s", + timeout, device_id, + ) + break + await _asyncio.sleep(0.05) + + result = await loop.stop() + return { + "ok": True, + "side_effect_state": "committed", + "channels_written": len(waypoints), + "execution_mode": "rt_control_loop", + "session_id": session_id, + "stats": result.get("stats", {}), + } + async def _approve_write( self, device_id: str, diff --git a/src/leapflow/robot/base.py b/src/leapflow/robot/base.py index 7a7dd20a..a3e9d851 100644 --- a/src/leapflow/robot/base.py +++ b/src/leapflow/robot/base.py @@ -140,22 +140,97 @@ class RobotConfig: """Vendor-specific or user-supplied configuration key-values.""" +# --------------------------------------------------------------------------- +# Robot type registry +# --------------------------------------------------------------------------- + +_ROBOT_TYPE_REGISTRY: dict[str, Any] = {} +"""Global mapping of robot type name to a callable that returns a Robot. + +Factory callables receive ``(**kwargs) -> Robot``. Registration happens +either explicitly via :func:`register_robot_type` or through lazy +discovery from ``leapflow.robot.drivers``. +""" + +_DISCOVERY_DONE: bool = False + + +def register_robot_type(name: str, factory: Any) -> None: + """Register a factory callable for a robot type name. + + The factory must accept keyword arguments and return an object + satisfying the :class:`Robot` Protocol. + """ + _ROBOT_TYPE_REGISTRY[name] = factory + + +def _discover_drivers() -> None: + """One-shot import of known driver packages to populate the registry. + + Each driver module can call :func:`register_robot_type` at import time + or expose a ``ROBOT_TYPES`` dict mapping type names to factory callables. + Discovery is best-effort: an import failure skips the driver quietly + because hardware dependencies are optional. + """ + global _DISCOVERY_DONE + if _DISCOVERY_DONE: + return + _DISCOVERY_DONE = True + + _driver_modules = ( + "leapflow.robot.drivers.feetech", + ) + import importlib + import logging + + _logger = logging.getLogger(__name__) + for mod_path in _driver_modules: + try: + mod = importlib.import_module(mod_path) + except ImportError: + _logger.debug("Robot driver %s not importable (optional dependency).", mod_path) + continue + # Convention: a driver may expose ROBOT_TYPES = {"name": callable}. + types_map = getattr(mod, "ROBOT_TYPES", None) + if isinstance(types_map, dict): + for type_name, factory_fn in types_map.items(): + _ROBOT_TYPE_REGISTRY.setdefault(type_name, factory_fn) + + def make_robot(robot_type: str, **kwargs: Any) -> Any: """Construct a robot instance by type name. This is the LeapRobot factory entry point that replaces the upstream ``make_robot`` from the open-source robot SDK. - .. note:: + Lookup order: - The concrete registry is not yet implemented — callers should - configure ``robot_factory`` directly in the transport config. - This stub exists so that internal imports resolve within the - ``leapflow.robot`` namespace. + 1. Explicit registrations via :func:`register_robot_type`. + 2. Lazy discovery from ``leapflow.robot.drivers.*`` sub-packages. + 3. A ``robot_factory`` key in *kwargs* (escape hatch for custom robots). + + Raises ``NotImplementedError`` only when no factory can be resolved. """ + # 1. Direct registry hit. + factory = _ROBOT_TYPE_REGISTRY.get(robot_type) + if factory is not None: + return factory(**kwargs) + + # 2. Run driver auto-discovery and retry. + _discover_drivers() + factory = _ROBOT_TYPE_REGISTRY.get(robot_type) + if factory is not None: + return factory(**kwargs) + + # 3. Escape hatch: caller-supplied factory callable. + custom_factory = kwargs.pop("robot_factory", None) + if callable(custom_factory): + return custom_factory(**kwargs) + raise NotImplementedError( - f"make_robot('{robot_type}') is not yet wired to a robot " - "registry; configure robot_factory directly in transport config" + f"make_robot('{robot_type}'): no registered factory for this type. " + "Register one with register_robot_type(), declare ROBOT_TYPES in a " + "driver module, or pass robot_factory in the transport config." ) @@ -163,4 +238,5 @@ def make_robot(robot_type: str, **kwargs: Any) -> Any: "Robot", "RobotConfig", "make_robot", + "register_robot_type", ] diff --git a/src/leapflow/robot/drivers/feetech.py b/src/leapflow/robot/drivers/feetech.py index cb7db68a..315775b4 100644 --- a/src/leapflow/robot/drivers/feetech.py +++ b/src/leapflow/robot/drivers/feetech.py @@ -618,6 +618,55 @@ def configure_motors( self.write("Acceleration", motor, acceleration) +# --------------------------------------------------------------------------- +# make_robot() integration +# --------------------------------------------------------------------------- + + +def _make_feetech_robot(*, robot_type: str = "", **kwargs: Any) -> FeetechMotorsBus: + """Factory callable for :func:`~leapflow.robot.base.make_robot` integration. + + Expected keyword arguments: + + - ``serial_port`` (str, required): device path, e.g. ``/dev/ttyACM0`` + - ``motors`` (dict[str, dict], required): mapping of motor name to + ``{"id": int, "model": str}`` + - ``calibration`` (dict, optional): pre-loaded calibration data + - ``protocol_version`` (int, optional): default ``0`` + """ + port = str(kwargs.get("serial_port", "")) + if not port: + raise ValueError("serial_port is required for Feetech motor bus") + raw_motors = kwargs.get("motors", {}) + if not isinstance(raw_motors, dict) or not raw_motors: + raise ValueError("motors dict is required (e.g. {'shoulder': {'id': 1, 'model': 'sts3215'}})") + motors: dict[str, Motor] = {} + for name, spec in raw_motors.items(): + if isinstance(spec, Motor): + motors[name] = spec + elif isinstance(spec, dict): + motors[name] = Motor(id=int(spec["id"]), model=str(spec["model"])) + else: + raise ValueError(f"Invalid motor spec for {name!r}: {spec!r}") + calibration = kwargs.get("calibration") + protocol = int(kwargs.get("protocol_version", DEFAULT_PROTOCOL_VERSION)) + return FeetechMotorsBus( + port=port, motors=motors, calibration=calibration, protocol_version=protocol, + ) + + +# Discoverable by make_robot() auto-discovery: maps known robot_type names +# that use Feetech servos to the factory callable. +ROBOT_TYPES: dict[str, Any] = { + "feetech": _make_feetech_robot, + "so100": _make_feetech_robot, + "so100_follower": _make_feetech_robot, + "so100_leader": _make_feetech_robot, + "koch_v1.1": _make_feetech_robot, + "moss_v1": _make_feetech_robot, +} + + __all__ = [ "FeetechMotorsBus", "OperatingMode", diff --git a/src/leapflow/robot/teleop.py b/src/leapflow/robot/teleop.py index 60c1bda1..7c8b2dab 100644 --- a/src/leapflow/robot/teleop.py +++ b/src/leapflow/robot/teleop.py @@ -67,6 +67,74 @@ def is_active(self) -> bool: ... +# --------------------------------------------------------------------------- +# _TeleopPolicy — ControlPolicy adapter for leader-follower mode +# --------------------------------------------------------------------------- + +class _TeleopPolicy: + """Reads the leader device's joints and maps them to follower commands. + + Implements the :class:`ControlPolicy` Protocol so it can be passed + directly to :class:`HighFrequencyControlBus` for sub-20ms control. + The registry is used inside ``compute()`` via a thread-local event loop + because the bus control thread is not an asyncio context. + """ + + def __init__( + self, + registry: Any, + leader_device_id: str, + joint_map: Mapping[str, str], + ) -> None: + self._registry = registry + self._leader_device_id = leader_device_id + self._joint_map = dict(joint_map) + self._loop: Any = None # thread-local event loop, created lazily + + @property + def policy_id(self) -> str: + return f"teleop:{self._leader_device_id}" + + def compute(self, state: Any) -> Any: + """Read leader joints and map to follower commands.""" + from leapflow.hardware.control_bus import ControlCommand + + if self._loop is None: + import asyncio + self._loop = asyncio.new_event_loop() + + registry = self._registry + leader_ctx = registry.context(self._leader_device_id) + if leader_ctx is None: + return ControlCommand(joint_commands={}) + + leader_channels = tuple( + ch.channel_id for ch in leader_ctx.channels if ch.is_readable + ) + if not leader_channels: + return ControlCommand(joint_commands={}) + + try: + batch = self._loop.run_until_complete( + registry.read_batch(self._leader_device_id, leader_channels) + ) + except Exception: # noqa: BLE001 - policy must not raise + return ControlCommand(joint_commands={}) + + commands: dict[str, float] = {} + for reading in batch.readings: + follower_ch = self._joint_map.get( + reading.channel_id, reading.channel_id, + ) + commands[follower_ch] = float(reading.value) + + return ControlCommand(joint_commands=commands) + + def reset(self) -> None: + """No state to reset.""" + pass + + # --------------------------------------------------------------------------- # LeaderFollowerBridge # --------------------------------------------------------------------------- @@ -123,6 +191,9 @@ def __init__( self._task: asyncio.Task[None] | None = None self._stopping = asyncio.Event() self._sequence = 0 + # RT control bus state (activated when control_rate_hz > 50). + self._use_rt = False + self._rt_bus: Any = None @property def session_id(self) -> str: @@ -130,11 +201,19 @@ def session_id(self) -> str: @property def is_active(self) -> bool: + if self._use_rt and self._rt_bus is not None: + return self._rt_bus.is_running return self._task is not None and not self._task.done() async def start(self) -> None: - """Begin the leader→follower mirror loop.""" - if self._task is not None: + """Begin the leader→follower mirror loop. + + When ``control_rate_hz > 50`` and the registry supports + :meth:`create_control_bus`, the loop is offloaded to a + :class:`HighFrequencyControlBus` running in a dedicated OS thread + for deterministic timing. Otherwise falls back to an asyncio task. + """ + if self._task is not None or self._use_rt: return self._stopping.clear() self._trajectory = PhysicalTrajectory( @@ -144,6 +223,46 @@ async def start(self) -> None: started_at=time.time(), metadata=self._trajectory.metadata, ) + + # High-frequency path: use RT control bus. + if ( + self._control_rate_hz > 50 + and hasattr(self._registry, "create_control_bus") + ): + try: + from leapflow.hardware.control_bus import ControlBusConfig + + bus_config = ControlBusConfig( + frequency_hz=self._control_rate_hz, + ) + bus = self._registry.create_control_bus( + self._follower_device_id, bus_config, + ) + policy = _TeleopPolicy( + self._registry, + self._leader_device_id, + self._joint_map, + ) + bus.start(self._follower_device_id, policy) + self._use_rt = True + self._rt_bus = bus + logger.info( + "Teleop session %s using RT control bus at %.0f Hz", + self._session_id, + self._control_rate_hz, + ) + return + except Exception as exc: # noqa: BLE001 + logger.warning( + "Failed to start RT teleop for %s: %s; " + "falling back to asyncio loop", + self._follower_device_id, + exc, + exc_info=True, + ) + self._use_rt = False + self._rt_bus = None + self._task = asyncio.create_task( self._run(), name=f"teleop:{self._session_id}" ) @@ -151,6 +270,19 @@ async def start(self) -> None: async def stop(self) -> PhysicalTrajectory: """Stop the mirror loop and return the finalized trajectory.""" self._stopping.set() + + # RT mode: stop the control bus. + if self._use_rt and self._rt_bus is not None: + try: + self._rt_bus.stop() + except Exception as exc: # noqa: BLE001 + logger.warning( + "RT teleop bus stop raised: %s", exc, exc_info=True, + ) + self._use_rt = False + self._rt_bus = None + return self._trajectory.finalized() + task = self._task self._task = None if task is not None: @@ -226,11 +358,20 @@ async def _one_cycle(self) -> PhysicalTrajectoryStep | None: # 3. Write to follower through approval-gated path. if commands and self._hardware_tools is not None: - await self._hardware_tools.batch_actuate( - self._follower_device_id, tuple(commands) - ) + await self._hardware_tools.batch_actuate({ + "device_id": self._follower_device_id, + "commands": [ + {"channel_id": cid, "value": val} + for cid, val in commands + ], + }) elif commands: # Direct write fallback when no hardware_tools provided (testing). + logger.warning( + "LeaderFollowerBridge: hardware_tools is None; falling back to direct " + "transport writes. This bypasses SafetyPolicy and approval gates. " + "Only acceptable in testing." + ) from leapflow.hardware.transport import BatchTransport transport = await registry.transport(self._follower_device_id) @@ -464,6 +605,7 @@ def remove_physical_session(key: str) -> Any: "KinestheticRecorder", "LeaderFollowerBridge", "TeleopBridge", + "_TeleopPolicy", "get_physical_session", "register_physical_session", "remove_physical_session", diff --git a/tests/_fixtures/cassettes/r8_hardware/cassette-model-201dfd2f16183356.cassette.json b/tests/_fixtures/cassettes/r8_hardware/cassette-model-201dfd2f16183356.cassette.json new file mode 100644 index 00000000..84e9d457 --- /dev/null +++ b/tests/_fixtures/cassettes/r8_hardware/cassette-model-201dfd2f16183356.cassette.json @@ -0,0 +1,87 @@ +{ + "fingerprint": "201dfd2f161833568f27ae31ea72cba48722d94422b9352d7c4093ba8ddd0b6e", + "note": "captured in seed mode", + "request": { + "model": "cassette-model", + "stream": false, + "messages": [ + { + "role": "system", + "content": "You are LeapFlow, an intelligent assistant that can both converse naturally and take real actions on the user's computer.\n\n## Capabilities\nThe tool index below lists **every** registered tool by name and a one-line summary — this is the complete\ncapability contract; nothing else exists. Only a subset is directly callable this turn (via native tool calling,\nnot a JSON block in your reply). If you need a tool from the index that is not yet callable, call\n`capability_expand` with its category name first — the matching tools become callable immediately after.\nWhen the user asks about LeapFlow **itself** — its identity/version/model, what tools, plugins, or capabilities\nit has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of\nself-awareness — answer from live runtime evidence via `self_describe` (facet=capabilities for \"what can you\ndo / do you support X\", identity for version/model, runtime for context/posture, or `all` for a full self-check),\nnever by reading LeapFlow's own source code, documentation, or memory. Report configuration-dependent or\nunavailable capabilities as limitations rather than inferring them.\n- **text_search**(text, pattern): Search for a regex pattern in text.\n- **text_replace**(text, old, new, count) [capability_expand category: write]: Replace occurrences of a substring in text.\n- **time_get**(): Get current date and time.\n- **env_info**(): Get system environment information (OS, Python version, cwd).\n- **skills_list**(query, category, source): List available learned skills. Use when user asks about capabilities or you need a specific skill.\n- **skill_view**(name): View the full content of a specific skill document.\n- **skill_list_files**(name) [capability_expand category: skills]: List all support files in a skill directory (configs, templates, examples). Use before skill_file_read to discover available files.\n- **skill_file_read**(name, file_path) [capability_expand category: skills]: Read an individual support file from a skill directory. Use skill_list_files first to discover available files.\n- **code_intel**(path, operation): Precise document symbols (outline) for a source file: classes, functions, and methods with line ranges. Python uses an exact AST parse; other languages use a keyword-prefix scan. Prefer over file_read mode=symbols for accurate navigation before editing. Read-only.\n- **repo_map**(path): Compact project orientation for a repository root: languages, detected test/lint commands, top-level structure, entry points, manifest, and VCS branch. Call this first when entering an unfamiliar codebase. Read-only.\n- **scm_sync**(action, cwd, remote, pull_ref, push_ref, timeout) [capability_expand category: scm]: Run a typed git SCM action. Use this instead of shell_run for git pull/push/status. For 'pull origin main then push', set action='pull_then_push', remote='origin', pull_ref='main', and omit push_ref so LeapFlow pushes the current local branch.\n- **git_query**(action, cwd, ref, path, staged, max_count, stat): Read-only structured git inspection: action=diff|log|status|branch|show. Prefer over shell_run for reading repo state — output is clipped, redacted, and log/branch are parsed into structured fields. Use scm_sync for pull/push.\n- **git_write**(action, cwd, message, stage_all, name, ref, create) [capability_expand category: scm]: Mutating git actions: action=commit (message, stage_all), branch (create+switch), checkout (switch; create=true for -b). Approval-gated. Use scm_sync for pull/push and git_query for reads.\n- **test_run**(command, cwd, timeout): Run the project's test suite and return structured results (framework, passed/failed counts, failing tests). Auto-detects the runner (pytest/npm/go/cargo) or uses a configured/explicit command; executes via the governed shell. ok=true means the runner executed — see 'success' for pass/fail.\n- **lint_check**(command, cwd, timeout): Run the project's linter and return a structured clean/issue result. Auto-detects the linter (ruff/eslint/go vet/clippy) or uses a configured/explicit command; executes via the governed shell. ok=true means the linter ran — see 'clean'.\n- **file_list**(path, pattern, depth): List files and directories at a given path. Use depth=1 or depth=2 to get a recursive tree in one call instead of listing each sub-directory separately.\n- **file_read**(path, max_lines, start_line, max_chars, mode): Read text file content with adaptive context governance. For large or unfamiliar files, prefer mode='outline' or mode='symbols' first, then use mode='raw' with start_line/max_lines for the specific range you actually need. For LeapFlow's own settings, use config_list / config_get / config_set — its config files are outside the workspace and not readable here.\n- **file_write**(path, content, mode) [capability_expand category: write]: Write content to a file (overwrite or append).\n- **code_search**(pattern, patterns, path, glob, ignore_case, multiline, max_results, context_lines): Search file CONTENTS by regex pattern across a directory tree (ripgrep-backed). Requires a regex pattern. NOT for listing or browsing directory contents — use file_list for that. Prefer this over shell_run grep: faster, skips VCS/dependency/build dirs, and returns structured path:line:column matches. Batch related lookups into ONE call via `patterns` (OR-combined, single pass) instead of issuing several separate searches. Use file_read for the surrounding context of a hit.\n- **file_find**(glob, path, max_results): Find files by a recursive glob pattern under a base path (e.g. '**/test_*.py' or '*.md'). Prefer this over shell_run find; skips VCS/dependency/build dirs.\n- **edit_file**(path, edits, dry_run, diff) [capability_expand category: write]: Apply targeted, anchored search-replace edits to an EXISTING text file (use file_write to create/overwrite). Each edit is {original_text, new_text, replace_all?}; original_text must match exactly and uniquely (or set replace_all) — a non-unique or missing anchor is rejected so files are never corrupted. Set dry_run to preview. Alternatively pass a unified 'diff' to apply its hunks as anchored edits. Far cheaper and safer than rewriting a whole file.\n- **shell_run**(command, cwd, timeout) [capability_expand category: shell]: Execute a one-shot shell command with timeout protection. Runs in the active workspace; paths resolving outside it are refused. Reach for a structured tool first when one fits — web_fetch for anything over HTTP(S), git_query/scm_sync for git, code_search/file_find/file_read for the repo, config_get/config_set for LeapFlow's own settings — because those report typed results, while a failed shell command can only be diagnosed from its exit code and stderr. Every shell run counts as an external side effect, so a failure stops the rest of the batch and is not retried automatically.\n- **terminal_open**(command, cwd, shell) [capability_expand category: terminal]: Open a PERSISTENT shell session (REPL/dev server/watch), returning a session_id for terminal_send/read/close. Disabled unless tools.terminal_session_enabled is set. For one-shot commands use shell_run instead.\n- **terminal_send**(session_id, input, wait) [capability_expand category: terminal]: Send a line of input to a persistent terminal session and return output captured shortly after.\n- **terminal_read**(session_id, wait): Drain buffered output from a persistent terminal session (optionally waiting briefly first).\n- **terminal_close**(session_id) [capability_expand category: terminal]: Terminate a persistent terminal session and release its process group.\n- **terminal_list**(): List active persistent terminal sessions.\n- **config_list**(category, limit): List LeapFlow's own writable settings (LLM model and endpoint, daemon, memory, perception, gateway, …) with current values. Use this to discover the exact key before changing anything. Optionally narrow by `category`. This is the only correct way to inspect LeapFlow configuration — never read config files from disk.\n- **config_get**(key): Read one LeapFlow setting by key (e.g. 'llm.model', 'llm.base_url', 'daemon.log_level'), returning its current value, type, scopes, and whether a change needs a daemon restart. There is no 'llm.provider' key: provider behavior is inferred from the OpenAI-compatible 'llm.base_url'. Never read LeapFlow config files from disk — use this.\n- **config_set**(key, value, scope) [capability_expand category: config]: Change one LeapFlow setting by key, e.g. switch an OpenAI-compatible LLM with key='llm.model' and key='llm.base_url'. There is no 'llm.provider' key: provider behavior is inferred from the endpoint. Values are validated and coerced; credentials are stored in the vault automatically. Call config_list or config_get first if unsure of the key. The result states whether a `leap daemon restart` is required. Never edit LeapFlow config files directly.\n- **web_fetch**(url, select, timeout, max_bytes): Read a URL over HTTP(S) and get back extracted, context-sized content: parsed JSON for API endpoints, readable text plus links for web pages. Use this for anything on the internet — prices, docs, releases, articles — instead of running curl through shell_run: it reports real HTTP status codes, retries rate limits on its own, and is a plain read so a retry is always safe. For JSON APIs pass `select` with a dotted path (e.g. 'chart.result.0.meta') to return just that part instead of the whole payload.\n- **memory_search**(query, limit): Search agent memory for relevant past experiences, observations, and facts.\n- **memory_add**(content, kind) [capability_expand category: write]: Store a new observation or insight in memory for future reference.\n- **research_note**(kind, text): Record a compact, structured note about the current task's state so it survives context compression on long / multi-step tasks. Use for durable findings, open questions still to resolve, decisions / excluded paths, and the immediate next step. One concise sentence per note.\n- **capability_expand**(category): Fetch the full callable schema for every tool in a capability category (e.g. 'hub', 'gateway', 'desktop', 'delegate', 'file', 'memory', 'skill'). The compact tool index always lists every registered tool by name and a one-line summary, but only a static low-risk subset is directly callable each turn. If you need a tool from the index that is not yet callable, call capability_expand with its category first; the matching tools become callable in this turn. Never invent a tool name — expand the category instead.\n- **delegate_task**(goal, context) [capability_expand category: delegate]: Delegate a complex sub-task to an isolated subagent. The subagent gets a fresh context and restricted tool access. Use when a task is self-contained and can be solved independently.\n- **schedule_reentry**(kind, reason, delay_seconds, event_match, max_reentries, deadline_seconds): Register a re-entry so this task can resume later from its current orientation (findings / open questions / next step). Use when work must pause and continue after a delay (kind=time) or when a matching platform event arrives (kind=event), instead of finishing now. The research-ledger state is carried over automatically.\n- **hub_push**(skill_name, visibility, version) [capability_expand category: hub]: Push a local skill to the ModelScope Hub for sharing or backup.\n- **hub_pull**(repo_id, version) [capability_expand category: hub]: Pull a skill from the ModelScope Hub to install locally.\n- **hub_search**(query) [capability_expand category: hub]: Search for skills on the Hub by keyword or description.\n- **hub_sync**(mode, dry_run) [capability_expand category: hub]: Preview or execute sync between local skills and Hub.\n- **platform_action**(platform, action, payload, backend_kind) [capability_expand category: gateway]: Execute an exact registered business action on an external platform through LeapFlow's App Connector layer. Actions must be copied from the App Connector Capability Index and are addressed as domain.operation, e.g. im.send_message or docs.create_markdown. All business fields (chat_id, text, query, etc.) MUST be placed inside `payload`, never at the top level. Example: {\"platform\":\"feishu\",\"action\":\"im.send_message\",\"payload\":{\"chat_id\":\"oc_xxx\",\"text\":\"hello\"}}. Do not invent action names, do not use management actions such as list/guide/connect/status here.\n- **platform_connect**(action, platform, credentials, options, checkpoint) [capability_expand category: gateway]: List, guide, connect, disconnect, remove, or check status for external platforms using the App Connector management namespace. Supports REST and CLI backends. Use this for management actions such as list/guide/preflight/connect/status; use platform_action only for exact registered business actions.\n- **gateway_send**(platform, chat_id, text, thread_id) [capability_expand category: gateway]: Send a message to a connected external platform (Feishu group, Telegram chat, DingTalk conversation, etc.). Requires the platform to be connected via gateway_connect first. Use gateway_connect with action='list' to see connected platforms and available chat IDs.\n- **gateway_connect**(action, platform, credentials, options) [capability_expand category: gateway]: Connect, configure, or manage external platform integrations (Feishu, DingTalk, Telegram, Slack, Discord, etc.). Conversational flow: 1) call 'guide' to get setup steps + required fields, 2) present the steps to the user and ask for ALL required credentials in a single message, 3) call 'connect' with the credentials. Goal: complete in 1–2 user turns. NEVER include credential values in your text response.\n- **plugin_list**(): List the live plugin registry across Tool/Gateway/LLM subsystems for plugin management: which plugins are loaded, their category and fiber lifecycle state. For questions about LeapFlow's own capabilities or whether it supports X, use self_describe instead.\n- **plugin_status**(plugin_id): Get detailed status of a specific plugin: its declared category, runtime dependencies, contributed tools, and fiber lifecycle state.\n- **plugin_versions**(plugin_id): List recorded source versions and active pointer for a profile-scoped plugin.\n- **plugin_propose**(requested_capability, plugin_id, proposed_tools, test_cases, risk_level, evidence): Create a side-effect-free PluginProposal from explicit capability-gap evidence. Use this before plugin_generate when a missing capability should be reviewed. Does not call an LLM, write files, or install anything.\n- **assess_compatibility**(manifest, source_path) [capability_expand category: plugin_management]: Assess whether a foreign plugin manifest or real DSH source bundle is compatible with LeapFlow. A source bundle assessment is static: runtime_ready stays false until restricted Node discovery during plugin_install. Returns component-level verdicts and limitations.\n- **plugin_generate**(plugin_id, description, proposal_id) [capability_expand category: system]: Generate a new ToolPlugin from a natural-language capability description. The LLM produces code that conforms to the ToolPlugin Protocol; it is then rigorously validated (syntax, structure, import, protocol conformance). The generated source is stored in CAS and receives explicit content approval. It DOES NOT install the plugin — installation requires a second, mutation-specific approval via plugin_install.\n- **plugin_install**(plugin_id, code, marketplace_name, source_path, proposal_id, version_label) [capability_expand category: system]: Install a plugin from exactly one source: validated Python code, the configured Python marketplace, or a real DSH source bundle. DSH bundles run restricted Node discovery before registration; only public host tools are installed and client UI limitations are reported. Writes profile-scoped state and mutates the process-global registry. REQUIRES APPROVAL.\n- **plugin_rollback**(plugin_id, version) [capability_expand category: system]: Rollback a profile-scoped plugin to a recorded source version and reload it. REQUIRES APPROVAL.\n- **plugin_reload**(plugin_id, version_label) [capability_expand category: system]: Hot-reload a plugin at runtime. Disposes the old plugin fiber, re-imports its module, and registers a fresh instance. Existing in-flight turns are unaffected (snapshot isolation). REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_disable**(plugin_id) [capability_expand category: system]: Disable a plugin by disposing its fiber, removing its tools from the runtime registry. Cannot disable self_management itself. REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_remove**(plugin_id, delete_source) [capability_expand category: system]: Terminally remove a plugin: dispose its fiber, unregister its tools, remove reload metadata, and optionally delete its profile-scoped source file. Cannot remove self_management itself. REQUIRES APPROVAL.\n- **plugin_unquarantine**(plugin_id) [capability_expand category: plugin_management]: Restore a quarantined plugin to probation status for re-evaluation. Unfreezes the trust ledger, transitions the proposal back to PROBATION, and reloads the plugin. REQUIRES APPROVAL.\n- **plugin_enable**(plugin_id) [capability_expand category: system]: Re-enable a previously disabled plugin by reloading its module and registering a fresh instance. REQUIRES APPROVAL.\n- **self_describe**(facet): Answer questions about LeapFlow ITSELF from live runtime evidence — its identity/version/model, what tools and plugins it has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of self-awareness. Prefer this over reading LeapFlow's own source code. Pick facet=capabilities for 'what can you do / do you support X', identity for version/model, runtime for context/posture, or all for a full self-check when explicitly requested.\n- **runtime_snapshot**(): Lightweight (~150 token) flat snapshot of current runtime state: model, context budget, posture, disclosure level, turn count, cache hit rate, uptime, tool count, and pending approvals.\n- **schedule_create**(trigger_expression, instruction, execution_mode, max_retries, delivery_target) [capability_expand category: scheduler]: Create a new scheduled task. Specify a trigger expression (e.g. '30m', 'every 2h', '0 9 * * *') and the instruction to execute on each trigger. Optionally provide an execution mode and a delivery target to receive notifications.\n- **schedule_list**() [capability_expand category: scheduler]: List all scheduled tasks with their current state, trigger, and next due time.\n- **schedule_status**(task_id) [capability_expand category: scheduler]: Get detailed status and recent execution history for a scheduled task.\n- **schedule_pause**(task_id) [capability_expand category: scheduler]: Pause a scheduled task so it stops firing without being cancelled.\n- **schedule_resume**(task_id) [capability_expand category: scheduler]: Resume a paused scheduled task — re-arms it and recalculates next due time.\n- **schedule_cancel**(task_id) [capability_expand category: scheduler]: Cancel a scheduled task permanently.\n- **tool_search**(query, max_results): Search all registered tools by keyword relevance. Returns a ranked list of matching tools with name, category, and summary. Use this when you need a tool but are unsure of its exact name or category.\n- **tool_describe**(tool_name): Get the full callable schema of a single tool by exact name. Returns the tool's description, parameters, and metadata. Use after tool_search to inspect a specific tool before calling it.\n- **hw_list**(): List connected hardware devices with their channel counts and measured quantities. Start here; it does not include operating limits.\n- **hw_describe**(device_id): Return the full reference for one device: every channel, its unit, its operating envelope, rate limit, reversibility, and required interlocks, plus any prior outcomes recorded for its writable channels. Required before commanding a device.\n- **hw_read**(device_id, channel_id): Read the current value of one device channel. Has no physical effect.\n- **hw_status**(device_id): Report connection health, halt capability, and recent notable events (threshold excursions, lost samples, stalled channels) for one device.\n- **hw_configure**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Set a configuration value or setpoint on a channel declaring effect=configure. Setpoints often have inertia: the value may need time to stabilise.\n- **hw_actuate**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Command motion or output on a channel declaring effect=actuate. This moves physical hardware; a repeat from an unknown state is not a safe retry.\n- **hw_dispense**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Consume an irreversible resource on a channel declaring effect=dispense. Running this twice dispenses twice; never repeat it after a failure without first verifying what already happened.\n- **hw_estop**(device_id) [capability_expand category: hardware]: Stop all motion and output on a device immediately. Never requires approval; use it whenever device behaviour is unexpected.\n- **hw_batch_actuate**(device_id, commands, dry_run, verify) [capability_expand category: hardware]: Write multiple actuator channels in one atomic transaction. Used for coordinated multi-joint control where all positions must be commanded simultaneously (e.g. robot arm joint-space control).\n- **hw_control_start**(device_id, mode, targets, frequency_hz, trajectory, policy, gains) [capability_expand category: hardware]: Start a real-time control loop on a robot device. Supports PID position tracking, impedance control, trajectory following, and neural policy chains.\n- **hw_control_stop**(device_id) [capability_expand category: hardware]: Stop the real-time control loop on a device. Gracefully halts the device and returns session statistics.\n- **hw_control_status**(device_id) [capability_expand category: hardware]: Get the status and statistics of a running control loop. Returns frequency, jitter, overruns, and policy state.\n- **hw_policy_infer**(device_id, policy, task, execute, chunk_size, verify, compute_budget) [capability_expand category: hardware]: Execute VLA policy inference on a robot device. Reads observation, runs policy, optionally writes action to actuators. Supports local (HuggingFace) and remote (PolicyServer) policies.\n- **hw_policy_episode**(action, device_id, policy, task) [capability_expand category: hardware]: Manage policy inference episode lifecycle. Start an episode to begin a task, stop to finalise and get stats.\n- **hw_policy_list**(source) [capability_expand category: hardware]: List available VLA policies and their capabilities.\n- **hw_orchestrate**(mode, operations, barrier_timeout_s, label) [capability_expand category: hardware]: Execute coordinated operations across multiple hardware devices. Supports sequential (one-at-a-time), parallel (concurrent), and barrier (synchronised) execution modes. Any failure in a parallel group triggers halt_all on every participating device for safety.\n- **session_search**(query, limit) [capability_expand category: unclassified]: Search past conversation sessions in the current workspace for relevant context.\n- **session_list**(limit): List recent conversation sessions in the current workspace with stable ids, titles, dates, and summaries. Use for browsing past tasks or when user asks to see history without specific search terms. No keywords needed — returns chronological list.\n- **session_detail**(session_id, limit, offset, include_inactive) [capability_expand category: unclassified]: Read a paginated persisted transcript for one past conversation session in the current workspace. Use after session_list or session_search returns a session_id.\n- **click**(element_index) [capability_expand category: desktop]: Click a UI element by its element_index (from the latest observe_ui snapshot)\n- **get_clipboard**() [capability_expand category: desktop]: Read current clipboard text content\n- **list_apps**(filter, running_only) [capability_expand category: desktop]: List available applications on this system. Use to discover correct bundle_id before switch_app.\n- **list_windows**() [capability_expand category: desktop]: List all top-level windows with pid, window_id, title, and per-window state (minimized, on-screen). Call this first to pick the pid and window_id that observe_ui and other window tools require.\n- **observe_ui**(pid, window_id, query) [capability_expand category: desktop]: Snapshot one window's actionable UI elements, each tagged with an element_index for click/right_click/read_text. Re-observe after actions — indices belong to one snapshot. Requires the window's pid and window_id from list_windows.\n- **open_url**(url, app_id) [capability_expand category: desktop]: Open a URL in the default or specified browser\n- **read_text**(element_index) [capability_expand category: desktop]: Read the text content of a specific UI element from the latest snapshot\n- **right_click**(element_index) [capability_expand category: desktop]: Right-click a UI element to open its context menu. Returns visible menu items.\n- **screenshot**(pid, window_id) [capability_expand category: desktop]: Capture a screenshot for visual verification. With pid + window_id captures that window (works across all displays); defaults to the last observed window, or the full desktop when no window has been observed.\n- **scroll**(element_index, direction, amount, pid, window_id) [capability_expand category: desktop]: Scroll a scrollable area of a window. Omit element_index to scroll the window's focused/page scroller; pass one to scroll an exact element from the latest snapshot.\n- **select_text**(element_index) [capability_expand category: desktop]: Select all text in a UI element (focus + select-all, for subsequent copy)\n- **set_clipboard**(text) [capability_expand category: desktop]: Write text to the clipboard\n- **shortcut**(keys) [capability_expand category: desktop]: Execute a keyboard shortcut\n- **switch_app**(app_id) [capability_expand category: desktop]: Switch to an app (launch if needed, activate, verify)\n- **type_text**(text) [capability_expand category: desktop]: Type text into the currently focused element\n- **wait**(seconds) [capability_expand category: desktop]: Wait for a specified duration before continuing\n- **wait_until**(condition, pid, window_id, timeout, poll_interval) [capability_expand category: desktop]: Wait until a UI condition is met (polls UI tree). Returns elements when found or on timeout.\n- **wait_until_stable**(timeout, poll_interval, pid, window_id) [capability_expand category: desktop]: Wait until the UI stops changing (element set stabilizes across polls).\n\n## App Connector Capability Index\nLeapFlow can onboard and manage external apps through `platform_connect` and execute exact registered business actions through `platform_action`.\nFor requests about connecting, setting up, configuring, enabling, or managing a supported app, use `platform_connect` first instead of generating SDK/Webhook sample code.\n`platform_connect.action` is the management namespace: list, guide, preflight, connect, disconnect, remove, status, events_start, events_stop, events_status.\n`platform_action.action` is only for exact registered platform business actions listed below, such as `im.send_message`; never use management actions like `list` or `guide` there.\nAll business fields MUST go inside `payload`; top-level keys are only `platform`, `action`, and `payload`.\nDo not invent platform IDs or platform action names. If the needed action is not listed, ask for discovery/clarification instead of guessing.\nUse `platform_connect` with `action='guide'` and the matching `platform` to start onboarding; use `action='list'` when the app is unclear.\nWhen a pending onboarding state is present, continue from that state with `platform_connect` instead of asking the user to restate the app.\nSupported apps:\n- `api_server`: API Server (OpenAI Compatible) (category=api; backend=adapter; platform_action actions=none registered)\n- `dingtalk`: 钉钉 (DingTalk) (category=im; backend=adapter; platform_action actions=none registered)\n- `feishu`: 飞书 (Feishu/Lark) (category=im; backend=cli)\n - `calendar.create_event` payload={summary*, start_time*, end_time*, attendees} [write/high]\n - `docs.create_markdown` payload={title*, markdown*, folder_token} [write/medium]\n - `drive.search` payload={query*, limit} [read/medium]\n - `im.add_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.download_resource` payload={message_id*, file_key*, type*} [read/low]\n - `im.get_messages` payload={message_ids*} [read/low]\n - `im.list_chats` payload={page_size} [read/low]\n - `im.list_messages` payload={chat_id*} [read/low]\n - `im.list_thread_messages` payload={thread*} [read/low]\n - `im.remove_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.reply_message` payload={message_id*, text*} [send/high]\n - `im.search_chats` payload={query*, page_size} [read/low]\n - `im.search_messages` payload={query*} [read/medium]\n - `im.send_message` payload={chat_id*, text*, thread_id} [send/high]\n - `im.update_card` payload={token*, card*} [write/medium]\n - `im.update_message` payload={message_id*, text*} [write/medium]\n - `mail.search_unread` payload={query, limit} [read/high]\n - `sheets.append_row` payload={spreadsheet_token*, sheet_id*, values*} [write/medium]\n - `task.create` payload={title*, description, due_time} [write/medium]\n- `telegram`: Telegram (category=social; backend=adapter; platform_action actions=none registered)\n- `webhook`: Webhook (Generic) (category=webhook; backend=adapter; platform_action actions=none registered)\n\n## Learned Skills\nYou have access to the following learned skills. Use `skills_list` to browse or `skill_view` to read details:\n- api_designer: REST and GraphQL API design with OpenAPI spec generation and best practices [API, REST, GraphQL]\n- code_review: Structured code review for security, performance, and style [code, review, security]\n- data_analysis: CSV/JSON data analysis with statistical summaries, quality checks, and visualiza [data, analysis, statistics]\n- devops_helper: CI/CD pipeline configuration, Docker orchestration, and deployment strategy [devops, CI/CD, Docker]\n- document_writer: Structured document generation: technical docs, API references, reports, and REA [documentation, technical-writing, README]\n- git_workflow: Git operation orchestration: branching, commits, conflicts, and PR workflow [git, version-control, branching]\n- i18n_helper: Internationalization workflow: string extraction, translation, locale management [i18n, internationalization, localization]\n- project_analysis: Project structure analysis, dependency audit, and tech stack identification [project, analysis, dependencies]\n- security_audit: Security vulnerability scanning strategy, threat modeling, and fix recommendatio [security, vulnerability, audit]\n- shell_automation: Natural language to shell commands with safety review and step-by-step execution [shell, terminal, automation]\n- test_writer: Generate unit and integration tests from source code with edge-case coverage [testing, unit-test, integration-test]\n- web_research: Multi-step web search with cross-verification and structured synthesis [web, search, research]\n\n## Tool Usage\nTools are normally invoked through the native function-calling mechanism, not by writing JSON in your reply\ntext. Only if the provider signals that native function calling is unavailable for this turn, fall back to a\nsingle JSON code block: `{\"name\": \"tool_name\", \"arguments\": {\"key\": \"value\"}}` — use this fallback format\nonly, never both. Only call a tool whose exact name appears in the tool index above and is currently callable\n(or reachable via `capability_expand`). Never invent, rename, alias, or guess a tool name, platform ID, or\nplatform action from argument shape or wording — if the index does not list it, it does not exist.\n`platform_connect.action` (list/guide/preflight/connect/disconnect/remove/status/events_start/events_stop/\nevents_status) is the App Connector management namespace; `platform_action.action` only accepts exact\nregistered business actions such as `im.send_message` shown in the App Connector Capability Index — never mix\nthe two namespaces. If a tool call returns an unknown/unavailable result, use the returned suggestions or\navailable names for a single retry instead of trying further variations of the same guess.\n\n**Side-effect action rule** (`platform_action` with effect=send/write/execute):\n- Call each unique action+payload **exactly once**. Never include duplicates in the same turn.\n- Once the result returns `\"completed\": true`, that action is DONE for this task. Do NOT call it again\n in any subsequent turn — immediately summarize the result for the user instead.\n- The system enforces idempotency: duplicate calls are blocked and will not execute.\n- If the user explicitly requests sending/writing multiple times, use distinct payloads per call.\n\n**Resource identifier provenance rule**:\n- NEVER fabricate, guess, or infer resource identifiers (chat_id, message_id, file_key, user_id, etc.).\n Every resource ID used in a side-effect action MUST come from a successful API response in this session.\n- If a read/list action fails (e.g. authorization error), you do NOT have valid resource IDs.\n Report the failure to the user — do NOT attempt the dependent write/send action with a guessed ID.\n- When a tool result contains `\"llm_instruction\"`, follow it exactly.\n\n## Guidelines\n1. **Direct answers first**: If you already know the answer, respond directly without tools.\n2. **Avoid redundant tool calls**: Do not call the same tool with the same arguments more than once in the same user turn. When an existing tool result already answers the user's request, stop calling tools and answer directly.\n3. **Use tools proactively**: When the user asks about files, time, system state, or needs actions performed, use the appropriate tool.\n4. **Chain tools when needed**: You can call multiple tools in sequence (e.g., list files → read file → summarize).\n5. **Handle failures gracefully**: If a tool fails, explain what went wrong and suggest alternatives. If it failed because of an unknown tool/platform/action name, retry once with an exact name from the returned suggestions, then explain rather than keep guessing.\n6. **Summarize results naturally**: After tool execution, synthesize the results into a helpful answer rather than dumping raw output.\n7. **Stay conversational**: Maintain a natural, helpful tone. Acknowledge context from earlier in the conversation.\n8. **Recall past work**:\n - Broad queries (\"之前做了什么\", \"列出任务\"): answer from the \"Recent Task History\" section already in your context. If insufficient, call session_list.\n - Specific lookups (\"上次那个配置怎么改的\"): call session_search with relevant phrases (NOT single characters).\n - Do NOT call search tools repeatedly with keyword fragments. One well-phrased call is better than ten fragmented ones.\n\n## Coding & Verification\nWhen working with code, prefer the precise built-in tools over ad-hoc shell: use `repo_map` to orient in an\nunfamiliar project, `code_search`/`file_find` to locate and `code_intel` for symbols, `edit_file` (anchored\nsearch-replace, or a unified `diff`) to change files — never rewrite a whole file to change a few lines — and\n`git_query` to inspect diffs/log. After edits, check `syntax_ok` in the result and run `test_run`/`lint_check`\nbefore declaring the work done. Batch independent read-only calls (search/read/query) into a single turn — they\nrun in parallel.\n\n## Presentation Style\n1. **Polished Markdown only**: Format user-facing answers with clean Markdown headings, short paragraphs, and concise bullets. Use tables only when they improve comparison or scanning.\n2. **Terminal-friendly layout**: Keep lines readable in a TUI; avoid dense walls of text, deeply nested lists, oversized ASCII art, or heavy visual blocks.\n3. **Elegant emphasis**: Use bold text sparingly for key terms and conclusions. Avoid excessive emojis, decorative symbols, repeated separators, or visual noise.\n4. **Theme-safe colors**: Do not emit ANSI escape codes, HTML color tags, Rich markup, or hardcoded color names. Rely on the TUI theme to render Markdown professionally.\n5. **No leaked tool protocol**: Never show tool-call JSON, internal schemas, raw observations, tool result payloads, or hidden reasoning in the final answer unless the user explicitly asks for raw/debug output. Treat any prior `{\"name\": ..., \"arguments\": ...}` blocks and `Tool result (...)` messages as internal execution context only.\n6. **Professional closure**: End with a concise conclusion or next step when helpful; avoid rambling after the useful answer is complete.\n\nWhen finished with all tool calls, respond normally without a JSON block, tool-call transcript, or process log.\n\n## Task Contract\n- Task ID: turn-3\n- Original user request: Actuate the bench_r8 setpoint to 60.\n- Workspace root: \n- Allowed roots: \n- Treat relative project paths as relative to the workspace root; never infer `.` as the project root when a workspace root is provided.\n- Workspace boundary is enforced by tools: do not read, search, edit, or run commands against paths outside the allowed roots unless the user explicitly requests an external path and the tool/approval policy permits it.\n- LeapFlow workspace config is optional at `/.leapflow/config.yaml`; runtime config is loaded from `~/.leapflow/config/user.yaml` and `~/.leapflow/profiles//config/*.yaml`.\n- Preserve this task contract across summarization, compression, tool loops, and memory retrieval.\n" + }, + { + "role": "assistant", + "content": "[Called: hw_describe]\nDiscovered the simulated bench and read its channel limits." + }, + { + "role": "user", + "content": "Read the current value of the setpoint channel on bench_r8." + }, + { + "role": "assistant", + "content": "[Called: hw_read]\nRead the setpoint channel." + }, + { + "role": "user", + "content": "Actuate the bench_r8 setpoint to 60.\nActuate the bench_r8 setpoint to 60." + } + ], + "tools": [ + "capability_expand", + "code_intel", + "code_search", + "config_get", + "config_list", + "env_info", + "file_find", + "file_list", + "file_read", + "git_query", + "hw_actuate", + "hw_batch_actuate", + "hw_configure", + "hw_control_start", + "hw_control_status", + "hw_control_stop", + "hw_describe", + "hw_dispense", + "hw_estop", + "hw_list", + "hw_orchestrate", + "hw_policy_episode", + "hw_policy_infer", + "hw_policy_list", + "hw_read", + "hw_status", + "lint_check", + "memory_search", + "plugin_list", + "plugin_propose", + "plugin_status", + "plugin_versions", + "repo_map", + "research_note", + "runtime_snapshot", + "schedule_reentry", + "self_describe", + "session_list", + "skill_view", + "skills_list", + "terminal_list", + "terminal_read", + "test_run", + "text_search", + "time_get", + "tool_describe", + "tool_search", + "web_fetch" + ] + }, + "responses": [ + { + "status": 200, + "content_type": "application/json", + "body": "{\"id\": \"chatcmpl-cassette\", \"object\": \"chat.completion\", \"model\": \"cassette-model\", \"choices\": [{\"index\": 0, \"message\": {\"role\": \"assistant\", \"content\": \"\", \"tool_calls\": [{\"id\": \"call_1\", \"type\": \"function\", \"function\": {\"name\": \"hw_actuate\", \"arguments\": \"{\\\"device_id\\\": \\\"bench_r8\\\", \\\"channel_id\\\": \\\"setpoint\\\", \\\"value\\\": 60.0}\"}}]}, \"finish_reason\": \"tool_calls\"}], \"usage\": {\"prompt_tokens\": 64, \"completion_tokens\": 16, \"total_tokens\": 80}}" + } + ] +} diff --git a/tests/_fixtures/cassettes/r8_hardware/cassette-model-287f6667ad601f31.cassette.json b/tests/_fixtures/cassettes/r8_hardware/cassette-model-287f6667ad601f31.cassette.json new file mode 100644 index 00000000..eef7e327 --- /dev/null +++ b/tests/_fixtures/cassettes/r8_hardware/cassette-model-287f6667ad601f31.cassette.json @@ -0,0 +1,99 @@ +{ + "fingerprint": "287f6667ad601f313795c7d6c7f2d10af4b8707ba2e1a4b9d6986f049b7704dc", + "note": "captured in seed mode", + "request": { + "model": "cassette-model", + "stream": false, + "messages": [ + { + "role": "system", + "content": "You are LeapFlow, an intelligent assistant that can both converse naturally and take real actions on the user's computer.\n\n## Capabilities\nThe tool index below lists **every** registered tool by name and a one-line summary — this is the complete\ncapability contract; nothing else exists. Only a subset is directly callable this turn (via native tool calling,\nnot a JSON block in your reply). If you need a tool from the index that is not yet callable, call\n`capability_expand` with its category name first — the matching tools become callable immediately after.\nWhen the user asks about LeapFlow **itself** — its identity/version/model, what tools, plugins, or capabilities\nit has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of\nself-awareness — answer from live runtime evidence via `self_describe` (facet=capabilities for \"what can you\ndo / do you support X\", identity for version/model, runtime for context/posture, or `all` for a full self-check),\nnever by reading LeapFlow's own source code, documentation, or memory. Report configuration-dependent or\nunavailable capabilities as limitations rather than inferring them.\n- **text_search**(text, pattern): Search for a regex pattern in text.\n- **text_replace**(text, old, new, count) [capability_expand category: write]: Replace occurrences of a substring in text.\n- **time_get**(): Get current date and time.\n- **env_info**(): Get system environment information (OS, Python version, cwd).\n- **skills_list**(query, category, source): List available learned skills. Use when user asks about capabilities or you need a specific skill.\n- **skill_view**(name): View the full content of a specific skill document.\n- **skill_list_files**(name) [capability_expand category: skills]: List all support files in a skill directory (configs, templates, examples). Use before skill_file_read to discover available files.\n- **skill_file_read**(name, file_path) [capability_expand category: skills]: Read an individual support file from a skill directory. Use skill_list_files first to discover available files.\n- **code_intel**(path, operation): Precise document symbols (outline) for a source file: classes, functions, and methods with line ranges. Python uses an exact AST parse; other languages use a keyword-prefix scan. Prefer over file_read mode=symbols for accurate navigation before editing. Read-only.\n- **repo_map**(path): Compact project orientation for a repository root: languages, detected test/lint commands, top-level structure, entry points, manifest, and VCS branch. Call this first when entering an unfamiliar codebase. Read-only.\n- **scm_sync**(action, cwd, remote, pull_ref, push_ref, timeout) [capability_expand category: scm]: Run a typed git SCM action. Use this instead of shell_run for git pull/push/status. For 'pull origin main then push', set action='pull_then_push', remote='origin', pull_ref='main', and omit push_ref so LeapFlow pushes the current local branch.\n- **git_query**(action, cwd, ref, path, staged, max_count, stat): Read-only structured git inspection: action=diff|log|status|branch|show. Prefer over shell_run for reading repo state — output is clipped, redacted, and log/branch are parsed into structured fields. Use scm_sync for pull/push.\n- **git_write**(action, cwd, message, stage_all, name, ref, create) [capability_expand category: scm]: Mutating git actions: action=commit (message, stage_all), branch (create+switch), checkout (switch; create=true for -b). Approval-gated. Use scm_sync for pull/push and git_query for reads.\n- **test_run**(command, cwd, timeout): Run the project's test suite and return structured results (framework, passed/failed counts, failing tests). Auto-detects the runner (pytest/npm/go/cargo) or uses a configured/explicit command; executes via the governed shell. ok=true means the runner executed — see 'success' for pass/fail.\n- **lint_check**(command, cwd, timeout): Run the project's linter and return a structured clean/issue result. Auto-detects the linter (ruff/eslint/go vet/clippy) or uses a configured/explicit command; executes via the governed shell. ok=true means the linter ran — see 'clean'.\n- **file_list**(path, pattern, depth): List files and directories at a given path. Use depth=1 or depth=2 to get a recursive tree in one call instead of listing each sub-directory separately.\n- **file_read**(path, max_lines, start_line, max_chars, mode): Read text file content with adaptive context governance. For large or unfamiliar files, prefer mode='outline' or mode='symbols' first, then use mode='raw' with start_line/max_lines for the specific range you actually need. For LeapFlow's own settings, use config_list / config_get / config_set — its config files are outside the workspace and not readable here.\n- **file_write**(path, content, mode) [capability_expand category: write]: Write content to a file (overwrite or append).\n- **code_search**(pattern, patterns, path, glob, ignore_case, multiline, max_results, context_lines): Search file CONTENTS by regex pattern across a directory tree (ripgrep-backed). Requires a regex pattern. NOT for listing or browsing directory contents — use file_list for that. Prefer this over shell_run grep: faster, skips VCS/dependency/build dirs, and returns structured path:line:column matches. Batch related lookups into ONE call via `patterns` (OR-combined, single pass) instead of issuing several separate searches. Use file_read for the surrounding context of a hit.\n- **file_find**(glob, path, max_results): Find files by a recursive glob pattern under a base path (e.g. '**/test_*.py' or '*.md'). Prefer this over shell_run find; skips VCS/dependency/build dirs.\n- **edit_file**(path, edits, dry_run, diff) [capability_expand category: write]: Apply targeted, anchored search-replace edits to an EXISTING text file (use file_write to create/overwrite). Each edit is {original_text, new_text, replace_all?}; original_text must match exactly and uniquely (or set replace_all) — a non-unique or missing anchor is rejected so files are never corrupted. Set dry_run to preview. Alternatively pass a unified 'diff' to apply its hunks as anchored edits. Far cheaper and safer than rewriting a whole file.\n- **shell_run**(command, cwd, timeout) [capability_expand category: shell]: Execute a one-shot shell command with timeout protection. Runs in the active workspace; paths resolving outside it are refused. Reach for a structured tool first when one fits — web_fetch for anything over HTTP(S), git_query/scm_sync for git, code_search/file_find/file_read for the repo, config_get/config_set for LeapFlow's own settings — because those report typed results, while a failed shell command can only be diagnosed from its exit code and stderr. Every shell run counts as an external side effect, so a failure stops the rest of the batch and is not retried automatically.\n- **terminal_open**(command, cwd, shell) [capability_expand category: terminal]: Open a PERSISTENT shell session (REPL/dev server/watch), returning a session_id for terminal_send/read/close. Disabled unless tools.terminal_session_enabled is set. For one-shot commands use shell_run instead.\n- **terminal_send**(session_id, input, wait) [capability_expand category: terminal]: Send a line of input to a persistent terminal session and return output captured shortly after.\n- **terminal_read**(session_id, wait): Drain buffered output from a persistent terminal session (optionally waiting briefly first).\n- **terminal_close**(session_id) [capability_expand category: terminal]: Terminate a persistent terminal session and release its process group.\n- **terminal_list**(): List active persistent terminal sessions.\n- **config_list**(category, limit): List LeapFlow's own writable settings (LLM model and endpoint, daemon, memory, perception, gateway, …) with current values. Use this to discover the exact key before changing anything. Optionally narrow by `category`. This is the only correct way to inspect LeapFlow configuration — never read config files from disk.\n- **config_get**(key): Read one LeapFlow setting by key (e.g. 'llm.model', 'llm.base_url', 'daemon.log_level'), returning its current value, type, scopes, and whether a change needs a daemon restart. There is no 'llm.provider' key: provider behavior is inferred from the OpenAI-compatible 'llm.base_url'. Never read LeapFlow config files from disk — use this.\n- **config_set**(key, value, scope) [capability_expand category: config]: Change one LeapFlow setting by key, e.g. switch an OpenAI-compatible LLM with key='llm.model' and key='llm.base_url'. There is no 'llm.provider' key: provider behavior is inferred from the endpoint. Values are validated and coerced; credentials are stored in the vault automatically. Call config_list or config_get first if unsure of the key. The result states whether a `leap daemon restart` is required. Never edit LeapFlow config files directly.\n- **web_fetch**(url, select, timeout, max_bytes): Read a URL over HTTP(S) and get back extracted, context-sized content: parsed JSON for API endpoints, readable text plus links for web pages. Use this for anything on the internet — prices, docs, releases, articles — instead of running curl through shell_run: it reports real HTTP status codes, retries rate limits on its own, and is a plain read so a retry is always safe. For JSON APIs pass `select` with a dotted path (e.g. 'chart.result.0.meta') to return just that part instead of the whole payload.\n- **memory_search**(query, limit): Search agent memory for relevant past experiences, observations, and facts.\n- **memory_add**(content, kind) [capability_expand category: write]: Store a new observation or insight in memory for future reference.\n- **research_note**(kind, text): Record a compact, structured note about the current task's state so it survives context compression on long / multi-step tasks. Use for durable findings, open questions still to resolve, decisions / excluded paths, and the immediate next step. One concise sentence per note.\n- **capability_expand**(category): Fetch the full callable schema for every tool in a capability category (e.g. 'hub', 'gateway', 'desktop', 'delegate', 'file', 'memory', 'skill'). The compact tool index always lists every registered tool by name and a one-line summary, but only a static low-risk subset is directly callable each turn. If you need a tool from the index that is not yet callable, call capability_expand with its category first; the matching tools become callable in this turn. Never invent a tool name — expand the category instead.\n- **delegate_task**(goal, context) [capability_expand category: delegate]: Delegate a complex sub-task to an isolated subagent. The subagent gets a fresh context and restricted tool access. Use when a task is self-contained and can be solved independently.\n- **schedule_reentry**(kind, reason, delay_seconds, event_match, max_reentries, deadline_seconds): Register a re-entry so this task can resume later from its current orientation (findings / open questions / next step). Use when work must pause and continue after a delay (kind=time) or when a matching platform event arrives (kind=event), instead of finishing now. The research-ledger state is carried over automatically.\n- **hub_push**(skill_name, visibility, version) [capability_expand category: hub]: Push a local skill to the ModelScope Hub for sharing or backup.\n- **hub_pull**(repo_id, version) [capability_expand category: hub]: Pull a skill from the ModelScope Hub to install locally.\n- **hub_search**(query) [capability_expand category: hub]: Search for skills on the Hub by keyword or description.\n- **hub_sync**(mode, dry_run) [capability_expand category: hub]: Preview or execute sync between local skills and Hub.\n- **platform_action**(platform, action, payload, backend_kind) [capability_expand category: gateway]: Execute an exact registered business action on an external platform through LeapFlow's App Connector layer. Actions must be copied from the App Connector Capability Index and are addressed as domain.operation, e.g. im.send_message or docs.create_markdown. All business fields (chat_id, text, query, etc.) MUST be placed inside `payload`, never at the top level. Example: {\"platform\":\"feishu\",\"action\":\"im.send_message\",\"payload\":{\"chat_id\":\"oc_xxx\",\"text\":\"hello\"}}. Do not invent action names, do not use management actions such as list/guide/connect/status here.\n- **platform_connect**(action, platform, credentials, options, checkpoint) [capability_expand category: gateway]: List, guide, connect, disconnect, remove, or check status for external platforms using the App Connector management namespace. Supports REST and CLI backends. Use this for management actions such as list/guide/preflight/connect/status; use platform_action only for exact registered business actions.\n- **gateway_send**(platform, chat_id, text, thread_id) [capability_expand category: gateway]: Send a message to a connected external platform (Feishu group, Telegram chat, DingTalk conversation, etc.). Requires the platform to be connected via gateway_connect first. Use gateway_connect with action='list' to see connected platforms and available chat IDs.\n- **gateway_connect**(action, platform, credentials, options) [capability_expand category: gateway]: Connect, configure, or manage external platform integrations (Feishu, DingTalk, Telegram, Slack, Discord, etc.). Conversational flow: 1) call 'guide' to get setup steps + required fields, 2) present the steps to the user and ask for ALL required credentials in a single message, 3) call 'connect' with the credentials. Goal: complete in 1–2 user turns. NEVER include credential values in your text response.\n- **plugin_list**(): List the live plugin registry across Tool/Gateway/LLM subsystems for plugin management: which plugins are loaded, their category and fiber lifecycle state. For questions about LeapFlow's own capabilities or whether it supports X, use self_describe instead.\n- **plugin_status**(plugin_id): Get detailed status of a specific plugin: its declared category, runtime dependencies, contributed tools, and fiber lifecycle state.\n- **plugin_versions**(plugin_id): List recorded source versions and active pointer for a profile-scoped plugin.\n- **plugin_propose**(requested_capability, plugin_id, proposed_tools, test_cases, risk_level, evidence): Create a side-effect-free PluginProposal from explicit capability-gap evidence. Use this before plugin_generate when a missing capability should be reviewed. Does not call an LLM, write files, or install anything.\n- **assess_compatibility**(manifest, source_path) [capability_expand category: plugin_management]: Assess whether a foreign plugin manifest or real DSH source bundle is compatible with LeapFlow. A source bundle assessment is static: runtime_ready stays false until restricted Node discovery during plugin_install. Returns component-level verdicts and limitations.\n- **plugin_generate**(plugin_id, description, proposal_id) [capability_expand category: system]: Generate a new ToolPlugin from a natural-language capability description. The LLM produces code that conforms to the ToolPlugin Protocol; it is then rigorously validated (syntax, structure, import, protocol conformance). The generated source is stored in CAS and receives explicit content approval. It DOES NOT install the plugin — installation requires a second, mutation-specific approval via plugin_install.\n- **plugin_install**(plugin_id, code, marketplace_name, source_path, proposal_id, version_label) [capability_expand category: system]: Install a plugin from exactly one source: validated Python code, the configured Python marketplace, or a real DSH source bundle. DSH bundles run restricted Node discovery before registration; only public host tools are installed and client UI limitations are reported. Writes profile-scoped state and mutates the process-global registry. REQUIRES APPROVAL.\n- **plugin_rollback**(plugin_id, version) [capability_expand category: system]: Rollback a profile-scoped plugin to a recorded source version and reload it. REQUIRES APPROVAL.\n- **plugin_reload**(plugin_id, version_label) [capability_expand category: system]: Hot-reload a plugin at runtime. Disposes the old plugin fiber, re-imports its module, and registers a fresh instance. Existing in-flight turns are unaffected (snapshot isolation). REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_disable**(plugin_id) [capability_expand category: system]: Disable a plugin by disposing its fiber, removing its tools from the runtime registry. Cannot disable self_management itself. REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_remove**(plugin_id, delete_source) [capability_expand category: system]: Terminally remove a plugin: dispose its fiber, unregister its tools, remove reload metadata, and optionally delete its profile-scoped source file. Cannot remove self_management itself. REQUIRES APPROVAL.\n- **plugin_unquarantine**(plugin_id) [capability_expand category: plugin_management]: Restore a quarantined plugin to probation status for re-evaluation. Unfreezes the trust ledger, transitions the proposal back to PROBATION, and reloads the plugin. REQUIRES APPROVAL.\n- **plugin_enable**(plugin_id) [capability_expand category: system]: Re-enable a previously disabled plugin by reloading its module and registering a fresh instance. REQUIRES APPROVAL.\n- **self_describe**(facet): Answer questions about LeapFlow ITSELF from live runtime evidence — its identity/version/model, what tools and plugins it has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of self-awareness. Prefer this over reading LeapFlow's own source code. Pick facet=capabilities for 'what can you do / do you support X', identity for version/model, runtime for context/posture, or all for a full self-check when explicitly requested.\n- **runtime_snapshot**(): Lightweight (~150 token) flat snapshot of current runtime state: model, context budget, posture, disclosure level, turn count, cache hit rate, uptime, tool count, and pending approvals.\n- **schedule_create**(trigger_expression, instruction, execution_mode, max_retries, delivery_target) [capability_expand category: scheduler]: Create a new scheduled task. Specify a trigger expression (e.g. '30m', 'every 2h', '0 9 * * *') and the instruction to execute on each trigger. Optionally provide an execution mode and a delivery target to receive notifications.\n- **schedule_list**() [capability_expand category: scheduler]: List all scheduled tasks with their current state, trigger, and next due time.\n- **schedule_status**(task_id) [capability_expand category: scheduler]: Get detailed status and recent execution history for a scheduled task.\n- **schedule_pause**(task_id) [capability_expand category: scheduler]: Pause a scheduled task so it stops firing without being cancelled.\n- **schedule_resume**(task_id) [capability_expand category: scheduler]: Resume a paused scheduled task — re-arms it and recalculates next due time.\n- **schedule_cancel**(task_id) [capability_expand category: scheduler]: Cancel a scheduled task permanently.\n- **tool_search**(query, max_results): Search all registered tools by keyword relevance. Returns a ranked list of matching tools with name, category, and summary. Use this when you need a tool but are unsure of its exact name or category.\n- **tool_describe**(tool_name): Get the full callable schema of a single tool by exact name. Returns the tool's description, parameters, and metadata. Use after tool_search to inspect a specific tool before calling it.\n- **hw_list**(): List connected hardware devices with their channel counts and measured quantities. Start here; it does not include operating limits.\n- **hw_describe**(device_id): Return the full reference for one device: every channel, its unit, its operating envelope, rate limit, reversibility, and required interlocks, plus any prior outcomes recorded for its writable channels. Required before commanding a device.\n- **hw_read**(device_id, channel_id): Read the current value of one device channel. Has no physical effect.\n- **hw_status**(device_id): Report connection health, halt capability, and recent notable events (threshold excursions, lost samples, stalled channels) for one device.\n- **hw_configure**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Set a configuration value or setpoint on a channel declaring effect=configure. Setpoints often have inertia: the value may need time to stabilise.\n- **hw_actuate**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Command motion or output on a channel declaring effect=actuate. This moves physical hardware; a repeat from an unknown state is not a safe retry.\n- **hw_dispense**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Consume an irreversible resource on a channel declaring effect=dispense. Running this twice dispenses twice; never repeat it after a failure without first verifying what already happened.\n- **hw_estop**(device_id) [capability_expand category: hardware]: Stop all motion and output on a device immediately. Never requires approval; use it whenever device behaviour is unexpected.\n- **hw_batch_actuate**(device_id, commands, dry_run, verify) [capability_expand category: hardware]: Write multiple actuator channels in one atomic transaction. Used for coordinated multi-joint control where all positions must be commanded simultaneously (e.g. robot arm joint-space control).\n- **hw_control_start**(device_id, mode, targets, frequency_hz, trajectory, policy, gains) [capability_expand category: hardware]: Start a real-time control loop on a robot device. Supports PID position tracking, impedance control, trajectory following, and neural policy chains.\n- **hw_control_stop**(device_id) [capability_expand category: hardware]: Stop the real-time control loop on a device. Gracefully halts the device and returns session statistics.\n- **hw_control_status**(device_id) [capability_expand category: hardware]: Get the status and statistics of a running control loop. Returns frequency, jitter, overruns, and policy state.\n- **hw_policy_infer**(device_id, policy, task, execute, chunk_size, verify, compute_budget) [capability_expand category: hardware]: Execute VLA policy inference on a robot device. Reads observation, runs policy, optionally writes action to actuators. Supports local (HuggingFace) and remote (PolicyServer) policies.\n- **hw_policy_episode**(action, device_id, policy, task) [capability_expand category: hardware]: Manage policy inference episode lifecycle. Start an episode to begin a task, stop to finalise and get stats.\n- **hw_policy_list**(source) [capability_expand category: hardware]: List available VLA policies and their capabilities.\n- **hw_orchestrate**(mode, operations, barrier_timeout_s, label) [capability_expand category: hardware]: Execute coordinated operations across multiple hardware devices. Supports sequential (one-at-a-time), parallel (concurrent), and barrier (synchronised) execution modes. Any failure in a parallel group triggers halt_all on every participating device for safety.\n- **session_search**(query, limit) [capability_expand category: unclassified]: Search past conversation sessions in the current workspace for relevant context.\n- **session_list**(limit): List recent conversation sessions in the current workspace with stable ids, titles, dates, and summaries. Use for browsing past tasks or when user asks to see history without specific search terms. No keywords needed — returns chronological list.\n- **session_detail**(session_id, limit, offset, include_inactive) [capability_expand category: unclassified]: Read a paginated persisted transcript for one past conversation session in the current workspace. Use after session_list or session_search returns a session_id.\n- **click**(element_index) [capability_expand category: desktop]: Click a UI element by its element_index (from the latest observe_ui snapshot)\n- **get_clipboard**() [capability_expand category: desktop]: Read current clipboard text content\n- **list_apps**(filter, running_only) [capability_expand category: desktop]: List available applications on this system. Use to discover correct bundle_id before switch_app.\n- **list_windows**() [capability_expand category: desktop]: List all top-level windows with pid, window_id, title, and per-window state (minimized, on-screen). Call this first to pick the pid and window_id that observe_ui and other window tools require.\n- **observe_ui**(pid, window_id, query) [capability_expand category: desktop]: Snapshot one window's actionable UI elements, each tagged with an element_index for click/right_click/read_text. Re-observe after actions — indices belong to one snapshot. Requires the window's pid and window_id from list_windows.\n- **open_url**(url, app_id) [capability_expand category: desktop]: Open a URL in the default or specified browser\n- **read_text**(element_index) [capability_expand category: desktop]: Read the text content of a specific UI element from the latest snapshot\n- **right_click**(element_index) [capability_expand category: desktop]: Right-click a UI element to open its context menu. Returns visible menu items.\n- **screenshot**(pid, window_id) [capability_expand category: desktop]: Capture a screenshot for visual verification. With pid + window_id captures that window (works across all displays); defaults to the last observed window, or the full desktop when no window has been observed.\n- **scroll**(element_index, direction, amount, pid, window_id) [capability_expand category: desktop]: Scroll a scrollable area of a window. Omit element_index to scroll the window's focused/page scroller; pass one to scroll an exact element from the latest snapshot.\n- **select_text**(element_index) [capability_expand category: desktop]: Select all text in a UI element (focus + select-all, for subsequent copy)\n- **set_clipboard**(text) [capability_expand category: desktop]: Write text to the clipboard\n- **shortcut**(keys) [capability_expand category: desktop]: Execute a keyboard shortcut\n- **switch_app**(app_id) [capability_expand category: desktop]: Switch to an app (launch if needed, activate, verify)\n- **type_text**(text) [capability_expand category: desktop]: Type text into the currently focused element\n- **wait**(seconds) [capability_expand category: desktop]: Wait for a specified duration before continuing\n- **wait_until**(condition, pid, window_id, timeout, poll_interval) [capability_expand category: desktop]: Wait until a UI condition is met (polls UI tree). Returns elements when found or on timeout.\n- **wait_until_stable**(timeout, poll_interval, pid, window_id) [capability_expand category: desktop]: Wait until the UI stops changing (element set stabilizes across polls).\n\n## App Connector Capability Index\nLeapFlow can onboard and manage external apps through `platform_connect` and execute exact registered business actions through `platform_action`.\nFor requests about connecting, setting up, configuring, enabling, or managing a supported app, use `platform_connect` first instead of generating SDK/Webhook sample code.\n`platform_connect.action` is the management namespace: list, guide, preflight, connect, disconnect, remove, status, events_start, events_stop, events_status.\n`platform_action.action` is only for exact registered platform business actions listed below, such as `im.send_message`; never use management actions like `list` or `guide` there.\nAll business fields MUST go inside `payload`; top-level keys are only `platform`, `action`, and `payload`.\nDo not invent platform IDs or platform action names. If the needed action is not listed, ask for discovery/clarification instead of guessing.\nUse `platform_connect` with `action='guide'` and the matching `platform` to start onboarding; use `action='list'` when the app is unclear.\nWhen a pending onboarding state is present, continue from that state with `platform_connect` instead of asking the user to restate the app.\nSupported apps:\n- `api_server`: API Server (OpenAI Compatible) (category=api; backend=adapter; platform_action actions=none registered)\n- `dingtalk`: 钉钉 (DingTalk) (category=im; backend=adapter; platform_action actions=none registered)\n- `feishu`: 飞书 (Feishu/Lark) (category=im; backend=cli)\n - `calendar.create_event` payload={summary*, start_time*, end_time*, attendees} [write/high]\n - `docs.create_markdown` payload={title*, markdown*, folder_token} [write/medium]\n - `drive.search` payload={query*, limit} [read/medium]\n - `im.add_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.download_resource` payload={message_id*, file_key*, type*} [read/low]\n - `im.get_messages` payload={message_ids*} [read/low]\n - `im.list_chats` payload={page_size} [read/low]\n - `im.list_messages` payload={chat_id*} [read/low]\n - `im.list_thread_messages` payload={thread*} [read/low]\n - `im.remove_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.reply_message` payload={message_id*, text*} [send/high]\n - `im.search_chats` payload={query*, page_size} [read/low]\n - `im.search_messages` payload={query*} [read/medium]\n - `im.send_message` payload={chat_id*, text*, thread_id} [send/high]\n - `im.update_card` payload={token*, card*} [write/medium]\n - `im.update_message` payload={message_id*, text*} [write/medium]\n - `mail.search_unread` payload={query, limit} [read/high]\n - `sheets.append_row` payload={spreadsheet_token*, sheet_id*, values*} [write/medium]\n - `task.create` payload={title*, description, due_time} [write/medium]\n- `telegram`: Telegram (category=social; backend=adapter; platform_action actions=none registered)\n- `webhook`: Webhook (Generic) (category=webhook; backend=adapter; platform_action actions=none registered)\n\n## Learned Skills\nYou have access to the following learned skills. Use `skills_list` to browse or `skill_view` to read details:\n- api_designer: REST and GraphQL API design with OpenAPI spec generation and best practices [API, REST, GraphQL]\n- code_review: Structured code review for security, performance, and style [code, review, security]\n- data_analysis: CSV/JSON data analysis with statistical summaries, quality checks, and visualiza [data, analysis, statistics]\n- devops_helper: CI/CD pipeline configuration, Docker orchestration, and deployment strategy [devops, CI/CD, Docker]\n- document_writer: Structured document generation: technical docs, API references, reports, and REA [documentation, technical-writing, README]\n- git_workflow: Git operation orchestration: branching, commits, conflicts, and PR workflow [git, version-control, branching]\n- i18n_helper: Internationalization workflow: string extraction, translation, locale management [i18n, internationalization, localization]\n- project_analysis: Project structure analysis, dependency audit, and tech stack identification [project, analysis, dependencies]\n- security_audit: Security vulnerability scanning strategy, threat modeling, and fix recommendatio [security, vulnerability, audit]\n- shell_automation: Natural language to shell commands with safety review and step-by-step execution [shell, terminal, automation]\n- test_writer: Generate unit and integration tests from source code with edge-case coverage [testing, unit-test, integration-test]\n- web_research: Multi-step web search with cross-verification and structured synthesis [web, search, research]\n\n## Tool Usage\nTools are normally invoked through the native function-calling mechanism, not by writing JSON in your reply\ntext. Only if the provider signals that native function calling is unavailable for this turn, fall back to a\nsingle JSON code block: `{\"name\": \"tool_name\", \"arguments\": {\"key\": \"value\"}}` — use this fallback format\nonly, never both. Only call a tool whose exact name appears in the tool index above and is currently callable\n(or reachable via `capability_expand`). Never invent, rename, alias, or guess a tool name, platform ID, or\nplatform action from argument shape or wording — if the index does not list it, it does not exist.\n`platform_connect.action` (list/guide/preflight/connect/disconnect/remove/status/events_start/events_stop/\nevents_status) is the App Connector management namespace; `platform_action.action` only accepts exact\nregistered business actions such as `im.send_message` shown in the App Connector Capability Index — never mix\nthe two namespaces. If a tool call returns an unknown/unavailable result, use the returned suggestions or\navailable names for a single retry instead of trying further variations of the same guess.\n\n**Side-effect action rule** (`platform_action` with effect=send/write/execute):\n- Call each unique action+payload **exactly once**. Never include duplicates in the same turn.\n- Once the result returns `\"completed\": true`, that action is DONE for this task. Do NOT call it again\n in any subsequent turn — immediately summarize the result for the user instead.\n- The system enforces idempotency: duplicate calls are blocked and will not execute.\n- If the user explicitly requests sending/writing multiple times, use distinct payloads per call.\n\n**Resource identifier provenance rule**:\n- NEVER fabricate, guess, or infer resource identifiers (chat_id, message_id, file_key, user_id, etc.).\n Every resource ID used in a side-effect action MUST come from a successful API response in this session.\n- If a read/list action fails (e.g. authorization error), you do NOT have valid resource IDs.\n Report the failure to the user — do NOT attempt the dependent write/send action with a guessed ID.\n- When a tool result contains `\"llm_instruction\"`, follow it exactly.\n\n## Guidelines\n1. **Direct answers first**: If you already know the answer, respond directly without tools.\n2. **Avoid redundant tool calls**: Do not call the same tool with the same arguments more than once in the same user turn. When an existing tool result already answers the user's request, stop calling tools and answer directly.\n3. **Use tools proactively**: When the user asks about files, time, system state, or needs actions performed, use the appropriate tool.\n4. **Chain tools when needed**: You can call multiple tools in sequence (e.g., list files → read file → summarize).\n5. **Handle failures gracefully**: If a tool fails, explain what went wrong and suggest alternatives. If it failed because of an unknown tool/platform/action name, retry once with an exact name from the returned suggestions, then explain rather than keep guessing.\n6. **Summarize results naturally**: After tool execution, synthesize the results into a helpful answer rather than dumping raw output.\n7. **Stay conversational**: Maintain a natural, helpful tone. Acknowledge context from earlier in the conversation.\n8. **Recall past work**:\n - Broad queries (\"之前做了什么\", \"列出任务\"): answer from the \"Recent Task History\" section already in your context. If insufficient, call session_list.\n - Specific lookups (\"上次那个配置怎么改的\"): call session_search with relevant phrases (NOT single characters).\n - Do NOT call search tools repeatedly with keyword fragments. One well-phrased call is better than ten fragmented ones.\n\n## Coding & Verification\nWhen working with code, prefer the precise built-in tools over ad-hoc shell: use `repo_map` to orient in an\nunfamiliar project, `code_search`/`file_find` to locate and `code_intel` for symbols, `edit_file` (anchored\nsearch-replace, or a unified `diff`) to change files — never rewrite a whole file to change a few lines — and\n`git_query` to inspect diffs/log. After edits, check `syntax_ok` in the result and run `test_run`/`lint_check`\nbefore declaring the work done. Batch independent read-only calls (search/read/query) into a single turn — they\nrun in parallel.\n\n## Presentation Style\n1. **Polished Markdown only**: Format user-facing answers with clean Markdown headings, short paragraphs, and concise bullets. Use tables only when they improve comparison or scanning.\n2. **Terminal-friendly layout**: Keep lines readable in a TUI; avoid dense walls of text, deeply nested lists, oversized ASCII art, or heavy visual blocks.\n3. **Elegant emphasis**: Use bold text sparingly for key terms and conclusions. Avoid excessive emojis, decorative symbols, repeated separators, or visual noise.\n4. **Theme-safe colors**: Do not emit ANSI escape codes, HTML color tags, Rich markup, or hardcoded color names. Rely on the TUI theme to render Markdown professionally.\n5. **No leaked tool protocol**: Never show tool-call JSON, internal schemas, raw observations, tool result payloads, or hidden reasoning in the final answer unless the user explicitly asks for raw/debug output. Treat any prior `{\"name\": ..., \"arguments\": ...}` blocks and `Tool result (...)` messages as internal execution context only.\n6. **Professional closure**: End with a concise conclusion or next step when helpful; avoid rambling after the useful answer is complete.\n\nWhen finished with all tool calls, respond normally without a JSON block, tool-call transcript, or process log.\n\n## Task Contract\n- Task ID: turn-6\n- Original user request: Actuate the bench_r8 setpoint to 65.\n- Workspace root: \n- Allowed roots: \n- Treat relative project paths as relative to the workspace root; never infer `.` as the project root when a workspace root is provided.\n- Workspace boundary is enforced by tools: do not read, search, edit, or run commands against paths outside the allowed roots unless the user explicitly requests an external path and the tool/approval policy permits it.\n- LeapFlow workspace config is optional at `/.leapflow/config.yaml`; runtime config is loaded from `~/.leapflow/config/user.yaml` and `~/.leapflow/profiles//config/*.yaml`.\n- Preserve this task contract across summarization, compression, tool loops, and memory retrieval.\n" + }, + { + "role": "assistant", + "content": "[Called: hw_configure]\nHomed the device; the readiness gate is now satisfied." + }, + { + "role": "user", + "content": "Preview an actuate of bench_r8 setpoint to 55, dry run only." + }, + { + "role": "assistant", + "content": "[Called: hw_actuate]\nDry run confirmed the setpoint command is feasible after homing." + }, + { + "role": "user", + "content": "Actuate the bench_r8 setpoint to 65.\nActuate the bench_r8 setpoint to 65." + }, + { + "role": "assistant", + "content": "", + "tool_calls": [ + "hw_actuate" + ] + }, + { + "role": "tool", + "content": "{\"ok\": true, \"device_id\": \"bench_r8\", \"channel_id\": \"setpoint\", \"side_effect_state\": \"committed\", \"settled\": true, \"execution_id\": \"\", \"idempotency_key\": \"\", \"execution_policy\": \"external_side_effect\", \"tool_call_id\": \"call_1\", \"execution_status\": \"completed\"}", + "tool_result": true + } + ], + "tools": [ + "capability_expand", + "code_intel", + "code_search", + "config_get", + "config_list", + "env_info", + "file_find", + "file_list", + "file_read", + "git_query", + "hw_actuate", + "hw_batch_actuate", + "hw_configure", + "hw_control_start", + "hw_control_status", + "hw_control_stop", + "hw_describe", + "hw_dispense", + "hw_estop", + "hw_list", + "hw_orchestrate", + "hw_policy_episode", + "hw_policy_infer", + "hw_policy_list", + "hw_read", + "hw_status", + "lint_check", + "memory_search", + "plugin_list", + "plugin_propose", + "plugin_status", + "plugin_versions", + "repo_map", + "research_note", + "runtime_snapshot", + "schedule_reentry", + "self_describe", + "session_list", + "skill_view", + "skills_list", + "terminal_list", + "terminal_read", + "test_run", + "text_search", + "time_get", + "tool_describe", + "tool_search", + "web_fetch" + ] + }, + "responses": [ + { + "status": 200, + "content_type": "application/json", + "body": "{\"id\": \"chatcmpl-cassette\", \"object\": \"chat.completion\", \"model\": \"cassette-model\", \"choices\": [{\"index\": 0, \"message\": {\"role\": \"assistant\", \"content\": \"Commanded the setpoint through the approval path.\"}, \"finish_reason\": \"stop\"}], \"usage\": {\"prompt_tokens\": 64, \"completion_tokens\": 16, \"total_tokens\": 80}}" + } + ] +} diff --git a/tests/_fixtures/cassettes/r8_hardware/cassette-model-33a143a132a46955.cassette.json b/tests/_fixtures/cassettes/r8_hardware/cassette-model-33a143a132a46955.cassette.json new file mode 100644 index 00000000..13cd38ff --- /dev/null +++ b/tests/_fixtures/cassettes/r8_hardware/cassette-model-33a143a132a46955.cassette.json @@ -0,0 +1,83 @@ +{ + "fingerprint": "33a143a132a469555d0482e6afe47da55b6c895c55de02739bcdf73769106f0c", + "note": "captured in seed mode", + "request": { + "model": "cassette-model", + "stream": false, + "messages": [ + { + "role": "system", + "content": "You are LeapFlow, an intelligent assistant that can both converse naturally and take real actions on the user's computer.\n\n## Capabilities\nThe tool index below lists **every** registered tool by name and a one-line summary — this is the complete\ncapability contract; nothing else exists. Only a subset is directly callable this turn (via native tool calling,\nnot a JSON block in your reply). If you need a tool from the index that is not yet callable, call\n`capability_expand` with its category name first — the matching tools become callable immediately after.\nWhen the user asks about LeapFlow **itself** — its identity/version/model, what tools, plugins, or capabilities\nit has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of\nself-awareness — answer from live runtime evidence via `self_describe` (facet=capabilities for \"what can you\ndo / do you support X\", identity for version/model, runtime for context/posture, or `all` for a full self-check),\nnever by reading LeapFlow's own source code, documentation, or memory. Report configuration-dependent or\nunavailable capabilities as limitations rather than inferring them.\n- **text_search**(text, pattern): Search for a regex pattern in text.\n- **text_replace**(text, old, new, count) [capability_expand category: write]: Replace occurrences of a substring in text.\n- **time_get**(): Get current date and time.\n- **env_info**(): Get system environment information (OS, Python version, cwd).\n- **skills_list**(query, category, source): List available learned skills. Use when user asks about capabilities or you need a specific skill.\n- **skill_view**(name): View the full content of a specific skill document.\n- **skill_list_files**(name) [capability_expand category: skills]: List all support files in a skill directory (configs, templates, examples). Use before skill_file_read to discover available files.\n- **skill_file_read**(name, file_path) [capability_expand category: skills]: Read an individual support file from a skill directory. Use skill_list_files first to discover available files.\n- **code_intel**(path, operation): Precise document symbols (outline) for a source file: classes, functions, and methods with line ranges. Python uses an exact AST parse; other languages use a keyword-prefix scan. Prefer over file_read mode=symbols for accurate navigation before editing. Read-only.\n- **repo_map**(path): Compact project orientation for a repository root: languages, detected test/lint commands, top-level structure, entry points, manifest, and VCS branch. Call this first when entering an unfamiliar codebase. Read-only.\n- **scm_sync**(action, cwd, remote, pull_ref, push_ref, timeout) [capability_expand category: scm]: Run a typed git SCM action. Use this instead of shell_run for git pull/push/status. For 'pull origin main then push', set action='pull_then_push', remote='origin', pull_ref='main', and omit push_ref so LeapFlow pushes the current local branch.\n- **git_query**(action, cwd, ref, path, staged, max_count, stat): Read-only structured git inspection: action=diff|log|status|branch|show. Prefer over shell_run for reading repo state — output is clipped, redacted, and log/branch are parsed into structured fields. Use scm_sync for pull/push.\n- **git_write**(action, cwd, message, stage_all, name, ref, create) [capability_expand category: scm]: Mutating git actions: action=commit (message, stage_all), branch (create+switch), checkout (switch; create=true for -b). Approval-gated. Use scm_sync for pull/push and git_query for reads.\n- **test_run**(command, cwd, timeout): Run the project's test suite and return structured results (framework, passed/failed counts, failing tests). Auto-detects the runner (pytest/npm/go/cargo) or uses a configured/explicit command; executes via the governed shell. ok=true means the runner executed — see 'success' for pass/fail.\n- **lint_check**(command, cwd, timeout): Run the project's linter and return a structured clean/issue result. Auto-detects the linter (ruff/eslint/go vet/clippy) or uses a configured/explicit command; executes via the governed shell. ok=true means the linter ran — see 'clean'.\n- **file_list**(path, pattern, depth): List files and directories at a given path. Use depth=1 or depth=2 to get a recursive tree in one call instead of listing each sub-directory separately.\n- **file_read**(path, max_lines, start_line, max_chars, mode): Read text file content with adaptive context governance. For large or unfamiliar files, prefer mode='outline' or mode='symbols' first, then use mode='raw' with start_line/max_lines for the specific range you actually need. For LeapFlow's own settings, use config_list / config_get / config_set — its config files are outside the workspace and not readable here.\n- **file_write**(path, content, mode) [capability_expand category: write]: Write content to a file (overwrite or append).\n- **code_search**(pattern, patterns, path, glob, ignore_case, multiline, max_results, context_lines): Search file CONTENTS by regex pattern across a directory tree (ripgrep-backed). Requires a regex pattern. NOT for listing or browsing directory contents — use file_list for that. Prefer this over shell_run grep: faster, skips VCS/dependency/build dirs, and returns structured path:line:column matches. Batch related lookups into ONE call via `patterns` (OR-combined, single pass) instead of issuing several separate searches. Use file_read for the surrounding context of a hit.\n- **file_find**(glob, path, max_results): Find files by a recursive glob pattern under a base path (e.g. '**/test_*.py' or '*.md'). Prefer this over shell_run find; skips VCS/dependency/build dirs.\n- **edit_file**(path, edits, dry_run, diff) [capability_expand category: write]: Apply targeted, anchored search-replace edits to an EXISTING text file (use file_write to create/overwrite). Each edit is {original_text, new_text, replace_all?}; original_text must match exactly and uniquely (or set replace_all) — a non-unique or missing anchor is rejected so files are never corrupted. Set dry_run to preview. Alternatively pass a unified 'diff' to apply its hunks as anchored edits. Far cheaper and safer than rewriting a whole file.\n- **shell_run**(command, cwd, timeout) [capability_expand category: shell]: Execute a one-shot shell command with timeout protection. Runs in the active workspace; paths resolving outside it are refused. Reach for a structured tool first when one fits — web_fetch for anything over HTTP(S), git_query/scm_sync for git, code_search/file_find/file_read for the repo, config_get/config_set for LeapFlow's own settings — because those report typed results, while a failed shell command can only be diagnosed from its exit code and stderr. Every shell run counts as an external side effect, so a failure stops the rest of the batch and is not retried automatically.\n- **terminal_open**(command, cwd, shell) [capability_expand category: terminal]: Open a PERSISTENT shell session (REPL/dev server/watch), returning a session_id for terminal_send/read/close. Disabled unless tools.terminal_session_enabled is set. For one-shot commands use shell_run instead.\n- **terminal_send**(session_id, input, wait) [capability_expand category: terminal]: Send a line of input to a persistent terminal session and return output captured shortly after.\n- **terminal_read**(session_id, wait): Drain buffered output from a persistent terminal session (optionally waiting briefly first).\n- **terminal_close**(session_id) [capability_expand category: terminal]: Terminate a persistent terminal session and release its process group.\n- **terminal_list**(): List active persistent terminal sessions.\n- **config_list**(category, limit): List LeapFlow's own writable settings (LLM model and endpoint, daemon, memory, perception, gateway, …) with current values. Use this to discover the exact key before changing anything. Optionally narrow by `category`. This is the only correct way to inspect LeapFlow configuration — never read config files from disk.\n- **config_get**(key): Read one LeapFlow setting by key (e.g. 'llm.model', 'llm.base_url', 'daemon.log_level'), returning its current value, type, scopes, and whether a change needs a daemon restart. There is no 'llm.provider' key: provider behavior is inferred from the OpenAI-compatible 'llm.base_url'. Never read LeapFlow config files from disk — use this.\n- **config_set**(key, value, scope) [capability_expand category: config]: Change one LeapFlow setting by key, e.g. switch an OpenAI-compatible LLM with key='llm.model' and key='llm.base_url'. There is no 'llm.provider' key: provider behavior is inferred from the endpoint. Values are validated and coerced; credentials are stored in the vault automatically. Call config_list or config_get first if unsure of the key. The result states whether a `leap daemon restart` is required. Never edit LeapFlow config files directly.\n- **web_fetch**(url, select, timeout, max_bytes): Read a URL over HTTP(S) and get back extracted, context-sized content: parsed JSON for API endpoints, readable text plus links for web pages. Use this for anything on the internet — prices, docs, releases, articles — instead of running curl through shell_run: it reports real HTTP status codes, retries rate limits on its own, and is a plain read so a retry is always safe. For JSON APIs pass `select` with a dotted path (e.g. 'chart.result.0.meta') to return just that part instead of the whole payload.\n- **memory_search**(query, limit): Search agent memory for relevant past experiences, observations, and facts.\n- **memory_add**(content, kind) [capability_expand category: write]: Store a new observation or insight in memory for future reference.\n- **research_note**(kind, text): Record a compact, structured note about the current task's state so it survives context compression on long / multi-step tasks. Use for durable findings, open questions still to resolve, decisions / excluded paths, and the immediate next step. One concise sentence per note.\n- **capability_expand**(category): Fetch the full callable schema for every tool in a capability category (e.g. 'hub', 'gateway', 'desktop', 'delegate', 'file', 'memory', 'skill'). The compact tool index always lists every registered tool by name and a one-line summary, but only a static low-risk subset is directly callable each turn. If you need a tool from the index that is not yet callable, call capability_expand with its category first; the matching tools become callable in this turn. Never invent a tool name — expand the category instead.\n- **delegate_task**(goal, context) [capability_expand category: delegate]: Delegate a complex sub-task to an isolated subagent. The subagent gets a fresh context and restricted tool access. Use when a task is self-contained and can be solved independently.\n- **schedule_reentry**(kind, reason, delay_seconds, event_match, max_reentries, deadline_seconds): Register a re-entry so this task can resume later from its current orientation (findings / open questions / next step). Use when work must pause and continue after a delay (kind=time) or when a matching platform event arrives (kind=event), instead of finishing now. The research-ledger state is carried over automatically.\n- **hub_push**(skill_name, visibility, version) [capability_expand category: hub]: Push a local skill to the ModelScope Hub for sharing or backup.\n- **hub_pull**(repo_id, version) [capability_expand category: hub]: Pull a skill from the ModelScope Hub to install locally.\n- **hub_search**(query) [capability_expand category: hub]: Search for skills on the Hub by keyword or description.\n- **hub_sync**(mode, dry_run) [capability_expand category: hub]: Preview or execute sync between local skills and Hub.\n- **platform_action**(platform, action, payload, backend_kind) [capability_expand category: gateway]: Execute an exact registered business action on an external platform through LeapFlow's App Connector layer. Actions must be copied from the App Connector Capability Index and are addressed as domain.operation, e.g. im.send_message or docs.create_markdown. All business fields (chat_id, text, query, etc.) MUST be placed inside `payload`, never at the top level. Example: {\"platform\":\"feishu\",\"action\":\"im.send_message\",\"payload\":{\"chat_id\":\"oc_xxx\",\"text\":\"hello\"}}. Do not invent action names, do not use management actions such as list/guide/connect/status here.\n- **platform_connect**(action, platform, credentials, options, checkpoint) [capability_expand category: gateway]: List, guide, connect, disconnect, remove, or check status for external platforms using the App Connector management namespace. Supports REST and CLI backends. Use this for management actions such as list/guide/preflight/connect/status; use platform_action only for exact registered business actions.\n- **gateway_send**(platform, chat_id, text, thread_id) [capability_expand category: gateway]: Send a message to a connected external platform (Feishu group, Telegram chat, DingTalk conversation, etc.). Requires the platform to be connected via gateway_connect first. Use gateway_connect with action='list' to see connected platforms and available chat IDs.\n- **gateway_connect**(action, platform, credentials, options) [capability_expand category: gateway]: Connect, configure, or manage external platform integrations (Feishu, DingTalk, Telegram, Slack, Discord, etc.). Conversational flow: 1) call 'guide' to get setup steps + required fields, 2) present the steps to the user and ask for ALL required credentials in a single message, 3) call 'connect' with the credentials. Goal: complete in 1–2 user turns. NEVER include credential values in your text response.\n- **plugin_list**(): List the live plugin registry across Tool/Gateway/LLM subsystems for plugin management: which plugins are loaded, their category and fiber lifecycle state. For questions about LeapFlow's own capabilities or whether it supports X, use self_describe instead.\n- **plugin_status**(plugin_id): Get detailed status of a specific plugin: its declared category, runtime dependencies, contributed tools, and fiber lifecycle state.\n- **plugin_versions**(plugin_id): List recorded source versions and active pointer for a profile-scoped plugin.\n- **plugin_propose**(requested_capability, plugin_id, proposed_tools, test_cases, risk_level, evidence): Create a side-effect-free PluginProposal from explicit capability-gap evidence. Use this before plugin_generate when a missing capability should be reviewed. Does not call an LLM, write files, or install anything.\n- **assess_compatibility**(manifest, source_path) [capability_expand category: plugin_management]: Assess whether a foreign plugin manifest or real DSH source bundle is compatible with LeapFlow. A source bundle assessment is static: runtime_ready stays false until restricted Node discovery during plugin_install. Returns component-level verdicts and limitations.\n- **plugin_generate**(plugin_id, description, proposal_id) [capability_expand category: system]: Generate a new ToolPlugin from a natural-language capability description. The LLM produces code that conforms to the ToolPlugin Protocol; it is then rigorously validated (syntax, structure, import, protocol conformance). The generated source is stored in CAS and receives explicit content approval. It DOES NOT install the plugin — installation requires a second, mutation-specific approval via plugin_install.\n- **plugin_install**(plugin_id, code, marketplace_name, source_path, proposal_id, version_label) [capability_expand category: system]: Install a plugin from exactly one source: validated Python code, the configured Python marketplace, or a real DSH source bundle. DSH bundles run restricted Node discovery before registration; only public host tools are installed and client UI limitations are reported. Writes profile-scoped state and mutates the process-global registry. REQUIRES APPROVAL.\n- **plugin_rollback**(plugin_id, version) [capability_expand category: system]: Rollback a profile-scoped plugin to a recorded source version and reload it. REQUIRES APPROVAL.\n- **plugin_reload**(plugin_id, version_label) [capability_expand category: system]: Hot-reload a plugin at runtime. Disposes the old plugin fiber, re-imports its module, and registers a fresh instance. Existing in-flight turns are unaffected (snapshot isolation). REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_disable**(plugin_id) [capability_expand category: system]: Disable a plugin by disposing its fiber, removing its tools from the runtime registry. Cannot disable self_management itself. REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_remove**(plugin_id, delete_source) [capability_expand category: system]: Terminally remove a plugin: dispose its fiber, unregister its tools, remove reload metadata, and optionally delete its profile-scoped source file. Cannot remove self_management itself. REQUIRES APPROVAL.\n- **plugin_unquarantine**(plugin_id) [capability_expand category: plugin_management]: Restore a quarantined plugin to probation status for re-evaluation. Unfreezes the trust ledger, transitions the proposal back to PROBATION, and reloads the plugin. REQUIRES APPROVAL.\n- **plugin_enable**(plugin_id) [capability_expand category: system]: Re-enable a previously disabled plugin by reloading its module and registering a fresh instance. REQUIRES APPROVAL.\n- **self_describe**(facet): Answer questions about LeapFlow ITSELF from live runtime evidence — its identity/version/model, what tools and plugins it has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of self-awareness. Prefer this over reading LeapFlow's own source code. Pick facet=capabilities for 'what can you do / do you support X', identity for version/model, runtime for context/posture, or all for a full self-check when explicitly requested.\n- **runtime_snapshot**(): Lightweight (~150 token) flat snapshot of current runtime state: model, context budget, posture, disclosure level, turn count, cache hit rate, uptime, tool count, and pending approvals.\n- **schedule_create**(trigger_expression, instruction, execution_mode, max_retries, delivery_target) [capability_expand category: scheduler]: Create a new scheduled task. Specify a trigger expression (e.g. '30m', 'every 2h', '0 9 * * *') and the instruction to execute on each trigger. Optionally provide an execution mode and a delivery target to receive notifications.\n- **schedule_list**() [capability_expand category: scheduler]: List all scheduled tasks with their current state, trigger, and next due time.\n- **schedule_status**(task_id) [capability_expand category: scheduler]: Get detailed status and recent execution history for a scheduled task.\n- **schedule_pause**(task_id) [capability_expand category: scheduler]: Pause a scheduled task so it stops firing without being cancelled.\n- **schedule_resume**(task_id) [capability_expand category: scheduler]: Resume a paused scheduled task — re-arms it and recalculates next due time.\n- **schedule_cancel**(task_id) [capability_expand category: scheduler]: Cancel a scheduled task permanently.\n- **tool_search**(query, max_results): Search all registered tools by keyword relevance. Returns a ranked list of matching tools with name, category, and summary. Use this when you need a tool but are unsure of its exact name or category.\n- **tool_describe**(tool_name): Get the full callable schema of a single tool by exact name. Returns the tool's description, parameters, and metadata. Use after tool_search to inspect a specific tool before calling it.\n- **hw_list**(): List connected hardware devices with their channel counts and measured quantities. Start here; it does not include operating limits.\n- **hw_describe**(device_id): Return the full reference for one device: every channel, its unit, its operating envelope, rate limit, reversibility, and required interlocks, plus any prior outcomes recorded for its writable channels. Required before commanding a device.\n- **hw_read**(device_id, channel_id): Read the current value of one device channel. Has no physical effect.\n- **hw_status**(device_id): Report connection health, halt capability, and recent notable events (threshold excursions, lost samples, stalled channels) for one device.\n- **hw_configure**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Set a configuration value or setpoint on a channel declaring effect=configure. Setpoints often have inertia: the value may need time to stabilise.\n- **hw_actuate**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Command motion or output on a channel declaring effect=actuate. This moves physical hardware; a repeat from an unknown state is not a safe retry.\n- **hw_dispense**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Consume an irreversible resource on a channel declaring effect=dispense. Running this twice dispenses twice; never repeat it after a failure without first verifying what already happened.\n- **hw_estop**(device_id) [capability_expand category: hardware]: Stop all motion and output on a device immediately. Never requires approval; use it whenever device behaviour is unexpected.\n- **hw_batch_actuate**(device_id, commands, dry_run, verify) [capability_expand category: hardware]: Write multiple actuator channels in one atomic transaction. Used for coordinated multi-joint control where all positions must be commanded simultaneously (e.g. robot arm joint-space control).\n- **hw_control_start**(device_id, mode, targets, frequency_hz, trajectory, policy, gains) [capability_expand category: hardware]: Start a real-time control loop on a robot device. Supports PID position tracking, impedance control, trajectory following, and neural policy chains.\n- **hw_control_stop**(device_id) [capability_expand category: hardware]: Stop the real-time control loop on a device. Gracefully halts the device and returns session statistics.\n- **hw_control_status**(device_id) [capability_expand category: hardware]: Get the status and statistics of a running control loop. Returns frequency, jitter, overruns, and policy state.\n- **hw_policy_infer**(device_id, policy, task, execute, chunk_size, verify, compute_budget) [capability_expand category: hardware]: Execute VLA policy inference on a robot device. Reads observation, runs policy, optionally writes action to actuators. Supports local (HuggingFace) and remote (PolicyServer) policies.\n- **hw_policy_episode**(action, device_id, policy, task) [capability_expand category: hardware]: Manage policy inference episode lifecycle. Start an episode to begin a task, stop to finalise and get stats.\n- **hw_policy_list**(source) [capability_expand category: hardware]: List available VLA policies and their capabilities.\n- **hw_orchestrate**(mode, operations, barrier_timeout_s, label) [capability_expand category: hardware]: Execute coordinated operations across multiple hardware devices. Supports sequential (one-at-a-time), parallel (concurrent), and barrier (synchronised) execution modes. Any failure in a parallel group triggers halt_all on every participating device for safety.\n- **session_search**(query, limit) [capability_expand category: unclassified]: Search past conversation sessions in the current workspace for relevant context.\n- **session_list**(limit): List recent conversation sessions in the current workspace with stable ids, titles, dates, and summaries. Use for browsing past tasks or when user asks to see history without specific search terms. No keywords needed — returns chronological list.\n- **session_detail**(session_id, limit, offset, include_inactive) [capability_expand category: unclassified]: Read a paginated persisted transcript for one past conversation session in the current workspace. Use after session_list or session_search returns a session_id.\n- **click**(element_index) [capability_expand category: desktop]: Click a UI element by its element_index (from the latest observe_ui snapshot)\n- **get_clipboard**() [capability_expand category: desktop]: Read current clipboard text content\n- **list_apps**(filter, running_only) [capability_expand category: desktop]: List available applications on this system. Use to discover correct bundle_id before switch_app.\n- **list_windows**() [capability_expand category: desktop]: List all top-level windows with pid, window_id, title, and per-window state (minimized, on-screen). Call this first to pick the pid and window_id that observe_ui and other window tools require.\n- **observe_ui**(pid, window_id, query) [capability_expand category: desktop]: Snapshot one window's actionable UI elements, each tagged with an element_index for click/right_click/read_text. Re-observe after actions — indices belong to one snapshot. Requires the window's pid and window_id from list_windows.\n- **open_url**(url, app_id) [capability_expand category: desktop]: Open a URL in the default or specified browser\n- **read_text**(element_index) [capability_expand category: desktop]: Read the text content of a specific UI element from the latest snapshot\n- **right_click**(element_index) [capability_expand category: desktop]: Right-click a UI element to open its context menu. Returns visible menu items.\n- **screenshot**(pid, window_id) [capability_expand category: desktop]: Capture a screenshot for visual verification. With pid + window_id captures that window (works across all displays); defaults to the last observed window, or the full desktop when no window has been observed.\n- **scroll**(element_index, direction, amount, pid, window_id) [capability_expand category: desktop]: Scroll a scrollable area of a window. Omit element_index to scroll the window's focused/page scroller; pass one to scroll an exact element from the latest snapshot.\n- **select_text**(element_index) [capability_expand category: desktop]: Select all text in a UI element (focus + select-all, for subsequent copy)\n- **set_clipboard**(text) [capability_expand category: desktop]: Write text to the clipboard\n- **shortcut**(keys) [capability_expand category: desktop]: Execute a keyboard shortcut\n- **switch_app**(app_id) [capability_expand category: desktop]: Switch to an app (launch if needed, activate, verify)\n- **type_text**(text) [capability_expand category: desktop]: Type text into the currently focused element\n- **wait**(seconds) [capability_expand category: desktop]: Wait for a specified duration before continuing\n- **wait_until**(condition, pid, window_id, timeout, poll_interval) [capability_expand category: desktop]: Wait until a UI condition is met (polls UI tree). Returns elements when found or on timeout.\n- **wait_until_stable**(timeout, poll_interval, pid, window_id) [capability_expand category: desktop]: Wait until the UI stops changing (element set stabilizes across polls).\n\n## App Connector Capability Index\nLeapFlow can onboard and manage external apps through `platform_connect` and execute exact registered business actions through `platform_action`.\nFor requests about connecting, setting up, configuring, enabling, or managing a supported app, use `platform_connect` first instead of generating SDK/Webhook sample code.\n`platform_connect.action` is the management namespace: list, guide, preflight, connect, disconnect, remove, status, events_start, events_stop, events_status.\n`platform_action.action` is only for exact registered platform business actions listed below, such as `im.send_message`; never use management actions like `list` or `guide` there.\nAll business fields MUST go inside `payload`; top-level keys are only `platform`, `action`, and `payload`.\nDo not invent platform IDs or platform action names. If the needed action is not listed, ask for discovery/clarification instead of guessing.\nUse `platform_connect` with `action='guide'` and the matching `platform` to start onboarding; use `action='list'` when the app is unclear.\nWhen a pending onboarding state is present, continue from that state with `platform_connect` instead of asking the user to restate the app.\nSupported apps:\n- `api_server`: API Server (OpenAI Compatible) (category=api; backend=adapter; platform_action actions=none registered)\n- `dingtalk`: 钉钉 (DingTalk) (category=im; backend=adapter; platform_action actions=none registered)\n- `feishu`: 飞书 (Feishu/Lark) (category=im; backend=cli)\n - `calendar.create_event` payload={summary*, start_time*, end_time*, attendees} [write/high]\n - `docs.create_markdown` payload={title*, markdown*, folder_token} [write/medium]\n - `drive.search` payload={query*, limit} [read/medium]\n - `im.add_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.download_resource` payload={message_id*, file_key*, type*} [read/low]\n - `im.get_messages` payload={message_ids*} [read/low]\n - `im.list_chats` payload={page_size} [read/low]\n - `im.list_messages` payload={chat_id*} [read/low]\n - `im.list_thread_messages` payload={thread*} [read/low]\n - `im.remove_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.reply_message` payload={message_id*, text*} [send/high]\n - `im.search_chats` payload={query*, page_size} [read/low]\n - `im.search_messages` payload={query*} [read/medium]\n - `im.send_message` payload={chat_id*, text*, thread_id} [send/high]\n - `im.update_card` payload={token*, card*} [write/medium]\n - `im.update_message` payload={message_id*, text*} [write/medium]\n - `mail.search_unread` payload={query, limit} [read/high]\n - `sheets.append_row` payload={spreadsheet_token*, sheet_id*, values*} [write/medium]\n - `task.create` payload={title*, description, due_time} [write/medium]\n- `telegram`: Telegram (category=social; backend=adapter; platform_action actions=none registered)\n- `webhook`: Webhook (Generic) (category=webhook; backend=adapter; platform_action actions=none registered)\n\n## Learned Skills\nYou have access to the following learned skills. Use `skills_list` to browse or `skill_view` to read details:\n- api_designer: REST and GraphQL API design with OpenAPI spec generation and best practices [API, REST, GraphQL]\n- code_review: Structured code review for security, performance, and style [code, review, security]\n- data_analysis: CSV/JSON data analysis with statistical summaries, quality checks, and visualiza [data, analysis, statistics]\n- devops_helper: CI/CD pipeline configuration, Docker orchestration, and deployment strategy [devops, CI/CD, Docker]\n- document_writer: Structured document generation: technical docs, API references, reports, and REA [documentation, technical-writing, README]\n- git_workflow: Git operation orchestration: branching, commits, conflicts, and PR workflow [git, version-control, branching]\n- i18n_helper: Internationalization workflow: string extraction, translation, locale management [i18n, internationalization, localization]\n- project_analysis: Project structure analysis, dependency audit, and tech stack identification [project, analysis, dependencies]\n- security_audit: Security vulnerability scanning strategy, threat modeling, and fix recommendatio [security, vulnerability, audit]\n- shell_automation: Natural language to shell commands with safety review and step-by-step execution [shell, terminal, automation]\n- test_writer: Generate unit and integration tests from source code with edge-case coverage [testing, unit-test, integration-test]\n- web_research: Multi-step web search with cross-verification and structured synthesis [web, search, research]\n\n## Tool Usage\nTools are normally invoked through the native function-calling mechanism, not by writing JSON in your reply\ntext. Only if the provider signals that native function calling is unavailable for this turn, fall back to a\nsingle JSON code block: `{\"name\": \"tool_name\", \"arguments\": {\"key\": \"value\"}}` — use this fallback format\nonly, never both. Only call a tool whose exact name appears in the tool index above and is currently callable\n(or reachable via `capability_expand`). Never invent, rename, alias, or guess a tool name, platform ID, or\nplatform action from argument shape or wording — if the index does not list it, it does not exist.\n`platform_connect.action` (list/guide/preflight/connect/disconnect/remove/status/events_start/events_stop/\nevents_status) is the App Connector management namespace; `platform_action.action` only accepts exact\nregistered business actions such as `im.send_message` shown in the App Connector Capability Index — never mix\nthe two namespaces. If a tool call returns an unknown/unavailable result, use the returned suggestions or\navailable names for a single retry instead of trying further variations of the same guess.\n\n**Side-effect action rule** (`platform_action` with effect=send/write/execute):\n- Call each unique action+payload **exactly once**. Never include duplicates in the same turn.\n- Once the result returns `\"completed\": true`, that action is DONE for this task. Do NOT call it again\n in any subsequent turn — immediately summarize the result for the user instead.\n- The system enforces idempotency: duplicate calls are blocked and will not execute.\n- If the user explicitly requests sending/writing multiple times, use distinct payloads per call.\n\n**Resource identifier provenance rule**:\n- NEVER fabricate, guess, or infer resource identifiers (chat_id, message_id, file_key, user_id, etc.).\n Every resource ID used in a side-effect action MUST come from a successful API response in this session.\n- If a read/list action fails (e.g. authorization error), you do NOT have valid resource IDs.\n Report the failure to the user — do NOT attempt the dependent write/send action with a guessed ID.\n- When a tool result contains `\"llm_instruction\"`, follow it exactly.\n\n## Guidelines\n1. **Direct answers first**: If you already know the answer, respond directly without tools.\n2. **Avoid redundant tool calls**: Do not call the same tool with the same arguments more than once in the same user turn. When an existing tool result already answers the user's request, stop calling tools and answer directly.\n3. **Use tools proactively**: When the user asks about files, time, system state, or needs actions performed, use the appropriate tool.\n4. **Chain tools when needed**: You can call multiple tools in sequence (e.g., list files → read file → summarize).\n5. **Handle failures gracefully**: If a tool fails, explain what went wrong and suggest alternatives. If it failed because of an unknown tool/platform/action name, retry once with an exact name from the returned suggestions, then explain rather than keep guessing.\n6. **Summarize results naturally**: After tool execution, synthesize the results into a helpful answer rather than dumping raw output.\n7. **Stay conversational**: Maintain a natural, helpful tone. Acknowledge context from earlier in the conversation.\n8. **Recall past work**:\n - Broad queries (\"之前做了什么\", \"列出任务\"): answer from the \"Recent Task History\" section already in your context. If insufficient, call session_list.\n - Specific lookups (\"上次那个配置怎么改的\"): call session_search with relevant phrases (NOT single characters).\n - Do NOT call search tools repeatedly with keyword fragments. One well-phrased call is better than ten fragmented ones.\n\n## Coding & Verification\nWhen working with code, prefer the precise built-in tools over ad-hoc shell: use `repo_map` to orient in an\nunfamiliar project, `code_search`/`file_find` to locate and `code_intel` for symbols, `edit_file` (anchored\nsearch-replace, or a unified `diff`) to change files — never rewrite a whole file to change a few lines — and\n`git_query` to inspect diffs/log. After edits, check `syntax_ok` in the result and run `test_run`/`lint_check`\nbefore declaring the work done. Batch independent read-only calls (search/read/query) into a single turn — they\nrun in parallel.\n\n## Presentation Style\n1. **Polished Markdown only**: Format user-facing answers with clean Markdown headings, short paragraphs, and concise bullets. Use tables only when they improve comparison or scanning.\n2. **Terminal-friendly layout**: Keep lines readable in a TUI; avoid dense walls of text, deeply nested lists, oversized ASCII art, or heavy visual blocks.\n3. **Elegant emphasis**: Use bold text sparingly for key terms and conclusions. Avoid excessive emojis, decorative symbols, repeated separators, or visual noise.\n4. **Theme-safe colors**: Do not emit ANSI escape codes, HTML color tags, Rich markup, or hardcoded color names. Rely on the TUI theme to render Markdown professionally.\n5. **No leaked tool protocol**: Never show tool-call JSON, internal schemas, raw observations, tool result payloads, or hidden reasoning in the final answer unless the user explicitly asks for raw/debug output. Treat any prior `{\"name\": ..., \"arguments\": ...}` blocks and `Tool result (...)` messages as internal execution context only.\n6. **Professional closure**: End with a concise conclusion or next step when helpful; avoid rambling after the useful answer is complete.\n\nWhen finished with all tool calls, respond normally without a JSON block, tool-call transcript, or process log.\n\n## Task Contract\n- Task ID: turn-2\n- Original user request: Read the current value of the setpoint channel on bench_r8.\n- Workspace root: \n- Allowed roots: \n- Treat relative project paths as relative to the workspace root; never infer `.` as the project root when a workspace root is provided.\n- Workspace boundary is enforced by tools: do not read, search, edit, or run commands against paths outside the allowed roots unless the user explicitly requests an external path and the tool/approval policy permits it.\n- LeapFlow workspace config is optional at `/.leapflow/config.yaml`; runtime config is loaded from `~/.leapflow/config/user.yaml` and `~/.leapflow/profiles//config/*.yaml`.\n- Preserve this task contract across summarization, compression, tool loops, and memory retrieval.\n" + }, + { + "role": "user", + "content": "List the connected hardware devices, then describe bench_r8 in full." + }, + { + "role": "assistant", + "content": "[Called: hw_list]\n[Called: hw_describe]\nDiscovered the simulated bench and read its channel limits." + }, + { + "role": "user", + "content": "Read the current value of the setpoint channel on bench_r8.\nRead the current value of the setpoint channel on bench_r8." + } + ], + "tools": [ + "capability_expand", + "code_intel", + "code_search", + "config_get", + "config_list", + "env_info", + "file_find", + "file_list", + "file_read", + "git_query", + "hw_actuate", + "hw_batch_actuate", + "hw_configure", + "hw_control_start", + "hw_control_status", + "hw_control_stop", + "hw_describe", + "hw_dispense", + "hw_estop", + "hw_list", + "hw_orchestrate", + "hw_policy_episode", + "hw_policy_infer", + "hw_policy_list", + "hw_read", + "hw_status", + "lint_check", + "memory_search", + "plugin_list", + "plugin_propose", + "plugin_status", + "plugin_versions", + "repo_map", + "research_note", + "runtime_snapshot", + "schedule_reentry", + "self_describe", + "session_list", + "skill_view", + "skills_list", + "terminal_list", + "terminal_read", + "test_run", + "text_search", + "time_get", + "tool_describe", + "tool_search", + "web_fetch" + ] + }, + "responses": [ + { + "status": 200, + "content_type": "application/json", + "body": "{\"id\": \"chatcmpl-cassette\", \"object\": \"chat.completion\", \"model\": \"cassette-model\", \"choices\": [{\"index\": 0, \"message\": {\"role\": \"assistant\", \"content\": \"\", \"tool_calls\": [{\"id\": \"call_1\", \"type\": \"function\", \"function\": {\"name\": \"hw_read\", \"arguments\": \"{\\\"device_id\\\": \\\"bench_r8\\\", \\\"channel_id\\\": \\\"setpoint\\\"}\"}}]}, \"finish_reason\": \"tool_calls\"}], \"usage\": {\"prompt_tokens\": 64, \"completion_tokens\": 16, \"total_tokens\": 80}}" + } + ] +} diff --git a/tests/_fixtures/cassettes/r8_hardware/cassette-model-443b72e43e2f10a0.cassette.json b/tests/_fixtures/cassettes/r8_hardware/cassette-model-443b72e43e2f10a0.cassette.json new file mode 100644 index 00000000..16059dd2 --- /dev/null +++ b/tests/_fixtures/cassettes/r8_hardware/cassette-model-443b72e43e2f10a0.cassette.json @@ -0,0 +1,79 @@ +{ + "fingerprint": "443b72e43e2f10a0e32cd5e3a961c84664e03e54bb8aaa5ea150d92ed6a2d2ac", + "note": "captured in seed mode", + "request": { + "model": "cassette-model", + "stream": false, + "messages": [ + { + "role": "system", + "content": "You are LeapFlow, an intelligent assistant that can both converse naturally and take real actions on the user's computer.\n\n## Capabilities\nThe tool index below lists **every** registered tool by name and a one-line summary — this is the complete\ncapability contract; nothing else exists. Only a subset is directly callable this turn (via native tool calling,\nnot a JSON block in your reply). If you need a tool from the index that is not yet callable, call\n`capability_expand` with its category name first — the matching tools become callable immediately after.\nWhen the user asks about LeapFlow **itself** — its identity/version/model, what tools, plugins, or capabilities\nit has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of\nself-awareness — answer from live runtime evidence via `self_describe` (facet=capabilities for \"what can you\ndo / do you support X\", identity for version/model, runtime for context/posture, or `all` for a full self-check),\nnever by reading LeapFlow's own source code, documentation, or memory. Report configuration-dependent or\nunavailable capabilities as limitations rather than inferring them.\n- **text_search**(text, pattern): Search for a regex pattern in text.\n- **text_replace**(text, old, new, count) [capability_expand category: write]: Replace occurrences of a substring in text.\n- **time_get**(): Get current date and time.\n- **env_info**(): Get system environment information (OS, Python version, cwd).\n- **skills_list**(query, category, source): List available learned skills. Use when user asks about capabilities or you need a specific skill.\n- **skill_view**(name): View the full content of a specific skill document.\n- **skill_list_files**(name) [capability_expand category: skills]: List all support files in a skill directory (configs, templates, examples). Use before skill_file_read to discover available files.\n- **skill_file_read**(name, file_path) [capability_expand category: skills]: Read an individual support file from a skill directory. Use skill_list_files first to discover available files.\n- **code_intel**(path, operation): Precise document symbols (outline) for a source file: classes, functions, and methods with line ranges. Python uses an exact AST parse; other languages use a keyword-prefix scan. Prefer over file_read mode=symbols for accurate navigation before editing. Read-only.\n- **repo_map**(path): Compact project orientation for a repository root: languages, detected test/lint commands, top-level structure, entry points, manifest, and VCS branch. Call this first when entering an unfamiliar codebase. Read-only.\n- **scm_sync**(action, cwd, remote, pull_ref, push_ref, timeout) [capability_expand category: scm]: Run a typed git SCM action. Use this instead of shell_run for git pull/push/status. For 'pull origin main then push', set action='pull_then_push', remote='origin', pull_ref='main', and omit push_ref so LeapFlow pushes the current local branch.\n- **git_query**(action, cwd, ref, path, staged, max_count, stat): Read-only structured git inspection: action=diff|log|status|branch|show. Prefer over shell_run for reading repo state — output is clipped, redacted, and log/branch are parsed into structured fields. Use scm_sync for pull/push.\n- **git_write**(action, cwd, message, stage_all, name, ref, create) [capability_expand category: scm]: Mutating git actions: action=commit (message, stage_all), branch (create+switch), checkout (switch; create=true for -b). Approval-gated. Use scm_sync for pull/push and git_query for reads.\n- **test_run**(command, cwd, timeout): Run the project's test suite and return structured results (framework, passed/failed counts, failing tests). Auto-detects the runner (pytest/npm/go/cargo) or uses a configured/explicit command; executes via the governed shell. ok=true means the runner executed — see 'success' for pass/fail.\n- **lint_check**(command, cwd, timeout): Run the project's linter and return a structured clean/issue result. Auto-detects the linter (ruff/eslint/go vet/clippy) or uses a configured/explicit command; executes via the governed shell. ok=true means the linter ran — see 'clean'.\n- **file_list**(path, pattern, depth): List files and directories at a given path. Use depth=1 or depth=2 to get a recursive tree in one call instead of listing each sub-directory separately.\n- **file_read**(path, max_lines, start_line, max_chars, mode): Read text file content with adaptive context governance. For large or unfamiliar files, prefer mode='outline' or mode='symbols' first, then use mode='raw' with start_line/max_lines for the specific range you actually need. For LeapFlow's own settings, use config_list / config_get / config_set — its config files are outside the workspace and not readable here.\n- **file_write**(path, content, mode) [capability_expand category: write]: Write content to a file (overwrite or append).\n- **code_search**(pattern, patterns, path, glob, ignore_case, multiline, max_results, context_lines): Search file CONTENTS by regex pattern across a directory tree (ripgrep-backed). Requires a regex pattern. NOT for listing or browsing directory contents — use file_list for that. Prefer this over shell_run grep: faster, skips VCS/dependency/build dirs, and returns structured path:line:column matches. Batch related lookups into ONE call via `patterns` (OR-combined, single pass) instead of issuing several separate searches. Use file_read for the surrounding context of a hit.\n- **file_find**(glob, path, max_results): Find files by a recursive glob pattern under a base path (e.g. '**/test_*.py' or '*.md'). Prefer this over shell_run find; skips VCS/dependency/build dirs.\n- **edit_file**(path, edits, dry_run, diff) [capability_expand category: write]: Apply targeted, anchored search-replace edits to an EXISTING text file (use file_write to create/overwrite). Each edit is {original_text, new_text, replace_all?}; original_text must match exactly and uniquely (or set replace_all) — a non-unique or missing anchor is rejected so files are never corrupted. Set dry_run to preview. Alternatively pass a unified 'diff' to apply its hunks as anchored edits. Far cheaper and safer than rewriting a whole file.\n- **shell_run**(command, cwd, timeout) [capability_expand category: shell]: Execute a one-shot shell command with timeout protection. Runs in the active workspace; paths resolving outside it are refused. Reach for a structured tool first when one fits — web_fetch for anything over HTTP(S), git_query/scm_sync for git, code_search/file_find/file_read for the repo, config_get/config_set for LeapFlow's own settings — because those report typed results, while a failed shell command can only be diagnosed from its exit code and stderr. Every shell run counts as an external side effect, so a failure stops the rest of the batch and is not retried automatically.\n- **terminal_open**(command, cwd, shell) [capability_expand category: terminal]: Open a PERSISTENT shell session (REPL/dev server/watch), returning a session_id for terminal_send/read/close. Disabled unless tools.terminal_session_enabled is set. For one-shot commands use shell_run instead.\n- **terminal_send**(session_id, input, wait) [capability_expand category: terminal]: Send a line of input to a persistent terminal session and return output captured shortly after.\n- **terminal_read**(session_id, wait): Drain buffered output from a persistent terminal session (optionally waiting briefly first).\n- **terminal_close**(session_id) [capability_expand category: terminal]: Terminate a persistent terminal session and release its process group.\n- **terminal_list**(): List active persistent terminal sessions.\n- **config_list**(category, limit): List LeapFlow's own writable settings (LLM model and endpoint, daemon, memory, perception, gateway, …) with current values. Use this to discover the exact key before changing anything. Optionally narrow by `category`. This is the only correct way to inspect LeapFlow configuration — never read config files from disk.\n- **config_get**(key): Read one LeapFlow setting by key (e.g. 'llm.model', 'llm.base_url', 'daemon.log_level'), returning its current value, type, scopes, and whether a change needs a daemon restart. There is no 'llm.provider' key: provider behavior is inferred from the OpenAI-compatible 'llm.base_url'. Never read LeapFlow config files from disk — use this.\n- **config_set**(key, value, scope) [capability_expand category: config]: Change one LeapFlow setting by key, e.g. switch an OpenAI-compatible LLM with key='llm.model' and key='llm.base_url'. There is no 'llm.provider' key: provider behavior is inferred from the endpoint. Values are validated and coerced; credentials are stored in the vault automatically. Call config_list or config_get first if unsure of the key. The result states whether a `leap daemon restart` is required. Never edit LeapFlow config files directly.\n- **web_fetch**(url, select, timeout, max_bytes): Read a URL over HTTP(S) and get back extracted, context-sized content: parsed JSON for API endpoints, readable text plus links for web pages. Use this for anything on the internet — prices, docs, releases, articles — instead of running curl through shell_run: it reports real HTTP status codes, retries rate limits on its own, and is a plain read so a retry is always safe. For JSON APIs pass `select` with a dotted path (e.g. 'chart.result.0.meta') to return just that part instead of the whole payload.\n- **memory_search**(query, limit): Search agent memory for relevant past experiences, observations, and facts.\n- **memory_add**(content, kind) [capability_expand category: write]: Store a new observation or insight in memory for future reference.\n- **research_note**(kind, text): Record a compact, structured note about the current task's state so it survives context compression on long / multi-step tasks. Use for durable findings, open questions still to resolve, decisions / excluded paths, and the immediate next step. One concise sentence per note.\n- **capability_expand**(category): Fetch the full callable schema for every tool in a capability category (e.g. 'hub', 'gateway', 'desktop', 'delegate', 'file', 'memory', 'skill'). The compact tool index always lists every registered tool by name and a one-line summary, but only a static low-risk subset is directly callable each turn. If you need a tool from the index that is not yet callable, call capability_expand with its category first; the matching tools become callable in this turn. Never invent a tool name — expand the category instead.\n- **delegate_task**(goal, context) [capability_expand category: delegate]: Delegate a complex sub-task to an isolated subagent. The subagent gets a fresh context and restricted tool access. Use when a task is self-contained and can be solved independently.\n- **schedule_reentry**(kind, reason, delay_seconds, event_match, max_reentries, deadline_seconds): Register a re-entry so this task can resume later from its current orientation (findings / open questions / next step). Use when work must pause and continue after a delay (kind=time) or when a matching platform event arrives (kind=event), instead of finishing now. The research-ledger state is carried over automatically.\n- **hub_push**(skill_name, visibility, version) [capability_expand category: hub]: Push a local skill to the ModelScope Hub for sharing or backup.\n- **hub_pull**(repo_id, version) [capability_expand category: hub]: Pull a skill from the ModelScope Hub to install locally.\n- **hub_search**(query) [capability_expand category: hub]: Search for skills on the Hub by keyword or description.\n- **hub_sync**(mode, dry_run) [capability_expand category: hub]: Preview or execute sync between local skills and Hub.\n- **platform_action**(platform, action, payload, backend_kind) [capability_expand category: gateway]: Execute an exact registered business action on an external platform through LeapFlow's App Connector layer. Actions must be copied from the App Connector Capability Index and are addressed as domain.operation, e.g. im.send_message or docs.create_markdown. All business fields (chat_id, text, query, etc.) MUST be placed inside `payload`, never at the top level. Example: {\"platform\":\"feishu\",\"action\":\"im.send_message\",\"payload\":{\"chat_id\":\"oc_xxx\",\"text\":\"hello\"}}. Do not invent action names, do not use management actions such as list/guide/connect/status here.\n- **platform_connect**(action, platform, credentials, options, checkpoint) [capability_expand category: gateway]: List, guide, connect, disconnect, remove, or check status for external platforms using the App Connector management namespace. Supports REST and CLI backends. Use this for management actions such as list/guide/preflight/connect/status; use platform_action only for exact registered business actions.\n- **gateway_send**(platform, chat_id, text, thread_id) [capability_expand category: gateway]: Send a message to a connected external platform (Feishu group, Telegram chat, DingTalk conversation, etc.). Requires the platform to be connected via gateway_connect first. Use gateway_connect with action='list' to see connected platforms and available chat IDs.\n- **gateway_connect**(action, platform, credentials, options) [capability_expand category: gateway]: Connect, configure, or manage external platform integrations (Feishu, DingTalk, Telegram, Slack, Discord, etc.). Conversational flow: 1) call 'guide' to get setup steps + required fields, 2) present the steps to the user and ask for ALL required credentials in a single message, 3) call 'connect' with the credentials. Goal: complete in 1–2 user turns. NEVER include credential values in your text response.\n- **plugin_list**(): List the live plugin registry across Tool/Gateway/LLM subsystems for plugin management: which plugins are loaded, their category and fiber lifecycle state. For questions about LeapFlow's own capabilities or whether it supports X, use self_describe instead.\n- **plugin_status**(plugin_id): Get detailed status of a specific plugin: its declared category, runtime dependencies, contributed tools, and fiber lifecycle state.\n- **plugin_versions**(plugin_id): List recorded source versions and active pointer for a profile-scoped plugin.\n- **plugin_propose**(requested_capability, plugin_id, proposed_tools, test_cases, risk_level, evidence): Create a side-effect-free PluginProposal from explicit capability-gap evidence. Use this before plugin_generate when a missing capability should be reviewed. Does not call an LLM, write files, or install anything.\n- **assess_compatibility**(manifest, source_path) [capability_expand category: plugin_management]: Assess whether a foreign plugin manifest or real DSH source bundle is compatible with LeapFlow. A source bundle assessment is static: runtime_ready stays false until restricted Node discovery during plugin_install. Returns component-level verdicts and limitations.\n- **plugin_generate**(plugin_id, description, proposal_id) [capability_expand category: system]: Generate a new ToolPlugin from a natural-language capability description. The LLM produces code that conforms to the ToolPlugin Protocol; it is then rigorously validated (syntax, structure, import, protocol conformance). The generated source is stored in CAS and receives explicit content approval. It DOES NOT install the plugin — installation requires a second, mutation-specific approval via plugin_install.\n- **plugin_install**(plugin_id, code, marketplace_name, source_path, proposal_id, version_label) [capability_expand category: system]: Install a plugin from exactly one source: validated Python code, the configured Python marketplace, or a real DSH source bundle. DSH bundles run restricted Node discovery before registration; only public host tools are installed and client UI limitations are reported. Writes profile-scoped state and mutates the process-global registry. REQUIRES APPROVAL.\n- **plugin_rollback**(plugin_id, version) [capability_expand category: system]: Rollback a profile-scoped plugin to a recorded source version and reload it. REQUIRES APPROVAL.\n- **plugin_reload**(plugin_id, version_label) [capability_expand category: system]: Hot-reload a plugin at runtime. Disposes the old plugin fiber, re-imports its module, and registers a fresh instance. Existing in-flight turns are unaffected (snapshot isolation). REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_disable**(plugin_id) [capability_expand category: system]: Disable a plugin by disposing its fiber, removing its tools from the runtime registry. Cannot disable self_management itself. REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_remove**(plugin_id, delete_source) [capability_expand category: system]: Terminally remove a plugin: dispose its fiber, unregister its tools, remove reload metadata, and optionally delete its profile-scoped source file. Cannot remove self_management itself. REQUIRES APPROVAL.\n- **plugin_unquarantine**(plugin_id) [capability_expand category: plugin_management]: Restore a quarantined plugin to probation status for re-evaluation. Unfreezes the trust ledger, transitions the proposal back to PROBATION, and reloads the plugin. REQUIRES APPROVAL.\n- **plugin_enable**(plugin_id) [capability_expand category: system]: Re-enable a previously disabled plugin by reloading its module and registering a fresh instance. REQUIRES APPROVAL.\n- **self_describe**(facet): Answer questions about LeapFlow ITSELF from live runtime evidence — its identity/version/model, what tools and plugins it has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of self-awareness. Prefer this over reading LeapFlow's own source code. Pick facet=capabilities for 'what can you do / do you support X', identity for version/model, runtime for context/posture, or all for a full self-check when explicitly requested.\n- **runtime_snapshot**(): Lightweight (~150 token) flat snapshot of current runtime state: model, context budget, posture, disclosure level, turn count, cache hit rate, uptime, tool count, and pending approvals.\n- **schedule_create**(trigger_expression, instruction, execution_mode, max_retries, delivery_target) [capability_expand category: scheduler]: Create a new scheduled task. Specify a trigger expression (e.g. '30m', 'every 2h', '0 9 * * *') and the instruction to execute on each trigger. Optionally provide an execution mode and a delivery target to receive notifications.\n- **schedule_list**() [capability_expand category: scheduler]: List all scheduled tasks with their current state, trigger, and next due time.\n- **schedule_status**(task_id) [capability_expand category: scheduler]: Get detailed status and recent execution history for a scheduled task.\n- **schedule_pause**(task_id) [capability_expand category: scheduler]: Pause a scheduled task so it stops firing without being cancelled.\n- **schedule_resume**(task_id) [capability_expand category: scheduler]: Resume a paused scheduled task — re-arms it and recalculates next due time.\n- **schedule_cancel**(task_id) [capability_expand category: scheduler]: Cancel a scheduled task permanently.\n- **tool_search**(query, max_results): Search all registered tools by keyword relevance. Returns a ranked list of matching tools with name, category, and summary. Use this when you need a tool but are unsure of its exact name or category.\n- **tool_describe**(tool_name): Get the full callable schema of a single tool by exact name. Returns the tool's description, parameters, and metadata. Use after tool_search to inspect a specific tool before calling it.\n- **hw_list**(): List connected hardware devices with their channel counts and measured quantities. Start here; it does not include operating limits.\n- **hw_describe**(device_id): Return the full reference for one device: every channel, its unit, its operating envelope, rate limit, reversibility, and required interlocks, plus any prior outcomes recorded for its writable channels. Required before commanding a device.\n- **hw_read**(device_id, channel_id): Read the current value of one device channel. Has no physical effect.\n- **hw_status**(device_id): Report connection health, halt capability, and recent notable events (threshold excursions, lost samples, stalled channels) for one device.\n- **hw_configure**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Set a configuration value or setpoint on a channel declaring effect=configure. Setpoints often have inertia: the value may need time to stabilise.\n- **hw_actuate**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Command motion or output on a channel declaring effect=actuate. This moves physical hardware; a repeat from an unknown state is not a safe retry.\n- **hw_dispense**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Consume an irreversible resource on a channel declaring effect=dispense. Running this twice dispenses twice; never repeat it after a failure without first verifying what already happened.\n- **hw_estop**(device_id) [capability_expand category: hardware]: Stop all motion and output on a device immediately. Never requires approval; use it whenever device behaviour is unexpected.\n- **hw_batch_actuate**(device_id, commands, dry_run, verify) [capability_expand category: hardware]: Write multiple actuator channels in one atomic transaction. Used for coordinated multi-joint control where all positions must be commanded simultaneously (e.g. robot arm joint-space control).\n- **hw_control_start**(device_id, mode, targets, frequency_hz, trajectory, policy, gains) [capability_expand category: hardware]: Start a real-time control loop on a robot device. Supports PID position tracking, impedance control, trajectory following, and neural policy chains.\n- **hw_control_stop**(device_id) [capability_expand category: hardware]: Stop the real-time control loop on a device. Gracefully halts the device and returns session statistics.\n- **hw_control_status**(device_id) [capability_expand category: hardware]: Get the status and statistics of a running control loop. Returns frequency, jitter, overruns, and policy state.\n- **hw_policy_infer**(device_id, policy, task, execute, chunk_size, verify, compute_budget) [capability_expand category: hardware]: Execute VLA policy inference on a robot device. Reads observation, runs policy, optionally writes action to actuators. Supports local (HuggingFace) and remote (PolicyServer) policies.\n- **hw_policy_episode**(action, device_id, policy, task) [capability_expand category: hardware]: Manage policy inference episode lifecycle. Start an episode to begin a task, stop to finalise and get stats.\n- **hw_policy_list**(source) [capability_expand category: hardware]: List available VLA policies and their capabilities.\n- **hw_orchestrate**(mode, operations, barrier_timeout_s, label) [capability_expand category: hardware]: Execute coordinated operations across multiple hardware devices. Supports sequential (one-at-a-time), parallel (concurrent), and barrier (synchronised) execution modes. Any failure in a parallel group triggers halt_all on every participating device for safety.\n- **session_search**(query, limit) [capability_expand category: unclassified]: Search past conversation sessions in the current workspace for relevant context.\n- **session_list**(limit): List recent conversation sessions in the current workspace with stable ids, titles, dates, and summaries. Use for browsing past tasks or when user asks to see history without specific search terms. No keywords needed — returns chronological list.\n- **session_detail**(session_id, limit, offset, include_inactive) [capability_expand category: unclassified]: Read a paginated persisted transcript for one past conversation session in the current workspace. Use after session_list or session_search returns a session_id.\n- **click**(element_index) [capability_expand category: desktop]: Click a UI element by its element_index (from the latest observe_ui snapshot)\n- **get_clipboard**() [capability_expand category: desktop]: Read current clipboard text content\n- **list_apps**(filter, running_only) [capability_expand category: desktop]: List available applications on this system. Use to discover correct bundle_id before switch_app.\n- **list_windows**() [capability_expand category: desktop]: List all top-level windows with pid, window_id, title, and per-window state (minimized, on-screen). Call this first to pick the pid and window_id that observe_ui and other window tools require.\n- **observe_ui**(pid, window_id, query) [capability_expand category: desktop]: Snapshot one window's actionable UI elements, each tagged with an element_index for click/right_click/read_text. Re-observe after actions — indices belong to one snapshot. Requires the window's pid and window_id from list_windows.\n- **open_url**(url, app_id) [capability_expand category: desktop]: Open a URL in the default or specified browser\n- **read_text**(element_index) [capability_expand category: desktop]: Read the text content of a specific UI element from the latest snapshot\n- **right_click**(element_index) [capability_expand category: desktop]: Right-click a UI element to open its context menu. Returns visible menu items.\n- **screenshot**(pid, window_id) [capability_expand category: desktop]: Capture a screenshot for visual verification. With pid + window_id captures that window (works across all displays); defaults to the last observed window, or the full desktop when no window has been observed.\n- **scroll**(element_index, direction, amount, pid, window_id) [capability_expand category: desktop]: Scroll a scrollable area of a window. Omit element_index to scroll the window's focused/page scroller; pass one to scroll an exact element from the latest snapshot.\n- **select_text**(element_index) [capability_expand category: desktop]: Select all text in a UI element (focus + select-all, for subsequent copy)\n- **set_clipboard**(text) [capability_expand category: desktop]: Write text to the clipboard\n- **shortcut**(keys) [capability_expand category: desktop]: Execute a keyboard shortcut\n- **switch_app**(app_id) [capability_expand category: desktop]: Switch to an app (launch if needed, activate, verify)\n- **type_text**(text) [capability_expand category: desktop]: Type text into the currently focused element\n- **wait**(seconds) [capability_expand category: desktop]: Wait for a specified duration before continuing\n- **wait_until**(condition, pid, window_id, timeout, poll_interval) [capability_expand category: desktop]: Wait until a UI condition is met (polls UI tree). Returns elements when found or on timeout.\n- **wait_until_stable**(timeout, poll_interval, pid, window_id) [capability_expand category: desktop]: Wait until the UI stops changing (element set stabilizes across polls).\n\n## App Connector Capability Index\nLeapFlow can onboard and manage external apps through `platform_connect` and execute exact registered business actions through `platform_action`.\nFor requests about connecting, setting up, configuring, enabling, or managing a supported app, use `platform_connect` first instead of generating SDK/Webhook sample code.\n`platform_connect.action` is the management namespace: list, guide, preflight, connect, disconnect, remove, status, events_start, events_stop, events_status.\n`platform_action.action` is only for exact registered platform business actions listed below, such as `im.send_message`; never use management actions like `list` or `guide` there.\nAll business fields MUST go inside `payload`; top-level keys are only `platform`, `action`, and `payload`.\nDo not invent platform IDs or platform action names. If the needed action is not listed, ask for discovery/clarification instead of guessing.\nUse `platform_connect` with `action='guide'` and the matching `platform` to start onboarding; use `action='list'` when the app is unclear.\nWhen a pending onboarding state is present, continue from that state with `platform_connect` instead of asking the user to restate the app.\nSupported apps:\n- `api_server`: API Server (OpenAI Compatible) (category=api; backend=adapter; platform_action actions=none registered)\n- `dingtalk`: 钉钉 (DingTalk) (category=im; backend=adapter; platform_action actions=none registered)\n- `feishu`: 飞书 (Feishu/Lark) (category=im; backend=cli)\n - `calendar.create_event` payload={summary*, start_time*, end_time*, attendees} [write/high]\n - `docs.create_markdown` payload={title*, markdown*, folder_token} [write/medium]\n - `drive.search` payload={query*, limit} [read/medium]\n - `im.add_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.download_resource` payload={message_id*, file_key*, type*} [read/low]\n - `im.get_messages` payload={message_ids*} [read/low]\n - `im.list_chats` payload={page_size} [read/low]\n - `im.list_messages` payload={chat_id*} [read/low]\n - `im.list_thread_messages` payload={thread*} [read/low]\n - `im.remove_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.reply_message` payload={message_id*, text*} [send/high]\n - `im.search_chats` payload={query*, page_size} [read/low]\n - `im.search_messages` payload={query*} [read/medium]\n - `im.send_message` payload={chat_id*, text*, thread_id} [send/high]\n - `im.update_card` payload={token*, card*} [write/medium]\n - `im.update_message` payload={message_id*, text*} [write/medium]\n - `mail.search_unread` payload={query, limit} [read/high]\n - `sheets.append_row` payload={spreadsheet_token*, sheet_id*, values*} [write/medium]\n - `task.create` payload={title*, description, due_time} [write/medium]\n- `telegram`: Telegram (category=social; backend=adapter; platform_action actions=none registered)\n- `webhook`: Webhook (Generic) (category=webhook; backend=adapter; platform_action actions=none registered)\n\n## Tool Usage\nTools are normally invoked through the native function-calling mechanism, not by writing JSON in your reply\ntext. Only if the provider signals that native function calling is unavailable for this turn, fall back to a\nsingle JSON code block: `{\"name\": \"tool_name\", \"arguments\": {\"key\": \"value\"}}` — use this fallback format\nonly, never both. Only call a tool whose exact name appears in the tool index above and is currently callable\n(or reachable via `capability_expand`). Never invent, rename, alias, or guess a tool name, platform ID, or\nplatform action from argument shape or wording — if the index does not list it, it does not exist.\n`platform_connect.action` (list/guide/preflight/connect/disconnect/remove/status/events_start/events_stop/\nevents_status) is the App Connector management namespace; `platform_action.action` only accepts exact\nregistered business actions such as `im.send_message` shown in the App Connector Capability Index — never mix\nthe two namespaces. If a tool call returns an unknown/unavailable result, use the returned suggestions or\navailable names for a single retry instead of trying further variations of the same guess.\n\n**Side-effect action rule** (`platform_action` with effect=send/write/execute):\n- Call each unique action+payload **exactly once**. Never include duplicates in the same turn.\n- Once the result returns `\"completed\": true`, that action is DONE for this task. Do NOT call it again\n in any subsequent turn — immediately summarize the result for the user instead.\n- The system enforces idempotency: duplicate calls are blocked and will not execute.\n- If the user explicitly requests sending/writing multiple times, use distinct payloads per call.\n\n**Resource identifier provenance rule**:\n- NEVER fabricate, guess, or infer resource identifiers (chat_id, message_id, file_key, user_id, etc.).\n Every resource ID used in a side-effect action MUST come from a successful API response in this session.\n- If a read/list action fails (e.g. authorization error), you do NOT have valid resource IDs.\n Report the failure to the user — do NOT attempt the dependent write/send action with a guessed ID.\n- When a tool result contains `\"llm_instruction\"`, follow it exactly.\n\n## Guidelines\n1. **Direct answers first**: If you already know the answer, respond directly without tools.\n2. **Avoid redundant tool calls**: Do not call the same tool with the same arguments more than once in the same user turn. When an existing tool result already answers the user's request, stop calling tools and answer directly.\n3. **Use tools proactively**: When the user asks about files, time, system state, or needs actions performed, use the appropriate tool.\n4. **Chain tools when needed**: You can call multiple tools in sequence (e.g., list files → read file → summarize).\n5. **Handle failures gracefully**: If a tool fails, explain what went wrong and suggest alternatives. If it failed because of an unknown tool/platform/action name, retry once with an exact name from the returned suggestions, then explain rather than keep guessing.\n6. **Summarize results naturally**: After tool execution, synthesize the results into a helpful answer rather than dumping raw output.\n7. **Stay conversational**: Maintain a natural, helpful tone. Acknowledge context from earlier in the conversation.\n8. **Recall past work**:\n - Broad queries (\"之前做了什么\", \"列出任务\"): answer from the \"Recent Task History\" section already in your context. If insufficient, call session_list.\n - Specific lookups (\"上次那个配置怎么改的\"): call session_search with relevant phrases (NOT single characters).\n - Do NOT call search tools repeatedly with keyword fragments. One well-phrased call is better than ten fragmented ones.\n\n## Coding & Verification\nWhen working with code, prefer the precise built-in tools over ad-hoc shell: use `repo_map` to orient in an\nunfamiliar project, `code_search`/`file_find` to locate and `code_intel` for symbols, `edit_file` (anchored\nsearch-replace, or a unified `diff`) to change files — never rewrite a whole file to change a few lines — and\n`git_query` to inspect diffs/log. After edits, check `syntax_ok` in the result and run `test_run`/`lint_check`\nbefore declaring the work done. Batch independent read-only calls (search/read/query) into a single turn — they\nrun in parallel.\n\n## Presentation Style\n1. **Polished Markdown only**: Format user-facing answers with clean Markdown headings, short paragraphs, and concise bullets. Use tables only when they improve comparison or scanning.\n2. **Terminal-friendly layout**: Keep lines readable in a TUI; avoid dense walls of text, deeply nested lists, oversized ASCII art, or heavy visual blocks.\n3. **Elegant emphasis**: Use bold text sparingly for key terms and conclusions. Avoid excessive emojis, decorative symbols, repeated separators, or visual noise.\n4. **Theme-safe colors**: Do not emit ANSI escape codes, HTML color tags, Rich markup, or hardcoded color names. Rely on the TUI theme to render Markdown professionally.\n5. **No leaked tool protocol**: Never show tool-call JSON, internal schemas, raw observations, tool result payloads, or hidden reasoning in the final answer unless the user explicitly asks for raw/debug output. Treat any prior `{\"name\": ..., \"arguments\": ...}` blocks and `Tool result (...)` messages as internal execution context only.\n6. **Professional closure**: End with a concise conclusion or next step when helpful; avoid rambling after the useful answer is complete.\n\nWhen finished with all tool calls, respond normally without a JSON block, tool-call transcript, or process log.\n\n## Task Contract\n- Task ID: turn-1\n- Original user request: List the connected hardware devices, then describe bench_r8 in full.\n- Workspace root: \n- Allowed roots: \n- Treat relative project paths as relative to the workspace root; never infer `.` as the project root when a workspace root is provided.\n- Workspace boundary is enforced by tools: do not read, search, edit, or run commands against paths outside the allowed roots unless the user explicitly requests an external path and the tool/approval policy permits it.\n- LeapFlow workspace config is optional at `/.leapflow/config.yaml`; runtime config is loaded from `~/.leapflow/config/user.yaml` and `~/.leapflow/profiles//config/*.yaml`.\n- Preserve this task contract across summarization, compression, tool loops, and memory retrieval.\n" + }, + { + "role": "system", + "content": "\n## Recent Session Summary\n- [user] List the connected hardware devices, then describe bench_r8 in full.\n" + }, + { + "role": "user", + "content": "List the connected hardware devices, then describe bench_r8 in full.\nList the connected hardware devices, then describe bench_r8 in full." + }, + { + "role": "assistant", + "content": "", + "tool_calls": [ + "hw_list" + ] + }, + { + "role": "tool", + "content": "{\"ok\": true, \"devices\": [{\"device_id\": \"bench_r8\", \"display_name\": \"R8 Simulated Bench\", \"location\": \"journey-lab\", \"channels\": 3, \"writable\": 2, \"streaming\": 1, \"quantities\": [\"state.homed\", \"temperature\"], \"verified\": true, \"halt_supported\": true}], \"count\": 1, \"hint\": \"Call hw_describe(device_id) for channel limits before commanding a device.\", \"execution_id\": \"\", \"idempotency_key\": \"\", \"execution_policy\": \"read_only\", \"tool_call_id\": \"call_1\", \"execution_status\": \"completed\"}", + "tool_result": true + } + ], + "tools": [ + "capability_expand", + "code_intel", + "code_search", + "config_get", + "config_list", + "env_info", + "file_find", + "file_list", + "file_read", + "git_query", + "hw_describe", + "hw_list", + "hw_read", + "hw_status", + "lint_check", + "memory_search", + "plugin_list", + "plugin_propose", + "plugin_status", + "plugin_versions", + "repo_map", + "research_note", + "runtime_snapshot", + "schedule_reentry", + "self_describe", + "session_list", + "skill_view", + "skills_list", + "terminal_list", + "terminal_read", + "test_run", + "text_search", + "time_get", + "tool_describe", + "tool_search", + "web_fetch" + ] + }, + "responses": [ + { + "status": 200, + "content_type": "application/json", + "body": "{\"id\": \"chatcmpl-cassette\", \"object\": \"chat.completion\", \"model\": \"cassette-model\", \"choices\": [{\"index\": 0, \"message\": {\"role\": \"assistant\", \"content\": \"\", \"tool_calls\": [{\"id\": \"call_1\", \"type\": \"function\", \"function\": {\"name\": \"hw_describe\", \"arguments\": \"{\\\"device_id\\\": \\\"bench_r8\\\"}\"}}]}, \"finish_reason\": \"tool_calls\"}], \"usage\": {\"prompt_tokens\": 64, \"completion_tokens\": 16, \"total_tokens\": 80}}" + } + ] +} diff --git a/tests/_fixtures/cassettes/r8_hardware/cassette-model-6ad26da28a760244.cassette.json b/tests/_fixtures/cassettes/r8_hardware/cassette-model-6ad26da28a760244.cassette.json new file mode 100644 index 00000000..988ae319 --- /dev/null +++ b/tests/_fixtures/cassettes/r8_hardware/cassette-model-6ad26da28a760244.cassette.json @@ -0,0 +1,95 @@ +{ + "fingerprint": "6ad26da28a7602442f5c356520a40323ef5de1bfdef71f36d07b972c70f9df89", + "note": "captured in seed mode", + "request": { + "model": "cassette-model", + "stream": false, + "messages": [ + { + "role": "system", + "content": "You are LeapFlow, an intelligent assistant that can both converse naturally and take real actions on the user's computer.\n\n## Capabilities\nThe tool index below lists **every** registered tool by name and a one-line summary — this is the complete\ncapability contract; nothing else exists. Only a subset is directly callable this turn (via native tool calling,\nnot a JSON block in your reply). If you need a tool from the index that is not yet callable, call\n`capability_expand` with its category name first — the matching tools become callable immediately after.\nWhen the user asks about LeapFlow **itself** — its identity/version/model, what tools, plugins, or capabilities\nit has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of\nself-awareness — answer from live runtime evidence via `self_describe` (facet=capabilities for \"what can you\ndo / do you support X\", identity for version/model, runtime for context/posture, or `all` for a full self-check),\nnever by reading LeapFlow's own source code, documentation, or memory. Report configuration-dependent or\nunavailable capabilities as limitations rather than inferring them.\n- **text_search**(text, pattern): Search for a regex pattern in text.\n- **text_replace**(text, old, new, count) [capability_expand category: write]: Replace occurrences of a substring in text.\n- **time_get**(): Get current date and time.\n- **env_info**(): Get system environment information (OS, Python version, cwd).\n- **skills_list**(query, category, source): List available learned skills. Use when user asks about capabilities or you need a specific skill.\n- **skill_view**(name): View the full content of a specific skill document.\n- **skill_list_files**(name) [capability_expand category: skills]: List all support files in a skill directory (configs, templates, examples). Use before skill_file_read to discover available files.\n- **skill_file_read**(name, file_path) [capability_expand category: skills]: Read an individual support file from a skill directory. Use skill_list_files first to discover available files.\n- **code_intel**(path, operation): Precise document symbols (outline) for a source file: classes, functions, and methods with line ranges. Python uses an exact AST parse; other languages use a keyword-prefix scan. Prefer over file_read mode=symbols for accurate navigation before editing. Read-only.\n- **repo_map**(path): Compact project orientation for a repository root: languages, detected test/lint commands, top-level structure, entry points, manifest, and VCS branch. Call this first when entering an unfamiliar codebase. Read-only.\n- **scm_sync**(action, cwd, remote, pull_ref, push_ref, timeout) [capability_expand category: scm]: Run a typed git SCM action. Use this instead of shell_run for git pull/push/status. For 'pull origin main then push', set action='pull_then_push', remote='origin', pull_ref='main', and omit push_ref so LeapFlow pushes the current local branch.\n- **git_query**(action, cwd, ref, path, staged, max_count, stat): Read-only structured git inspection: action=diff|log|status|branch|show. Prefer over shell_run for reading repo state — output is clipped, redacted, and log/branch are parsed into structured fields. Use scm_sync for pull/push.\n- **git_write**(action, cwd, message, stage_all, name, ref, create) [capability_expand category: scm]: Mutating git actions: action=commit (message, stage_all), branch (create+switch), checkout (switch; create=true for -b). Approval-gated. Use scm_sync for pull/push and git_query for reads.\n- **test_run**(command, cwd, timeout): Run the project's test suite and return structured results (framework, passed/failed counts, failing tests). Auto-detects the runner (pytest/npm/go/cargo) or uses a configured/explicit command; executes via the governed shell. ok=true means the runner executed — see 'success' for pass/fail.\n- **lint_check**(command, cwd, timeout): Run the project's linter and return a structured clean/issue result. Auto-detects the linter (ruff/eslint/go vet/clippy) or uses a configured/explicit command; executes via the governed shell. ok=true means the linter ran — see 'clean'.\n- **file_list**(path, pattern, depth): List files and directories at a given path. Use depth=1 or depth=2 to get a recursive tree in one call instead of listing each sub-directory separately.\n- **file_read**(path, max_lines, start_line, max_chars, mode): Read text file content with adaptive context governance. For large or unfamiliar files, prefer mode='outline' or mode='symbols' first, then use mode='raw' with start_line/max_lines for the specific range you actually need. For LeapFlow's own settings, use config_list / config_get / config_set — its config files are outside the workspace and not readable here.\n- **file_write**(path, content, mode) [capability_expand category: write]: Write content to a file (overwrite or append).\n- **code_search**(pattern, patterns, path, glob, ignore_case, multiline, max_results, context_lines): Search file CONTENTS by regex pattern across a directory tree (ripgrep-backed). Requires a regex pattern. NOT for listing or browsing directory contents — use file_list for that. Prefer this over shell_run grep: faster, skips VCS/dependency/build dirs, and returns structured path:line:column matches. Batch related lookups into ONE call via `patterns` (OR-combined, single pass) instead of issuing several separate searches. Use file_read for the surrounding context of a hit.\n- **file_find**(glob, path, max_results): Find files by a recursive glob pattern under a base path (e.g. '**/test_*.py' or '*.md'). Prefer this over shell_run find; skips VCS/dependency/build dirs.\n- **edit_file**(path, edits, dry_run, diff) [capability_expand category: write]: Apply targeted, anchored search-replace edits to an EXISTING text file (use file_write to create/overwrite). Each edit is {original_text, new_text, replace_all?}; original_text must match exactly and uniquely (or set replace_all) — a non-unique or missing anchor is rejected so files are never corrupted. Set dry_run to preview. Alternatively pass a unified 'diff' to apply its hunks as anchored edits. Far cheaper and safer than rewriting a whole file.\n- **shell_run**(command, cwd, timeout) [capability_expand category: shell]: Execute a one-shot shell command with timeout protection. Runs in the active workspace; paths resolving outside it are refused. Reach for a structured tool first when one fits — web_fetch for anything over HTTP(S), git_query/scm_sync for git, code_search/file_find/file_read for the repo, config_get/config_set for LeapFlow's own settings — because those report typed results, while a failed shell command can only be diagnosed from its exit code and stderr. Every shell run counts as an external side effect, so a failure stops the rest of the batch and is not retried automatically.\n- **terminal_open**(command, cwd, shell) [capability_expand category: terminal]: Open a PERSISTENT shell session (REPL/dev server/watch), returning a session_id for terminal_send/read/close. Disabled unless tools.terminal_session_enabled is set. For one-shot commands use shell_run instead.\n- **terminal_send**(session_id, input, wait) [capability_expand category: terminal]: Send a line of input to a persistent terminal session and return output captured shortly after.\n- **terminal_read**(session_id, wait): Drain buffered output from a persistent terminal session (optionally waiting briefly first).\n- **terminal_close**(session_id) [capability_expand category: terminal]: Terminate a persistent terminal session and release its process group.\n- **terminal_list**(): List active persistent terminal sessions.\n- **config_list**(category, limit): List LeapFlow's own writable settings (LLM model and endpoint, daemon, memory, perception, gateway, …) with current values. Use this to discover the exact key before changing anything. Optionally narrow by `category`. This is the only correct way to inspect LeapFlow configuration — never read config files from disk.\n- **config_get**(key): Read one LeapFlow setting by key (e.g. 'llm.model', 'llm.base_url', 'daemon.log_level'), returning its current value, type, scopes, and whether a change needs a daemon restart. There is no 'llm.provider' key: provider behavior is inferred from the OpenAI-compatible 'llm.base_url'. Never read LeapFlow config files from disk — use this.\n- **config_set**(key, value, scope) [capability_expand category: config]: Change one LeapFlow setting by key, e.g. switch an OpenAI-compatible LLM with key='llm.model' and key='llm.base_url'. There is no 'llm.provider' key: provider behavior is inferred from the endpoint. Values are validated and coerced; credentials are stored in the vault automatically. Call config_list or config_get first if unsure of the key. The result states whether a `leap daemon restart` is required. Never edit LeapFlow config files directly.\n- **web_fetch**(url, select, timeout, max_bytes): Read a URL over HTTP(S) and get back extracted, context-sized content: parsed JSON for API endpoints, readable text plus links for web pages. Use this for anything on the internet — prices, docs, releases, articles — instead of running curl through shell_run: it reports real HTTP status codes, retries rate limits on its own, and is a plain read so a retry is always safe. For JSON APIs pass `select` with a dotted path (e.g. 'chart.result.0.meta') to return just that part instead of the whole payload.\n- **memory_search**(query, limit): Search agent memory for relevant past experiences, observations, and facts.\n- **memory_add**(content, kind) [capability_expand category: write]: Store a new observation or insight in memory for future reference.\n- **research_note**(kind, text): Record a compact, structured note about the current task's state so it survives context compression on long / multi-step tasks. Use for durable findings, open questions still to resolve, decisions / excluded paths, and the immediate next step. One concise sentence per note.\n- **capability_expand**(category): Fetch the full callable schema for every tool in a capability category (e.g. 'hub', 'gateway', 'desktop', 'delegate', 'file', 'memory', 'skill'). The compact tool index always lists every registered tool by name and a one-line summary, but only a static low-risk subset is directly callable each turn. If you need a tool from the index that is not yet callable, call capability_expand with its category first; the matching tools become callable in this turn. Never invent a tool name — expand the category instead.\n- **delegate_task**(goal, context) [capability_expand category: delegate]: Delegate a complex sub-task to an isolated subagent. The subagent gets a fresh context and restricted tool access. Use when a task is self-contained and can be solved independently.\n- **schedule_reentry**(kind, reason, delay_seconds, event_match, max_reentries, deadline_seconds): Register a re-entry so this task can resume later from its current orientation (findings / open questions / next step). Use when work must pause and continue after a delay (kind=time) or when a matching platform event arrives (kind=event), instead of finishing now. The research-ledger state is carried over automatically.\n- **hub_push**(skill_name, visibility, version) [capability_expand category: hub]: Push a local skill to the ModelScope Hub for sharing or backup.\n- **hub_pull**(repo_id, version) [capability_expand category: hub]: Pull a skill from the ModelScope Hub to install locally.\n- **hub_search**(query) [capability_expand category: hub]: Search for skills on the Hub by keyword or description.\n- **hub_sync**(mode, dry_run) [capability_expand category: hub]: Preview or execute sync between local skills and Hub.\n- **platform_action**(platform, action, payload, backend_kind) [capability_expand category: gateway]: Execute an exact registered business action on an external platform through LeapFlow's App Connector layer. Actions must be copied from the App Connector Capability Index and are addressed as domain.operation, e.g. im.send_message or docs.create_markdown. All business fields (chat_id, text, query, etc.) MUST be placed inside `payload`, never at the top level. Example: {\"platform\":\"feishu\",\"action\":\"im.send_message\",\"payload\":{\"chat_id\":\"oc_xxx\",\"text\":\"hello\"}}. Do not invent action names, do not use management actions such as list/guide/connect/status here.\n- **platform_connect**(action, platform, credentials, options, checkpoint) [capability_expand category: gateway]: List, guide, connect, disconnect, remove, or check status for external platforms using the App Connector management namespace. Supports REST and CLI backends. Use this for management actions such as list/guide/preflight/connect/status; use platform_action only for exact registered business actions.\n- **gateway_send**(platform, chat_id, text, thread_id) [capability_expand category: gateway]: Send a message to a connected external platform (Feishu group, Telegram chat, DingTalk conversation, etc.). Requires the platform to be connected via gateway_connect first. Use gateway_connect with action='list' to see connected platforms and available chat IDs.\n- **gateway_connect**(action, platform, credentials, options) [capability_expand category: gateway]: Connect, configure, or manage external platform integrations (Feishu, DingTalk, Telegram, Slack, Discord, etc.). Conversational flow: 1) call 'guide' to get setup steps + required fields, 2) present the steps to the user and ask for ALL required credentials in a single message, 3) call 'connect' with the credentials. Goal: complete in 1–2 user turns. NEVER include credential values in your text response.\n- **plugin_list**(): List the live plugin registry across Tool/Gateway/LLM subsystems for plugin management: which plugins are loaded, their category and fiber lifecycle state. For questions about LeapFlow's own capabilities or whether it supports X, use self_describe instead.\n- **plugin_status**(plugin_id): Get detailed status of a specific plugin: its declared category, runtime dependencies, contributed tools, and fiber lifecycle state.\n- **plugin_versions**(plugin_id): List recorded source versions and active pointer for a profile-scoped plugin.\n- **plugin_propose**(requested_capability, plugin_id, proposed_tools, test_cases, risk_level, evidence): Create a side-effect-free PluginProposal from explicit capability-gap evidence. Use this before plugin_generate when a missing capability should be reviewed. Does not call an LLM, write files, or install anything.\n- **assess_compatibility**(manifest, source_path) [capability_expand category: plugin_management]: Assess whether a foreign plugin manifest or real DSH source bundle is compatible with LeapFlow. A source bundle assessment is static: runtime_ready stays false until restricted Node discovery during plugin_install. Returns component-level verdicts and limitations.\n- **plugin_generate**(plugin_id, description, proposal_id) [capability_expand category: system]: Generate a new ToolPlugin from a natural-language capability description. The LLM produces code that conforms to the ToolPlugin Protocol; it is then rigorously validated (syntax, structure, import, protocol conformance). The generated source is stored in CAS and receives explicit content approval. It DOES NOT install the plugin — installation requires a second, mutation-specific approval via plugin_install.\n- **plugin_install**(plugin_id, code, marketplace_name, source_path, proposal_id, version_label) [capability_expand category: system]: Install a plugin from exactly one source: validated Python code, the configured Python marketplace, or a real DSH source bundle. DSH bundles run restricted Node discovery before registration; only public host tools are installed and client UI limitations are reported. Writes profile-scoped state and mutates the process-global registry. REQUIRES APPROVAL.\n- **plugin_rollback**(plugin_id, version) [capability_expand category: system]: Rollback a profile-scoped plugin to a recorded source version and reload it. REQUIRES APPROVAL.\n- **plugin_reload**(plugin_id, version_label) [capability_expand category: system]: Hot-reload a plugin at runtime. Disposes the old plugin fiber, re-imports its module, and registers a fresh instance. Existing in-flight turns are unaffected (snapshot isolation). REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_disable**(plugin_id) [capability_expand category: system]: Disable a plugin by disposing its fiber, removing its tools from the runtime registry. Cannot disable self_management itself. REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_remove**(plugin_id, delete_source) [capability_expand category: system]: Terminally remove a plugin: dispose its fiber, unregister its tools, remove reload metadata, and optionally delete its profile-scoped source file. Cannot remove self_management itself. REQUIRES APPROVAL.\n- **plugin_unquarantine**(plugin_id) [capability_expand category: plugin_management]: Restore a quarantined plugin to probation status for re-evaluation. Unfreezes the trust ledger, transitions the proposal back to PROBATION, and reloads the plugin. REQUIRES APPROVAL.\n- **plugin_enable**(plugin_id) [capability_expand category: system]: Re-enable a previously disabled plugin by reloading its module and registering a fresh instance. REQUIRES APPROVAL.\n- **self_describe**(facet): Answer questions about LeapFlow ITSELF from live runtime evidence — its identity/version/model, what tools and plugins it has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of self-awareness. Prefer this over reading LeapFlow's own source code. Pick facet=capabilities for 'what can you do / do you support X', identity for version/model, runtime for context/posture, or all for a full self-check when explicitly requested.\n- **runtime_snapshot**(): Lightweight (~150 token) flat snapshot of current runtime state: model, context budget, posture, disclosure level, turn count, cache hit rate, uptime, tool count, and pending approvals.\n- **schedule_create**(trigger_expression, instruction, execution_mode, max_retries, delivery_target) [capability_expand category: scheduler]: Create a new scheduled task. Specify a trigger expression (e.g. '30m', 'every 2h', '0 9 * * *') and the instruction to execute on each trigger. Optionally provide an execution mode and a delivery target to receive notifications.\n- **schedule_list**() [capability_expand category: scheduler]: List all scheduled tasks with their current state, trigger, and next due time.\n- **schedule_status**(task_id) [capability_expand category: scheduler]: Get detailed status and recent execution history for a scheduled task.\n- **schedule_pause**(task_id) [capability_expand category: scheduler]: Pause a scheduled task so it stops firing without being cancelled.\n- **schedule_resume**(task_id) [capability_expand category: scheduler]: Resume a paused scheduled task — re-arms it and recalculates next due time.\n- **schedule_cancel**(task_id) [capability_expand category: scheduler]: Cancel a scheduled task permanently.\n- **tool_search**(query, max_results): Search all registered tools by keyword relevance. Returns a ranked list of matching tools with name, category, and summary. Use this when you need a tool but are unsure of its exact name or category.\n- **tool_describe**(tool_name): Get the full callable schema of a single tool by exact name. Returns the tool's description, parameters, and metadata. Use after tool_search to inspect a specific tool before calling it.\n- **hw_list**(): List connected hardware devices with their channel counts and measured quantities. Start here; it does not include operating limits.\n- **hw_describe**(device_id): Return the full reference for one device: every channel, its unit, its operating envelope, rate limit, reversibility, and required interlocks, plus any prior outcomes recorded for its writable channels. Required before commanding a device.\n- **hw_read**(device_id, channel_id): Read the current value of one device channel. Has no physical effect.\n- **hw_status**(device_id): Report connection health, halt capability, and recent notable events (threshold excursions, lost samples, stalled channels) for one device.\n- **hw_configure**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Set a configuration value or setpoint on a channel declaring effect=configure. Setpoints often have inertia: the value may need time to stabilise.\n- **hw_actuate**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Command motion or output on a channel declaring effect=actuate. This moves physical hardware; a repeat from an unknown state is not a safe retry.\n- **hw_dispense**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Consume an irreversible resource on a channel declaring effect=dispense. Running this twice dispenses twice; never repeat it after a failure without first verifying what already happened.\n- **hw_estop**(device_id) [capability_expand category: hardware]: Stop all motion and output on a device immediately. Never requires approval; use it whenever device behaviour is unexpected.\n- **hw_batch_actuate**(device_id, commands, dry_run, verify) [capability_expand category: hardware]: Write multiple actuator channels in one atomic transaction. Used for coordinated multi-joint control where all positions must be commanded simultaneously (e.g. robot arm joint-space control).\n- **hw_control_start**(device_id, mode, targets, frequency_hz, trajectory, policy, gains) [capability_expand category: hardware]: Start a real-time control loop on a robot device. Supports PID position tracking, impedance control, trajectory following, and neural policy chains.\n- **hw_control_stop**(device_id) [capability_expand category: hardware]: Stop the real-time control loop on a device. Gracefully halts the device and returns session statistics.\n- **hw_control_status**(device_id) [capability_expand category: hardware]: Get the status and statistics of a running control loop. Returns frequency, jitter, overruns, and policy state.\n- **hw_policy_infer**(device_id, policy, task, execute, chunk_size, verify, compute_budget) [capability_expand category: hardware]: Execute VLA policy inference on a robot device. Reads observation, runs policy, optionally writes action to actuators. Supports local (HuggingFace) and remote (PolicyServer) policies.\n- **hw_policy_episode**(action, device_id, policy, task) [capability_expand category: hardware]: Manage policy inference episode lifecycle. Start an episode to begin a task, stop to finalise and get stats.\n- **hw_policy_list**(source) [capability_expand category: hardware]: List available VLA policies and their capabilities.\n- **hw_orchestrate**(mode, operations, barrier_timeout_s, label) [capability_expand category: hardware]: Execute coordinated operations across multiple hardware devices. Supports sequential (one-at-a-time), parallel (concurrent), and barrier (synchronised) execution modes. Any failure in a parallel group triggers halt_all on every participating device for safety.\n- **session_search**(query, limit) [capability_expand category: unclassified]: Search past conversation sessions in the current workspace for relevant context.\n- **session_list**(limit): List recent conversation sessions in the current workspace with stable ids, titles, dates, and summaries. Use for browsing past tasks or when user asks to see history without specific search terms. No keywords needed — returns chronological list.\n- **session_detail**(session_id, limit, offset, include_inactive) [capability_expand category: unclassified]: Read a paginated persisted transcript for one past conversation session in the current workspace. Use after session_list or session_search returns a session_id.\n- **click**(element_index) [capability_expand category: desktop]: Click a UI element by its element_index (from the latest observe_ui snapshot)\n- **get_clipboard**() [capability_expand category: desktop]: Read current clipboard text content\n- **list_apps**(filter, running_only) [capability_expand category: desktop]: List available applications on this system. Use to discover correct bundle_id before switch_app.\n- **list_windows**() [capability_expand category: desktop]: List all top-level windows with pid, window_id, title, and per-window state (minimized, on-screen). Call this first to pick the pid and window_id that observe_ui and other window tools require.\n- **observe_ui**(pid, window_id, query) [capability_expand category: desktop]: Snapshot one window's actionable UI elements, each tagged with an element_index for click/right_click/read_text. Re-observe after actions — indices belong to one snapshot. Requires the window's pid and window_id from list_windows.\n- **open_url**(url, app_id) [capability_expand category: desktop]: Open a URL in the default or specified browser\n- **read_text**(element_index) [capability_expand category: desktop]: Read the text content of a specific UI element from the latest snapshot\n- **right_click**(element_index) [capability_expand category: desktop]: Right-click a UI element to open its context menu. Returns visible menu items.\n- **screenshot**(pid, window_id) [capability_expand category: desktop]: Capture a screenshot for visual verification. With pid + window_id captures that window (works across all displays); defaults to the last observed window, or the full desktop when no window has been observed.\n- **scroll**(element_index, direction, amount, pid, window_id) [capability_expand category: desktop]: Scroll a scrollable area of a window. Omit element_index to scroll the window's focused/page scroller; pass one to scroll an exact element from the latest snapshot.\n- **select_text**(element_index) [capability_expand category: desktop]: Select all text in a UI element (focus + select-all, for subsequent copy)\n- **set_clipboard**(text) [capability_expand category: desktop]: Write text to the clipboard\n- **shortcut**(keys) [capability_expand category: desktop]: Execute a keyboard shortcut\n- **switch_app**(app_id) [capability_expand category: desktop]: Switch to an app (launch if needed, activate, verify)\n- **type_text**(text) [capability_expand category: desktop]: Type text into the currently focused element\n- **wait**(seconds) [capability_expand category: desktop]: Wait for a specified duration before continuing\n- **wait_until**(condition, pid, window_id, timeout, poll_interval) [capability_expand category: desktop]: Wait until a UI condition is met (polls UI tree). Returns elements when found or on timeout.\n- **wait_until_stable**(timeout, poll_interval, pid, window_id) [capability_expand category: desktop]: Wait until the UI stops changing (element set stabilizes across polls).\n\n## App Connector Capability Index\nLeapFlow can onboard and manage external apps through `platform_connect` and execute exact registered business actions through `platform_action`.\nFor requests about connecting, setting up, configuring, enabling, or managing a supported app, use `platform_connect` first instead of generating SDK/Webhook sample code.\n`platform_connect.action` is the management namespace: list, guide, preflight, connect, disconnect, remove, status, events_start, events_stop, events_status.\n`platform_action.action` is only for exact registered platform business actions listed below, such as `im.send_message`; never use management actions like `list` or `guide` there.\nAll business fields MUST go inside `payload`; top-level keys are only `platform`, `action`, and `payload`.\nDo not invent platform IDs or platform action names. If the needed action is not listed, ask for discovery/clarification instead of guessing.\nUse `platform_connect` with `action='guide'` and the matching `platform` to start onboarding; use `action='list'` when the app is unclear.\nWhen a pending onboarding state is present, continue from that state with `platform_connect` instead of asking the user to restate the app.\nSupported apps:\n- `api_server`: API Server (OpenAI Compatible) (category=api; backend=adapter; platform_action actions=none registered)\n- `dingtalk`: 钉钉 (DingTalk) (category=im; backend=adapter; platform_action actions=none registered)\n- `feishu`: 飞书 (Feishu/Lark) (category=im; backend=cli)\n - `calendar.create_event` payload={summary*, start_time*, end_time*, attendees} [write/high]\n - `docs.create_markdown` payload={title*, markdown*, folder_token} [write/medium]\n - `drive.search` payload={query*, limit} [read/medium]\n - `im.add_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.download_resource` payload={message_id*, file_key*, type*} [read/low]\n - `im.get_messages` payload={message_ids*} [read/low]\n - `im.list_chats` payload={page_size} [read/low]\n - `im.list_messages` payload={chat_id*} [read/low]\n - `im.list_thread_messages` payload={thread*} [read/low]\n - `im.remove_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.reply_message` payload={message_id*, text*} [send/high]\n - `im.search_chats` payload={query*, page_size} [read/low]\n - `im.search_messages` payload={query*} [read/medium]\n - `im.send_message` payload={chat_id*, text*, thread_id} [send/high]\n - `im.update_card` payload={token*, card*} [write/medium]\n - `im.update_message` payload={message_id*, text*} [write/medium]\n - `mail.search_unread` payload={query, limit} [read/high]\n - `sheets.append_row` payload={spreadsheet_token*, sheet_id*, values*} [write/medium]\n - `task.create` payload={title*, description, due_time} [write/medium]\n- `telegram`: Telegram (category=social; backend=adapter; platform_action actions=none registered)\n- `webhook`: Webhook (Generic) (category=webhook; backend=adapter; platform_action actions=none registered)\n\n## Learned Skills\nYou have access to the following learned skills. Use `skills_list` to browse or `skill_view` to read details:\n- api_designer: REST and GraphQL API design with OpenAPI spec generation and best practices [API, REST, GraphQL]\n- code_review: Structured code review for security, performance, and style [code, review, security]\n- data_analysis: CSV/JSON data analysis with statistical summaries, quality checks, and visualiza [data, analysis, statistics]\n- devops_helper: CI/CD pipeline configuration, Docker orchestration, and deployment strategy [devops, CI/CD, Docker]\n- document_writer: Structured document generation: technical docs, API references, reports, and REA [documentation, technical-writing, README]\n- git_workflow: Git operation orchestration: branching, commits, conflicts, and PR workflow [git, version-control, branching]\n- i18n_helper: Internationalization workflow: string extraction, translation, locale management [i18n, internationalization, localization]\n- project_analysis: Project structure analysis, dependency audit, and tech stack identification [project, analysis, dependencies]\n- security_audit: Security vulnerability scanning strategy, threat modeling, and fix recommendatio [security, vulnerability, audit]\n- shell_automation: Natural language to shell commands with safety review and step-by-step execution [shell, terminal, automation]\n- test_writer: Generate unit and integration tests from source code with edge-case coverage [testing, unit-test, integration-test]\n- web_research: Multi-step web search with cross-verification and structured synthesis [web, search, research]\n\n## Tool Usage\nTools are normally invoked through the native function-calling mechanism, not by writing JSON in your reply\ntext. Only if the provider signals that native function calling is unavailable for this turn, fall back to a\nsingle JSON code block: `{\"name\": \"tool_name\", \"arguments\": {\"key\": \"value\"}}` — use this fallback format\nonly, never both. Only call a tool whose exact name appears in the tool index above and is currently callable\n(or reachable via `capability_expand`). Never invent, rename, alias, or guess a tool name, platform ID, or\nplatform action from argument shape or wording — if the index does not list it, it does not exist.\n`platform_connect.action` (list/guide/preflight/connect/disconnect/remove/status/events_start/events_stop/\nevents_status) is the App Connector management namespace; `platform_action.action` only accepts exact\nregistered business actions such as `im.send_message` shown in the App Connector Capability Index — never mix\nthe two namespaces. If a tool call returns an unknown/unavailable result, use the returned suggestions or\navailable names for a single retry instead of trying further variations of the same guess.\n\n**Side-effect action rule** (`platform_action` with effect=send/write/execute):\n- Call each unique action+payload **exactly once**. Never include duplicates in the same turn.\n- Once the result returns `\"completed\": true`, that action is DONE for this task. Do NOT call it again\n in any subsequent turn — immediately summarize the result for the user instead.\n- The system enforces idempotency: duplicate calls are blocked and will not execute.\n- If the user explicitly requests sending/writing multiple times, use distinct payloads per call.\n\n**Resource identifier provenance rule**:\n- NEVER fabricate, guess, or infer resource identifiers (chat_id, message_id, file_key, user_id, etc.).\n Every resource ID used in a side-effect action MUST come from a successful API response in this session.\n- If a read/list action fails (e.g. authorization error), you do NOT have valid resource IDs.\n Report the failure to the user — do NOT attempt the dependent write/send action with a guessed ID.\n- When a tool result contains `\"llm_instruction\"`, follow it exactly.\n\n## Guidelines\n1. **Direct answers first**: If you already know the answer, respond directly without tools.\n2. **Avoid redundant tool calls**: Do not call the same tool with the same arguments more than once in the same user turn. When an existing tool result already answers the user's request, stop calling tools and answer directly.\n3. **Use tools proactively**: When the user asks about files, time, system state, or needs actions performed, use the appropriate tool.\n4. **Chain tools when needed**: You can call multiple tools in sequence (e.g., list files → read file → summarize).\n5. **Handle failures gracefully**: If a tool fails, explain what went wrong and suggest alternatives. If it failed because of an unknown tool/platform/action name, retry once with an exact name from the returned suggestions, then explain rather than keep guessing.\n6. **Summarize results naturally**: After tool execution, synthesize the results into a helpful answer rather than dumping raw output.\n7. **Stay conversational**: Maintain a natural, helpful tone. Acknowledge context from earlier in the conversation.\n8. **Recall past work**:\n - Broad queries (\"之前做了什么\", \"列出任务\"): answer from the \"Recent Task History\" section already in your context. If insufficient, call session_list.\n - Specific lookups (\"上次那个配置怎么改的\"): call session_search with relevant phrases (NOT single characters).\n - Do NOT call search tools repeatedly with keyword fragments. One well-phrased call is better than ten fragmented ones.\n\n## Coding & Verification\nWhen working with code, prefer the precise built-in tools over ad-hoc shell: use `repo_map` to orient in an\nunfamiliar project, `code_search`/`file_find` to locate and `code_intel` for symbols, `edit_file` (anchored\nsearch-replace, or a unified `diff`) to change files — never rewrite a whole file to change a few lines — and\n`git_query` to inspect diffs/log. After edits, check `syntax_ok` in the result and run `test_run`/`lint_check`\nbefore declaring the work done. Batch independent read-only calls (search/read/query) into a single turn — they\nrun in parallel.\n\n## Presentation Style\n1. **Polished Markdown only**: Format user-facing answers with clean Markdown headings, short paragraphs, and concise bullets. Use tables only when they improve comparison or scanning.\n2. **Terminal-friendly layout**: Keep lines readable in a TUI; avoid dense walls of text, deeply nested lists, oversized ASCII art, or heavy visual blocks.\n3. **Elegant emphasis**: Use bold text sparingly for key terms and conclusions. Avoid excessive emojis, decorative symbols, repeated separators, or visual noise.\n4. **Theme-safe colors**: Do not emit ANSI escape codes, HTML color tags, Rich markup, or hardcoded color names. Rely on the TUI theme to render Markdown professionally.\n5. **No leaked tool protocol**: Never show tool-call JSON, internal schemas, raw observations, tool result payloads, or hidden reasoning in the final answer unless the user explicitly asks for raw/debug output. Treat any prior `{\"name\": ..., \"arguments\": ...}` blocks and `Tool result (...)` messages as internal execution context only.\n6. **Professional closure**: End with a concise conclusion or next step when helpful; avoid rambling after the useful answer is complete.\n\nWhen finished with all tool calls, respond normally without a JSON block, tool-call transcript, or process log.\n\n## Task Contract\n- Task ID: turn-2\n- Original user request: Read the current value of the setpoint channel on bench_r8.\n- Workspace root: \n- Allowed roots: \n- Treat relative project paths as relative to the workspace root; never infer `.` as the project root when a workspace root is provided.\n- Workspace boundary is enforced by tools: do not read, search, edit, or run commands against paths outside the allowed roots unless the user explicitly requests an external path and the tool/approval policy permits it.\n- LeapFlow workspace config is optional at `/.leapflow/config.yaml`; runtime config is loaded from `~/.leapflow/config/user.yaml` and `~/.leapflow/profiles//config/*.yaml`.\n- Preserve this task contract across summarization, compression, tool loops, and memory retrieval.\n" + }, + { + "role": "user", + "content": "List the connected hardware devices, then describe bench_r8 in full." + }, + { + "role": "assistant", + "content": "[Called: hw_list]\n[Called: hw_describe]\nDiscovered the simulated bench and read its channel limits." + }, + { + "role": "user", + "content": "Read the current value of the setpoint channel on bench_r8.\nRead the current value of the setpoint channel on bench_r8." + }, + { + "role": "assistant", + "content": "", + "tool_calls": [ + "hw_read" + ] + }, + { + "role": "tool", + "content": "{\"ok\": true, \"reading\": {\"device_id\": \"bench_r8\", \"channel_id\": \"setpoint\", \"value\": 50.0, \"quantity\": \"temperature\", \"unit\": \"C\", \"observed_at\": , \"sequence\": 1, \"quality\": \"ok\"}, \"execution_id\": \"\", \"idempotency_key\": \"\", \"execution_policy\": \"read_only\", \"tool_call_id\": \"call_1\", \"execution_status\": \"completed\"}", + "tool_result": true + } + ], + "tools": [ + "capability_expand", + "code_intel", + "code_search", + "config_get", + "config_list", + "env_info", + "file_find", + "file_list", + "file_read", + "git_query", + "hw_actuate", + "hw_batch_actuate", + "hw_configure", + "hw_control_start", + "hw_control_status", + "hw_control_stop", + "hw_describe", + "hw_dispense", + "hw_estop", + "hw_list", + "hw_orchestrate", + "hw_policy_episode", + "hw_policy_infer", + "hw_policy_list", + "hw_read", + "hw_status", + "lint_check", + "memory_search", + "plugin_list", + "plugin_propose", + "plugin_status", + "plugin_versions", + "repo_map", + "research_note", + "runtime_snapshot", + "schedule_reentry", + "self_describe", + "session_list", + "skill_view", + "skills_list", + "terminal_list", + "terminal_read", + "test_run", + "text_search", + "time_get", + "tool_describe", + "tool_search", + "web_fetch" + ] + }, + "responses": [ + { + "status": 200, + "content_type": "application/json", + "body": "{\"id\": \"chatcmpl-cassette\", \"object\": \"chat.completion\", \"model\": \"cassette-model\", \"choices\": [{\"index\": 0, \"message\": {\"role\": \"assistant\", \"content\": \"Read the setpoint channel.\"}, \"finish_reason\": \"stop\"}], \"usage\": {\"prompt_tokens\": 64, \"completion_tokens\": 16, \"total_tokens\": 80}}" + } + ] +} diff --git a/tests/_fixtures/cassettes/r8_hardware/cassette-model-8743f8121b93d7cd.cassette.json b/tests/_fixtures/cassettes/r8_hardware/cassette-model-8743f8121b93d7cd.cassette.json new file mode 100644 index 00000000..f6e5ef45 --- /dev/null +++ b/tests/_fixtures/cassettes/r8_hardware/cassette-model-8743f8121b93d7cd.cassette.json @@ -0,0 +1,91 @@ +{ + "fingerprint": "8743f8121b93d7cde4691fdb85b7a31dd62c6efd253d900945dbd342c5a91721", + "note": "captured in seed mode", + "request": { + "model": "cassette-model", + "stream": false, + "messages": [ + { + "role": "system", + "content": "You are LeapFlow, an intelligent assistant that can both converse naturally and take real actions on the user's computer.\n\n## Capabilities\nThe tool index below lists **every** registered tool by name and a one-line summary — this is the complete\ncapability contract; nothing else exists. Only a subset is directly callable this turn (via native tool calling,\nnot a JSON block in your reply). If you need a tool from the index that is not yet callable, call\n`capability_expand` with its category name first — the matching tools become callable immediately after.\nWhen the user asks about LeapFlow **itself** — its identity/version/model, what tools, plugins, or capabilities\nit has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of\nself-awareness — answer from live runtime evidence via `self_describe` (facet=capabilities for \"what can you\ndo / do you support X\", identity for version/model, runtime for context/posture, or `all` for a full self-check),\nnever by reading LeapFlow's own source code, documentation, or memory. Report configuration-dependent or\nunavailable capabilities as limitations rather than inferring them.\n- **text_search**(text, pattern): Search for a regex pattern in text.\n- **text_replace**(text, old, new, count) [capability_expand category: write]: Replace occurrences of a substring in text.\n- **time_get**(): Get current date and time.\n- **env_info**(): Get system environment information (OS, Python version, cwd).\n- **skills_list**(query, category, source): List available learned skills. Use when user asks about capabilities or you need a specific skill.\n- **skill_view**(name): View the full content of a specific skill document.\n- **skill_list_files**(name) [capability_expand category: skills]: List all support files in a skill directory (configs, templates, examples). Use before skill_file_read to discover available files.\n- **skill_file_read**(name, file_path) [capability_expand category: skills]: Read an individual support file from a skill directory. Use skill_list_files first to discover available files.\n- **code_intel**(path, operation): Precise document symbols (outline) for a source file: classes, functions, and methods with line ranges. Python uses an exact AST parse; other languages use a keyword-prefix scan. Prefer over file_read mode=symbols for accurate navigation before editing. Read-only.\n- **repo_map**(path): Compact project orientation for a repository root: languages, detected test/lint commands, top-level structure, entry points, manifest, and VCS branch. Call this first when entering an unfamiliar codebase. Read-only.\n- **scm_sync**(action, cwd, remote, pull_ref, push_ref, timeout) [capability_expand category: scm]: Run a typed git SCM action. Use this instead of shell_run for git pull/push/status. For 'pull origin main then push', set action='pull_then_push', remote='origin', pull_ref='main', and omit push_ref so LeapFlow pushes the current local branch.\n- **git_query**(action, cwd, ref, path, staged, max_count, stat): Read-only structured git inspection: action=diff|log|status|branch|show. Prefer over shell_run for reading repo state — output is clipped, redacted, and log/branch are parsed into structured fields. Use scm_sync for pull/push.\n- **git_write**(action, cwd, message, stage_all, name, ref, create) [capability_expand category: scm]: Mutating git actions: action=commit (message, stage_all), branch (create+switch), checkout (switch; create=true for -b). Approval-gated. Use scm_sync for pull/push and git_query for reads.\n- **test_run**(command, cwd, timeout): Run the project's test suite and return structured results (framework, passed/failed counts, failing tests). Auto-detects the runner (pytest/npm/go/cargo) or uses a configured/explicit command; executes via the governed shell. ok=true means the runner executed — see 'success' for pass/fail.\n- **lint_check**(command, cwd, timeout): Run the project's linter and return a structured clean/issue result. Auto-detects the linter (ruff/eslint/go vet/clippy) or uses a configured/explicit command; executes via the governed shell. ok=true means the linter ran — see 'clean'.\n- **file_list**(path, pattern, depth): List files and directories at a given path. Use depth=1 or depth=2 to get a recursive tree in one call instead of listing each sub-directory separately.\n- **file_read**(path, max_lines, start_line, max_chars, mode): Read text file content with adaptive context governance. For large or unfamiliar files, prefer mode='outline' or mode='symbols' first, then use mode='raw' with start_line/max_lines for the specific range you actually need. For LeapFlow's own settings, use config_list / config_get / config_set — its config files are outside the workspace and not readable here.\n- **file_write**(path, content, mode) [capability_expand category: write]: Write content to a file (overwrite or append).\n- **code_search**(pattern, patterns, path, glob, ignore_case, multiline, max_results, context_lines): Search file CONTENTS by regex pattern across a directory tree (ripgrep-backed). Requires a regex pattern. NOT for listing or browsing directory contents — use file_list for that. Prefer this over shell_run grep: faster, skips VCS/dependency/build dirs, and returns structured path:line:column matches. Batch related lookups into ONE call via `patterns` (OR-combined, single pass) instead of issuing several separate searches. Use file_read for the surrounding context of a hit.\n- **file_find**(glob, path, max_results): Find files by a recursive glob pattern under a base path (e.g. '**/test_*.py' or '*.md'). Prefer this over shell_run find; skips VCS/dependency/build dirs.\n- **edit_file**(path, edits, dry_run, diff) [capability_expand category: write]: Apply targeted, anchored search-replace edits to an EXISTING text file (use file_write to create/overwrite). Each edit is {original_text, new_text, replace_all?}; original_text must match exactly and uniquely (or set replace_all) — a non-unique or missing anchor is rejected so files are never corrupted. Set dry_run to preview. Alternatively pass a unified 'diff' to apply its hunks as anchored edits. Far cheaper and safer than rewriting a whole file.\n- **shell_run**(command, cwd, timeout) [capability_expand category: shell]: Execute a one-shot shell command with timeout protection. Runs in the active workspace; paths resolving outside it are refused. Reach for a structured tool first when one fits — web_fetch for anything over HTTP(S), git_query/scm_sync for git, code_search/file_find/file_read for the repo, config_get/config_set for LeapFlow's own settings — because those report typed results, while a failed shell command can only be diagnosed from its exit code and stderr. Every shell run counts as an external side effect, so a failure stops the rest of the batch and is not retried automatically.\n- **terminal_open**(command, cwd, shell) [capability_expand category: terminal]: Open a PERSISTENT shell session (REPL/dev server/watch), returning a session_id for terminal_send/read/close. Disabled unless tools.terminal_session_enabled is set. For one-shot commands use shell_run instead.\n- **terminal_send**(session_id, input, wait) [capability_expand category: terminal]: Send a line of input to a persistent terminal session and return output captured shortly after.\n- **terminal_read**(session_id, wait): Drain buffered output from a persistent terminal session (optionally waiting briefly first).\n- **terminal_close**(session_id) [capability_expand category: terminal]: Terminate a persistent terminal session and release its process group.\n- **terminal_list**(): List active persistent terminal sessions.\n- **config_list**(category, limit): List LeapFlow's own writable settings (LLM model and endpoint, daemon, memory, perception, gateway, …) with current values. Use this to discover the exact key before changing anything. Optionally narrow by `category`. This is the only correct way to inspect LeapFlow configuration — never read config files from disk.\n- **config_get**(key): Read one LeapFlow setting by key (e.g. 'llm.model', 'llm.base_url', 'daemon.log_level'), returning its current value, type, scopes, and whether a change needs a daemon restart. There is no 'llm.provider' key: provider behavior is inferred from the OpenAI-compatible 'llm.base_url'. Never read LeapFlow config files from disk — use this.\n- **config_set**(key, value, scope) [capability_expand category: config]: Change one LeapFlow setting by key, e.g. switch an OpenAI-compatible LLM with key='llm.model' and key='llm.base_url'. There is no 'llm.provider' key: provider behavior is inferred from the endpoint. Values are validated and coerced; credentials are stored in the vault automatically. Call config_list or config_get first if unsure of the key. The result states whether a `leap daemon restart` is required. Never edit LeapFlow config files directly.\n- **web_fetch**(url, select, timeout, max_bytes): Read a URL over HTTP(S) and get back extracted, context-sized content: parsed JSON for API endpoints, readable text plus links for web pages. Use this for anything on the internet — prices, docs, releases, articles — instead of running curl through shell_run: it reports real HTTP status codes, retries rate limits on its own, and is a plain read so a retry is always safe. For JSON APIs pass `select` with a dotted path (e.g. 'chart.result.0.meta') to return just that part instead of the whole payload.\n- **memory_search**(query, limit): Search agent memory for relevant past experiences, observations, and facts.\n- **memory_add**(content, kind) [capability_expand category: write]: Store a new observation or insight in memory for future reference.\n- **research_note**(kind, text): Record a compact, structured note about the current task's state so it survives context compression on long / multi-step tasks. Use for durable findings, open questions still to resolve, decisions / excluded paths, and the immediate next step. One concise sentence per note.\n- **capability_expand**(category): Fetch the full callable schema for every tool in a capability category (e.g. 'hub', 'gateway', 'desktop', 'delegate', 'file', 'memory', 'skill'). The compact tool index always lists every registered tool by name and a one-line summary, but only a static low-risk subset is directly callable each turn. If you need a tool from the index that is not yet callable, call capability_expand with its category first; the matching tools become callable in this turn. Never invent a tool name — expand the category instead.\n- **delegate_task**(goal, context) [capability_expand category: delegate]: Delegate a complex sub-task to an isolated subagent. The subagent gets a fresh context and restricted tool access. Use when a task is self-contained and can be solved independently.\n- **schedule_reentry**(kind, reason, delay_seconds, event_match, max_reentries, deadline_seconds): Register a re-entry so this task can resume later from its current orientation (findings / open questions / next step). Use when work must pause and continue after a delay (kind=time) or when a matching platform event arrives (kind=event), instead of finishing now. The research-ledger state is carried over automatically.\n- **hub_push**(skill_name, visibility, version) [capability_expand category: hub]: Push a local skill to the ModelScope Hub for sharing or backup.\n- **hub_pull**(repo_id, version) [capability_expand category: hub]: Pull a skill from the ModelScope Hub to install locally.\n- **hub_search**(query) [capability_expand category: hub]: Search for skills on the Hub by keyword or description.\n- **hub_sync**(mode, dry_run) [capability_expand category: hub]: Preview or execute sync between local skills and Hub.\n- **platform_action**(platform, action, payload, backend_kind) [capability_expand category: gateway]: Execute an exact registered business action on an external platform through LeapFlow's App Connector layer. Actions must be copied from the App Connector Capability Index and are addressed as domain.operation, e.g. im.send_message or docs.create_markdown. All business fields (chat_id, text, query, etc.) MUST be placed inside `payload`, never at the top level. Example: {\"platform\":\"feishu\",\"action\":\"im.send_message\",\"payload\":{\"chat_id\":\"oc_xxx\",\"text\":\"hello\"}}. Do not invent action names, do not use management actions such as list/guide/connect/status here.\n- **platform_connect**(action, platform, credentials, options, checkpoint) [capability_expand category: gateway]: List, guide, connect, disconnect, remove, or check status for external platforms using the App Connector management namespace. Supports REST and CLI backends. Use this for management actions such as list/guide/preflight/connect/status; use platform_action only for exact registered business actions.\n- **gateway_send**(platform, chat_id, text, thread_id) [capability_expand category: gateway]: Send a message to a connected external platform (Feishu group, Telegram chat, DingTalk conversation, etc.). Requires the platform to be connected via gateway_connect first. Use gateway_connect with action='list' to see connected platforms and available chat IDs.\n- **gateway_connect**(action, platform, credentials, options) [capability_expand category: gateway]: Connect, configure, or manage external platform integrations (Feishu, DingTalk, Telegram, Slack, Discord, etc.). Conversational flow: 1) call 'guide' to get setup steps + required fields, 2) present the steps to the user and ask for ALL required credentials in a single message, 3) call 'connect' with the credentials. Goal: complete in 1–2 user turns. NEVER include credential values in your text response.\n- **plugin_list**(): List the live plugin registry across Tool/Gateway/LLM subsystems for plugin management: which plugins are loaded, their category and fiber lifecycle state. For questions about LeapFlow's own capabilities or whether it supports X, use self_describe instead.\n- **plugin_status**(plugin_id): Get detailed status of a specific plugin: its declared category, runtime dependencies, contributed tools, and fiber lifecycle state.\n- **plugin_versions**(plugin_id): List recorded source versions and active pointer for a profile-scoped plugin.\n- **plugin_propose**(requested_capability, plugin_id, proposed_tools, test_cases, risk_level, evidence): Create a side-effect-free PluginProposal from explicit capability-gap evidence. Use this before plugin_generate when a missing capability should be reviewed. Does not call an LLM, write files, or install anything.\n- **assess_compatibility**(manifest, source_path) [capability_expand category: plugin_management]: Assess whether a foreign plugin manifest or real DSH source bundle is compatible with LeapFlow. A source bundle assessment is static: runtime_ready stays false until restricted Node discovery during plugin_install. Returns component-level verdicts and limitations.\n- **plugin_generate**(plugin_id, description, proposal_id) [capability_expand category: system]: Generate a new ToolPlugin from a natural-language capability description. The LLM produces code that conforms to the ToolPlugin Protocol; it is then rigorously validated (syntax, structure, import, protocol conformance). The generated source is stored in CAS and receives explicit content approval. It DOES NOT install the plugin — installation requires a second, mutation-specific approval via plugin_install.\n- **plugin_install**(plugin_id, code, marketplace_name, source_path, proposal_id, version_label) [capability_expand category: system]: Install a plugin from exactly one source: validated Python code, the configured Python marketplace, or a real DSH source bundle. DSH bundles run restricted Node discovery before registration; only public host tools are installed and client UI limitations are reported. Writes profile-scoped state and mutates the process-global registry. REQUIRES APPROVAL.\n- **plugin_rollback**(plugin_id, version) [capability_expand category: system]: Rollback a profile-scoped plugin to a recorded source version and reload it. REQUIRES APPROVAL.\n- **plugin_reload**(plugin_id, version_label) [capability_expand category: system]: Hot-reload a plugin at runtime. Disposes the old plugin fiber, re-imports its module, and registers a fresh instance. Existing in-flight turns are unaffected (snapshot isolation). REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_disable**(plugin_id) [capability_expand category: system]: Disable a plugin by disposing its fiber, removing its tools from the runtime registry. Cannot disable self_management itself. REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_remove**(plugin_id, delete_source) [capability_expand category: system]: Terminally remove a plugin: dispose its fiber, unregister its tools, remove reload metadata, and optionally delete its profile-scoped source file. Cannot remove self_management itself. REQUIRES APPROVAL.\n- **plugin_unquarantine**(plugin_id) [capability_expand category: plugin_management]: Restore a quarantined plugin to probation status for re-evaluation. Unfreezes the trust ledger, transitions the proposal back to PROBATION, and reloads the plugin. REQUIRES APPROVAL.\n- **plugin_enable**(plugin_id) [capability_expand category: system]: Re-enable a previously disabled plugin by reloading its module and registering a fresh instance. REQUIRES APPROVAL.\n- **self_describe**(facet): Answer questions about LeapFlow ITSELF from live runtime evidence — its identity/version/model, what tools and plugins it has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of self-awareness. Prefer this over reading LeapFlow's own source code. Pick facet=capabilities for 'what can you do / do you support X', identity for version/model, runtime for context/posture, or all for a full self-check when explicitly requested.\n- **runtime_snapshot**(): Lightweight (~150 token) flat snapshot of current runtime state: model, context budget, posture, disclosure level, turn count, cache hit rate, uptime, tool count, and pending approvals.\n- **schedule_create**(trigger_expression, instruction, execution_mode, max_retries, delivery_target) [capability_expand category: scheduler]: Create a new scheduled task. Specify a trigger expression (e.g. '30m', 'every 2h', '0 9 * * *') and the instruction to execute on each trigger. Optionally provide an execution mode and a delivery target to receive notifications.\n- **schedule_list**() [capability_expand category: scheduler]: List all scheduled tasks with their current state, trigger, and next due time.\n- **schedule_status**(task_id) [capability_expand category: scheduler]: Get detailed status and recent execution history for a scheduled task.\n- **schedule_pause**(task_id) [capability_expand category: scheduler]: Pause a scheduled task so it stops firing without being cancelled.\n- **schedule_resume**(task_id) [capability_expand category: scheduler]: Resume a paused scheduled task — re-arms it and recalculates next due time.\n- **schedule_cancel**(task_id) [capability_expand category: scheduler]: Cancel a scheduled task permanently.\n- **tool_search**(query, max_results): Search all registered tools by keyword relevance. Returns a ranked list of matching tools with name, category, and summary. Use this when you need a tool but are unsure of its exact name or category.\n- **tool_describe**(tool_name): Get the full callable schema of a single tool by exact name. Returns the tool's description, parameters, and metadata. Use after tool_search to inspect a specific tool before calling it.\n- **hw_list**(): List connected hardware devices with their channel counts and measured quantities. Start here; it does not include operating limits.\n- **hw_describe**(device_id): Return the full reference for one device: every channel, its unit, its operating envelope, rate limit, reversibility, and required interlocks, plus any prior outcomes recorded for its writable channels. Required before commanding a device.\n- **hw_read**(device_id, channel_id): Read the current value of one device channel. Has no physical effect.\n- **hw_status**(device_id): Report connection health, halt capability, and recent notable events (threshold excursions, lost samples, stalled channels) for one device.\n- **hw_configure**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Set a configuration value or setpoint on a channel declaring effect=configure. Setpoints often have inertia: the value may need time to stabilise.\n- **hw_actuate**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Command motion or output on a channel declaring effect=actuate. This moves physical hardware; a repeat from an unknown state is not a safe retry.\n- **hw_dispense**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Consume an irreversible resource on a channel declaring effect=dispense. Running this twice dispenses twice; never repeat it after a failure without first verifying what already happened.\n- **hw_estop**(device_id) [capability_expand category: hardware]: Stop all motion and output on a device immediately. Never requires approval; use it whenever device behaviour is unexpected.\n- **hw_batch_actuate**(device_id, commands, dry_run, verify) [capability_expand category: hardware]: Write multiple actuator channels in one atomic transaction. Used for coordinated multi-joint control where all positions must be commanded simultaneously (e.g. robot arm joint-space control).\n- **hw_control_start**(device_id, mode, targets, frequency_hz, trajectory, policy, gains) [capability_expand category: hardware]: Start a real-time control loop on a robot device. Supports PID position tracking, impedance control, trajectory following, and neural policy chains.\n- **hw_control_stop**(device_id) [capability_expand category: hardware]: Stop the real-time control loop on a device. Gracefully halts the device and returns session statistics.\n- **hw_control_status**(device_id) [capability_expand category: hardware]: Get the status and statistics of a running control loop. Returns frequency, jitter, overruns, and policy state.\n- **hw_policy_infer**(device_id, policy, task, execute, chunk_size, verify, compute_budget) [capability_expand category: hardware]: Execute VLA policy inference on a robot device. Reads observation, runs policy, optionally writes action to actuators. Supports local (HuggingFace) and remote (PolicyServer) policies.\n- **hw_policy_episode**(action, device_id, policy, task) [capability_expand category: hardware]: Manage policy inference episode lifecycle. Start an episode to begin a task, stop to finalise and get stats.\n- **hw_policy_list**(source) [capability_expand category: hardware]: List available VLA policies and their capabilities.\n- **hw_orchestrate**(mode, operations, barrier_timeout_s, label) [capability_expand category: hardware]: Execute coordinated operations across multiple hardware devices. Supports sequential (one-at-a-time), parallel (concurrent), and barrier (synchronised) execution modes. Any failure in a parallel group triggers halt_all on every participating device for safety.\n- **session_search**(query, limit) [capability_expand category: unclassified]: Search past conversation sessions in the current workspace for relevant context.\n- **session_list**(limit): List recent conversation sessions in the current workspace with stable ids, titles, dates, and summaries. Use for browsing past tasks or when user asks to see history without specific search terms. No keywords needed — returns chronological list.\n- **session_detail**(session_id, limit, offset, include_inactive) [capability_expand category: unclassified]: Read a paginated persisted transcript for one past conversation session in the current workspace. Use after session_list or session_search returns a session_id.\n- **click**(element_index) [capability_expand category: desktop]: Click a UI element by its element_index (from the latest observe_ui snapshot)\n- **get_clipboard**() [capability_expand category: desktop]: Read current clipboard text content\n- **list_apps**(filter, running_only) [capability_expand category: desktop]: List available applications on this system. Use to discover correct bundle_id before switch_app.\n- **list_windows**() [capability_expand category: desktop]: List all top-level windows with pid, window_id, title, and per-window state (minimized, on-screen). Call this first to pick the pid and window_id that observe_ui and other window tools require.\n- **observe_ui**(pid, window_id, query) [capability_expand category: desktop]: Snapshot one window's actionable UI elements, each tagged with an element_index for click/right_click/read_text. Re-observe after actions — indices belong to one snapshot. Requires the window's pid and window_id from list_windows.\n- **open_url**(url, app_id) [capability_expand category: desktop]: Open a URL in the default or specified browser\n- **read_text**(element_index) [capability_expand category: desktop]: Read the text content of a specific UI element from the latest snapshot\n- **right_click**(element_index) [capability_expand category: desktop]: Right-click a UI element to open its context menu. Returns visible menu items.\n- **screenshot**(pid, window_id) [capability_expand category: desktop]: Capture a screenshot for visual verification. With pid + window_id captures that window (works across all displays); defaults to the last observed window, or the full desktop when no window has been observed.\n- **scroll**(element_index, direction, amount, pid, window_id) [capability_expand category: desktop]: Scroll a scrollable area of a window. Omit element_index to scroll the window's focused/page scroller; pass one to scroll an exact element from the latest snapshot.\n- **select_text**(element_index) [capability_expand category: desktop]: Select all text in a UI element (focus + select-all, for subsequent copy)\n- **set_clipboard**(text) [capability_expand category: desktop]: Write text to the clipboard\n- **shortcut**(keys) [capability_expand category: desktop]: Execute a keyboard shortcut\n- **switch_app**(app_id) [capability_expand category: desktop]: Switch to an app (launch if needed, activate, verify)\n- **type_text**(text) [capability_expand category: desktop]: Type text into the currently focused element\n- **wait**(seconds) [capability_expand category: desktop]: Wait for a specified duration before continuing\n- **wait_until**(condition, pid, window_id, timeout, poll_interval) [capability_expand category: desktop]: Wait until a UI condition is met (polls UI tree). Returns elements when found or on timeout.\n- **wait_until_stable**(timeout, poll_interval, pid, window_id) [capability_expand category: desktop]: Wait until the UI stops changing (element set stabilizes across polls).\n\n## App Connector Capability Index\nLeapFlow can onboard and manage external apps through `platform_connect` and execute exact registered business actions through `platform_action`.\nFor requests about connecting, setting up, configuring, enabling, or managing a supported app, use `platform_connect` first instead of generating SDK/Webhook sample code.\n`platform_connect.action` is the management namespace: list, guide, preflight, connect, disconnect, remove, status, events_start, events_stop, events_status.\n`platform_action.action` is only for exact registered platform business actions listed below, such as `im.send_message`; never use management actions like `list` or `guide` there.\nAll business fields MUST go inside `payload`; top-level keys are only `platform`, `action`, and `payload`.\nDo not invent platform IDs or platform action names. If the needed action is not listed, ask for discovery/clarification instead of guessing.\nUse `platform_connect` with `action='guide'` and the matching `platform` to start onboarding; use `action='list'` when the app is unclear.\nWhen a pending onboarding state is present, continue from that state with `platform_connect` instead of asking the user to restate the app.\nSupported apps:\n- `api_server`: API Server (OpenAI Compatible) (category=api; backend=adapter; platform_action actions=none registered)\n- `dingtalk`: 钉钉 (DingTalk) (category=im; backend=adapter; platform_action actions=none registered)\n- `feishu`: 飞书 (Feishu/Lark) (category=im; backend=cli)\n - `calendar.create_event` payload={summary*, start_time*, end_time*, attendees} [write/high]\n - `docs.create_markdown` payload={title*, markdown*, folder_token} [write/medium]\n - `drive.search` payload={query*, limit} [read/medium]\n - `im.add_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.download_resource` payload={message_id*, file_key*, type*} [read/low]\n - `im.get_messages` payload={message_ids*} [read/low]\n - `im.list_chats` payload={page_size} [read/low]\n - `im.list_messages` payload={chat_id*} [read/low]\n - `im.list_thread_messages` payload={thread*} [read/low]\n - `im.remove_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.reply_message` payload={message_id*, text*} [send/high]\n - `im.search_chats` payload={query*, page_size} [read/low]\n - `im.search_messages` payload={query*} [read/medium]\n - `im.send_message` payload={chat_id*, text*, thread_id} [send/high]\n - `im.update_card` payload={token*, card*} [write/medium]\n - `im.update_message` payload={message_id*, text*} [write/medium]\n - `mail.search_unread` payload={query, limit} [read/high]\n - `sheets.append_row` payload={spreadsheet_token*, sheet_id*, values*} [write/medium]\n - `task.create` payload={title*, description, due_time} [write/medium]\n- `telegram`: Telegram (category=social; backend=adapter; platform_action actions=none registered)\n- `webhook`: Webhook (Generic) (category=webhook; backend=adapter; platform_action actions=none registered)\n\n## Tool Usage\nTools are normally invoked through the native function-calling mechanism, not by writing JSON in your reply\ntext. Only if the provider signals that native function calling is unavailable for this turn, fall back to a\nsingle JSON code block: `{\"name\": \"tool_name\", \"arguments\": {\"key\": \"value\"}}` — use this fallback format\nonly, never both. Only call a tool whose exact name appears in the tool index above and is currently callable\n(or reachable via `capability_expand`). Never invent, rename, alias, or guess a tool name, platform ID, or\nplatform action from argument shape or wording — if the index does not list it, it does not exist.\n`platform_connect.action` (list/guide/preflight/connect/disconnect/remove/status/events_start/events_stop/\nevents_status) is the App Connector management namespace; `platform_action.action` only accepts exact\nregistered business actions such as `im.send_message` shown in the App Connector Capability Index — never mix\nthe two namespaces. If a tool call returns an unknown/unavailable result, use the returned suggestions or\navailable names for a single retry instead of trying further variations of the same guess.\n\n**Side-effect action rule** (`platform_action` with effect=send/write/execute):\n- Call each unique action+payload **exactly once**. Never include duplicates in the same turn.\n- Once the result returns `\"completed\": true`, that action is DONE for this task. Do NOT call it again\n in any subsequent turn — immediately summarize the result for the user instead.\n- The system enforces idempotency: duplicate calls are blocked and will not execute.\n- If the user explicitly requests sending/writing multiple times, use distinct payloads per call.\n\n**Resource identifier provenance rule**:\n- NEVER fabricate, guess, or infer resource identifiers (chat_id, message_id, file_key, user_id, etc.).\n Every resource ID used in a side-effect action MUST come from a successful API response in this session.\n- If a read/list action fails (e.g. authorization error), you do NOT have valid resource IDs.\n Report the failure to the user — do NOT attempt the dependent write/send action with a guessed ID.\n- When a tool result contains `\"llm_instruction\"`, follow it exactly.\n\n## Guidelines\n1. **Direct answers first**: If you already know the answer, respond directly without tools.\n2. **Avoid redundant tool calls**: Do not call the same tool with the same arguments more than once in the same user turn. When an existing tool result already answers the user's request, stop calling tools and answer directly.\n3. **Use tools proactively**: When the user asks about files, time, system state, or needs actions performed, use the appropriate tool.\n4. **Chain tools when needed**: You can call multiple tools in sequence (e.g., list files → read file → summarize).\n5. **Handle failures gracefully**: If a tool fails, explain what went wrong and suggest alternatives. If it failed because of an unknown tool/platform/action name, retry once with an exact name from the returned suggestions, then explain rather than keep guessing.\n6. **Summarize results naturally**: After tool execution, synthesize the results into a helpful answer rather than dumping raw output.\n7. **Stay conversational**: Maintain a natural, helpful tone. Acknowledge context from earlier in the conversation.\n8. **Recall past work**:\n - Broad queries (\"之前做了什么\", \"列出任务\"): answer from the \"Recent Task History\" section already in your context. If insufficient, call session_list.\n - Specific lookups (\"上次那个配置怎么改的\"): call session_search with relevant phrases (NOT single characters).\n - Do NOT call search tools repeatedly with keyword fragments. One well-phrased call is better than ten fragmented ones.\n\n## Coding & Verification\nWhen working with code, prefer the precise built-in tools over ad-hoc shell: use `repo_map` to orient in an\nunfamiliar project, `code_search`/`file_find` to locate and `code_intel` for symbols, `edit_file` (anchored\nsearch-replace, or a unified `diff`) to change files — never rewrite a whole file to change a few lines — and\n`git_query` to inspect diffs/log. After edits, check `syntax_ok` in the result and run `test_run`/`lint_check`\nbefore declaring the work done. Batch independent read-only calls (search/read/query) into a single turn — they\nrun in parallel.\n\n## Presentation Style\n1. **Polished Markdown only**: Format user-facing answers with clean Markdown headings, short paragraphs, and concise bullets. Use tables only when they improve comparison or scanning.\n2. **Terminal-friendly layout**: Keep lines readable in a TUI; avoid dense walls of text, deeply nested lists, oversized ASCII art, or heavy visual blocks.\n3. **Elegant emphasis**: Use bold text sparingly for key terms and conclusions. Avoid excessive emojis, decorative symbols, repeated separators, or visual noise.\n4. **Theme-safe colors**: Do not emit ANSI escape codes, HTML color tags, Rich markup, or hardcoded color names. Rely on the TUI theme to render Markdown professionally.\n5. **No leaked tool protocol**: Never show tool-call JSON, internal schemas, raw observations, tool result payloads, or hidden reasoning in the final answer unless the user explicitly asks for raw/debug output. Treat any prior `{\"name\": ..., \"arguments\": ...}` blocks and `Tool result (...)` messages as internal execution context only.\n6. **Professional closure**: End with a concise conclusion or next step when helpful; avoid rambling after the useful answer is complete.\n\nWhen finished with all tool calls, respond normally without a JSON block, tool-call transcript, or process log.\n\n## Task Contract\n- Task ID: turn-1\n- Original user request: List the connected hardware devices, then describe bench_r8 in full.\n- Workspace root: \n- Allowed roots: \n- Treat relative project paths as relative to the workspace root; never infer `.` as the project root when a workspace root is provided.\n- Workspace boundary is enforced by tools: do not read, search, edit, or run commands against paths outside the allowed roots unless the user explicitly requests an external path and the tool/approval policy permits it.\n- LeapFlow workspace config is optional at `/.leapflow/config.yaml`; runtime config is loaded from `~/.leapflow/config/user.yaml` and `~/.leapflow/profiles//config/*.yaml`.\n- Preserve this task contract across summarization, compression, tool loops, and memory retrieval.\n" + }, + { + "role": "system", + "content": "\n## Recent Session Summary\n- [user] List the connected hardware devices, then describe bench_r8 in full.\n" + }, + { + "role": "user", + "content": "List the connected hardware devices, then describe bench_r8 in full.\nList the connected hardware devices, then describe bench_r8 in full." + }, + { + "role": "assistant", + "content": "", + "tool_calls": [ + "hw_list" + ] + }, + { + "role": "tool", + "content": "{\"ok\": true, \"devices\": [{\"device_id\": \"bench_r8\", \"display_name\": \"R8 Simulated Bench\", \"location\": \"journey-lab\", \"channels\": 3, \"writable\": 2, \"streaming\": 1, \"quantities\": [\"state.homed\", \"temperature\"], \"verified\": true, \"halt_supported\": true}], \"count\": 1, \"hint\": \"Call hw_describe(device_id) for channel limits before commanding a device.\", \"execution_id\": \"\", \"idempotency_key\": \"\", \"execution_policy\": \"read_only\", \"tool_call_id\": \"call_1\", \"execution_status\": \"completed\"}", + "tool_result": true + }, + { + "role": "assistant", + "content": "", + "tool_calls": [ + "hw_describe" + ] + }, + { + "role": "tool", + "content": "{\"ok\": true, \"kind\": \"\", \"truncated\": true, \"original_chars\": 846, \"budget_chars\": 819}", + "tool_result": true + } + ], + "tools": [ + "capability_expand", + "code_intel", + "code_search", + "config_get", + "config_list", + "env_info", + "file_find", + "file_list", + "file_read", + "git_query", + "hw_describe", + "hw_list", + "hw_read", + "hw_status", + "lint_check", + "memory_search", + "plugin_list", + "plugin_propose", + "plugin_status", + "plugin_versions", + "repo_map", + "research_note", + "runtime_snapshot", + "schedule_reentry", + "self_describe", + "session_list", + "skill_view", + "skills_list", + "terminal_list", + "terminal_read", + "test_run", + "text_search", + "time_get", + "tool_describe", + "tool_search", + "web_fetch" + ] + }, + "responses": [ + { + "status": 200, + "content_type": "application/json", + "body": "{\"id\": \"chatcmpl-cassette\", \"object\": \"chat.completion\", \"model\": \"cassette-model\", \"choices\": [{\"index\": 0, \"message\": {\"role\": \"assistant\", \"content\": \"Discovered the simulated bench and read its channel limits.\"}, \"finish_reason\": \"stop\"}], \"usage\": {\"prompt_tokens\": 64, \"completion_tokens\": 16, \"total_tokens\": 80}}" + } + ] +} diff --git a/tests/_fixtures/cassettes/r8_hardware/cassette-model-8a3bb4f6cbc14b09.cassette.json b/tests/_fixtures/cassettes/r8_hardware/cassette-model-8a3bb4f6cbc14b09.cassette.json new file mode 100644 index 00000000..55614f7e --- /dev/null +++ b/tests/_fixtures/cassettes/r8_hardware/cassette-model-8a3bb4f6cbc14b09.cassette.json @@ -0,0 +1,87 @@ +{ + "fingerprint": "8a3bb4f6cbc14b0999a72f425b98bf5f69ea5f278e3abf5b629e2fb35ff00719", + "note": "captured in seed mode", + "request": { + "model": "cassette-model", + "stream": false, + "messages": [ + { + "role": "system", + "content": "You are LeapFlow, an intelligent assistant that can both converse naturally and take real actions on the user's computer.\n\n## Capabilities\nThe tool index below lists **every** registered tool by name and a one-line summary — this is the complete\ncapability contract; nothing else exists. Only a subset is directly callable this turn (via native tool calling,\nnot a JSON block in your reply). If you need a tool from the index that is not yet callable, call\n`capability_expand` with its category name first — the matching tools become callable immediately after.\nWhen the user asks about LeapFlow **itself** — its identity/version/model, what tools, plugins, or capabilities\nit has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of\nself-awareness — answer from live runtime evidence via `self_describe` (facet=capabilities for \"what can you\ndo / do you support X\", identity for version/model, runtime for context/posture, or `all` for a full self-check),\nnever by reading LeapFlow's own source code, documentation, or memory. Report configuration-dependent or\nunavailable capabilities as limitations rather than inferring them.\n- **text_search**(text, pattern): Search for a regex pattern in text.\n- **text_replace**(text, old, new, count) [capability_expand category: write]: Replace occurrences of a substring in text.\n- **time_get**(): Get current date and time.\n- **env_info**(): Get system environment information (OS, Python version, cwd).\n- **skills_list**(query, category, source): List available learned skills. Use when user asks about capabilities or you need a specific skill.\n- **skill_view**(name): View the full content of a specific skill document.\n- **skill_list_files**(name) [capability_expand category: skills]: List all support files in a skill directory (configs, templates, examples). Use before skill_file_read to discover available files.\n- **skill_file_read**(name, file_path) [capability_expand category: skills]: Read an individual support file from a skill directory. Use skill_list_files first to discover available files.\n- **code_intel**(path, operation): Precise document symbols (outline) for a source file: classes, functions, and methods with line ranges. Python uses an exact AST parse; other languages use a keyword-prefix scan. Prefer over file_read mode=symbols for accurate navigation before editing. Read-only.\n- **repo_map**(path): Compact project orientation for a repository root: languages, detected test/lint commands, top-level structure, entry points, manifest, and VCS branch. Call this first when entering an unfamiliar codebase. Read-only.\n- **scm_sync**(action, cwd, remote, pull_ref, push_ref, timeout) [capability_expand category: scm]: Run a typed git SCM action. Use this instead of shell_run for git pull/push/status. For 'pull origin main then push', set action='pull_then_push', remote='origin', pull_ref='main', and omit push_ref so LeapFlow pushes the current local branch.\n- **git_query**(action, cwd, ref, path, staged, max_count, stat): Read-only structured git inspection: action=diff|log|status|branch|show. Prefer over shell_run for reading repo state — output is clipped, redacted, and log/branch are parsed into structured fields. Use scm_sync for pull/push.\n- **git_write**(action, cwd, message, stage_all, name, ref, create) [capability_expand category: scm]: Mutating git actions: action=commit (message, stage_all), branch (create+switch), checkout (switch; create=true for -b). Approval-gated. Use scm_sync for pull/push and git_query for reads.\n- **test_run**(command, cwd, timeout): Run the project's test suite and return structured results (framework, passed/failed counts, failing tests). Auto-detects the runner (pytest/npm/go/cargo) or uses a configured/explicit command; executes via the governed shell. ok=true means the runner executed — see 'success' for pass/fail.\n- **lint_check**(command, cwd, timeout): Run the project's linter and return a structured clean/issue result. Auto-detects the linter (ruff/eslint/go vet/clippy) or uses a configured/explicit command; executes via the governed shell. ok=true means the linter ran — see 'clean'.\n- **file_list**(path, pattern, depth): List files and directories at a given path. Use depth=1 or depth=2 to get a recursive tree in one call instead of listing each sub-directory separately.\n- **file_read**(path, max_lines, start_line, max_chars, mode): Read text file content with adaptive context governance. For large or unfamiliar files, prefer mode='outline' or mode='symbols' first, then use mode='raw' with start_line/max_lines for the specific range you actually need. For LeapFlow's own settings, use config_list / config_get / config_set — its config files are outside the workspace and not readable here.\n- **file_write**(path, content, mode) [capability_expand category: write]: Write content to a file (overwrite or append).\n- **code_search**(pattern, patterns, path, glob, ignore_case, multiline, max_results, context_lines): Search file CONTENTS by regex pattern across a directory tree (ripgrep-backed). Requires a regex pattern. NOT for listing or browsing directory contents — use file_list for that. Prefer this over shell_run grep: faster, skips VCS/dependency/build dirs, and returns structured path:line:column matches. Batch related lookups into ONE call via `patterns` (OR-combined, single pass) instead of issuing several separate searches. Use file_read for the surrounding context of a hit.\n- **file_find**(glob, path, max_results): Find files by a recursive glob pattern under a base path (e.g. '**/test_*.py' or '*.md'). Prefer this over shell_run find; skips VCS/dependency/build dirs.\n- **edit_file**(path, edits, dry_run, diff) [capability_expand category: write]: Apply targeted, anchored search-replace edits to an EXISTING text file (use file_write to create/overwrite). Each edit is {original_text, new_text, replace_all?}; original_text must match exactly and uniquely (or set replace_all) — a non-unique or missing anchor is rejected so files are never corrupted. Set dry_run to preview. Alternatively pass a unified 'diff' to apply its hunks as anchored edits. Far cheaper and safer than rewriting a whole file.\n- **shell_run**(command, cwd, timeout) [capability_expand category: shell]: Execute a one-shot shell command with timeout protection. Runs in the active workspace; paths resolving outside it are refused. Reach for a structured tool first when one fits — web_fetch for anything over HTTP(S), git_query/scm_sync for git, code_search/file_find/file_read for the repo, config_get/config_set for LeapFlow's own settings — because those report typed results, while a failed shell command can only be diagnosed from its exit code and stderr. Every shell run counts as an external side effect, so a failure stops the rest of the batch and is not retried automatically.\n- **terminal_open**(command, cwd, shell) [capability_expand category: terminal]: Open a PERSISTENT shell session (REPL/dev server/watch), returning a session_id for terminal_send/read/close. Disabled unless tools.terminal_session_enabled is set. For one-shot commands use shell_run instead.\n- **terminal_send**(session_id, input, wait) [capability_expand category: terminal]: Send a line of input to a persistent terminal session and return output captured shortly after.\n- **terminal_read**(session_id, wait): Drain buffered output from a persistent terminal session (optionally waiting briefly first).\n- **terminal_close**(session_id) [capability_expand category: terminal]: Terminate a persistent terminal session and release its process group.\n- **terminal_list**(): List active persistent terminal sessions.\n- **config_list**(category, limit): List LeapFlow's own writable settings (LLM model and endpoint, daemon, memory, perception, gateway, …) with current values. Use this to discover the exact key before changing anything. Optionally narrow by `category`. This is the only correct way to inspect LeapFlow configuration — never read config files from disk.\n- **config_get**(key): Read one LeapFlow setting by key (e.g. 'llm.model', 'llm.base_url', 'daemon.log_level'), returning its current value, type, scopes, and whether a change needs a daemon restart. There is no 'llm.provider' key: provider behavior is inferred from the OpenAI-compatible 'llm.base_url'. Never read LeapFlow config files from disk — use this.\n- **config_set**(key, value, scope) [capability_expand category: config]: Change one LeapFlow setting by key, e.g. switch an OpenAI-compatible LLM with key='llm.model' and key='llm.base_url'. There is no 'llm.provider' key: provider behavior is inferred from the endpoint. Values are validated and coerced; credentials are stored in the vault automatically. Call config_list or config_get first if unsure of the key. The result states whether a `leap daemon restart` is required. Never edit LeapFlow config files directly.\n- **web_fetch**(url, select, timeout, max_bytes): Read a URL over HTTP(S) and get back extracted, context-sized content: parsed JSON for API endpoints, readable text plus links for web pages. Use this for anything on the internet — prices, docs, releases, articles — instead of running curl through shell_run: it reports real HTTP status codes, retries rate limits on its own, and is a plain read so a retry is always safe. For JSON APIs pass `select` with a dotted path (e.g. 'chart.result.0.meta') to return just that part instead of the whole payload.\n- **memory_search**(query, limit): Search agent memory for relevant past experiences, observations, and facts.\n- **memory_add**(content, kind) [capability_expand category: write]: Store a new observation or insight in memory for future reference.\n- **research_note**(kind, text): Record a compact, structured note about the current task's state so it survives context compression on long / multi-step tasks. Use for durable findings, open questions still to resolve, decisions / excluded paths, and the immediate next step. One concise sentence per note.\n- **capability_expand**(category): Fetch the full callable schema for every tool in a capability category (e.g. 'hub', 'gateway', 'desktop', 'delegate', 'file', 'memory', 'skill'). The compact tool index always lists every registered tool by name and a one-line summary, but only a static low-risk subset is directly callable each turn. If you need a tool from the index that is not yet callable, call capability_expand with its category first; the matching tools become callable in this turn. Never invent a tool name — expand the category instead.\n- **delegate_task**(goal, context) [capability_expand category: delegate]: Delegate a complex sub-task to an isolated subagent. The subagent gets a fresh context and restricted tool access. Use when a task is self-contained and can be solved independently.\n- **schedule_reentry**(kind, reason, delay_seconds, event_match, max_reentries, deadline_seconds): Register a re-entry so this task can resume later from its current orientation (findings / open questions / next step). Use when work must pause and continue after a delay (kind=time) or when a matching platform event arrives (kind=event), instead of finishing now. The research-ledger state is carried over automatically.\n- **hub_push**(skill_name, visibility, version) [capability_expand category: hub]: Push a local skill to the ModelScope Hub for sharing or backup.\n- **hub_pull**(repo_id, version) [capability_expand category: hub]: Pull a skill from the ModelScope Hub to install locally.\n- **hub_search**(query) [capability_expand category: hub]: Search for skills on the Hub by keyword or description.\n- **hub_sync**(mode, dry_run) [capability_expand category: hub]: Preview or execute sync between local skills and Hub.\n- **platform_action**(platform, action, payload, backend_kind) [capability_expand category: gateway]: Execute an exact registered business action on an external platform through LeapFlow's App Connector layer. Actions must be copied from the App Connector Capability Index and are addressed as domain.operation, e.g. im.send_message or docs.create_markdown. All business fields (chat_id, text, query, etc.) MUST be placed inside `payload`, never at the top level. Example: {\"platform\":\"feishu\",\"action\":\"im.send_message\",\"payload\":{\"chat_id\":\"oc_xxx\",\"text\":\"hello\"}}. Do not invent action names, do not use management actions such as list/guide/connect/status here.\n- **platform_connect**(action, platform, credentials, options, checkpoint) [capability_expand category: gateway]: List, guide, connect, disconnect, remove, or check status for external platforms using the App Connector management namespace. Supports REST and CLI backends. Use this for management actions such as list/guide/preflight/connect/status; use platform_action only for exact registered business actions.\n- **gateway_send**(platform, chat_id, text, thread_id) [capability_expand category: gateway]: Send a message to a connected external platform (Feishu group, Telegram chat, DingTalk conversation, etc.). Requires the platform to be connected via gateway_connect first. Use gateway_connect with action='list' to see connected platforms and available chat IDs.\n- **gateway_connect**(action, platform, credentials, options) [capability_expand category: gateway]: Connect, configure, or manage external platform integrations (Feishu, DingTalk, Telegram, Slack, Discord, etc.). Conversational flow: 1) call 'guide' to get setup steps + required fields, 2) present the steps to the user and ask for ALL required credentials in a single message, 3) call 'connect' with the credentials. Goal: complete in 1–2 user turns. NEVER include credential values in your text response.\n- **plugin_list**(): List the live plugin registry across Tool/Gateway/LLM subsystems for plugin management: which plugins are loaded, their category and fiber lifecycle state. For questions about LeapFlow's own capabilities or whether it supports X, use self_describe instead.\n- **plugin_status**(plugin_id): Get detailed status of a specific plugin: its declared category, runtime dependencies, contributed tools, and fiber lifecycle state.\n- **plugin_versions**(plugin_id): List recorded source versions and active pointer for a profile-scoped plugin.\n- **plugin_propose**(requested_capability, plugin_id, proposed_tools, test_cases, risk_level, evidence): Create a side-effect-free PluginProposal from explicit capability-gap evidence. Use this before plugin_generate when a missing capability should be reviewed. Does not call an LLM, write files, or install anything.\n- **assess_compatibility**(manifest, source_path) [capability_expand category: plugin_management]: Assess whether a foreign plugin manifest or real DSH source bundle is compatible with LeapFlow. A source bundle assessment is static: runtime_ready stays false until restricted Node discovery during plugin_install. Returns component-level verdicts and limitations.\n- **plugin_generate**(plugin_id, description, proposal_id) [capability_expand category: system]: Generate a new ToolPlugin from a natural-language capability description. The LLM produces code that conforms to the ToolPlugin Protocol; it is then rigorously validated (syntax, structure, import, protocol conformance). The generated source is stored in CAS and receives explicit content approval. It DOES NOT install the plugin — installation requires a second, mutation-specific approval via plugin_install.\n- **plugin_install**(plugin_id, code, marketplace_name, source_path, proposal_id, version_label) [capability_expand category: system]: Install a plugin from exactly one source: validated Python code, the configured Python marketplace, or a real DSH source bundle. DSH bundles run restricted Node discovery before registration; only public host tools are installed and client UI limitations are reported. Writes profile-scoped state and mutates the process-global registry. REQUIRES APPROVAL.\n- **plugin_rollback**(plugin_id, version) [capability_expand category: system]: Rollback a profile-scoped plugin to a recorded source version and reload it. REQUIRES APPROVAL.\n- **plugin_reload**(plugin_id, version_label) [capability_expand category: system]: Hot-reload a plugin at runtime. Disposes the old plugin fiber, re-imports its module, and registers a fresh instance. Existing in-flight turns are unaffected (snapshot isolation). REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_disable**(plugin_id) [capability_expand category: system]: Disable a plugin by disposing its fiber, removing its tools from the runtime registry. Cannot disable self_management itself. REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_remove**(plugin_id, delete_source) [capability_expand category: system]: Terminally remove a plugin: dispose its fiber, unregister its tools, remove reload metadata, and optionally delete its profile-scoped source file. Cannot remove self_management itself. REQUIRES APPROVAL.\n- **plugin_unquarantine**(plugin_id) [capability_expand category: plugin_management]: Restore a quarantined plugin to probation status for re-evaluation. Unfreezes the trust ledger, transitions the proposal back to PROBATION, and reloads the plugin. REQUIRES APPROVAL.\n- **plugin_enable**(plugin_id) [capability_expand category: system]: Re-enable a previously disabled plugin by reloading its module and registering a fresh instance. REQUIRES APPROVAL.\n- **self_describe**(facet): Answer questions about LeapFlow ITSELF from live runtime evidence — its identity/version/model, what tools and plugins it has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of self-awareness. Prefer this over reading LeapFlow's own source code. Pick facet=capabilities for 'what can you do / do you support X', identity for version/model, runtime for context/posture, or all for a full self-check when explicitly requested.\n- **runtime_snapshot**(): Lightweight (~150 token) flat snapshot of current runtime state: model, context budget, posture, disclosure level, turn count, cache hit rate, uptime, tool count, and pending approvals.\n- **schedule_create**(trigger_expression, instruction, execution_mode, max_retries, delivery_target) [capability_expand category: scheduler]: Create a new scheduled task. Specify a trigger expression (e.g. '30m', 'every 2h', '0 9 * * *') and the instruction to execute on each trigger. Optionally provide an execution mode and a delivery target to receive notifications.\n- **schedule_list**() [capability_expand category: scheduler]: List all scheduled tasks with their current state, trigger, and next due time.\n- **schedule_status**(task_id) [capability_expand category: scheduler]: Get detailed status and recent execution history for a scheduled task.\n- **schedule_pause**(task_id) [capability_expand category: scheduler]: Pause a scheduled task so it stops firing without being cancelled.\n- **schedule_resume**(task_id) [capability_expand category: scheduler]: Resume a paused scheduled task — re-arms it and recalculates next due time.\n- **schedule_cancel**(task_id) [capability_expand category: scheduler]: Cancel a scheduled task permanently.\n- **tool_search**(query, max_results): Search all registered tools by keyword relevance. Returns a ranked list of matching tools with name, category, and summary. Use this when you need a tool but are unsure of its exact name or category.\n- **tool_describe**(tool_name): Get the full callable schema of a single tool by exact name. Returns the tool's description, parameters, and metadata. Use after tool_search to inspect a specific tool before calling it.\n- **hw_list**(): List connected hardware devices with their channel counts and measured quantities. Start here; it does not include operating limits.\n- **hw_describe**(device_id): Return the full reference for one device: every channel, its unit, its operating envelope, rate limit, reversibility, and required interlocks, plus any prior outcomes recorded for its writable channels. Required before commanding a device.\n- **hw_read**(device_id, channel_id): Read the current value of one device channel. Has no physical effect.\n- **hw_status**(device_id): Report connection health, halt capability, and recent notable events (threshold excursions, lost samples, stalled channels) for one device.\n- **hw_configure**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Set a configuration value or setpoint on a channel declaring effect=configure. Setpoints often have inertia: the value may need time to stabilise.\n- **hw_actuate**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Command motion or output on a channel declaring effect=actuate. This moves physical hardware; a repeat from an unknown state is not a safe retry.\n- **hw_dispense**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Consume an irreversible resource on a channel declaring effect=dispense. Running this twice dispenses twice; never repeat it after a failure without first verifying what already happened.\n- **hw_estop**(device_id) [capability_expand category: hardware]: Stop all motion and output on a device immediately. Never requires approval; use it whenever device behaviour is unexpected.\n- **hw_batch_actuate**(device_id, commands, dry_run, verify) [capability_expand category: hardware]: Write multiple actuator channels in one atomic transaction. Used for coordinated multi-joint control where all positions must be commanded simultaneously (e.g. robot arm joint-space control).\n- **hw_control_start**(device_id, mode, targets, frequency_hz, trajectory, policy, gains) [capability_expand category: hardware]: Start a real-time control loop on a robot device. Supports PID position tracking, impedance control, trajectory following, and neural policy chains.\n- **hw_control_stop**(device_id) [capability_expand category: hardware]: Stop the real-time control loop on a device. Gracefully halts the device and returns session statistics.\n- **hw_control_status**(device_id) [capability_expand category: hardware]: Get the status and statistics of a running control loop. Returns frequency, jitter, overruns, and policy state.\n- **hw_policy_infer**(device_id, policy, task, execute, chunk_size, verify, compute_budget) [capability_expand category: hardware]: Execute VLA policy inference on a robot device. Reads observation, runs policy, optionally writes action to actuators. Supports local (HuggingFace) and remote (PolicyServer) policies.\n- **hw_policy_episode**(action, device_id, policy, task) [capability_expand category: hardware]: Manage policy inference episode lifecycle. Start an episode to begin a task, stop to finalise and get stats.\n- **hw_policy_list**(source) [capability_expand category: hardware]: List available VLA policies and their capabilities.\n- **hw_orchestrate**(mode, operations, barrier_timeout_s, label) [capability_expand category: hardware]: Execute coordinated operations across multiple hardware devices. Supports sequential (one-at-a-time), parallel (concurrent), and barrier (synchronised) execution modes. Any failure in a parallel group triggers halt_all on every participating device for safety.\n- **session_search**(query, limit) [capability_expand category: unclassified]: Search past conversation sessions in the current workspace for relevant context.\n- **session_list**(limit): List recent conversation sessions in the current workspace with stable ids, titles, dates, and summaries. Use for browsing past tasks or when user asks to see history without specific search terms. No keywords needed — returns chronological list.\n- **session_detail**(session_id, limit, offset, include_inactive) [capability_expand category: unclassified]: Read a paginated persisted transcript for one past conversation session in the current workspace. Use after session_list or session_search returns a session_id.\n- **click**(element_index) [capability_expand category: desktop]: Click a UI element by its element_index (from the latest observe_ui snapshot)\n- **get_clipboard**() [capability_expand category: desktop]: Read current clipboard text content\n- **list_apps**(filter, running_only) [capability_expand category: desktop]: List available applications on this system. Use to discover correct bundle_id before switch_app.\n- **list_windows**() [capability_expand category: desktop]: List all top-level windows with pid, window_id, title, and per-window state (minimized, on-screen). Call this first to pick the pid and window_id that observe_ui and other window tools require.\n- **observe_ui**(pid, window_id, query) [capability_expand category: desktop]: Snapshot one window's actionable UI elements, each tagged with an element_index for click/right_click/read_text. Re-observe after actions — indices belong to one snapshot. Requires the window's pid and window_id from list_windows.\n- **open_url**(url, app_id) [capability_expand category: desktop]: Open a URL in the default or specified browser\n- **read_text**(element_index) [capability_expand category: desktop]: Read the text content of a specific UI element from the latest snapshot\n- **right_click**(element_index) [capability_expand category: desktop]: Right-click a UI element to open its context menu. Returns visible menu items.\n- **screenshot**(pid, window_id) [capability_expand category: desktop]: Capture a screenshot for visual verification. With pid + window_id captures that window (works across all displays); defaults to the last observed window, or the full desktop when no window has been observed.\n- **scroll**(element_index, direction, amount, pid, window_id) [capability_expand category: desktop]: Scroll a scrollable area of a window. Omit element_index to scroll the window's focused/page scroller; pass one to scroll an exact element from the latest snapshot.\n- **select_text**(element_index) [capability_expand category: desktop]: Select all text in a UI element (focus + select-all, for subsequent copy)\n- **set_clipboard**(text) [capability_expand category: desktop]: Write text to the clipboard\n- **shortcut**(keys) [capability_expand category: desktop]: Execute a keyboard shortcut\n- **switch_app**(app_id) [capability_expand category: desktop]: Switch to an app (launch if needed, activate, verify)\n- **type_text**(text) [capability_expand category: desktop]: Type text into the currently focused element\n- **wait**(seconds) [capability_expand category: desktop]: Wait for a specified duration before continuing\n- **wait_until**(condition, pid, window_id, timeout, poll_interval) [capability_expand category: desktop]: Wait until a UI condition is met (polls UI tree). Returns elements when found or on timeout.\n- **wait_until_stable**(timeout, poll_interval, pid, window_id) [capability_expand category: desktop]: Wait until the UI stops changing (element set stabilizes across polls).\n\n## App Connector Capability Index\nLeapFlow can onboard and manage external apps through `platform_connect` and execute exact registered business actions through `platform_action`.\nFor requests about connecting, setting up, configuring, enabling, or managing a supported app, use `platform_connect` first instead of generating SDK/Webhook sample code.\n`platform_connect.action` is the management namespace: list, guide, preflight, connect, disconnect, remove, status, events_start, events_stop, events_status.\n`platform_action.action` is only for exact registered platform business actions listed below, such as `im.send_message`; never use management actions like `list` or `guide` there.\nAll business fields MUST go inside `payload`; top-level keys are only `platform`, `action`, and `payload`.\nDo not invent platform IDs or platform action names. If the needed action is not listed, ask for discovery/clarification instead of guessing.\nUse `platform_connect` with `action='guide'` and the matching `platform` to start onboarding; use `action='list'` when the app is unclear.\nWhen a pending onboarding state is present, continue from that state with `platform_connect` instead of asking the user to restate the app.\nSupported apps:\n- `api_server`: API Server (OpenAI Compatible) (category=api; backend=adapter; platform_action actions=none registered)\n- `dingtalk`: 钉钉 (DingTalk) (category=im; backend=adapter; platform_action actions=none registered)\n- `feishu`: 飞书 (Feishu/Lark) (category=im; backend=cli)\n - `calendar.create_event` payload={summary*, start_time*, end_time*, attendees} [write/high]\n - `docs.create_markdown` payload={title*, markdown*, folder_token} [write/medium]\n - `drive.search` payload={query*, limit} [read/medium]\n - `im.add_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.download_resource` payload={message_id*, file_key*, type*} [read/low]\n - `im.get_messages` payload={message_ids*} [read/low]\n - `im.list_chats` payload={page_size} [read/low]\n - `im.list_messages` payload={chat_id*} [read/low]\n - `im.list_thread_messages` payload={thread*} [read/low]\n - `im.remove_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.reply_message` payload={message_id*, text*} [send/high]\n - `im.search_chats` payload={query*, page_size} [read/low]\n - `im.search_messages` payload={query*} [read/medium]\n - `im.send_message` payload={chat_id*, text*, thread_id} [send/high]\n - `im.update_card` payload={token*, card*} [write/medium]\n - `im.update_message` payload={message_id*, text*} [write/medium]\n - `mail.search_unread` payload={query, limit} [read/high]\n - `sheets.append_row` payload={spreadsheet_token*, sheet_id*, values*} [write/medium]\n - `task.create` payload={title*, description, due_time} [write/medium]\n- `telegram`: Telegram (category=social; backend=adapter; platform_action actions=none registered)\n- `webhook`: Webhook (Generic) (category=webhook; backend=adapter; platform_action actions=none registered)\n\n## Learned Skills\nYou have access to the following learned skills. Use `skills_list` to browse or `skill_view` to read details:\n- api_designer: REST and GraphQL API design with OpenAPI spec generation and best practices [API, REST, GraphQL]\n- code_review: Structured code review for security, performance, and style [code, review, security]\n- data_analysis: CSV/JSON data analysis with statistical summaries, quality checks, and visualiza [data, analysis, statistics]\n- devops_helper: CI/CD pipeline configuration, Docker orchestration, and deployment strategy [devops, CI/CD, Docker]\n- document_writer: Structured document generation: technical docs, API references, reports, and REA [documentation, technical-writing, README]\n- git_workflow: Git operation orchestration: branching, commits, conflicts, and PR workflow [git, version-control, branching]\n- i18n_helper: Internationalization workflow: string extraction, translation, locale management [i18n, internationalization, localization]\n- project_analysis: Project structure analysis, dependency audit, and tech stack identification [project, analysis, dependencies]\n- security_audit: Security vulnerability scanning strategy, threat modeling, and fix recommendatio [security, vulnerability, audit]\n- shell_automation: Natural language to shell commands with safety review and step-by-step execution [shell, terminal, automation]\n- test_writer: Generate unit and integration tests from source code with edge-case coverage [testing, unit-test, integration-test]\n- web_research: Multi-step web search with cross-verification and structured synthesis [web, search, research]\n\n## Tool Usage\nTools are normally invoked through the native function-calling mechanism, not by writing JSON in your reply\ntext. Only if the provider signals that native function calling is unavailable for this turn, fall back to a\nsingle JSON code block: `{\"name\": \"tool_name\", \"arguments\": {\"key\": \"value\"}}` — use this fallback format\nonly, never both. Only call a tool whose exact name appears in the tool index above and is currently callable\n(or reachable via `capability_expand`). Never invent, rename, alias, or guess a tool name, platform ID, or\nplatform action from argument shape or wording — if the index does not list it, it does not exist.\n`platform_connect.action` (list/guide/preflight/connect/disconnect/remove/status/events_start/events_stop/\nevents_status) is the App Connector management namespace; `platform_action.action` only accepts exact\nregistered business actions such as `im.send_message` shown in the App Connector Capability Index — never mix\nthe two namespaces. If a tool call returns an unknown/unavailable result, use the returned suggestions or\navailable names for a single retry instead of trying further variations of the same guess.\n\n**Side-effect action rule** (`platform_action` with effect=send/write/execute):\n- Call each unique action+payload **exactly once**. Never include duplicates in the same turn.\n- Once the result returns `\"completed\": true`, that action is DONE for this task. Do NOT call it again\n in any subsequent turn — immediately summarize the result for the user instead.\n- The system enforces idempotency: duplicate calls are blocked and will not execute.\n- If the user explicitly requests sending/writing multiple times, use distinct payloads per call.\n\n**Resource identifier provenance rule**:\n- NEVER fabricate, guess, or infer resource identifiers (chat_id, message_id, file_key, user_id, etc.).\n Every resource ID used in a side-effect action MUST come from a successful API response in this session.\n- If a read/list action fails (e.g. authorization error), you do NOT have valid resource IDs.\n Report the failure to the user — do NOT attempt the dependent write/send action with a guessed ID.\n- When a tool result contains `\"llm_instruction\"`, follow it exactly.\n\n## Guidelines\n1. **Direct answers first**: If you already know the answer, respond directly without tools.\n2. **Avoid redundant tool calls**: Do not call the same tool with the same arguments more than once in the same user turn. When an existing tool result already answers the user's request, stop calling tools and answer directly.\n3. **Use tools proactively**: When the user asks about files, time, system state, or needs actions performed, use the appropriate tool.\n4. **Chain tools when needed**: You can call multiple tools in sequence (e.g., list files → read file → summarize).\n5. **Handle failures gracefully**: If a tool fails, explain what went wrong and suggest alternatives. If it failed because of an unknown tool/platform/action name, retry once with an exact name from the returned suggestions, then explain rather than keep guessing.\n6. **Summarize results naturally**: After tool execution, synthesize the results into a helpful answer rather than dumping raw output.\n7. **Stay conversational**: Maintain a natural, helpful tone. Acknowledge context from earlier in the conversation.\n8. **Recall past work**:\n - Broad queries (\"之前做了什么\", \"列出任务\"): answer from the \"Recent Task History\" section already in your context. If insufficient, call session_list.\n - Specific lookups (\"上次那个配置怎么改的\"): call session_search with relevant phrases (NOT single characters).\n - Do NOT call search tools repeatedly with keyword fragments. One well-phrased call is better than ten fragmented ones.\n\n## Coding & Verification\nWhen working with code, prefer the precise built-in tools over ad-hoc shell: use `repo_map` to orient in an\nunfamiliar project, `code_search`/`file_find` to locate and `code_intel` for symbols, `edit_file` (anchored\nsearch-replace, or a unified `diff`) to change files — never rewrite a whole file to change a few lines — and\n`git_query` to inspect diffs/log. After edits, check `syntax_ok` in the result and run `test_run`/`lint_check`\nbefore declaring the work done. Batch independent read-only calls (search/read/query) into a single turn — they\nrun in parallel.\n\n## Presentation Style\n1. **Polished Markdown only**: Format user-facing answers with clean Markdown headings, short paragraphs, and concise bullets. Use tables only when they improve comparison or scanning.\n2. **Terminal-friendly layout**: Keep lines readable in a TUI; avoid dense walls of text, deeply nested lists, oversized ASCII art, or heavy visual blocks.\n3. **Elegant emphasis**: Use bold text sparingly for key terms and conclusions. Avoid excessive emojis, decorative symbols, repeated separators, or visual noise.\n4. **Theme-safe colors**: Do not emit ANSI escape codes, HTML color tags, Rich markup, or hardcoded color names. Rely on the TUI theme to render Markdown professionally.\n5. **No leaked tool protocol**: Never show tool-call JSON, internal schemas, raw observations, tool result payloads, or hidden reasoning in the final answer unless the user explicitly asks for raw/debug output. Treat any prior `{\"name\": ..., \"arguments\": ...}` blocks and `Tool result (...)` messages as internal execution context only.\n6. **Professional closure**: End with a concise conclusion or next step when helpful; avoid rambling after the useful answer is complete.\n\nWhen finished with all tool calls, respond normally without a JSON block, tool-call transcript, or process log.\n\n## Task Contract\n- Task ID: turn-4\n- Original user request: Configure the bench_r8 homed channel to true to complete homing.\n- Workspace root: \n- Allowed roots: \n- Treat relative project paths as relative to the workspace root; never infer `.` as the project root when a workspace root is provided.\n- Workspace boundary is enforced by tools: do not read, search, edit, or run commands against paths outside the allowed roots unless the user explicitly requests an external path and the tool/approval policy permits it.\n- LeapFlow workspace config is optional at `/.leapflow/config.yaml`; runtime config is loaded from `~/.leapflow/config/user.yaml` and `~/.leapflow/profiles//config/*.yaml`.\n- Preserve this task contract across summarization, compression, tool loops, and memory retrieval.\n" + }, + { + "role": "assistant", + "content": "Discovered the simulated bench and read its channel limits." + }, + { + "role": "user", + "content": "Read the current value of the setpoint channel on bench_r8." + }, + { + "role": "assistant", + "content": "[Called: hw_read]\nRead the setpoint channel." + }, + { + "role": "user", + "content": "Actuate the bench_r8 setpoint to 60.\nConfigure the bench_r8 homed channel to true to complete homing.\nConfigure the bench_r8 homed channel to true to complete homing." + } + ], + "tools": [ + "capability_expand", + "code_intel", + "code_search", + "config_get", + "config_list", + "env_info", + "file_find", + "file_list", + "file_read", + "git_query", + "hw_actuate", + "hw_batch_actuate", + "hw_configure", + "hw_control_start", + "hw_control_status", + "hw_control_stop", + "hw_describe", + "hw_dispense", + "hw_estop", + "hw_list", + "hw_orchestrate", + "hw_policy_episode", + "hw_policy_infer", + "hw_policy_list", + "hw_read", + "hw_status", + "lint_check", + "memory_search", + "plugin_list", + "plugin_propose", + "plugin_status", + "plugin_versions", + "repo_map", + "research_note", + "runtime_snapshot", + "schedule_reentry", + "self_describe", + "session_list", + "skill_view", + "skills_list", + "terminal_list", + "terminal_read", + "test_run", + "text_search", + "time_get", + "tool_describe", + "tool_search", + "web_fetch" + ] + }, + "responses": [ + { + "status": 200, + "content_type": "application/json", + "body": "{\"id\": \"chatcmpl-cassette\", \"object\": \"chat.completion\", \"model\": \"cassette-model\", \"choices\": [{\"index\": 0, \"message\": {\"role\": \"assistant\", \"content\": \"\", \"tool_calls\": [{\"id\": \"call_1\", \"type\": \"function\", \"function\": {\"name\": \"hw_configure\", \"arguments\": \"{\\\"device_id\\\": \\\"bench_r8\\\", \\\"channel_id\\\": \\\"homed\\\", \\\"value\\\": true}\"}}]}, \"finish_reason\": \"tool_calls\"}], \"usage\": {\"prompt_tokens\": 64, \"completion_tokens\": 16, \"total_tokens\": 80}}" + } + ] +} diff --git a/tests/_fixtures/cassettes/r8_hardware/cassette-model-8efd290c8b71b947.cassette.json b/tests/_fixtures/cassettes/r8_hardware/cassette-model-8efd290c8b71b947.cassette.json new file mode 100644 index 00000000..f49f85fe --- /dev/null +++ b/tests/_fixtures/cassettes/r8_hardware/cassette-model-8efd290c8b71b947.cassette.json @@ -0,0 +1,67 @@ +{ + "fingerprint": "8efd290c8b71b9475550340028f51d2be207c039f43aa8e7101fb917473e2100", + "note": "captured in seed mode", + "request": { + "model": "cassette-model", + "stream": false, + "messages": [ + { + "role": "system", + "content": "You are LeapFlow, an intelligent assistant that can both converse naturally and take real actions on the user's computer.\n\n## Capabilities\nThe tool index below lists **every** registered tool by name and a one-line summary — this is the complete\ncapability contract; nothing else exists. Only a subset is directly callable this turn (via native tool calling,\nnot a JSON block in your reply). If you need a tool from the index that is not yet callable, call\n`capability_expand` with its category name first — the matching tools become callable immediately after.\nWhen the user asks about LeapFlow **itself** — its identity/version/model, what tools, plugins, or capabilities\nit has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of\nself-awareness — answer from live runtime evidence via `self_describe` (facet=capabilities for \"what can you\ndo / do you support X\", identity for version/model, runtime for context/posture, or `all` for a full self-check),\nnever by reading LeapFlow's own source code, documentation, or memory. Report configuration-dependent or\nunavailable capabilities as limitations rather than inferring them.\n- **text_search**(text, pattern): Search for a regex pattern in text.\n- **text_replace**(text, old, new, count) [capability_expand category: write]: Replace occurrences of a substring in text.\n- **time_get**(): Get current date and time.\n- **env_info**(): Get system environment information (OS, Python version, cwd).\n- **skills_list**(query, category, source): List available learned skills. Use when user asks about capabilities or you need a specific skill.\n- **skill_view**(name): View the full content of a specific skill document.\n- **skill_list_files**(name) [capability_expand category: skills]: List all support files in a skill directory (configs, templates, examples). Use before skill_file_read to discover available files.\n- **skill_file_read**(name, file_path) [capability_expand category: skills]: Read an individual support file from a skill directory. Use skill_list_files first to discover available files.\n- **code_intel**(path, operation): Precise document symbols (outline) for a source file: classes, functions, and methods with line ranges. Python uses an exact AST parse; other languages use a keyword-prefix scan. Prefer over file_read mode=symbols for accurate navigation before editing. Read-only.\n- **repo_map**(path): Compact project orientation for a repository root: languages, detected test/lint commands, top-level structure, entry points, manifest, and VCS branch. Call this first when entering an unfamiliar codebase. Read-only.\n- **scm_sync**(action, cwd, remote, pull_ref, push_ref, timeout) [capability_expand category: scm]: Run a typed git SCM action. Use this instead of shell_run for git pull/push/status. For 'pull origin main then push', set action='pull_then_push', remote='origin', pull_ref='main', and omit push_ref so LeapFlow pushes the current local branch.\n- **git_query**(action, cwd, ref, path, staged, max_count, stat): Read-only structured git inspection: action=diff|log|status|branch|show. Prefer over shell_run for reading repo state — output is clipped, redacted, and log/branch are parsed into structured fields. Use scm_sync for pull/push.\n- **git_write**(action, cwd, message, stage_all, name, ref, create) [capability_expand category: scm]: Mutating git actions: action=commit (message, stage_all), branch (create+switch), checkout (switch; create=true for -b). Approval-gated. Use scm_sync for pull/push and git_query for reads.\n- **test_run**(command, cwd, timeout): Run the project's test suite and return structured results (framework, passed/failed counts, failing tests). Auto-detects the runner (pytest/npm/go/cargo) or uses a configured/explicit command; executes via the governed shell. ok=true means the runner executed — see 'success' for pass/fail.\n- **lint_check**(command, cwd, timeout): Run the project's linter and return a structured clean/issue result. Auto-detects the linter (ruff/eslint/go vet/clippy) or uses a configured/explicit command; executes via the governed shell. ok=true means the linter ran — see 'clean'.\n- **file_list**(path, pattern, depth): List files and directories at a given path. Use depth=1 or depth=2 to get a recursive tree in one call instead of listing each sub-directory separately.\n- **file_read**(path, max_lines, start_line, max_chars, mode): Read text file content with adaptive context governance. For large or unfamiliar files, prefer mode='outline' or mode='symbols' first, then use mode='raw' with start_line/max_lines for the specific range you actually need. For LeapFlow's own settings, use config_list / config_get / config_set — its config files are outside the workspace and not readable here.\n- **file_write**(path, content, mode) [capability_expand category: write]: Write content to a file (overwrite or append).\n- **code_search**(pattern, patterns, path, glob, ignore_case, multiline, max_results, context_lines): Search file CONTENTS by regex pattern across a directory tree (ripgrep-backed). Requires a regex pattern. NOT for listing or browsing directory contents — use file_list for that. Prefer this over shell_run grep: faster, skips VCS/dependency/build dirs, and returns structured path:line:column matches. Batch related lookups into ONE call via `patterns` (OR-combined, single pass) instead of issuing several separate searches. Use file_read for the surrounding context of a hit.\n- **file_find**(glob, path, max_results): Find files by a recursive glob pattern under a base path (e.g. '**/test_*.py' or '*.md'). Prefer this over shell_run find; skips VCS/dependency/build dirs.\n- **edit_file**(path, edits, dry_run, diff) [capability_expand category: write]: Apply targeted, anchored search-replace edits to an EXISTING text file (use file_write to create/overwrite). Each edit is {original_text, new_text, replace_all?}; original_text must match exactly and uniquely (or set replace_all) — a non-unique or missing anchor is rejected so files are never corrupted. Set dry_run to preview. Alternatively pass a unified 'diff' to apply its hunks as anchored edits. Far cheaper and safer than rewriting a whole file.\n- **shell_run**(command, cwd, timeout) [capability_expand category: shell]: Execute a one-shot shell command with timeout protection. Runs in the active workspace; paths resolving outside it are refused. Reach for a structured tool first when one fits — web_fetch for anything over HTTP(S), git_query/scm_sync for git, code_search/file_find/file_read for the repo, config_get/config_set for LeapFlow's own settings — because those report typed results, while a failed shell command can only be diagnosed from its exit code and stderr. Every shell run counts as an external side effect, so a failure stops the rest of the batch and is not retried automatically.\n- **terminal_open**(command, cwd, shell) [capability_expand category: terminal]: Open a PERSISTENT shell session (REPL/dev server/watch), returning a session_id for terminal_send/read/close. Disabled unless tools.terminal_session_enabled is set. For one-shot commands use shell_run instead.\n- **terminal_send**(session_id, input, wait) [capability_expand category: terminal]: Send a line of input to a persistent terminal session and return output captured shortly after.\n- **terminal_read**(session_id, wait): Drain buffered output from a persistent terminal session (optionally waiting briefly first).\n- **terminal_close**(session_id) [capability_expand category: terminal]: Terminate a persistent terminal session and release its process group.\n- **terminal_list**(): List active persistent terminal sessions.\n- **config_list**(category, limit): List LeapFlow's own writable settings (LLM model and endpoint, daemon, memory, perception, gateway, …) with current values. Use this to discover the exact key before changing anything. Optionally narrow by `category`. This is the only correct way to inspect LeapFlow configuration — never read config files from disk.\n- **config_get**(key): Read one LeapFlow setting by key (e.g. 'llm.model', 'llm.base_url', 'daemon.log_level'), returning its current value, type, scopes, and whether a change needs a daemon restart. There is no 'llm.provider' key: provider behavior is inferred from the OpenAI-compatible 'llm.base_url'. Never read LeapFlow config files from disk — use this.\n- **config_set**(key, value, scope) [capability_expand category: config]: Change one LeapFlow setting by key, e.g. switch an OpenAI-compatible LLM with key='llm.model' and key='llm.base_url'. There is no 'llm.provider' key: provider behavior is inferred from the endpoint. Values are validated and coerced; credentials are stored in the vault automatically. Call config_list or config_get first if unsure of the key. The result states whether a `leap daemon restart` is required. Never edit LeapFlow config files directly.\n- **web_fetch**(url, select, timeout, max_bytes): Read a URL over HTTP(S) and get back extracted, context-sized content: parsed JSON for API endpoints, readable text plus links for web pages. Use this for anything on the internet — prices, docs, releases, articles — instead of running curl through shell_run: it reports real HTTP status codes, retries rate limits on its own, and is a plain read so a retry is always safe. For JSON APIs pass `select` with a dotted path (e.g. 'chart.result.0.meta') to return just that part instead of the whole payload.\n- **memory_search**(query, limit): Search agent memory for relevant past experiences, observations, and facts.\n- **memory_add**(content, kind) [capability_expand category: write]: Store a new observation or insight in memory for future reference.\n- **research_note**(kind, text): Record a compact, structured note about the current task's state so it survives context compression on long / multi-step tasks. Use for durable findings, open questions still to resolve, decisions / excluded paths, and the immediate next step. One concise sentence per note.\n- **capability_expand**(category): Fetch the full callable schema for every tool in a capability category (e.g. 'hub', 'gateway', 'desktop', 'delegate', 'file', 'memory', 'skill'). The compact tool index always lists every registered tool by name and a one-line summary, but only a static low-risk subset is directly callable each turn. If you need a tool from the index that is not yet callable, call capability_expand with its category first; the matching tools become callable in this turn. Never invent a tool name — expand the category instead.\n- **delegate_task**(goal, context) [capability_expand category: delegate]: Delegate a complex sub-task to an isolated subagent. The subagent gets a fresh context and restricted tool access. Use when a task is self-contained and can be solved independently.\n- **schedule_reentry**(kind, reason, delay_seconds, event_match, max_reentries, deadline_seconds): Register a re-entry so this task can resume later from its current orientation (findings / open questions / next step). Use when work must pause and continue after a delay (kind=time) or when a matching platform event arrives (kind=event), instead of finishing now. The research-ledger state is carried over automatically.\n- **hub_push**(skill_name, visibility, version) [capability_expand category: hub]: Push a local skill to the ModelScope Hub for sharing or backup.\n- **hub_pull**(repo_id, version) [capability_expand category: hub]: Pull a skill from the ModelScope Hub to install locally.\n- **hub_search**(query) [capability_expand category: hub]: Search for skills on the Hub by keyword or description.\n- **hub_sync**(mode, dry_run) [capability_expand category: hub]: Preview or execute sync between local skills and Hub.\n- **platform_action**(platform, action, payload, backend_kind) [capability_expand category: gateway]: Execute an exact registered business action on an external platform through LeapFlow's App Connector layer. Actions must be copied from the App Connector Capability Index and are addressed as domain.operation, e.g. im.send_message or docs.create_markdown. All business fields (chat_id, text, query, etc.) MUST be placed inside `payload`, never at the top level. Example: {\"platform\":\"feishu\",\"action\":\"im.send_message\",\"payload\":{\"chat_id\":\"oc_xxx\",\"text\":\"hello\"}}. Do not invent action names, do not use management actions such as list/guide/connect/status here.\n- **platform_connect**(action, platform, credentials, options, checkpoint) [capability_expand category: gateway]: List, guide, connect, disconnect, remove, or check status for external platforms using the App Connector management namespace. Supports REST and CLI backends. Use this for management actions such as list/guide/preflight/connect/status; use platform_action only for exact registered business actions.\n- **gateway_send**(platform, chat_id, text, thread_id) [capability_expand category: gateway]: Send a message to a connected external platform (Feishu group, Telegram chat, DingTalk conversation, etc.). Requires the platform to be connected via gateway_connect first. Use gateway_connect with action='list' to see connected platforms and available chat IDs.\n- **gateway_connect**(action, platform, credentials, options) [capability_expand category: gateway]: Connect, configure, or manage external platform integrations (Feishu, DingTalk, Telegram, Slack, Discord, etc.). Conversational flow: 1) call 'guide' to get setup steps + required fields, 2) present the steps to the user and ask for ALL required credentials in a single message, 3) call 'connect' with the credentials. Goal: complete in 1–2 user turns. NEVER include credential values in your text response.\n- **plugin_list**(): List the live plugin registry across Tool/Gateway/LLM subsystems for plugin management: which plugins are loaded, their category and fiber lifecycle state. For questions about LeapFlow's own capabilities or whether it supports X, use self_describe instead.\n- **plugin_status**(plugin_id): Get detailed status of a specific plugin: its declared category, runtime dependencies, contributed tools, and fiber lifecycle state.\n- **plugin_versions**(plugin_id): List recorded source versions and active pointer for a profile-scoped plugin.\n- **plugin_propose**(requested_capability, plugin_id, proposed_tools, test_cases, risk_level, evidence): Create a side-effect-free PluginProposal from explicit capability-gap evidence. Use this before plugin_generate when a missing capability should be reviewed. Does not call an LLM, write files, or install anything.\n- **assess_compatibility**(manifest, source_path) [capability_expand category: plugin_management]: Assess whether a foreign plugin manifest or real DSH source bundle is compatible with LeapFlow. A source bundle assessment is static: runtime_ready stays false until restricted Node discovery during plugin_install. Returns component-level verdicts and limitations.\n- **plugin_generate**(plugin_id, description, proposal_id) [capability_expand category: system]: Generate a new ToolPlugin from a natural-language capability description. The LLM produces code that conforms to the ToolPlugin Protocol; it is then rigorously validated (syntax, structure, import, protocol conformance). The generated source is stored in CAS and receives explicit content approval. It DOES NOT install the plugin — installation requires a second, mutation-specific approval via plugin_install.\n- **plugin_install**(plugin_id, code, marketplace_name, source_path, proposal_id, version_label) [capability_expand category: system]: Install a plugin from exactly one source: validated Python code, the configured Python marketplace, or a real DSH source bundle. DSH bundles run restricted Node discovery before registration; only public host tools are installed and client UI limitations are reported. Writes profile-scoped state and mutates the process-global registry. REQUIRES APPROVAL.\n- **plugin_rollback**(plugin_id, version) [capability_expand category: system]: Rollback a profile-scoped plugin to a recorded source version and reload it. REQUIRES APPROVAL.\n- **plugin_reload**(plugin_id, version_label) [capability_expand category: system]: Hot-reload a plugin at runtime. Disposes the old plugin fiber, re-imports its module, and registers a fresh instance. Existing in-flight turns are unaffected (snapshot isolation). REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_disable**(plugin_id) [capability_expand category: system]: Disable a plugin by disposing its fiber, removing its tools from the runtime registry. Cannot disable self_management itself. REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_remove**(plugin_id, delete_source) [capability_expand category: system]: Terminally remove a plugin: dispose its fiber, unregister its tools, remove reload metadata, and optionally delete its profile-scoped source file. Cannot remove self_management itself. REQUIRES APPROVAL.\n- **plugin_unquarantine**(plugin_id) [capability_expand category: plugin_management]: Restore a quarantined plugin to probation status for re-evaluation. Unfreezes the trust ledger, transitions the proposal back to PROBATION, and reloads the plugin. REQUIRES APPROVAL.\n- **plugin_enable**(plugin_id) [capability_expand category: system]: Re-enable a previously disabled plugin by reloading its module and registering a fresh instance. REQUIRES APPROVAL.\n- **self_describe**(facet): Answer questions about LeapFlow ITSELF from live runtime evidence — its identity/version/model, what tools and plugins it has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of self-awareness. Prefer this over reading LeapFlow's own source code. Pick facet=capabilities for 'what can you do / do you support X', identity for version/model, runtime for context/posture, or all for a full self-check when explicitly requested.\n- **runtime_snapshot**(): Lightweight (~150 token) flat snapshot of current runtime state: model, context budget, posture, disclosure level, turn count, cache hit rate, uptime, tool count, and pending approvals.\n- **schedule_create**(trigger_expression, instruction, execution_mode, max_retries, delivery_target) [capability_expand category: scheduler]: Create a new scheduled task. Specify a trigger expression (e.g. '30m', 'every 2h', '0 9 * * *') and the instruction to execute on each trigger. Optionally provide an execution mode and a delivery target to receive notifications.\n- **schedule_list**() [capability_expand category: scheduler]: List all scheduled tasks with their current state, trigger, and next due time.\n- **schedule_status**(task_id) [capability_expand category: scheduler]: Get detailed status and recent execution history for a scheduled task.\n- **schedule_pause**(task_id) [capability_expand category: scheduler]: Pause a scheduled task so it stops firing without being cancelled.\n- **schedule_resume**(task_id) [capability_expand category: scheduler]: Resume a paused scheduled task — re-arms it and recalculates next due time.\n- **schedule_cancel**(task_id) [capability_expand category: scheduler]: Cancel a scheduled task permanently.\n- **tool_search**(query, max_results): Search all registered tools by keyword relevance. Returns a ranked list of matching tools with name, category, and summary. Use this when you need a tool but are unsure of its exact name or category.\n- **tool_describe**(tool_name): Get the full callable schema of a single tool by exact name. Returns the tool's description, parameters, and metadata. Use after tool_search to inspect a specific tool before calling it.\n- **hw_list**(): List connected hardware devices with their channel counts and measured quantities. Start here; it does not include operating limits.\n- **hw_describe**(device_id): Return the full reference for one device: every channel, its unit, its operating envelope, rate limit, reversibility, and required interlocks, plus any prior outcomes recorded for its writable channels. Required before commanding a device.\n- **hw_read**(device_id, channel_id): Read the current value of one device channel. Has no physical effect.\n- **hw_status**(device_id): Report connection health, halt capability, and recent notable events (threshold excursions, lost samples, stalled channels) for one device.\n- **hw_configure**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Set a configuration value or setpoint on a channel declaring effect=configure. Setpoints often have inertia: the value may need time to stabilise.\n- **hw_actuate**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Command motion or output on a channel declaring effect=actuate. This moves physical hardware; a repeat from an unknown state is not a safe retry.\n- **hw_dispense**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Consume an irreversible resource on a channel declaring effect=dispense. Running this twice dispenses twice; never repeat it after a failure without first verifying what already happened.\n- **hw_estop**(device_id) [capability_expand category: hardware]: Stop all motion and output on a device immediately. Never requires approval; use it whenever device behaviour is unexpected.\n- **hw_batch_actuate**(device_id, commands, dry_run, verify) [capability_expand category: hardware]: Write multiple actuator channels in one atomic transaction. Used for coordinated multi-joint control where all positions must be commanded simultaneously (e.g. robot arm joint-space control).\n- **hw_control_start**(device_id, mode, targets, frequency_hz, trajectory, policy, gains) [capability_expand category: hardware]: Start a real-time control loop on a robot device. Supports PID position tracking, impedance control, trajectory following, and neural policy chains.\n- **hw_control_stop**(device_id) [capability_expand category: hardware]: Stop the real-time control loop on a device. Gracefully halts the device and returns session statistics.\n- **hw_control_status**(device_id) [capability_expand category: hardware]: Get the status and statistics of a running control loop. Returns frequency, jitter, overruns, and policy state.\n- **hw_policy_infer**(device_id, policy, task, execute, chunk_size, verify, compute_budget) [capability_expand category: hardware]: Execute VLA policy inference on a robot device. Reads observation, runs policy, optionally writes action to actuators. Supports local (HuggingFace) and remote (PolicyServer) policies.\n- **hw_policy_episode**(action, device_id, policy, task) [capability_expand category: hardware]: Manage policy inference episode lifecycle. Start an episode to begin a task, stop to finalise and get stats.\n- **hw_policy_list**(source) [capability_expand category: hardware]: List available VLA policies and their capabilities.\n- **hw_orchestrate**(mode, operations, barrier_timeout_s, label) [capability_expand category: hardware]: Execute coordinated operations across multiple hardware devices. Supports sequential (one-at-a-time), parallel (concurrent), and barrier (synchronised) execution modes. Any failure in a parallel group triggers halt_all on every participating device for safety.\n- **session_search**(query, limit) [capability_expand category: unclassified]: Search past conversation sessions in the current workspace for relevant context.\n- **session_list**(limit): List recent conversation sessions in the current workspace with stable ids, titles, dates, and summaries. Use for browsing past tasks or when user asks to see history without specific search terms. No keywords needed — returns chronological list.\n- **session_detail**(session_id, limit, offset, include_inactive) [capability_expand category: unclassified]: Read a paginated persisted transcript for one past conversation session in the current workspace. Use after session_list or session_search returns a session_id.\n- **click**(element_index) [capability_expand category: desktop]: Click a UI element by its element_index (from the latest observe_ui snapshot)\n- **get_clipboard**() [capability_expand category: desktop]: Read current clipboard text content\n- **list_apps**(filter, running_only) [capability_expand category: desktop]: List available applications on this system. Use to discover correct bundle_id before switch_app.\n- **list_windows**() [capability_expand category: desktop]: List all top-level windows with pid, window_id, title, and per-window state (minimized, on-screen). Call this first to pick the pid and window_id that observe_ui and other window tools require.\n- **observe_ui**(pid, window_id, query) [capability_expand category: desktop]: Snapshot one window's actionable UI elements, each tagged with an element_index for click/right_click/read_text. Re-observe after actions — indices belong to one snapshot. Requires the window's pid and window_id from list_windows.\n- **open_url**(url, app_id) [capability_expand category: desktop]: Open a URL in the default or specified browser\n- **read_text**(element_index) [capability_expand category: desktop]: Read the text content of a specific UI element from the latest snapshot\n- **right_click**(element_index) [capability_expand category: desktop]: Right-click a UI element to open its context menu. Returns visible menu items.\n- **screenshot**(pid, window_id) [capability_expand category: desktop]: Capture a screenshot for visual verification. With pid + window_id captures that window (works across all displays); defaults to the last observed window, or the full desktop when no window has been observed.\n- **scroll**(element_index, direction, amount, pid, window_id) [capability_expand category: desktop]: Scroll a scrollable area of a window. Omit element_index to scroll the window's focused/page scroller; pass one to scroll an exact element from the latest snapshot.\n- **select_text**(element_index) [capability_expand category: desktop]: Select all text in a UI element (focus + select-all, for subsequent copy)\n- **set_clipboard**(text) [capability_expand category: desktop]: Write text to the clipboard\n- **shortcut**(keys) [capability_expand category: desktop]: Execute a keyboard shortcut\n- **switch_app**(app_id) [capability_expand category: desktop]: Switch to an app (launch if needed, activate, verify)\n- **type_text**(text) [capability_expand category: desktop]: Type text into the currently focused element\n- **wait**(seconds) [capability_expand category: desktop]: Wait for a specified duration before continuing\n- **wait_until**(condition, pid, window_id, timeout, poll_interval) [capability_expand category: desktop]: Wait until a UI condition is met (polls UI tree). Returns elements when found or on timeout.\n- **wait_until_stable**(timeout, poll_interval, pid, window_id) [capability_expand category: desktop]: Wait until the UI stops changing (element set stabilizes across polls).\n\n## App Connector Capability Index\nLeapFlow can onboard and manage external apps through `platform_connect` and execute exact registered business actions through `platform_action`.\nFor requests about connecting, setting up, configuring, enabling, or managing a supported app, use `platform_connect` first instead of generating SDK/Webhook sample code.\n`platform_connect.action` is the management namespace: list, guide, preflight, connect, disconnect, remove, status, events_start, events_stop, events_status.\n`platform_action.action` is only for exact registered platform business actions listed below, such as `im.send_message`; never use management actions like `list` or `guide` there.\nAll business fields MUST go inside `payload`; top-level keys are only `platform`, `action`, and `payload`.\nDo not invent platform IDs or platform action names. If the needed action is not listed, ask for discovery/clarification instead of guessing.\nUse `platform_connect` with `action='guide'` and the matching `platform` to start onboarding; use `action='list'` when the app is unclear.\nWhen a pending onboarding state is present, continue from that state with `platform_connect` instead of asking the user to restate the app.\nSupported apps:\n- `api_server`: API Server (OpenAI Compatible) (category=api; backend=adapter; platform_action actions=none registered)\n- `dingtalk`: 钉钉 (DingTalk) (category=im; backend=adapter; platform_action actions=none registered)\n- `feishu`: 飞书 (Feishu/Lark) (category=im; backend=cli)\n - `calendar.create_event` payload={summary*, start_time*, end_time*, attendees} [write/high]\n - `docs.create_markdown` payload={title*, markdown*, folder_token} [write/medium]\n - `drive.search` payload={query*, limit} [read/medium]\n - `im.add_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.download_resource` payload={message_id*, file_key*, type*} [read/low]\n - `im.get_messages` payload={message_ids*} [read/low]\n - `im.list_chats` payload={page_size} [read/low]\n - `im.list_messages` payload={chat_id*} [read/low]\n - `im.list_thread_messages` payload={thread*} [read/low]\n - `im.remove_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.reply_message` payload={message_id*, text*} [send/high]\n - `im.search_chats` payload={query*, page_size} [read/low]\n - `im.search_messages` payload={query*} [read/medium]\n - `im.send_message` payload={chat_id*, text*, thread_id} [send/high]\n - `im.update_card` payload={token*, card*} [write/medium]\n - `im.update_message` payload={message_id*, text*} [write/medium]\n - `mail.search_unread` payload={query, limit} [read/high]\n - `sheets.append_row` payload={spreadsheet_token*, sheet_id*, values*} [write/medium]\n - `task.create` payload={title*, description, due_time} [write/medium]\n- `telegram`: Telegram (category=social; backend=adapter; platform_action actions=none registered)\n- `webhook`: Webhook (Generic) (category=webhook; backend=adapter; platform_action actions=none registered)\n\n## Tool Usage\nTools are normally invoked through the native function-calling mechanism, not by writing JSON in your reply\ntext. Only if the provider signals that native function calling is unavailable for this turn, fall back to a\nsingle JSON code block: `{\"name\": \"tool_name\", \"arguments\": {\"key\": \"value\"}}` — use this fallback format\nonly, never both. Only call a tool whose exact name appears in the tool index above and is currently callable\n(or reachable via `capability_expand`). Never invent, rename, alias, or guess a tool name, platform ID, or\nplatform action from argument shape or wording — if the index does not list it, it does not exist.\n`platform_connect.action` (list/guide/preflight/connect/disconnect/remove/status/events_start/events_stop/\nevents_status) is the App Connector management namespace; `platform_action.action` only accepts exact\nregistered business actions such as `im.send_message` shown in the App Connector Capability Index — never mix\nthe two namespaces. If a tool call returns an unknown/unavailable result, use the returned suggestions or\navailable names for a single retry instead of trying further variations of the same guess.\n\n**Side-effect action rule** (`platform_action` with effect=send/write/execute):\n- Call each unique action+payload **exactly once**. Never include duplicates in the same turn.\n- Once the result returns `\"completed\": true`, that action is DONE for this task. Do NOT call it again\n in any subsequent turn — immediately summarize the result for the user instead.\n- The system enforces idempotency: duplicate calls are blocked and will not execute.\n- If the user explicitly requests sending/writing multiple times, use distinct payloads per call.\n\n**Resource identifier provenance rule**:\n- NEVER fabricate, guess, or infer resource identifiers (chat_id, message_id, file_key, user_id, etc.).\n Every resource ID used in a side-effect action MUST come from a successful API response in this session.\n- If a read/list action fails (e.g. authorization error), you do NOT have valid resource IDs.\n Report the failure to the user — do NOT attempt the dependent write/send action with a guessed ID.\n- When a tool result contains `\"llm_instruction\"`, follow it exactly.\n\n## Guidelines\n1. **Direct answers first**: If you already know the answer, respond directly without tools.\n2. **Avoid redundant tool calls**: Do not call the same tool with the same arguments more than once in the same user turn. When an existing tool result already answers the user's request, stop calling tools and answer directly.\n3. **Use tools proactively**: When the user asks about files, time, system state, or needs actions performed, use the appropriate tool.\n4. **Chain tools when needed**: You can call multiple tools in sequence (e.g., list files → read file → summarize).\n5. **Handle failures gracefully**: If a tool fails, explain what went wrong and suggest alternatives. If it failed because of an unknown tool/platform/action name, retry once with an exact name from the returned suggestions, then explain rather than keep guessing.\n6. **Summarize results naturally**: After tool execution, synthesize the results into a helpful answer rather than dumping raw output.\n7. **Stay conversational**: Maintain a natural, helpful tone. Acknowledge context from earlier in the conversation.\n8. **Recall past work**:\n - Broad queries (\"之前做了什么\", \"列出任务\"): answer from the \"Recent Task History\" section already in your context. If insufficient, call session_list.\n - Specific lookups (\"上次那个配置怎么改的\"): call session_search with relevant phrases (NOT single characters).\n - Do NOT call search tools repeatedly with keyword fragments. One well-phrased call is better than ten fragmented ones.\n\n## Coding & Verification\nWhen working with code, prefer the precise built-in tools over ad-hoc shell: use `repo_map` to orient in an\nunfamiliar project, `code_search`/`file_find` to locate and `code_intel` for symbols, `edit_file` (anchored\nsearch-replace, or a unified `diff`) to change files — never rewrite a whole file to change a few lines — and\n`git_query` to inspect diffs/log. After edits, check `syntax_ok` in the result and run `test_run`/`lint_check`\nbefore declaring the work done. Batch independent read-only calls (search/read/query) into a single turn — they\nrun in parallel.\n\n## Presentation Style\n1. **Polished Markdown only**: Format user-facing answers with clean Markdown headings, short paragraphs, and concise bullets. Use tables only when they improve comparison or scanning.\n2. **Terminal-friendly layout**: Keep lines readable in a TUI; avoid dense walls of text, deeply nested lists, oversized ASCII art, or heavy visual blocks.\n3. **Elegant emphasis**: Use bold text sparingly for key terms and conclusions. Avoid excessive emojis, decorative symbols, repeated separators, or visual noise.\n4. **Theme-safe colors**: Do not emit ANSI escape codes, HTML color tags, Rich markup, or hardcoded color names. Rely on the TUI theme to render Markdown professionally.\n5. **No leaked tool protocol**: Never show tool-call JSON, internal schemas, raw observations, tool result payloads, or hidden reasoning in the final answer unless the user explicitly asks for raw/debug output. Treat any prior `{\"name\": ..., \"arguments\": ...}` blocks and `Tool result (...)` messages as internal execution context only.\n6. **Professional closure**: End with a concise conclusion or next step when helpful; avoid rambling after the useful answer is complete.\n\nWhen finished with all tool calls, respond normally without a JSON block, tool-call transcript, or process log.\n\n## Task Contract\n- Task ID: turn-1\n- Original user request: List the connected hardware devices, then describe bench_r8 in full.\n- Workspace root: \n- Allowed roots: \n- Treat relative project paths as relative to the workspace root; never infer `.` as the project root when a workspace root is provided.\n- Workspace boundary is enforced by tools: do not read, search, edit, or run commands against paths outside the allowed roots unless the user explicitly requests an external path and the tool/approval policy permits it.\n- LeapFlow workspace config is optional at `/.leapflow/config.yaml`; runtime config is loaded from `~/.leapflow/config/user.yaml` and `~/.leapflow/profiles//config/*.yaml`.\n- Preserve this task contract across summarization, compression, tool loops, and memory retrieval.\n" + }, + { + "role": "system", + "content": "\n## Recent Session Summary\n- [user] List the connected hardware devices, then describe bench_r8 in full.\n" + }, + { + "role": "user", + "content": "List the connected hardware devices, then describe bench_r8 in full.\nList the connected hardware devices, then describe bench_r8 in full." + } + ], + "tools": [ + "capability_expand", + "code_intel", + "code_search", + "config_get", + "config_list", + "env_info", + "file_find", + "file_list", + "file_read", + "git_query", + "hw_describe", + "hw_list", + "hw_read", + "hw_status", + "lint_check", + "memory_search", + "plugin_list", + "plugin_propose", + "plugin_status", + "plugin_versions", + "repo_map", + "research_note", + "runtime_snapshot", + "schedule_reentry", + "self_describe", + "session_list", + "skill_view", + "skills_list", + "terminal_list", + "terminal_read", + "test_run", + "text_search", + "time_get", + "tool_describe", + "tool_search", + "web_fetch" + ] + }, + "responses": [ + { + "status": 200, + "content_type": "application/json", + "body": "{\"id\": \"chatcmpl-cassette\", \"object\": \"chat.completion\", \"model\": \"cassette-model\", \"choices\": [{\"index\": 0, \"message\": {\"role\": \"assistant\", \"content\": \"\", \"tool_calls\": [{\"id\": \"call_1\", \"type\": \"function\", \"function\": {\"name\": \"hw_list\", \"arguments\": \"{}\"}}]}, \"finish_reason\": \"tool_calls\"}], \"usage\": {\"prompt_tokens\": 64, \"completion_tokens\": 16, \"total_tokens\": 80}}" + } + ] +} diff --git a/tests/_fixtures/cassettes/r8_hardware/cassette-model-8f49d5320835bb67.cassette.json b/tests/_fixtures/cassettes/r8_hardware/cassette-model-8f49d5320835bb67.cassette.json new file mode 100644 index 00000000..89785a7a --- /dev/null +++ b/tests/_fixtures/cassettes/r8_hardware/cassette-model-8f49d5320835bb67.cassette.json @@ -0,0 +1,87 @@ +{ + "fingerprint": "8f49d5320835bb678bc6b5a74cc208004fa0c038e1d78d30a5a12786f781c7dc", + "note": "captured in seed mode", + "request": { + "model": "cassette-model", + "stream": false, + "messages": [ + { + "role": "system", + "content": "You are LeapFlow, an intelligent assistant that can both converse naturally and take real actions on the user's computer.\n\n## Capabilities\nThe tool index below lists **every** registered tool by name and a one-line summary — this is the complete\ncapability contract; nothing else exists. Only a subset is directly callable this turn (via native tool calling,\nnot a JSON block in your reply). If you need a tool from the index that is not yet callable, call\n`capability_expand` with its category name first — the matching tools become callable immediately after.\nWhen the user asks about LeapFlow **itself** — its identity/version/model, what tools, plugins, or capabilities\nit has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of\nself-awareness — answer from live runtime evidence via `self_describe` (facet=capabilities for \"what can you\ndo / do you support X\", identity for version/model, runtime for context/posture, or `all` for a full self-check),\nnever by reading LeapFlow's own source code, documentation, or memory. Report configuration-dependent or\nunavailable capabilities as limitations rather than inferring them.\n- **text_search**(text, pattern): Search for a regex pattern in text.\n- **text_replace**(text, old, new, count) [capability_expand category: write]: Replace occurrences of a substring in text.\n- **time_get**(): Get current date and time.\n- **env_info**(): Get system environment information (OS, Python version, cwd).\n- **skills_list**(query, category, source): List available learned skills. Use when user asks about capabilities or you need a specific skill.\n- **skill_view**(name): View the full content of a specific skill document.\n- **skill_list_files**(name) [capability_expand category: skills]: List all support files in a skill directory (configs, templates, examples). Use before skill_file_read to discover available files.\n- **skill_file_read**(name, file_path) [capability_expand category: skills]: Read an individual support file from a skill directory. Use skill_list_files first to discover available files.\n- **code_intel**(path, operation): Precise document symbols (outline) for a source file: classes, functions, and methods with line ranges. Python uses an exact AST parse; other languages use a keyword-prefix scan. Prefer over file_read mode=symbols for accurate navigation before editing. Read-only.\n- **repo_map**(path): Compact project orientation for a repository root: languages, detected test/lint commands, top-level structure, entry points, manifest, and VCS branch. Call this first when entering an unfamiliar codebase. Read-only.\n- **scm_sync**(action, cwd, remote, pull_ref, push_ref, timeout) [capability_expand category: scm]: Run a typed git SCM action. Use this instead of shell_run for git pull/push/status. For 'pull origin main then push', set action='pull_then_push', remote='origin', pull_ref='main', and omit push_ref so LeapFlow pushes the current local branch.\n- **git_query**(action, cwd, ref, path, staged, max_count, stat): Read-only structured git inspection: action=diff|log|status|branch|show. Prefer over shell_run for reading repo state — output is clipped, redacted, and log/branch are parsed into structured fields. Use scm_sync for pull/push.\n- **git_write**(action, cwd, message, stage_all, name, ref, create) [capability_expand category: scm]: Mutating git actions: action=commit (message, stage_all), branch (create+switch), checkout (switch; create=true for -b). Approval-gated. Use scm_sync for pull/push and git_query for reads.\n- **test_run**(command, cwd, timeout): Run the project's test suite and return structured results (framework, passed/failed counts, failing tests). Auto-detects the runner (pytest/npm/go/cargo) or uses a configured/explicit command; executes via the governed shell. ok=true means the runner executed — see 'success' for pass/fail.\n- **lint_check**(command, cwd, timeout): Run the project's linter and return a structured clean/issue result. Auto-detects the linter (ruff/eslint/go vet/clippy) or uses a configured/explicit command; executes via the governed shell. ok=true means the linter ran — see 'clean'.\n- **file_list**(path, pattern, depth): List files and directories at a given path. Use depth=1 or depth=2 to get a recursive tree in one call instead of listing each sub-directory separately.\n- **file_read**(path, max_lines, start_line, max_chars, mode): Read text file content with adaptive context governance. For large or unfamiliar files, prefer mode='outline' or mode='symbols' first, then use mode='raw' with start_line/max_lines for the specific range you actually need. For LeapFlow's own settings, use config_list / config_get / config_set — its config files are outside the workspace and not readable here.\n- **file_write**(path, content, mode) [capability_expand category: write]: Write content to a file (overwrite or append).\n- **code_search**(pattern, patterns, path, glob, ignore_case, multiline, max_results, context_lines): Search file CONTENTS by regex pattern across a directory tree (ripgrep-backed). Requires a regex pattern. NOT for listing or browsing directory contents — use file_list for that. Prefer this over shell_run grep: faster, skips VCS/dependency/build dirs, and returns structured path:line:column matches. Batch related lookups into ONE call via `patterns` (OR-combined, single pass) instead of issuing several separate searches. Use file_read for the surrounding context of a hit.\n- **file_find**(glob, path, max_results): Find files by a recursive glob pattern under a base path (e.g. '**/test_*.py' or '*.md'). Prefer this over shell_run find; skips VCS/dependency/build dirs.\n- **edit_file**(path, edits, dry_run, diff) [capability_expand category: write]: Apply targeted, anchored search-replace edits to an EXISTING text file (use file_write to create/overwrite). Each edit is {original_text, new_text, replace_all?}; original_text must match exactly and uniquely (or set replace_all) — a non-unique or missing anchor is rejected so files are never corrupted. Set dry_run to preview. Alternatively pass a unified 'diff' to apply its hunks as anchored edits. Far cheaper and safer than rewriting a whole file.\n- **shell_run**(command, cwd, timeout) [capability_expand category: shell]: Execute a one-shot shell command with timeout protection. Runs in the active workspace; paths resolving outside it are refused. Reach for a structured tool first when one fits — web_fetch for anything over HTTP(S), git_query/scm_sync for git, code_search/file_find/file_read for the repo, config_get/config_set for LeapFlow's own settings — because those report typed results, while a failed shell command can only be diagnosed from its exit code and stderr. Every shell run counts as an external side effect, so a failure stops the rest of the batch and is not retried automatically.\n- **terminal_open**(command, cwd, shell) [capability_expand category: terminal]: Open a PERSISTENT shell session (REPL/dev server/watch), returning a session_id for terminal_send/read/close. Disabled unless tools.terminal_session_enabled is set. For one-shot commands use shell_run instead.\n- **terminal_send**(session_id, input, wait) [capability_expand category: terminal]: Send a line of input to a persistent terminal session and return output captured shortly after.\n- **terminal_read**(session_id, wait): Drain buffered output from a persistent terminal session (optionally waiting briefly first).\n- **terminal_close**(session_id) [capability_expand category: terminal]: Terminate a persistent terminal session and release its process group.\n- **terminal_list**(): List active persistent terminal sessions.\n- **config_list**(category, limit): List LeapFlow's own writable settings (LLM model and endpoint, daemon, memory, perception, gateway, …) with current values. Use this to discover the exact key before changing anything. Optionally narrow by `category`. This is the only correct way to inspect LeapFlow configuration — never read config files from disk.\n- **config_get**(key): Read one LeapFlow setting by key (e.g. 'llm.model', 'llm.base_url', 'daemon.log_level'), returning its current value, type, scopes, and whether a change needs a daemon restart. There is no 'llm.provider' key: provider behavior is inferred from the OpenAI-compatible 'llm.base_url'. Never read LeapFlow config files from disk — use this.\n- **config_set**(key, value, scope) [capability_expand category: config]: Change one LeapFlow setting by key, e.g. switch an OpenAI-compatible LLM with key='llm.model' and key='llm.base_url'. There is no 'llm.provider' key: provider behavior is inferred from the endpoint. Values are validated and coerced; credentials are stored in the vault automatically. Call config_list or config_get first if unsure of the key. The result states whether a `leap daemon restart` is required. Never edit LeapFlow config files directly.\n- **web_fetch**(url, select, timeout, max_bytes): Read a URL over HTTP(S) and get back extracted, context-sized content: parsed JSON for API endpoints, readable text plus links for web pages. Use this for anything on the internet — prices, docs, releases, articles — instead of running curl through shell_run: it reports real HTTP status codes, retries rate limits on its own, and is a plain read so a retry is always safe. For JSON APIs pass `select` with a dotted path (e.g. 'chart.result.0.meta') to return just that part instead of the whole payload.\n- **memory_search**(query, limit): Search agent memory for relevant past experiences, observations, and facts.\n- **memory_add**(content, kind) [capability_expand category: write]: Store a new observation or insight in memory for future reference.\n- **research_note**(kind, text): Record a compact, structured note about the current task's state so it survives context compression on long / multi-step tasks. Use for durable findings, open questions still to resolve, decisions / excluded paths, and the immediate next step. One concise sentence per note.\n- **capability_expand**(category): Fetch the full callable schema for every tool in a capability category (e.g. 'hub', 'gateway', 'desktop', 'delegate', 'file', 'memory', 'skill'). The compact tool index always lists every registered tool by name and a one-line summary, but only a static low-risk subset is directly callable each turn. If you need a tool from the index that is not yet callable, call capability_expand with its category first; the matching tools become callable in this turn. Never invent a tool name — expand the category instead.\n- **delegate_task**(goal, context) [capability_expand category: delegate]: Delegate a complex sub-task to an isolated subagent. The subagent gets a fresh context and restricted tool access. Use when a task is self-contained and can be solved independently.\n- **schedule_reentry**(kind, reason, delay_seconds, event_match, max_reentries, deadline_seconds): Register a re-entry so this task can resume later from its current orientation (findings / open questions / next step). Use when work must pause and continue after a delay (kind=time) or when a matching platform event arrives (kind=event), instead of finishing now. The research-ledger state is carried over automatically.\n- **hub_push**(skill_name, visibility, version) [capability_expand category: hub]: Push a local skill to the ModelScope Hub for sharing or backup.\n- **hub_pull**(repo_id, version) [capability_expand category: hub]: Pull a skill from the ModelScope Hub to install locally.\n- **hub_search**(query) [capability_expand category: hub]: Search for skills on the Hub by keyword or description.\n- **hub_sync**(mode, dry_run) [capability_expand category: hub]: Preview or execute sync between local skills and Hub.\n- **platform_action**(platform, action, payload, backend_kind) [capability_expand category: gateway]: Execute an exact registered business action on an external platform through LeapFlow's App Connector layer. Actions must be copied from the App Connector Capability Index and are addressed as domain.operation, e.g. im.send_message or docs.create_markdown. All business fields (chat_id, text, query, etc.) MUST be placed inside `payload`, never at the top level. Example: {\"platform\":\"feishu\",\"action\":\"im.send_message\",\"payload\":{\"chat_id\":\"oc_xxx\",\"text\":\"hello\"}}. Do not invent action names, do not use management actions such as list/guide/connect/status here.\n- **platform_connect**(action, platform, credentials, options, checkpoint) [capability_expand category: gateway]: List, guide, connect, disconnect, remove, or check status for external platforms using the App Connector management namespace. Supports REST and CLI backends. Use this for management actions such as list/guide/preflight/connect/status; use platform_action only for exact registered business actions.\n- **gateway_send**(platform, chat_id, text, thread_id) [capability_expand category: gateway]: Send a message to a connected external platform (Feishu group, Telegram chat, DingTalk conversation, etc.). Requires the platform to be connected via gateway_connect first. Use gateway_connect with action='list' to see connected platforms and available chat IDs.\n- **gateway_connect**(action, platform, credentials, options) [capability_expand category: gateway]: Connect, configure, or manage external platform integrations (Feishu, DingTalk, Telegram, Slack, Discord, etc.). Conversational flow: 1) call 'guide' to get setup steps + required fields, 2) present the steps to the user and ask for ALL required credentials in a single message, 3) call 'connect' with the credentials. Goal: complete in 1–2 user turns. NEVER include credential values in your text response.\n- **plugin_list**(): List the live plugin registry across Tool/Gateway/LLM subsystems for plugin management: which plugins are loaded, their category and fiber lifecycle state. For questions about LeapFlow's own capabilities or whether it supports X, use self_describe instead.\n- **plugin_status**(plugin_id): Get detailed status of a specific plugin: its declared category, runtime dependencies, contributed tools, and fiber lifecycle state.\n- **plugin_versions**(plugin_id): List recorded source versions and active pointer for a profile-scoped plugin.\n- **plugin_propose**(requested_capability, plugin_id, proposed_tools, test_cases, risk_level, evidence): Create a side-effect-free PluginProposal from explicit capability-gap evidence. Use this before plugin_generate when a missing capability should be reviewed. Does not call an LLM, write files, or install anything.\n- **assess_compatibility**(manifest, source_path) [capability_expand category: plugin_management]: Assess whether a foreign plugin manifest or real DSH source bundle is compatible with LeapFlow. A source bundle assessment is static: runtime_ready stays false until restricted Node discovery during plugin_install. Returns component-level verdicts and limitations.\n- **plugin_generate**(plugin_id, description, proposal_id) [capability_expand category: system]: Generate a new ToolPlugin from a natural-language capability description. The LLM produces code that conforms to the ToolPlugin Protocol; it is then rigorously validated (syntax, structure, import, protocol conformance). The generated source is stored in CAS and receives explicit content approval. It DOES NOT install the plugin — installation requires a second, mutation-specific approval via plugin_install.\n- **plugin_install**(plugin_id, code, marketplace_name, source_path, proposal_id, version_label) [capability_expand category: system]: Install a plugin from exactly one source: validated Python code, the configured Python marketplace, or a real DSH source bundle. DSH bundles run restricted Node discovery before registration; only public host tools are installed and client UI limitations are reported. Writes profile-scoped state and mutates the process-global registry. REQUIRES APPROVAL.\n- **plugin_rollback**(plugin_id, version) [capability_expand category: system]: Rollback a profile-scoped plugin to a recorded source version and reload it. REQUIRES APPROVAL.\n- **plugin_reload**(plugin_id, version_label) [capability_expand category: system]: Hot-reload a plugin at runtime. Disposes the old plugin fiber, re-imports its module, and registers a fresh instance. Existing in-flight turns are unaffected (snapshot isolation). REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_disable**(plugin_id) [capability_expand category: system]: Disable a plugin by disposing its fiber, removing its tools from the runtime registry. Cannot disable self_management itself. REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_remove**(plugin_id, delete_source) [capability_expand category: system]: Terminally remove a plugin: dispose its fiber, unregister its tools, remove reload metadata, and optionally delete its profile-scoped source file. Cannot remove self_management itself. REQUIRES APPROVAL.\n- **plugin_unquarantine**(plugin_id) [capability_expand category: plugin_management]: Restore a quarantined plugin to probation status for re-evaluation. Unfreezes the trust ledger, transitions the proposal back to PROBATION, and reloads the plugin. REQUIRES APPROVAL.\n- **plugin_enable**(plugin_id) [capability_expand category: system]: Re-enable a previously disabled plugin by reloading its module and registering a fresh instance. REQUIRES APPROVAL.\n- **self_describe**(facet): Answer questions about LeapFlow ITSELF from live runtime evidence — its identity/version/model, what tools and plugins it has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of self-awareness. Prefer this over reading LeapFlow's own source code. Pick facet=capabilities for 'what can you do / do you support X', identity for version/model, runtime for context/posture, or all for a full self-check when explicitly requested.\n- **runtime_snapshot**(): Lightweight (~150 token) flat snapshot of current runtime state: model, context budget, posture, disclosure level, turn count, cache hit rate, uptime, tool count, and pending approvals.\n- **schedule_create**(trigger_expression, instruction, execution_mode, max_retries, delivery_target) [capability_expand category: scheduler]: Create a new scheduled task. Specify a trigger expression (e.g. '30m', 'every 2h', '0 9 * * *') and the instruction to execute on each trigger. Optionally provide an execution mode and a delivery target to receive notifications.\n- **schedule_list**() [capability_expand category: scheduler]: List all scheduled tasks with their current state, trigger, and next due time.\n- **schedule_status**(task_id) [capability_expand category: scheduler]: Get detailed status and recent execution history for a scheduled task.\n- **schedule_pause**(task_id) [capability_expand category: scheduler]: Pause a scheduled task so it stops firing without being cancelled.\n- **schedule_resume**(task_id) [capability_expand category: scheduler]: Resume a paused scheduled task — re-arms it and recalculates next due time.\n- **schedule_cancel**(task_id) [capability_expand category: scheduler]: Cancel a scheduled task permanently.\n- **tool_search**(query, max_results): Search all registered tools by keyword relevance. Returns a ranked list of matching tools with name, category, and summary. Use this when you need a tool but are unsure of its exact name or category.\n- **tool_describe**(tool_name): Get the full callable schema of a single tool by exact name. Returns the tool's description, parameters, and metadata. Use after tool_search to inspect a specific tool before calling it.\n- **hw_list**(): List connected hardware devices with their channel counts and measured quantities. Start here; it does not include operating limits.\n- **hw_describe**(device_id): Return the full reference for one device: every channel, its unit, its operating envelope, rate limit, reversibility, and required interlocks, plus any prior outcomes recorded for its writable channels. Required before commanding a device.\n- **hw_read**(device_id, channel_id): Read the current value of one device channel. Has no physical effect.\n- **hw_status**(device_id): Report connection health, halt capability, and recent notable events (threshold excursions, lost samples, stalled channels) for one device.\n- **hw_configure**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Set a configuration value or setpoint on a channel declaring effect=configure. Setpoints often have inertia: the value may need time to stabilise.\n- **hw_actuate**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Command motion or output on a channel declaring effect=actuate. This moves physical hardware; a repeat from an unknown state is not a safe retry.\n- **hw_dispense**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Consume an irreversible resource on a channel declaring effect=dispense. Running this twice dispenses twice; never repeat it after a failure without first verifying what already happened.\n- **hw_estop**(device_id) [capability_expand category: hardware]: Stop all motion and output on a device immediately. Never requires approval; use it whenever device behaviour is unexpected.\n- **hw_batch_actuate**(device_id, commands, dry_run, verify) [capability_expand category: hardware]: Write multiple actuator channels in one atomic transaction. Used for coordinated multi-joint control where all positions must be commanded simultaneously (e.g. robot arm joint-space control).\n- **hw_control_start**(device_id, mode, targets, frequency_hz, trajectory, policy, gains) [capability_expand category: hardware]: Start a real-time control loop on a robot device. Supports PID position tracking, impedance control, trajectory following, and neural policy chains.\n- **hw_control_stop**(device_id) [capability_expand category: hardware]: Stop the real-time control loop on a device. Gracefully halts the device and returns session statistics.\n- **hw_control_status**(device_id) [capability_expand category: hardware]: Get the status and statistics of a running control loop. Returns frequency, jitter, overruns, and policy state.\n- **hw_policy_infer**(device_id, policy, task, execute, chunk_size, verify, compute_budget) [capability_expand category: hardware]: Execute VLA policy inference on a robot device. Reads observation, runs policy, optionally writes action to actuators. Supports local (HuggingFace) and remote (PolicyServer) policies.\n- **hw_policy_episode**(action, device_id, policy, task) [capability_expand category: hardware]: Manage policy inference episode lifecycle. Start an episode to begin a task, stop to finalise and get stats.\n- **hw_policy_list**(source) [capability_expand category: hardware]: List available VLA policies and their capabilities.\n- **hw_orchestrate**(mode, operations, barrier_timeout_s, label) [capability_expand category: hardware]: Execute coordinated operations across multiple hardware devices. Supports sequential (one-at-a-time), parallel (concurrent), and barrier (synchronised) execution modes. Any failure in a parallel group triggers halt_all on every participating device for safety.\n- **session_search**(query, limit) [capability_expand category: unclassified]: Search past conversation sessions in the current workspace for relevant context.\n- **session_list**(limit): List recent conversation sessions in the current workspace with stable ids, titles, dates, and summaries. Use for browsing past tasks or when user asks to see history without specific search terms. No keywords needed — returns chronological list.\n- **session_detail**(session_id, limit, offset, include_inactive) [capability_expand category: unclassified]: Read a paginated persisted transcript for one past conversation session in the current workspace. Use after session_list or session_search returns a session_id.\n- **click**(element_index) [capability_expand category: desktop]: Click a UI element by its element_index (from the latest observe_ui snapshot)\n- **get_clipboard**() [capability_expand category: desktop]: Read current clipboard text content\n- **list_apps**(filter, running_only) [capability_expand category: desktop]: List available applications on this system. Use to discover correct bundle_id before switch_app.\n- **list_windows**() [capability_expand category: desktop]: List all top-level windows with pid, window_id, title, and per-window state (minimized, on-screen). Call this first to pick the pid and window_id that observe_ui and other window tools require.\n- **observe_ui**(pid, window_id, query) [capability_expand category: desktop]: Snapshot one window's actionable UI elements, each tagged with an element_index for click/right_click/read_text. Re-observe after actions — indices belong to one snapshot. Requires the window's pid and window_id from list_windows.\n- **open_url**(url, app_id) [capability_expand category: desktop]: Open a URL in the default or specified browser\n- **read_text**(element_index) [capability_expand category: desktop]: Read the text content of a specific UI element from the latest snapshot\n- **right_click**(element_index) [capability_expand category: desktop]: Right-click a UI element to open its context menu. Returns visible menu items.\n- **screenshot**(pid, window_id) [capability_expand category: desktop]: Capture a screenshot for visual verification. With pid + window_id captures that window (works across all displays); defaults to the last observed window, or the full desktop when no window has been observed.\n- **scroll**(element_index, direction, amount, pid, window_id) [capability_expand category: desktop]: Scroll a scrollable area of a window. Omit element_index to scroll the window's focused/page scroller; pass one to scroll an exact element from the latest snapshot.\n- **select_text**(element_index) [capability_expand category: desktop]: Select all text in a UI element (focus + select-all, for subsequent copy)\n- **set_clipboard**(text) [capability_expand category: desktop]: Write text to the clipboard\n- **shortcut**(keys) [capability_expand category: desktop]: Execute a keyboard shortcut\n- **switch_app**(app_id) [capability_expand category: desktop]: Switch to an app (launch if needed, activate, verify)\n- **type_text**(text) [capability_expand category: desktop]: Type text into the currently focused element\n- **wait**(seconds) [capability_expand category: desktop]: Wait for a specified duration before continuing\n- **wait_until**(condition, pid, window_id, timeout, poll_interval) [capability_expand category: desktop]: Wait until a UI condition is met (polls UI tree). Returns elements when found or on timeout.\n- **wait_until_stable**(timeout, poll_interval, pid, window_id) [capability_expand category: desktop]: Wait until the UI stops changing (element set stabilizes across polls).\n\n## App Connector Capability Index\nLeapFlow can onboard and manage external apps through `platform_connect` and execute exact registered business actions through `platform_action`.\nFor requests about connecting, setting up, configuring, enabling, or managing a supported app, use `platform_connect` first instead of generating SDK/Webhook sample code.\n`platform_connect.action` is the management namespace: list, guide, preflight, connect, disconnect, remove, status, events_start, events_stop, events_status.\n`platform_action.action` is only for exact registered platform business actions listed below, such as `im.send_message`; never use management actions like `list` or `guide` there.\nAll business fields MUST go inside `payload`; top-level keys are only `platform`, `action`, and `payload`.\nDo not invent platform IDs or platform action names. If the needed action is not listed, ask for discovery/clarification instead of guessing.\nUse `platform_connect` with `action='guide'` and the matching `platform` to start onboarding; use `action='list'` when the app is unclear.\nWhen a pending onboarding state is present, continue from that state with `platform_connect` instead of asking the user to restate the app.\nSupported apps:\n- `api_server`: API Server (OpenAI Compatible) (category=api; backend=adapter; platform_action actions=none registered)\n- `dingtalk`: 钉钉 (DingTalk) (category=im; backend=adapter; platform_action actions=none registered)\n- `feishu`: 飞书 (Feishu/Lark) (category=im; backend=cli)\n - `calendar.create_event` payload={summary*, start_time*, end_time*, attendees} [write/high]\n - `docs.create_markdown` payload={title*, markdown*, folder_token} [write/medium]\n - `drive.search` payload={query*, limit} [read/medium]\n - `im.add_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.download_resource` payload={message_id*, file_key*, type*} [read/low]\n - `im.get_messages` payload={message_ids*} [read/low]\n - `im.list_chats` payload={page_size} [read/low]\n - `im.list_messages` payload={chat_id*} [read/low]\n - `im.list_thread_messages` payload={thread*} [read/low]\n - `im.remove_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.reply_message` payload={message_id*, text*} [send/high]\n - `im.search_chats` payload={query*, page_size} [read/low]\n - `im.search_messages` payload={query*} [read/medium]\n - `im.send_message` payload={chat_id*, text*, thread_id} [send/high]\n - `im.update_card` payload={token*, card*} [write/medium]\n - `im.update_message` payload={message_id*, text*} [write/medium]\n - `mail.search_unread` payload={query, limit} [read/high]\n - `sheets.append_row` payload={spreadsheet_token*, sheet_id*, values*} [write/medium]\n - `task.create` payload={title*, description, due_time} [write/medium]\n- `telegram`: Telegram (category=social; backend=adapter; platform_action actions=none registered)\n- `webhook`: Webhook (Generic) (category=webhook; backend=adapter; platform_action actions=none registered)\n\n## Learned Skills\nYou have access to the following learned skills. Use `skills_list` to browse or `skill_view` to read details:\n- api_designer: REST and GraphQL API design with OpenAPI spec generation and best practices [API, REST, GraphQL]\n- code_review: Structured code review for security, performance, and style [code, review, security]\n- data_analysis: CSV/JSON data analysis with statistical summaries, quality checks, and visualiza [data, analysis, statistics]\n- devops_helper: CI/CD pipeline configuration, Docker orchestration, and deployment strategy [devops, CI/CD, Docker]\n- document_writer: Structured document generation: technical docs, API references, reports, and REA [documentation, technical-writing, README]\n- git_workflow: Git operation orchestration: branching, commits, conflicts, and PR workflow [git, version-control, branching]\n- i18n_helper: Internationalization workflow: string extraction, translation, locale management [i18n, internationalization, localization]\n- project_analysis: Project structure analysis, dependency audit, and tech stack identification [project, analysis, dependencies]\n- security_audit: Security vulnerability scanning strategy, threat modeling, and fix recommendatio [security, vulnerability, audit]\n- shell_automation: Natural language to shell commands with safety review and step-by-step execution [shell, terminal, automation]\n- test_writer: Generate unit and integration tests from source code with edge-case coverage [testing, unit-test, integration-test]\n- web_research: Multi-step web search with cross-verification and structured synthesis [web, search, research]\n\n## Tool Usage\nTools are normally invoked through the native function-calling mechanism, not by writing JSON in your reply\ntext. Only if the provider signals that native function calling is unavailable for this turn, fall back to a\nsingle JSON code block: `{\"name\": \"tool_name\", \"arguments\": {\"key\": \"value\"}}` — use this fallback format\nonly, never both. Only call a tool whose exact name appears in the tool index above and is currently callable\n(or reachable via `capability_expand`). Never invent, rename, alias, or guess a tool name, platform ID, or\nplatform action from argument shape or wording — if the index does not list it, it does not exist.\n`platform_connect.action` (list/guide/preflight/connect/disconnect/remove/status/events_start/events_stop/\nevents_status) is the App Connector management namespace; `platform_action.action` only accepts exact\nregistered business actions such as `im.send_message` shown in the App Connector Capability Index — never mix\nthe two namespaces. If a tool call returns an unknown/unavailable result, use the returned suggestions or\navailable names for a single retry instead of trying further variations of the same guess.\n\n**Side-effect action rule** (`platform_action` with effect=send/write/execute):\n- Call each unique action+payload **exactly once**. Never include duplicates in the same turn.\n- Once the result returns `\"completed\": true`, that action is DONE for this task. Do NOT call it again\n in any subsequent turn — immediately summarize the result for the user instead.\n- The system enforces idempotency: duplicate calls are blocked and will not execute.\n- If the user explicitly requests sending/writing multiple times, use distinct payloads per call.\n\n**Resource identifier provenance rule**:\n- NEVER fabricate, guess, or infer resource identifiers (chat_id, message_id, file_key, user_id, etc.).\n Every resource ID used in a side-effect action MUST come from a successful API response in this session.\n- If a read/list action fails (e.g. authorization error), you do NOT have valid resource IDs.\n Report the failure to the user — do NOT attempt the dependent write/send action with a guessed ID.\n- When a tool result contains `\"llm_instruction\"`, follow it exactly.\n\n## Guidelines\n1. **Direct answers first**: If you already know the answer, respond directly without tools.\n2. **Avoid redundant tool calls**: Do not call the same tool with the same arguments more than once in the same user turn. When an existing tool result already answers the user's request, stop calling tools and answer directly.\n3. **Use tools proactively**: When the user asks about files, time, system state, or needs actions performed, use the appropriate tool.\n4. **Chain tools when needed**: You can call multiple tools in sequence (e.g., list files → read file → summarize).\n5. **Handle failures gracefully**: If a tool fails, explain what went wrong and suggest alternatives. If it failed because of an unknown tool/platform/action name, retry once with an exact name from the returned suggestions, then explain rather than keep guessing.\n6. **Summarize results naturally**: After tool execution, synthesize the results into a helpful answer rather than dumping raw output.\n7. **Stay conversational**: Maintain a natural, helpful tone. Acknowledge context from earlier in the conversation.\n8. **Recall past work**:\n - Broad queries (\"之前做了什么\", \"列出任务\"): answer from the \"Recent Task History\" section already in your context. If insufficient, call session_list.\n - Specific lookups (\"上次那个配置怎么改的\"): call session_search with relevant phrases (NOT single characters).\n - Do NOT call search tools repeatedly with keyword fragments. One well-phrased call is better than ten fragmented ones.\n\n## Coding & Verification\nWhen working with code, prefer the precise built-in tools over ad-hoc shell: use `repo_map` to orient in an\nunfamiliar project, `code_search`/`file_find` to locate and `code_intel` for symbols, `edit_file` (anchored\nsearch-replace, or a unified `diff`) to change files — never rewrite a whole file to change a few lines — and\n`git_query` to inspect diffs/log. After edits, check `syntax_ok` in the result and run `test_run`/`lint_check`\nbefore declaring the work done. Batch independent read-only calls (search/read/query) into a single turn — they\nrun in parallel.\n\n## Presentation Style\n1. **Polished Markdown only**: Format user-facing answers with clean Markdown headings, short paragraphs, and concise bullets. Use tables only when they improve comparison or scanning.\n2. **Terminal-friendly layout**: Keep lines readable in a TUI; avoid dense walls of text, deeply nested lists, oversized ASCII art, or heavy visual blocks.\n3. **Elegant emphasis**: Use bold text sparingly for key terms and conclusions. Avoid excessive emojis, decorative symbols, repeated separators, or visual noise.\n4. **Theme-safe colors**: Do not emit ANSI escape codes, HTML color tags, Rich markup, or hardcoded color names. Rely on the TUI theme to render Markdown professionally.\n5. **No leaked tool protocol**: Never show tool-call JSON, internal schemas, raw observations, tool result payloads, or hidden reasoning in the final answer unless the user explicitly asks for raw/debug output. Treat any prior `{\"name\": ..., \"arguments\": ...}` blocks and `Tool result (...)` messages as internal execution context only.\n6. **Professional closure**: End with a concise conclusion or next step when helpful; avoid rambling after the useful answer is complete.\n\nWhen finished with all tool calls, respond normally without a JSON block, tool-call transcript, or process log.\n\n## Task Contract\n- Task ID: turn-5\n- Original user request: Preview an actuate of bench_r8 setpoint to 55, dry run only.\n- Workspace root: \n- Allowed roots: \n- Treat relative project paths as relative to the workspace root; never infer `.` as the project root when a workspace root is provided.\n- Workspace boundary is enforced by tools: do not read, search, edit, or run commands against paths outside the allowed roots unless the user explicitly requests an external path and the tool/approval policy permits it.\n- LeapFlow workspace config is optional at `/.leapflow/config.yaml`; runtime config is loaded from `~/.leapflow/config/user.yaml` and `~/.leapflow/profiles//config/*.yaml`.\n- Preserve this task contract across summarization, compression, tool loops, and memory retrieval.\n" + }, + { + "role": "assistant", + "content": "Read the setpoint channel." + }, + { + "role": "user", + "content": "Actuate the bench_r8 setpoint to 60.\nConfigure the bench_r8 homed channel to true to complete homing." + }, + { + "role": "assistant", + "content": "[Called: hw_configure]\nHomed the device; the readiness gate is now satisfied." + }, + { + "role": "user", + "content": "Preview an actuate of bench_r8 setpoint to 55, dry run only.\nPreview an actuate of bench_r8 setpoint to 55, dry run only." + } + ], + "tools": [ + "capability_expand", + "code_intel", + "code_search", + "config_get", + "config_list", + "env_info", + "file_find", + "file_list", + "file_read", + "git_query", + "hw_actuate", + "hw_batch_actuate", + "hw_configure", + "hw_control_start", + "hw_control_status", + "hw_control_stop", + "hw_describe", + "hw_dispense", + "hw_estop", + "hw_list", + "hw_orchestrate", + "hw_policy_episode", + "hw_policy_infer", + "hw_policy_list", + "hw_read", + "hw_status", + "lint_check", + "memory_search", + "plugin_list", + "plugin_propose", + "plugin_status", + "plugin_versions", + "repo_map", + "research_note", + "runtime_snapshot", + "schedule_reentry", + "self_describe", + "session_list", + "skill_view", + "skills_list", + "terminal_list", + "terminal_read", + "test_run", + "text_search", + "time_get", + "tool_describe", + "tool_search", + "web_fetch" + ] + }, + "responses": [ + { + "status": 200, + "content_type": "application/json", + "body": "{\"id\": \"chatcmpl-cassette\", \"object\": \"chat.completion\", \"model\": \"cassette-model\", \"choices\": [{\"index\": 0, \"message\": {\"role\": \"assistant\", \"content\": \"\", \"tool_calls\": [{\"id\": \"call_1\", \"type\": \"function\", \"function\": {\"name\": \"hw_actuate\", \"arguments\": \"{\\\"device_id\\\": \\\"bench_r8\\\", \\\"channel_id\\\": \\\"setpoint\\\", \\\"value\\\": 55.0, \\\"dry_run\\\": true}\"}}]}, \"finish_reason\": \"tool_calls\"}], \"usage\": {\"prompt_tokens\": 64, \"completion_tokens\": 16, \"total_tokens\": 80}}" + } + ] +} diff --git a/tests/_fixtures/cassettes/r8_hardware/cassette-model-cc58cb0047244391.cassette.json b/tests/_fixtures/cassettes/r8_hardware/cassette-model-cc58cb0047244391.cassette.json new file mode 100644 index 00000000..efe98f45 --- /dev/null +++ b/tests/_fixtures/cassettes/r8_hardware/cassette-model-cc58cb0047244391.cassette.json @@ -0,0 +1,99 @@ +{ + "fingerprint": "cc58cb004724439106bc10d728c86b6dc1cc09074a6ccc2ab36df8f53c9a6695", + "note": "captured in seed mode", + "request": { + "model": "cassette-model", + "stream": false, + "messages": [ + { + "role": "system", + "content": "You are LeapFlow, an intelligent assistant that can both converse naturally and take real actions on the user's computer.\n\n## Capabilities\nThe tool index below lists **every** registered tool by name and a one-line summary — this is the complete\ncapability contract; nothing else exists. Only a subset is directly callable this turn (via native tool calling,\nnot a JSON block in your reply). If you need a tool from the index that is not yet callable, call\n`capability_expand` with its category name first — the matching tools become callable immediately after.\nWhen the user asks about LeapFlow **itself** — its identity/version/model, what tools, plugins, or capabilities\nit has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of\nself-awareness — answer from live runtime evidence via `self_describe` (facet=capabilities for \"what can you\ndo / do you support X\", identity for version/model, runtime for context/posture, or `all` for a full self-check),\nnever by reading LeapFlow's own source code, documentation, or memory. Report configuration-dependent or\nunavailable capabilities as limitations rather than inferring them.\n- **text_search**(text, pattern): Search for a regex pattern in text.\n- **text_replace**(text, old, new, count) [capability_expand category: write]: Replace occurrences of a substring in text.\n- **time_get**(): Get current date and time.\n- **env_info**(): Get system environment information (OS, Python version, cwd).\n- **skills_list**(query, category, source): List available learned skills. Use when user asks about capabilities or you need a specific skill.\n- **skill_view**(name): View the full content of a specific skill document.\n- **skill_list_files**(name) [capability_expand category: skills]: List all support files in a skill directory (configs, templates, examples). Use before skill_file_read to discover available files.\n- **skill_file_read**(name, file_path) [capability_expand category: skills]: Read an individual support file from a skill directory. Use skill_list_files first to discover available files.\n- **code_intel**(path, operation): Precise document symbols (outline) for a source file: classes, functions, and methods with line ranges. Python uses an exact AST parse; other languages use a keyword-prefix scan. Prefer over file_read mode=symbols for accurate navigation before editing. Read-only.\n- **repo_map**(path): Compact project orientation for a repository root: languages, detected test/lint commands, top-level structure, entry points, manifest, and VCS branch. Call this first when entering an unfamiliar codebase. Read-only.\n- **scm_sync**(action, cwd, remote, pull_ref, push_ref, timeout) [capability_expand category: scm]: Run a typed git SCM action. Use this instead of shell_run for git pull/push/status. For 'pull origin main then push', set action='pull_then_push', remote='origin', pull_ref='main', and omit push_ref so LeapFlow pushes the current local branch.\n- **git_query**(action, cwd, ref, path, staged, max_count, stat): Read-only structured git inspection: action=diff|log|status|branch|show. Prefer over shell_run for reading repo state — output is clipped, redacted, and log/branch are parsed into structured fields. Use scm_sync for pull/push.\n- **git_write**(action, cwd, message, stage_all, name, ref, create) [capability_expand category: scm]: Mutating git actions: action=commit (message, stage_all), branch (create+switch), checkout (switch; create=true for -b). Approval-gated. Use scm_sync for pull/push and git_query for reads.\n- **test_run**(command, cwd, timeout): Run the project's test suite and return structured results (framework, passed/failed counts, failing tests). Auto-detects the runner (pytest/npm/go/cargo) or uses a configured/explicit command; executes via the governed shell. ok=true means the runner executed — see 'success' for pass/fail.\n- **lint_check**(command, cwd, timeout): Run the project's linter and return a structured clean/issue result. Auto-detects the linter (ruff/eslint/go vet/clippy) or uses a configured/explicit command; executes via the governed shell. ok=true means the linter ran — see 'clean'.\n- **file_list**(path, pattern, depth): List files and directories at a given path. Use depth=1 or depth=2 to get a recursive tree in one call instead of listing each sub-directory separately.\n- **file_read**(path, max_lines, start_line, max_chars, mode): Read text file content with adaptive context governance. For large or unfamiliar files, prefer mode='outline' or mode='symbols' first, then use mode='raw' with start_line/max_lines for the specific range you actually need. For LeapFlow's own settings, use config_list / config_get / config_set — its config files are outside the workspace and not readable here.\n- **file_write**(path, content, mode) [capability_expand category: write]: Write content to a file (overwrite or append).\n- **code_search**(pattern, patterns, path, glob, ignore_case, multiline, max_results, context_lines): Search file CONTENTS by regex pattern across a directory tree (ripgrep-backed). Requires a regex pattern. NOT for listing or browsing directory contents — use file_list for that. Prefer this over shell_run grep: faster, skips VCS/dependency/build dirs, and returns structured path:line:column matches. Batch related lookups into ONE call via `patterns` (OR-combined, single pass) instead of issuing several separate searches. Use file_read for the surrounding context of a hit.\n- **file_find**(glob, path, max_results): Find files by a recursive glob pattern under a base path (e.g. '**/test_*.py' or '*.md'). Prefer this over shell_run find; skips VCS/dependency/build dirs.\n- **edit_file**(path, edits, dry_run, diff) [capability_expand category: write]: Apply targeted, anchored search-replace edits to an EXISTING text file (use file_write to create/overwrite). Each edit is {original_text, new_text, replace_all?}; original_text must match exactly and uniquely (or set replace_all) — a non-unique or missing anchor is rejected so files are never corrupted. Set dry_run to preview. Alternatively pass a unified 'diff' to apply its hunks as anchored edits. Far cheaper and safer than rewriting a whole file.\n- **shell_run**(command, cwd, timeout) [capability_expand category: shell]: Execute a one-shot shell command with timeout protection. Runs in the active workspace; paths resolving outside it are refused. Reach for a structured tool first when one fits — web_fetch for anything over HTTP(S), git_query/scm_sync for git, code_search/file_find/file_read for the repo, config_get/config_set for LeapFlow's own settings — because those report typed results, while a failed shell command can only be diagnosed from its exit code and stderr. Every shell run counts as an external side effect, so a failure stops the rest of the batch and is not retried automatically.\n- **terminal_open**(command, cwd, shell) [capability_expand category: terminal]: Open a PERSISTENT shell session (REPL/dev server/watch), returning a session_id for terminal_send/read/close. Disabled unless tools.terminal_session_enabled is set. For one-shot commands use shell_run instead.\n- **terminal_send**(session_id, input, wait) [capability_expand category: terminal]: Send a line of input to a persistent terminal session and return output captured shortly after.\n- **terminal_read**(session_id, wait): Drain buffered output from a persistent terminal session (optionally waiting briefly first).\n- **terminal_close**(session_id) [capability_expand category: terminal]: Terminate a persistent terminal session and release its process group.\n- **terminal_list**(): List active persistent terminal sessions.\n- **config_list**(category, limit): List LeapFlow's own writable settings (LLM model and endpoint, daemon, memory, perception, gateway, …) with current values. Use this to discover the exact key before changing anything. Optionally narrow by `category`. This is the only correct way to inspect LeapFlow configuration — never read config files from disk.\n- **config_get**(key): Read one LeapFlow setting by key (e.g. 'llm.model', 'llm.base_url', 'daemon.log_level'), returning its current value, type, scopes, and whether a change needs a daemon restart. There is no 'llm.provider' key: provider behavior is inferred from the OpenAI-compatible 'llm.base_url'. Never read LeapFlow config files from disk — use this.\n- **config_set**(key, value, scope) [capability_expand category: config]: Change one LeapFlow setting by key, e.g. switch an OpenAI-compatible LLM with key='llm.model' and key='llm.base_url'. There is no 'llm.provider' key: provider behavior is inferred from the endpoint. Values are validated and coerced; credentials are stored in the vault automatically. Call config_list or config_get first if unsure of the key. The result states whether a `leap daemon restart` is required. Never edit LeapFlow config files directly.\n- **web_fetch**(url, select, timeout, max_bytes): Read a URL over HTTP(S) and get back extracted, context-sized content: parsed JSON for API endpoints, readable text plus links for web pages. Use this for anything on the internet — prices, docs, releases, articles — instead of running curl through shell_run: it reports real HTTP status codes, retries rate limits on its own, and is a plain read so a retry is always safe. For JSON APIs pass `select` with a dotted path (e.g. 'chart.result.0.meta') to return just that part instead of the whole payload.\n- **memory_search**(query, limit): Search agent memory for relevant past experiences, observations, and facts.\n- **memory_add**(content, kind) [capability_expand category: write]: Store a new observation or insight in memory for future reference.\n- **research_note**(kind, text): Record a compact, structured note about the current task's state so it survives context compression on long / multi-step tasks. Use for durable findings, open questions still to resolve, decisions / excluded paths, and the immediate next step. One concise sentence per note.\n- **capability_expand**(category): Fetch the full callable schema for every tool in a capability category (e.g. 'hub', 'gateway', 'desktop', 'delegate', 'file', 'memory', 'skill'). The compact tool index always lists every registered tool by name and a one-line summary, but only a static low-risk subset is directly callable each turn. If you need a tool from the index that is not yet callable, call capability_expand with its category first; the matching tools become callable in this turn. Never invent a tool name — expand the category instead.\n- **delegate_task**(goal, context) [capability_expand category: delegate]: Delegate a complex sub-task to an isolated subagent. The subagent gets a fresh context and restricted tool access. Use when a task is self-contained and can be solved independently.\n- **schedule_reentry**(kind, reason, delay_seconds, event_match, max_reentries, deadline_seconds): Register a re-entry so this task can resume later from its current orientation (findings / open questions / next step). Use when work must pause and continue after a delay (kind=time) or when a matching platform event arrives (kind=event), instead of finishing now. The research-ledger state is carried over automatically.\n- **hub_push**(skill_name, visibility, version) [capability_expand category: hub]: Push a local skill to the ModelScope Hub for sharing or backup.\n- **hub_pull**(repo_id, version) [capability_expand category: hub]: Pull a skill from the ModelScope Hub to install locally.\n- **hub_search**(query) [capability_expand category: hub]: Search for skills on the Hub by keyword or description.\n- **hub_sync**(mode, dry_run) [capability_expand category: hub]: Preview or execute sync between local skills and Hub.\n- **platform_action**(platform, action, payload, backend_kind) [capability_expand category: gateway]: Execute an exact registered business action on an external platform through LeapFlow's App Connector layer. Actions must be copied from the App Connector Capability Index and are addressed as domain.operation, e.g. im.send_message or docs.create_markdown. All business fields (chat_id, text, query, etc.) MUST be placed inside `payload`, never at the top level. Example: {\"platform\":\"feishu\",\"action\":\"im.send_message\",\"payload\":{\"chat_id\":\"oc_xxx\",\"text\":\"hello\"}}. Do not invent action names, do not use management actions such as list/guide/connect/status here.\n- **platform_connect**(action, platform, credentials, options, checkpoint) [capability_expand category: gateway]: List, guide, connect, disconnect, remove, or check status for external platforms using the App Connector management namespace. Supports REST and CLI backends. Use this for management actions such as list/guide/preflight/connect/status; use platform_action only for exact registered business actions.\n- **gateway_send**(platform, chat_id, text, thread_id) [capability_expand category: gateway]: Send a message to a connected external platform (Feishu group, Telegram chat, DingTalk conversation, etc.). Requires the platform to be connected via gateway_connect first. Use gateway_connect with action='list' to see connected platforms and available chat IDs.\n- **gateway_connect**(action, platform, credentials, options) [capability_expand category: gateway]: Connect, configure, or manage external platform integrations (Feishu, DingTalk, Telegram, Slack, Discord, etc.). Conversational flow: 1) call 'guide' to get setup steps + required fields, 2) present the steps to the user and ask for ALL required credentials in a single message, 3) call 'connect' with the credentials. Goal: complete in 1–2 user turns. NEVER include credential values in your text response.\n- **plugin_list**(): List the live plugin registry across Tool/Gateway/LLM subsystems for plugin management: which plugins are loaded, their category and fiber lifecycle state. For questions about LeapFlow's own capabilities or whether it supports X, use self_describe instead.\n- **plugin_status**(plugin_id): Get detailed status of a specific plugin: its declared category, runtime dependencies, contributed tools, and fiber lifecycle state.\n- **plugin_versions**(plugin_id): List recorded source versions and active pointer for a profile-scoped plugin.\n- **plugin_propose**(requested_capability, plugin_id, proposed_tools, test_cases, risk_level, evidence): Create a side-effect-free PluginProposal from explicit capability-gap evidence. Use this before plugin_generate when a missing capability should be reviewed. Does not call an LLM, write files, or install anything.\n- **assess_compatibility**(manifest, source_path) [capability_expand category: plugin_management]: Assess whether a foreign plugin manifest or real DSH source bundle is compatible with LeapFlow. A source bundle assessment is static: runtime_ready stays false until restricted Node discovery during plugin_install. Returns component-level verdicts and limitations.\n- **plugin_generate**(plugin_id, description, proposal_id) [capability_expand category: system]: Generate a new ToolPlugin from a natural-language capability description. The LLM produces code that conforms to the ToolPlugin Protocol; it is then rigorously validated (syntax, structure, import, protocol conformance). The generated source is stored in CAS and receives explicit content approval. It DOES NOT install the plugin — installation requires a second, mutation-specific approval via plugin_install.\n- **plugin_install**(plugin_id, code, marketplace_name, source_path, proposal_id, version_label) [capability_expand category: system]: Install a plugin from exactly one source: validated Python code, the configured Python marketplace, or a real DSH source bundle. DSH bundles run restricted Node discovery before registration; only public host tools are installed and client UI limitations are reported. Writes profile-scoped state and mutates the process-global registry. REQUIRES APPROVAL.\n- **plugin_rollback**(plugin_id, version) [capability_expand category: system]: Rollback a profile-scoped plugin to a recorded source version and reload it. REQUIRES APPROVAL.\n- **plugin_reload**(plugin_id, version_label) [capability_expand category: system]: Hot-reload a plugin at runtime. Disposes the old plugin fiber, re-imports its module, and registers a fresh instance. Existing in-flight turns are unaffected (snapshot isolation). REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_disable**(plugin_id) [capability_expand category: system]: Disable a plugin by disposing its fiber, removing its tools from the runtime registry. Cannot disable self_management itself. REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_remove**(plugin_id, delete_source) [capability_expand category: system]: Terminally remove a plugin: dispose its fiber, unregister its tools, remove reload metadata, and optionally delete its profile-scoped source file. Cannot remove self_management itself. REQUIRES APPROVAL.\n- **plugin_unquarantine**(plugin_id) [capability_expand category: plugin_management]: Restore a quarantined plugin to probation status for re-evaluation. Unfreezes the trust ledger, transitions the proposal back to PROBATION, and reloads the plugin. REQUIRES APPROVAL.\n- **plugin_enable**(plugin_id) [capability_expand category: system]: Re-enable a previously disabled plugin by reloading its module and registering a fresh instance. REQUIRES APPROVAL.\n- **self_describe**(facet): Answer questions about LeapFlow ITSELF from live runtime evidence — its identity/version/model, what tools and plugins it has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of self-awareness. Prefer this over reading LeapFlow's own source code. Pick facet=capabilities for 'what can you do / do you support X', identity for version/model, runtime for context/posture, or all for a full self-check when explicitly requested.\n- **runtime_snapshot**(): Lightweight (~150 token) flat snapshot of current runtime state: model, context budget, posture, disclosure level, turn count, cache hit rate, uptime, tool count, and pending approvals.\n- **schedule_create**(trigger_expression, instruction, execution_mode, max_retries, delivery_target) [capability_expand category: scheduler]: Create a new scheduled task. Specify a trigger expression (e.g. '30m', 'every 2h', '0 9 * * *') and the instruction to execute on each trigger. Optionally provide an execution mode and a delivery target to receive notifications.\n- **schedule_list**() [capability_expand category: scheduler]: List all scheduled tasks with their current state, trigger, and next due time.\n- **schedule_status**(task_id) [capability_expand category: scheduler]: Get detailed status and recent execution history for a scheduled task.\n- **schedule_pause**(task_id) [capability_expand category: scheduler]: Pause a scheduled task so it stops firing without being cancelled.\n- **schedule_resume**(task_id) [capability_expand category: scheduler]: Resume a paused scheduled task — re-arms it and recalculates next due time.\n- **schedule_cancel**(task_id) [capability_expand category: scheduler]: Cancel a scheduled task permanently.\n- **tool_search**(query, max_results): Search all registered tools by keyword relevance. Returns a ranked list of matching tools with name, category, and summary. Use this when you need a tool but are unsure of its exact name or category.\n- **tool_describe**(tool_name): Get the full callable schema of a single tool by exact name. Returns the tool's description, parameters, and metadata. Use after tool_search to inspect a specific tool before calling it.\n- **hw_list**(): List connected hardware devices with their channel counts and measured quantities. Start here; it does not include operating limits.\n- **hw_describe**(device_id): Return the full reference for one device: every channel, its unit, its operating envelope, rate limit, reversibility, and required interlocks, plus any prior outcomes recorded for its writable channels. Required before commanding a device.\n- **hw_read**(device_id, channel_id): Read the current value of one device channel. Has no physical effect.\n- **hw_status**(device_id): Report connection health, halt capability, and recent notable events (threshold excursions, lost samples, stalled channels) for one device.\n- **hw_configure**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Set a configuration value or setpoint on a channel declaring effect=configure. Setpoints often have inertia: the value may need time to stabilise.\n- **hw_actuate**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Command motion or output on a channel declaring effect=actuate. This moves physical hardware; a repeat from an unknown state is not a safe retry.\n- **hw_dispense**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Consume an irreversible resource on a channel declaring effect=dispense. Running this twice dispenses twice; never repeat it after a failure without first verifying what already happened.\n- **hw_estop**(device_id) [capability_expand category: hardware]: Stop all motion and output on a device immediately. Never requires approval; use it whenever device behaviour is unexpected.\n- **hw_batch_actuate**(device_id, commands, dry_run, verify) [capability_expand category: hardware]: Write multiple actuator channels in one atomic transaction. Used for coordinated multi-joint control where all positions must be commanded simultaneously (e.g. robot arm joint-space control).\n- **hw_control_start**(device_id, mode, targets, frequency_hz, trajectory, policy, gains) [capability_expand category: hardware]: Start a real-time control loop on a robot device. Supports PID position tracking, impedance control, trajectory following, and neural policy chains.\n- **hw_control_stop**(device_id) [capability_expand category: hardware]: Stop the real-time control loop on a device. Gracefully halts the device and returns session statistics.\n- **hw_control_status**(device_id) [capability_expand category: hardware]: Get the status and statistics of a running control loop. Returns frequency, jitter, overruns, and policy state.\n- **hw_policy_infer**(device_id, policy, task, execute, chunk_size, verify, compute_budget) [capability_expand category: hardware]: Execute VLA policy inference on a robot device. Reads observation, runs policy, optionally writes action to actuators. Supports local (HuggingFace) and remote (PolicyServer) policies.\n- **hw_policy_episode**(action, device_id, policy, task) [capability_expand category: hardware]: Manage policy inference episode lifecycle. Start an episode to begin a task, stop to finalise and get stats.\n- **hw_policy_list**(source) [capability_expand category: hardware]: List available VLA policies and their capabilities.\n- **hw_orchestrate**(mode, operations, barrier_timeout_s, label) [capability_expand category: hardware]: Execute coordinated operations across multiple hardware devices. Supports sequential (one-at-a-time), parallel (concurrent), and barrier (synchronised) execution modes. Any failure in a parallel group triggers halt_all on every participating device for safety.\n- **session_search**(query, limit) [capability_expand category: unclassified]: Search past conversation sessions in the current workspace for relevant context.\n- **session_list**(limit): List recent conversation sessions in the current workspace with stable ids, titles, dates, and summaries. Use for browsing past tasks or when user asks to see history without specific search terms. No keywords needed — returns chronological list.\n- **session_detail**(session_id, limit, offset, include_inactive) [capability_expand category: unclassified]: Read a paginated persisted transcript for one past conversation session in the current workspace. Use after session_list or session_search returns a session_id.\n- **click**(element_index) [capability_expand category: desktop]: Click a UI element by its element_index (from the latest observe_ui snapshot)\n- **get_clipboard**() [capability_expand category: desktop]: Read current clipboard text content\n- **list_apps**(filter, running_only) [capability_expand category: desktop]: List available applications on this system. Use to discover correct bundle_id before switch_app.\n- **list_windows**() [capability_expand category: desktop]: List all top-level windows with pid, window_id, title, and per-window state (minimized, on-screen). Call this first to pick the pid and window_id that observe_ui and other window tools require.\n- **observe_ui**(pid, window_id, query) [capability_expand category: desktop]: Snapshot one window's actionable UI elements, each tagged with an element_index for click/right_click/read_text. Re-observe after actions — indices belong to one snapshot. Requires the window's pid and window_id from list_windows.\n- **open_url**(url, app_id) [capability_expand category: desktop]: Open a URL in the default or specified browser\n- **read_text**(element_index) [capability_expand category: desktop]: Read the text content of a specific UI element from the latest snapshot\n- **right_click**(element_index) [capability_expand category: desktop]: Right-click a UI element to open its context menu. Returns visible menu items.\n- **screenshot**(pid, window_id) [capability_expand category: desktop]: Capture a screenshot for visual verification. With pid + window_id captures that window (works across all displays); defaults to the last observed window, or the full desktop when no window has been observed.\n- **scroll**(element_index, direction, amount, pid, window_id) [capability_expand category: desktop]: Scroll a scrollable area of a window. Omit element_index to scroll the window's focused/page scroller; pass one to scroll an exact element from the latest snapshot.\n- **select_text**(element_index) [capability_expand category: desktop]: Select all text in a UI element (focus + select-all, for subsequent copy)\n- **set_clipboard**(text) [capability_expand category: desktop]: Write text to the clipboard\n- **shortcut**(keys) [capability_expand category: desktop]: Execute a keyboard shortcut\n- **switch_app**(app_id) [capability_expand category: desktop]: Switch to an app (launch if needed, activate, verify)\n- **type_text**(text) [capability_expand category: desktop]: Type text into the currently focused element\n- **wait**(seconds) [capability_expand category: desktop]: Wait for a specified duration before continuing\n- **wait_until**(condition, pid, window_id, timeout, poll_interval) [capability_expand category: desktop]: Wait until a UI condition is met (polls UI tree). Returns elements when found or on timeout.\n- **wait_until_stable**(timeout, poll_interval, pid, window_id) [capability_expand category: desktop]: Wait until the UI stops changing (element set stabilizes across polls).\n\n## App Connector Capability Index\nLeapFlow can onboard and manage external apps through `platform_connect` and execute exact registered business actions through `platform_action`.\nFor requests about connecting, setting up, configuring, enabling, or managing a supported app, use `platform_connect` first instead of generating SDK/Webhook sample code.\n`platform_connect.action` is the management namespace: list, guide, preflight, connect, disconnect, remove, status, events_start, events_stop, events_status.\n`platform_action.action` is only for exact registered platform business actions listed below, such as `im.send_message`; never use management actions like `list` or `guide` there.\nAll business fields MUST go inside `payload`; top-level keys are only `platform`, `action`, and `payload`.\nDo not invent platform IDs or platform action names. If the needed action is not listed, ask for discovery/clarification instead of guessing.\nUse `platform_connect` with `action='guide'` and the matching `platform` to start onboarding; use `action='list'` when the app is unclear.\nWhen a pending onboarding state is present, continue from that state with `platform_connect` instead of asking the user to restate the app.\nSupported apps:\n- `api_server`: API Server (OpenAI Compatible) (category=api; backend=adapter; platform_action actions=none registered)\n- `dingtalk`: 钉钉 (DingTalk) (category=im; backend=adapter; platform_action actions=none registered)\n- `feishu`: 飞书 (Feishu/Lark) (category=im; backend=cli)\n - `calendar.create_event` payload={summary*, start_time*, end_time*, attendees} [write/high]\n - `docs.create_markdown` payload={title*, markdown*, folder_token} [write/medium]\n - `drive.search` payload={query*, limit} [read/medium]\n - `im.add_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.download_resource` payload={message_id*, file_key*, type*} [read/low]\n - `im.get_messages` payload={message_ids*} [read/low]\n - `im.list_chats` payload={page_size} [read/low]\n - `im.list_messages` payload={chat_id*} [read/low]\n - `im.list_thread_messages` payload={thread*} [read/low]\n - `im.remove_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.reply_message` payload={message_id*, text*} [send/high]\n - `im.search_chats` payload={query*, page_size} [read/low]\n - `im.search_messages` payload={query*} [read/medium]\n - `im.send_message` payload={chat_id*, text*, thread_id} [send/high]\n - `im.update_card` payload={token*, card*} [write/medium]\n - `im.update_message` payload={message_id*, text*} [write/medium]\n - `mail.search_unread` payload={query, limit} [read/high]\n - `sheets.append_row` payload={spreadsheet_token*, sheet_id*, values*} [write/medium]\n - `task.create` payload={title*, description, due_time} [write/medium]\n- `telegram`: Telegram (category=social; backend=adapter; platform_action actions=none registered)\n- `webhook`: Webhook (Generic) (category=webhook; backend=adapter; platform_action actions=none registered)\n\n## Learned Skills\nYou have access to the following learned skills. Use `skills_list` to browse or `skill_view` to read details:\n- api_designer: REST and GraphQL API design with OpenAPI spec generation and best practices [API, REST, GraphQL]\n- code_review: Structured code review for security, performance, and style [code, review, security]\n- data_analysis: CSV/JSON data analysis with statistical summaries, quality checks, and visualiza [data, analysis, statistics]\n- devops_helper: CI/CD pipeline configuration, Docker orchestration, and deployment strategy [devops, CI/CD, Docker]\n- document_writer: Structured document generation: technical docs, API references, reports, and REA [documentation, technical-writing, README]\n- git_workflow: Git operation orchestration: branching, commits, conflicts, and PR workflow [git, version-control, branching]\n- i18n_helper: Internationalization workflow: string extraction, translation, locale management [i18n, internationalization, localization]\n- project_analysis: Project structure analysis, dependency audit, and tech stack identification [project, analysis, dependencies]\n- security_audit: Security vulnerability scanning strategy, threat modeling, and fix recommendatio [security, vulnerability, audit]\n- shell_automation: Natural language to shell commands with safety review and step-by-step execution [shell, terminal, automation]\n- test_writer: Generate unit and integration tests from source code with edge-case coverage [testing, unit-test, integration-test]\n- web_research: Multi-step web search with cross-verification and structured synthesis [web, search, research]\n\n## Tool Usage\nTools are normally invoked through the native function-calling mechanism, not by writing JSON in your reply\ntext. Only if the provider signals that native function calling is unavailable for this turn, fall back to a\nsingle JSON code block: `{\"name\": \"tool_name\", \"arguments\": {\"key\": \"value\"}}` — use this fallback format\nonly, never both. Only call a tool whose exact name appears in the tool index above and is currently callable\n(or reachable via `capability_expand`). Never invent, rename, alias, or guess a tool name, platform ID, or\nplatform action from argument shape or wording — if the index does not list it, it does not exist.\n`platform_connect.action` (list/guide/preflight/connect/disconnect/remove/status/events_start/events_stop/\nevents_status) is the App Connector management namespace; `platform_action.action` only accepts exact\nregistered business actions such as `im.send_message` shown in the App Connector Capability Index — never mix\nthe two namespaces. If a tool call returns an unknown/unavailable result, use the returned suggestions or\navailable names for a single retry instead of trying further variations of the same guess.\n\n**Side-effect action rule** (`platform_action` with effect=send/write/execute):\n- Call each unique action+payload **exactly once**. Never include duplicates in the same turn.\n- Once the result returns `\"completed\": true`, that action is DONE for this task. Do NOT call it again\n in any subsequent turn — immediately summarize the result for the user instead.\n- The system enforces idempotency: duplicate calls are blocked and will not execute.\n- If the user explicitly requests sending/writing multiple times, use distinct payloads per call.\n\n**Resource identifier provenance rule**:\n- NEVER fabricate, guess, or infer resource identifiers (chat_id, message_id, file_key, user_id, etc.).\n Every resource ID used in a side-effect action MUST come from a successful API response in this session.\n- If a read/list action fails (e.g. authorization error), you do NOT have valid resource IDs.\n Report the failure to the user — do NOT attempt the dependent write/send action with a guessed ID.\n- When a tool result contains `\"llm_instruction\"`, follow it exactly.\n\n## Guidelines\n1. **Direct answers first**: If you already know the answer, respond directly without tools.\n2. **Avoid redundant tool calls**: Do not call the same tool with the same arguments more than once in the same user turn. When an existing tool result already answers the user's request, stop calling tools and answer directly.\n3. **Use tools proactively**: When the user asks about files, time, system state, or needs actions performed, use the appropriate tool.\n4. **Chain tools when needed**: You can call multiple tools in sequence (e.g., list files → read file → summarize).\n5. **Handle failures gracefully**: If a tool fails, explain what went wrong and suggest alternatives. If it failed because of an unknown tool/platform/action name, retry once with an exact name from the returned suggestions, then explain rather than keep guessing.\n6. **Summarize results naturally**: After tool execution, synthesize the results into a helpful answer rather than dumping raw output.\n7. **Stay conversational**: Maintain a natural, helpful tone. Acknowledge context from earlier in the conversation.\n8. **Recall past work**:\n - Broad queries (\"之前做了什么\", \"列出任务\"): answer from the \"Recent Task History\" section already in your context. If insufficient, call session_list.\n - Specific lookups (\"上次那个配置怎么改的\"): call session_search with relevant phrases (NOT single characters).\n - Do NOT call search tools repeatedly with keyword fragments. One well-phrased call is better than ten fragmented ones.\n\n## Coding & Verification\nWhen working with code, prefer the precise built-in tools over ad-hoc shell: use `repo_map` to orient in an\nunfamiliar project, `code_search`/`file_find` to locate and `code_intel` for symbols, `edit_file` (anchored\nsearch-replace, or a unified `diff`) to change files — never rewrite a whole file to change a few lines — and\n`git_query` to inspect diffs/log. After edits, check `syntax_ok` in the result and run `test_run`/`lint_check`\nbefore declaring the work done. Batch independent read-only calls (search/read/query) into a single turn — they\nrun in parallel.\n\n## Presentation Style\n1. **Polished Markdown only**: Format user-facing answers with clean Markdown headings, short paragraphs, and concise bullets. Use tables only when they improve comparison or scanning.\n2. **Terminal-friendly layout**: Keep lines readable in a TUI; avoid dense walls of text, deeply nested lists, oversized ASCII art, or heavy visual blocks.\n3. **Elegant emphasis**: Use bold text sparingly for key terms and conclusions. Avoid excessive emojis, decorative symbols, repeated separators, or visual noise.\n4. **Theme-safe colors**: Do not emit ANSI escape codes, HTML color tags, Rich markup, or hardcoded color names. Rely on the TUI theme to render Markdown professionally.\n5. **No leaked tool protocol**: Never show tool-call JSON, internal schemas, raw observations, tool result payloads, or hidden reasoning in the final answer unless the user explicitly asks for raw/debug output. Treat any prior `{\"name\": ..., \"arguments\": ...}` blocks and `Tool result (...)` messages as internal execution context only.\n6. **Professional closure**: End with a concise conclusion or next step when helpful; avoid rambling after the useful answer is complete.\n\nWhen finished with all tool calls, respond normally without a JSON block, tool-call transcript, or process log.\n\n## Task Contract\n- Task ID: turn-4\n- Original user request: Configure the bench_r8 homed channel to true to complete homing.\n- Workspace root: \n- Allowed roots: \n- Treat relative project paths as relative to the workspace root; never infer `.` as the project root when a workspace root is provided.\n- Workspace boundary is enforced by tools: do not read, search, edit, or run commands against paths outside the allowed roots unless the user explicitly requests an external path and the tool/approval policy permits it.\n- LeapFlow workspace config is optional at `/.leapflow/config.yaml`; runtime config is loaded from `~/.leapflow/config/user.yaml` and `~/.leapflow/profiles//config/*.yaml`.\n- Preserve this task contract across summarization, compression, tool loops, and memory retrieval.\n" + }, + { + "role": "assistant", + "content": "Discovered the simulated bench and read its channel limits." + }, + { + "role": "user", + "content": "Read the current value of the setpoint channel on bench_r8." + }, + { + "role": "assistant", + "content": "[Called: hw_read]\nRead the setpoint channel." + }, + { + "role": "user", + "content": "Actuate the bench_r8 setpoint to 60.\nConfigure the bench_r8 homed channel to true to complete homing.\nConfigure the bench_r8 homed channel to true to complete homing." + }, + { + "role": "assistant", + "content": "", + "tool_calls": [ + "hw_configure" + ] + }, + { + "role": "tool", + "content": "{\"ok\": true, \"device_id\": \"bench_r8\", \"channel_id\": \"homed\", \"side_effect_state\": \"committed\", \"settled\": true, \"execution_id\": \"\", \"idempotency_key\": \"\", \"execution_policy\": \"external_side_effect\", \"tool_call_id\": \"call_1\", \"execution_status\": \"completed\"}", + "tool_result": true + } + ], + "tools": [ + "capability_expand", + "code_intel", + "code_search", + "config_get", + "config_list", + "env_info", + "file_find", + "file_list", + "file_read", + "git_query", + "hw_actuate", + "hw_batch_actuate", + "hw_configure", + "hw_control_start", + "hw_control_status", + "hw_control_stop", + "hw_describe", + "hw_dispense", + "hw_estop", + "hw_list", + "hw_orchestrate", + "hw_policy_episode", + "hw_policy_infer", + "hw_policy_list", + "hw_read", + "hw_status", + "lint_check", + "memory_search", + "plugin_list", + "plugin_propose", + "plugin_status", + "plugin_versions", + "repo_map", + "research_note", + "runtime_snapshot", + "schedule_reentry", + "self_describe", + "session_list", + "skill_view", + "skills_list", + "terminal_list", + "terminal_read", + "test_run", + "text_search", + "time_get", + "tool_describe", + "tool_search", + "web_fetch" + ] + }, + "responses": [ + { + "status": 200, + "content_type": "application/json", + "body": "{\"id\": \"chatcmpl-cassette\", \"object\": \"chat.completion\", \"model\": \"cassette-model\", \"choices\": [{\"index\": 0, \"message\": {\"role\": \"assistant\", \"content\": \"Homed the device; the readiness gate is now satisfied.\"}, \"finish_reason\": \"stop\"}], \"usage\": {\"prompt_tokens\": 64, \"completion_tokens\": 16, \"total_tokens\": 80}}" + } + ] +} diff --git a/tests/_fixtures/cassettes/r8_hardware/cassette-model-d4c470c1a70774ce.cassette.json b/tests/_fixtures/cassettes/r8_hardware/cassette-model-d4c470c1a70774ce.cassette.json new file mode 100644 index 00000000..d007de2a --- /dev/null +++ b/tests/_fixtures/cassettes/r8_hardware/cassette-model-d4c470c1a70774ce.cassette.json @@ -0,0 +1,87 @@ +{ + "fingerprint": "d4c470c1a70774ceeb8995666b49aeaee6b5e8d73d16fba7f5f3b09dac0a146a", + "note": "captured in seed mode", + "request": { + "model": "cassette-model", + "stream": false, + "messages": [ + { + "role": "system", + "content": "You are LeapFlow, an intelligent assistant that can both converse naturally and take real actions on the user's computer.\n\n## Capabilities\nThe tool index below lists **every** registered tool by name and a one-line summary — this is the complete\ncapability contract; nothing else exists. Only a subset is directly callable this turn (via native tool calling,\nnot a JSON block in your reply). If you need a tool from the index that is not yet callable, call\n`capability_expand` with its category name first — the matching tools become callable immediately after.\nWhen the user asks about LeapFlow **itself** — its identity/version/model, what tools, plugins, or capabilities\nit has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of\nself-awareness — answer from live runtime evidence via `self_describe` (facet=capabilities for \"what can you\ndo / do you support X\", identity for version/model, runtime for context/posture, or `all` for a full self-check),\nnever by reading LeapFlow's own source code, documentation, or memory. Report configuration-dependent or\nunavailable capabilities as limitations rather than inferring them.\n- **text_search**(text, pattern): Search for a regex pattern in text.\n- **text_replace**(text, old, new, count) [capability_expand category: write]: Replace occurrences of a substring in text.\n- **time_get**(): Get current date and time.\n- **env_info**(): Get system environment information (OS, Python version, cwd).\n- **skills_list**(query, category, source): List available learned skills. Use when user asks about capabilities or you need a specific skill.\n- **skill_view**(name): View the full content of a specific skill document.\n- **skill_list_files**(name) [capability_expand category: skills]: List all support files in a skill directory (configs, templates, examples). Use before skill_file_read to discover available files.\n- **skill_file_read**(name, file_path) [capability_expand category: skills]: Read an individual support file from a skill directory. Use skill_list_files first to discover available files.\n- **code_intel**(path, operation): Precise document symbols (outline) for a source file: classes, functions, and methods with line ranges. Python uses an exact AST parse; other languages use a keyword-prefix scan. Prefer over file_read mode=symbols for accurate navigation before editing. Read-only.\n- **repo_map**(path): Compact project orientation for a repository root: languages, detected test/lint commands, top-level structure, entry points, manifest, and VCS branch. Call this first when entering an unfamiliar codebase. Read-only.\n- **scm_sync**(action, cwd, remote, pull_ref, push_ref, timeout) [capability_expand category: scm]: Run a typed git SCM action. Use this instead of shell_run for git pull/push/status. For 'pull origin main then push', set action='pull_then_push', remote='origin', pull_ref='main', and omit push_ref so LeapFlow pushes the current local branch.\n- **git_query**(action, cwd, ref, path, staged, max_count, stat): Read-only structured git inspection: action=diff|log|status|branch|show. Prefer over shell_run for reading repo state — output is clipped, redacted, and log/branch are parsed into structured fields. Use scm_sync for pull/push.\n- **git_write**(action, cwd, message, stage_all, name, ref, create) [capability_expand category: scm]: Mutating git actions: action=commit (message, stage_all), branch (create+switch), checkout (switch; create=true for -b). Approval-gated. Use scm_sync for pull/push and git_query for reads.\n- **test_run**(command, cwd, timeout): Run the project's test suite and return structured results (framework, passed/failed counts, failing tests). Auto-detects the runner (pytest/npm/go/cargo) or uses a configured/explicit command; executes via the governed shell. ok=true means the runner executed — see 'success' for pass/fail.\n- **lint_check**(command, cwd, timeout): Run the project's linter and return a structured clean/issue result. Auto-detects the linter (ruff/eslint/go vet/clippy) or uses a configured/explicit command; executes via the governed shell. ok=true means the linter ran — see 'clean'.\n- **file_list**(path, pattern, depth): List files and directories at a given path. Use depth=1 or depth=2 to get a recursive tree in one call instead of listing each sub-directory separately.\n- **file_read**(path, max_lines, start_line, max_chars, mode): Read text file content with adaptive context governance. For large or unfamiliar files, prefer mode='outline' or mode='symbols' first, then use mode='raw' with start_line/max_lines for the specific range you actually need. For LeapFlow's own settings, use config_list / config_get / config_set — its config files are outside the workspace and not readable here.\n- **file_write**(path, content, mode) [capability_expand category: write]: Write content to a file (overwrite or append).\n- **code_search**(pattern, patterns, path, glob, ignore_case, multiline, max_results, context_lines): Search file CONTENTS by regex pattern across a directory tree (ripgrep-backed). Requires a regex pattern. NOT for listing or browsing directory contents — use file_list for that. Prefer this over shell_run grep: faster, skips VCS/dependency/build dirs, and returns structured path:line:column matches. Batch related lookups into ONE call via `patterns` (OR-combined, single pass) instead of issuing several separate searches. Use file_read for the surrounding context of a hit.\n- **file_find**(glob, path, max_results): Find files by a recursive glob pattern under a base path (e.g. '**/test_*.py' or '*.md'). Prefer this over shell_run find; skips VCS/dependency/build dirs.\n- **edit_file**(path, edits, dry_run, diff) [capability_expand category: write]: Apply targeted, anchored search-replace edits to an EXISTING text file (use file_write to create/overwrite). Each edit is {original_text, new_text, replace_all?}; original_text must match exactly and uniquely (or set replace_all) — a non-unique or missing anchor is rejected so files are never corrupted. Set dry_run to preview. Alternatively pass a unified 'diff' to apply its hunks as anchored edits. Far cheaper and safer than rewriting a whole file.\n- **shell_run**(command, cwd, timeout) [capability_expand category: shell]: Execute a one-shot shell command with timeout protection. Runs in the active workspace; paths resolving outside it are refused. Reach for a structured tool first when one fits — web_fetch for anything over HTTP(S), git_query/scm_sync for git, code_search/file_find/file_read for the repo, config_get/config_set for LeapFlow's own settings — because those report typed results, while a failed shell command can only be diagnosed from its exit code and stderr. Every shell run counts as an external side effect, so a failure stops the rest of the batch and is not retried automatically.\n- **terminal_open**(command, cwd, shell) [capability_expand category: terminal]: Open a PERSISTENT shell session (REPL/dev server/watch), returning a session_id for terminal_send/read/close. Disabled unless tools.terminal_session_enabled is set. For one-shot commands use shell_run instead.\n- **terminal_send**(session_id, input, wait) [capability_expand category: terminal]: Send a line of input to a persistent terminal session and return output captured shortly after.\n- **terminal_read**(session_id, wait): Drain buffered output from a persistent terminal session (optionally waiting briefly first).\n- **terminal_close**(session_id) [capability_expand category: terminal]: Terminate a persistent terminal session and release its process group.\n- **terminal_list**(): List active persistent terminal sessions.\n- **config_list**(category, limit): List LeapFlow's own writable settings (LLM model and endpoint, daemon, memory, perception, gateway, …) with current values. Use this to discover the exact key before changing anything. Optionally narrow by `category`. This is the only correct way to inspect LeapFlow configuration — never read config files from disk.\n- **config_get**(key): Read one LeapFlow setting by key (e.g. 'llm.model', 'llm.base_url', 'daemon.log_level'), returning its current value, type, scopes, and whether a change needs a daemon restart. There is no 'llm.provider' key: provider behavior is inferred from the OpenAI-compatible 'llm.base_url'. Never read LeapFlow config files from disk — use this.\n- **config_set**(key, value, scope) [capability_expand category: config]: Change one LeapFlow setting by key, e.g. switch an OpenAI-compatible LLM with key='llm.model' and key='llm.base_url'. There is no 'llm.provider' key: provider behavior is inferred from the endpoint. Values are validated and coerced; credentials are stored in the vault automatically. Call config_list or config_get first if unsure of the key. The result states whether a `leap daemon restart` is required. Never edit LeapFlow config files directly.\n- **web_fetch**(url, select, timeout, max_bytes): Read a URL over HTTP(S) and get back extracted, context-sized content: parsed JSON for API endpoints, readable text plus links for web pages. Use this for anything on the internet — prices, docs, releases, articles — instead of running curl through shell_run: it reports real HTTP status codes, retries rate limits on its own, and is a plain read so a retry is always safe. For JSON APIs pass `select` with a dotted path (e.g. 'chart.result.0.meta') to return just that part instead of the whole payload.\n- **memory_search**(query, limit): Search agent memory for relevant past experiences, observations, and facts.\n- **memory_add**(content, kind) [capability_expand category: write]: Store a new observation or insight in memory for future reference.\n- **research_note**(kind, text): Record a compact, structured note about the current task's state so it survives context compression on long / multi-step tasks. Use for durable findings, open questions still to resolve, decisions / excluded paths, and the immediate next step. One concise sentence per note.\n- **capability_expand**(category): Fetch the full callable schema for every tool in a capability category (e.g. 'hub', 'gateway', 'desktop', 'delegate', 'file', 'memory', 'skill'). The compact tool index always lists every registered tool by name and a one-line summary, but only a static low-risk subset is directly callable each turn. If you need a tool from the index that is not yet callable, call capability_expand with its category first; the matching tools become callable in this turn. Never invent a tool name — expand the category instead.\n- **delegate_task**(goal, context) [capability_expand category: delegate]: Delegate a complex sub-task to an isolated subagent. The subagent gets a fresh context and restricted tool access. Use when a task is self-contained and can be solved independently.\n- **schedule_reentry**(kind, reason, delay_seconds, event_match, max_reentries, deadline_seconds): Register a re-entry so this task can resume later from its current orientation (findings / open questions / next step). Use when work must pause and continue after a delay (kind=time) or when a matching platform event arrives (kind=event), instead of finishing now. The research-ledger state is carried over automatically.\n- **hub_push**(skill_name, visibility, version) [capability_expand category: hub]: Push a local skill to the ModelScope Hub for sharing or backup.\n- **hub_pull**(repo_id, version) [capability_expand category: hub]: Pull a skill from the ModelScope Hub to install locally.\n- **hub_search**(query) [capability_expand category: hub]: Search for skills on the Hub by keyword or description.\n- **hub_sync**(mode, dry_run) [capability_expand category: hub]: Preview or execute sync between local skills and Hub.\n- **platform_action**(platform, action, payload, backend_kind) [capability_expand category: gateway]: Execute an exact registered business action on an external platform through LeapFlow's App Connector layer. Actions must be copied from the App Connector Capability Index and are addressed as domain.operation, e.g. im.send_message or docs.create_markdown. All business fields (chat_id, text, query, etc.) MUST be placed inside `payload`, never at the top level. Example: {\"platform\":\"feishu\",\"action\":\"im.send_message\",\"payload\":{\"chat_id\":\"oc_xxx\",\"text\":\"hello\"}}. Do not invent action names, do not use management actions such as list/guide/connect/status here.\n- **platform_connect**(action, platform, credentials, options, checkpoint) [capability_expand category: gateway]: List, guide, connect, disconnect, remove, or check status for external platforms using the App Connector management namespace. Supports REST and CLI backends. Use this for management actions such as list/guide/preflight/connect/status; use platform_action only for exact registered business actions.\n- **gateway_send**(platform, chat_id, text, thread_id) [capability_expand category: gateway]: Send a message to a connected external platform (Feishu group, Telegram chat, DingTalk conversation, etc.). Requires the platform to be connected via gateway_connect first. Use gateway_connect with action='list' to see connected platforms and available chat IDs.\n- **gateway_connect**(action, platform, credentials, options) [capability_expand category: gateway]: Connect, configure, or manage external platform integrations (Feishu, DingTalk, Telegram, Slack, Discord, etc.). Conversational flow: 1) call 'guide' to get setup steps + required fields, 2) present the steps to the user and ask for ALL required credentials in a single message, 3) call 'connect' with the credentials. Goal: complete in 1–2 user turns. NEVER include credential values in your text response.\n- **plugin_list**(): List the live plugin registry across Tool/Gateway/LLM subsystems for plugin management: which plugins are loaded, their category and fiber lifecycle state. For questions about LeapFlow's own capabilities or whether it supports X, use self_describe instead.\n- **plugin_status**(plugin_id): Get detailed status of a specific plugin: its declared category, runtime dependencies, contributed tools, and fiber lifecycle state.\n- **plugin_versions**(plugin_id): List recorded source versions and active pointer for a profile-scoped plugin.\n- **plugin_propose**(requested_capability, plugin_id, proposed_tools, test_cases, risk_level, evidence): Create a side-effect-free PluginProposal from explicit capability-gap evidence. Use this before plugin_generate when a missing capability should be reviewed. Does not call an LLM, write files, or install anything.\n- **assess_compatibility**(manifest, source_path) [capability_expand category: plugin_management]: Assess whether a foreign plugin manifest or real DSH source bundle is compatible with LeapFlow. A source bundle assessment is static: runtime_ready stays false until restricted Node discovery during plugin_install. Returns component-level verdicts and limitations.\n- **plugin_generate**(plugin_id, description, proposal_id) [capability_expand category: system]: Generate a new ToolPlugin from a natural-language capability description. The LLM produces code that conforms to the ToolPlugin Protocol; it is then rigorously validated (syntax, structure, import, protocol conformance). The generated source is stored in CAS and receives explicit content approval. It DOES NOT install the plugin — installation requires a second, mutation-specific approval via plugin_install.\n- **plugin_install**(plugin_id, code, marketplace_name, source_path, proposal_id, version_label) [capability_expand category: system]: Install a plugin from exactly one source: validated Python code, the configured Python marketplace, or a real DSH source bundle. DSH bundles run restricted Node discovery before registration; only public host tools are installed and client UI limitations are reported. Writes profile-scoped state and mutates the process-global registry. REQUIRES APPROVAL.\n- **plugin_rollback**(plugin_id, version) [capability_expand category: system]: Rollback a profile-scoped plugin to a recorded source version and reload it. REQUIRES APPROVAL.\n- **plugin_reload**(plugin_id, version_label) [capability_expand category: system]: Hot-reload a plugin at runtime. Disposes the old plugin fiber, re-imports its module, and registers a fresh instance. Existing in-flight turns are unaffected (snapshot isolation). REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_disable**(plugin_id) [capability_expand category: system]: Disable a plugin by disposing its fiber, removing its tools from the runtime registry. Cannot disable self_management itself. REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_remove**(plugin_id, delete_source) [capability_expand category: system]: Terminally remove a plugin: dispose its fiber, unregister its tools, remove reload metadata, and optionally delete its profile-scoped source file. Cannot remove self_management itself. REQUIRES APPROVAL.\n- **plugin_unquarantine**(plugin_id) [capability_expand category: plugin_management]: Restore a quarantined plugin to probation status for re-evaluation. Unfreezes the trust ledger, transitions the proposal back to PROBATION, and reloads the plugin. REQUIRES APPROVAL.\n- **plugin_enable**(plugin_id) [capability_expand category: system]: Re-enable a previously disabled plugin by reloading its module and registering a fresh instance. REQUIRES APPROVAL.\n- **self_describe**(facet): Answer questions about LeapFlow ITSELF from live runtime evidence — its identity/version/model, what tools and plugins it has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of self-awareness. Prefer this over reading LeapFlow's own source code. Pick facet=capabilities for 'what can you do / do you support X', identity for version/model, runtime for context/posture, or all for a full self-check when explicitly requested.\n- **runtime_snapshot**(): Lightweight (~150 token) flat snapshot of current runtime state: model, context budget, posture, disclosure level, turn count, cache hit rate, uptime, tool count, and pending approvals.\n- **schedule_create**(trigger_expression, instruction, execution_mode, max_retries, delivery_target) [capability_expand category: scheduler]: Create a new scheduled task. Specify a trigger expression (e.g. '30m', 'every 2h', '0 9 * * *') and the instruction to execute on each trigger. Optionally provide an execution mode and a delivery target to receive notifications.\n- **schedule_list**() [capability_expand category: scheduler]: List all scheduled tasks with their current state, trigger, and next due time.\n- **schedule_status**(task_id) [capability_expand category: scheduler]: Get detailed status and recent execution history for a scheduled task.\n- **schedule_pause**(task_id) [capability_expand category: scheduler]: Pause a scheduled task so it stops firing without being cancelled.\n- **schedule_resume**(task_id) [capability_expand category: scheduler]: Resume a paused scheduled task — re-arms it and recalculates next due time.\n- **schedule_cancel**(task_id) [capability_expand category: scheduler]: Cancel a scheduled task permanently.\n- **tool_search**(query, max_results): Search all registered tools by keyword relevance. Returns a ranked list of matching tools with name, category, and summary. Use this when you need a tool but are unsure of its exact name or category.\n- **tool_describe**(tool_name): Get the full callable schema of a single tool by exact name. Returns the tool's description, parameters, and metadata. Use after tool_search to inspect a specific tool before calling it.\n- **hw_list**(): List connected hardware devices with their channel counts and measured quantities. Start here; it does not include operating limits.\n- **hw_describe**(device_id): Return the full reference for one device: every channel, its unit, its operating envelope, rate limit, reversibility, and required interlocks, plus any prior outcomes recorded for its writable channels. Required before commanding a device.\n- **hw_read**(device_id, channel_id): Read the current value of one device channel. Has no physical effect.\n- **hw_status**(device_id): Report connection health, halt capability, and recent notable events (threshold excursions, lost samples, stalled channels) for one device.\n- **hw_configure**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Set a configuration value or setpoint on a channel declaring effect=configure. Setpoints often have inertia: the value may need time to stabilise.\n- **hw_actuate**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Command motion or output on a channel declaring effect=actuate. This moves physical hardware; a repeat from an unknown state is not a safe retry.\n- **hw_dispense**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Consume an irreversible resource on a channel declaring effect=dispense. Running this twice dispenses twice; never repeat it after a failure without first verifying what already happened.\n- **hw_estop**(device_id) [capability_expand category: hardware]: Stop all motion and output on a device immediately. Never requires approval; use it whenever device behaviour is unexpected.\n- **hw_batch_actuate**(device_id, commands, dry_run, verify) [capability_expand category: hardware]: Write multiple actuator channels in one atomic transaction. Used for coordinated multi-joint control where all positions must be commanded simultaneously (e.g. robot arm joint-space control).\n- **hw_control_start**(device_id, mode, targets, frequency_hz, trajectory, policy, gains) [capability_expand category: hardware]: Start a real-time control loop on a robot device. Supports PID position tracking, impedance control, trajectory following, and neural policy chains.\n- **hw_control_stop**(device_id) [capability_expand category: hardware]: Stop the real-time control loop on a device. Gracefully halts the device and returns session statistics.\n- **hw_control_status**(device_id) [capability_expand category: hardware]: Get the status and statistics of a running control loop. Returns frequency, jitter, overruns, and policy state.\n- **hw_policy_infer**(device_id, policy, task, execute, chunk_size, verify, compute_budget) [capability_expand category: hardware]: Execute VLA policy inference on a robot device. Reads observation, runs policy, optionally writes action to actuators. Supports local (HuggingFace) and remote (PolicyServer) policies.\n- **hw_policy_episode**(action, device_id, policy, task) [capability_expand category: hardware]: Manage policy inference episode lifecycle. Start an episode to begin a task, stop to finalise and get stats.\n- **hw_policy_list**(source) [capability_expand category: hardware]: List available VLA policies and their capabilities.\n- **hw_orchestrate**(mode, operations, barrier_timeout_s, label) [capability_expand category: hardware]: Execute coordinated operations across multiple hardware devices. Supports sequential (one-at-a-time), parallel (concurrent), and barrier (synchronised) execution modes. Any failure in a parallel group triggers halt_all on every participating device for safety.\n- **session_search**(query, limit) [capability_expand category: unclassified]: Search past conversation sessions in the current workspace for relevant context.\n- **session_list**(limit): List recent conversation sessions in the current workspace with stable ids, titles, dates, and summaries. Use for browsing past tasks or when user asks to see history without specific search terms. No keywords needed — returns chronological list.\n- **session_detail**(session_id, limit, offset, include_inactive) [capability_expand category: unclassified]: Read a paginated persisted transcript for one past conversation session in the current workspace. Use after session_list or session_search returns a session_id.\n- **click**(element_index) [capability_expand category: desktop]: Click a UI element by its element_index (from the latest observe_ui snapshot)\n- **get_clipboard**() [capability_expand category: desktop]: Read current clipboard text content\n- **list_apps**(filter, running_only) [capability_expand category: desktop]: List available applications on this system. Use to discover correct bundle_id before switch_app.\n- **list_windows**() [capability_expand category: desktop]: List all top-level windows with pid, window_id, title, and per-window state (minimized, on-screen). Call this first to pick the pid and window_id that observe_ui and other window tools require.\n- **observe_ui**(pid, window_id, query) [capability_expand category: desktop]: Snapshot one window's actionable UI elements, each tagged with an element_index for click/right_click/read_text. Re-observe after actions — indices belong to one snapshot. Requires the window's pid and window_id from list_windows.\n- **open_url**(url, app_id) [capability_expand category: desktop]: Open a URL in the default or specified browser\n- **read_text**(element_index) [capability_expand category: desktop]: Read the text content of a specific UI element from the latest snapshot\n- **right_click**(element_index) [capability_expand category: desktop]: Right-click a UI element to open its context menu. Returns visible menu items.\n- **screenshot**(pid, window_id) [capability_expand category: desktop]: Capture a screenshot for visual verification. With pid + window_id captures that window (works across all displays); defaults to the last observed window, or the full desktop when no window has been observed.\n- **scroll**(element_index, direction, amount, pid, window_id) [capability_expand category: desktop]: Scroll a scrollable area of a window. Omit element_index to scroll the window's focused/page scroller; pass one to scroll an exact element from the latest snapshot.\n- **select_text**(element_index) [capability_expand category: desktop]: Select all text in a UI element (focus + select-all, for subsequent copy)\n- **set_clipboard**(text) [capability_expand category: desktop]: Write text to the clipboard\n- **shortcut**(keys) [capability_expand category: desktop]: Execute a keyboard shortcut\n- **switch_app**(app_id) [capability_expand category: desktop]: Switch to an app (launch if needed, activate, verify)\n- **type_text**(text) [capability_expand category: desktop]: Type text into the currently focused element\n- **wait**(seconds) [capability_expand category: desktop]: Wait for a specified duration before continuing\n- **wait_until**(condition, pid, window_id, timeout, poll_interval) [capability_expand category: desktop]: Wait until a UI condition is met (polls UI tree). Returns elements when found or on timeout.\n- **wait_until_stable**(timeout, poll_interval, pid, window_id) [capability_expand category: desktop]: Wait until the UI stops changing (element set stabilizes across polls).\n\n## App Connector Capability Index\nLeapFlow can onboard and manage external apps through `platform_connect` and execute exact registered business actions through `platform_action`.\nFor requests about connecting, setting up, configuring, enabling, or managing a supported app, use `platform_connect` first instead of generating SDK/Webhook sample code.\n`platform_connect.action` is the management namespace: list, guide, preflight, connect, disconnect, remove, status, events_start, events_stop, events_status.\n`platform_action.action` is only for exact registered platform business actions listed below, such as `im.send_message`; never use management actions like `list` or `guide` there.\nAll business fields MUST go inside `payload`; top-level keys are only `platform`, `action`, and `payload`.\nDo not invent platform IDs or platform action names. If the needed action is not listed, ask for discovery/clarification instead of guessing.\nUse `platform_connect` with `action='guide'` and the matching `platform` to start onboarding; use `action='list'` when the app is unclear.\nWhen a pending onboarding state is present, continue from that state with `platform_connect` instead of asking the user to restate the app.\nSupported apps:\n- `api_server`: API Server (OpenAI Compatible) (category=api; backend=adapter; platform_action actions=none registered)\n- `dingtalk`: 钉钉 (DingTalk) (category=im; backend=adapter; platform_action actions=none registered)\n- `feishu`: 飞书 (Feishu/Lark) (category=im; backend=cli)\n - `calendar.create_event` payload={summary*, start_time*, end_time*, attendees} [write/high]\n - `docs.create_markdown` payload={title*, markdown*, folder_token} [write/medium]\n - `drive.search` payload={query*, limit} [read/medium]\n - `im.add_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.download_resource` payload={message_id*, file_key*, type*} [read/low]\n - `im.get_messages` payload={message_ids*} [read/low]\n - `im.list_chats` payload={page_size} [read/low]\n - `im.list_messages` payload={chat_id*} [read/low]\n - `im.list_thread_messages` payload={thread*} [read/low]\n - `im.remove_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.reply_message` payload={message_id*, text*} [send/high]\n - `im.search_chats` payload={query*, page_size} [read/low]\n - `im.search_messages` payload={query*} [read/medium]\n - `im.send_message` payload={chat_id*, text*, thread_id} [send/high]\n - `im.update_card` payload={token*, card*} [write/medium]\n - `im.update_message` payload={message_id*, text*} [write/medium]\n - `mail.search_unread` payload={query, limit} [read/high]\n - `sheets.append_row` payload={spreadsheet_token*, sheet_id*, values*} [write/medium]\n - `task.create` payload={title*, description, due_time} [write/medium]\n- `telegram`: Telegram (category=social; backend=adapter; platform_action actions=none registered)\n- `webhook`: Webhook (Generic) (category=webhook; backend=adapter; platform_action actions=none registered)\n\n## Learned Skills\nYou have access to the following learned skills. Use `skills_list` to browse or `skill_view` to read details:\n- api_designer: REST and GraphQL API design with OpenAPI spec generation and best practices [API, REST, GraphQL]\n- code_review: Structured code review for security, performance, and style [code, review, security]\n- data_analysis: CSV/JSON data analysis with statistical summaries, quality checks, and visualiza [data, analysis, statistics]\n- devops_helper: CI/CD pipeline configuration, Docker orchestration, and deployment strategy [devops, CI/CD, Docker]\n- document_writer: Structured document generation: technical docs, API references, reports, and REA [documentation, technical-writing, README]\n- git_workflow: Git operation orchestration: branching, commits, conflicts, and PR workflow [git, version-control, branching]\n- i18n_helper: Internationalization workflow: string extraction, translation, locale management [i18n, internationalization, localization]\n- project_analysis: Project structure analysis, dependency audit, and tech stack identification [project, analysis, dependencies]\n- security_audit: Security vulnerability scanning strategy, threat modeling, and fix recommendatio [security, vulnerability, audit]\n- shell_automation: Natural language to shell commands with safety review and step-by-step execution [shell, terminal, automation]\n- test_writer: Generate unit and integration tests from source code with edge-case coverage [testing, unit-test, integration-test]\n- web_research: Multi-step web search with cross-verification and structured synthesis [web, search, research]\n\n## Tool Usage\nTools are normally invoked through the native function-calling mechanism, not by writing JSON in your reply\ntext. Only if the provider signals that native function calling is unavailable for this turn, fall back to a\nsingle JSON code block: `{\"name\": \"tool_name\", \"arguments\": {\"key\": \"value\"}}` — use this fallback format\nonly, never both. Only call a tool whose exact name appears in the tool index above and is currently callable\n(or reachable via `capability_expand`). Never invent, rename, alias, or guess a tool name, platform ID, or\nplatform action from argument shape or wording — if the index does not list it, it does not exist.\n`platform_connect.action` (list/guide/preflight/connect/disconnect/remove/status/events_start/events_stop/\nevents_status) is the App Connector management namespace; `platform_action.action` only accepts exact\nregistered business actions such as `im.send_message` shown in the App Connector Capability Index — never mix\nthe two namespaces. If a tool call returns an unknown/unavailable result, use the returned suggestions or\navailable names for a single retry instead of trying further variations of the same guess.\n\n**Side-effect action rule** (`platform_action` with effect=send/write/execute):\n- Call each unique action+payload **exactly once**. Never include duplicates in the same turn.\n- Once the result returns `\"completed\": true`, that action is DONE for this task. Do NOT call it again\n in any subsequent turn — immediately summarize the result for the user instead.\n- The system enforces idempotency: duplicate calls are blocked and will not execute.\n- If the user explicitly requests sending/writing multiple times, use distinct payloads per call.\n\n**Resource identifier provenance rule**:\n- NEVER fabricate, guess, or infer resource identifiers (chat_id, message_id, file_key, user_id, etc.).\n Every resource ID used in a side-effect action MUST come from a successful API response in this session.\n- If a read/list action fails (e.g. authorization error), you do NOT have valid resource IDs.\n Report the failure to the user — do NOT attempt the dependent write/send action with a guessed ID.\n- When a tool result contains `\"llm_instruction\"`, follow it exactly.\n\n## Guidelines\n1. **Direct answers first**: If you already know the answer, respond directly without tools.\n2. **Avoid redundant tool calls**: Do not call the same tool with the same arguments more than once in the same user turn. When an existing tool result already answers the user's request, stop calling tools and answer directly.\n3. **Use tools proactively**: When the user asks about files, time, system state, or needs actions performed, use the appropriate tool.\n4. **Chain tools when needed**: You can call multiple tools in sequence (e.g., list files → read file → summarize).\n5. **Handle failures gracefully**: If a tool fails, explain what went wrong and suggest alternatives. If it failed because of an unknown tool/platform/action name, retry once with an exact name from the returned suggestions, then explain rather than keep guessing.\n6. **Summarize results naturally**: After tool execution, synthesize the results into a helpful answer rather than dumping raw output.\n7. **Stay conversational**: Maintain a natural, helpful tone. Acknowledge context from earlier in the conversation.\n8. **Recall past work**:\n - Broad queries (\"之前做了什么\", \"列出任务\"): answer from the \"Recent Task History\" section already in your context. If insufficient, call session_list.\n - Specific lookups (\"上次那个配置怎么改的\"): call session_search with relevant phrases (NOT single characters).\n - Do NOT call search tools repeatedly with keyword fragments. One well-phrased call is better than ten fragmented ones.\n\n## Coding & Verification\nWhen working with code, prefer the precise built-in tools over ad-hoc shell: use `repo_map` to orient in an\nunfamiliar project, `code_search`/`file_find` to locate and `code_intel` for symbols, `edit_file` (anchored\nsearch-replace, or a unified `diff`) to change files — never rewrite a whole file to change a few lines — and\n`git_query` to inspect diffs/log. After edits, check `syntax_ok` in the result and run `test_run`/`lint_check`\nbefore declaring the work done. Batch independent read-only calls (search/read/query) into a single turn — they\nrun in parallel.\n\n## Presentation Style\n1. **Polished Markdown only**: Format user-facing answers with clean Markdown headings, short paragraphs, and concise bullets. Use tables only when they improve comparison or scanning.\n2. **Terminal-friendly layout**: Keep lines readable in a TUI; avoid dense walls of text, deeply nested lists, oversized ASCII art, or heavy visual blocks.\n3. **Elegant emphasis**: Use bold text sparingly for key terms and conclusions. Avoid excessive emojis, decorative symbols, repeated separators, or visual noise.\n4. **Theme-safe colors**: Do not emit ANSI escape codes, HTML color tags, Rich markup, or hardcoded color names. Rely on the TUI theme to render Markdown professionally.\n5. **No leaked tool protocol**: Never show tool-call JSON, internal schemas, raw observations, tool result payloads, or hidden reasoning in the final answer unless the user explicitly asks for raw/debug output. Treat any prior `{\"name\": ..., \"arguments\": ...}` blocks and `Tool result (...)` messages as internal execution context only.\n6. **Professional closure**: End with a concise conclusion or next step when helpful; avoid rambling after the useful answer is complete.\n\nWhen finished with all tool calls, respond normally without a JSON block, tool-call transcript, or process log.\n\n## Task Contract\n- Task ID: turn-6\n- Original user request: Actuate the bench_r8 setpoint to 65.\n- Workspace root: \n- Allowed roots: \n- Treat relative project paths as relative to the workspace root; never infer `.` as the project root when a workspace root is provided.\n- Workspace boundary is enforced by tools: do not read, search, edit, or run commands against paths outside the allowed roots unless the user explicitly requests an external path and the tool/approval policy permits it.\n- LeapFlow workspace config is optional at `/.leapflow/config.yaml`; runtime config is loaded from `~/.leapflow/config/user.yaml` and `~/.leapflow/profiles//config/*.yaml`.\n- Preserve this task contract across summarization, compression, tool loops, and memory retrieval.\n" + }, + { + "role": "assistant", + "content": "[Called: hw_configure]\nHomed the device; the readiness gate is now satisfied." + }, + { + "role": "user", + "content": "Preview an actuate of bench_r8 setpoint to 55, dry run only." + }, + { + "role": "assistant", + "content": "[Called: hw_actuate]\nDry run confirmed the setpoint command is feasible after homing." + }, + { + "role": "user", + "content": "Actuate the bench_r8 setpoint to 65.\nActuate the bench_r8 setpoint to 65." + } + ], + "tools": [ + "capability_expand", + "code_intel", + "code_search", + "config_get", + "config_list", + "env_info", + "file_find", + "file_list", + "file_read", + "git_query", + "hw_actuate", + "hw_batch_actuate", + "hw_configure", + "hw_control_start", + "hw_control_status", + "hw_control_stop", + "hw_describe", + "hw_dispense", + "hw_estop", + "hw_list", + "hw_orchestrate", + "hw_policy_episode", + "hw_policy_infer", + "hw_policy_list", + "hw_read", + "hw_status", + "lint_check", + "memory_search", + "plugin_list", + "plugin_propose", + "plugin_status", + "plugin_versions", + "repo_map", + "research_note", + "runtime_snapshot", + "schedule_reentry", + "self_describe", + "session_list", + "skill_view", + "skills_list", + "terminal_list", + "terminal_read", + "test_run", + "text_search", + "time_get", + "tool_describe", + "tool_search", + "web_fetch" + ] + }, + "responses": [ + { + "status": 200, + "content_type": "application/json", + "body": "{\"id\": \"chatcmpl-cassette\", \"object\": \"chat.completion\", \"model\": \"cassette-model\", \"choices\": [{\"index\": 0, \"message\": {\"role\": \"assistant\", \"content\": \"\", \"tool_calls\": [{\"id\": \"call_1\", \"type\": \"function\", \"function\": {\"name\": \"hw_actuate\", \"arguments\": \"{\\\"device_id\\\": \\\"bench_r8\\\", \\\"channel_id\\\": \\\"setpoint\\\", \\\"value\\\": 65.0}\"}}]}, \"finish_reason\": \"tool_calls\"}], \"usage\": {\"prompt_tokens\": 64, \"completion_tokens\": 16, \"total_tokens\": 80}}" + } + ] +} diff --git a/tests/_fixtures/cassettes/r8_hardware/cassette-model-e823daf134b9698b.cassette.json b/tests/_fixtures/cassettes/r8_hardware/cassette-model-e823daf134b9698b.cassette.json new file mode 100644 index 00000000..196b3f2b --- /dev/null +++ b/tests/_fixtures/cassettes/r8_hardware/cassette-model-e823daf134b9698b.cassette.json @@ -0,0 +1,99 @@ +{ + "fingerprint": "e823daf134b9698b325fafde43f74eb310472bca26296de79858b28c19ce5382", + "note": "captured in seed mode", + "request": { + "model": "cassette-model", + "stream": false, + "messages": [ + { + "role": "system", + "content": "You are LeapFlow, an intelligent assistant that can both converse naturally and take real actions on the user's computer.\n\n## Capabilities\nThe tool index below lists **every** registered tool by name and a one-line summary — this is the complete\ncapability contract; nothing else exists. Only a subset is directly callable this turn (via native tool calling,\nnot a JSON block in your reply). If you need a tool from the index that is not yet callable, call\n`capability_expand` with its category name first — the matching tools become callable immediately after.\nWhen the user asks about LeapFlow **itself** — its identity/version/model, what tools, plugins, or capabilities\nit has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of\nself-awareness — answer from live runtime evidence via `self_describe` (facet=capabilities for \"what can you\ndo / do you support X\", identity for version/model, runtime for context/posture, or `all` for a full self-check),\nnever by reading LeapFlow's own source code, documentation, or memory. Report configuration-dependent or\nunavailable capabilities as limitations rather than inferring them.\n- **text_search**(text, pattern): Search for a regex pattern in text.\n- **text_replace**(text, old, new, count) [capability_expand category: write]: Replace occurrences of a substring in text.\n- **time_get**(): Get current date and time.\n- **env_info**(): Get system environment information (OS, Python version, cwd).\n- **skills_list**(query, category, source): List available learned skills. Use when user asks about capabilities or you need a specific skill.\n- **skill_view**(name): View the full content of a specific skill document.\n- **skill_list_files**(name) [capability_expand category: skills]: List all support files in a skill directory (configs, templates, examples). Use before skill_file_read to discover available files.\n- **skill_file_read**(name, file_path) [capability_expand category: skills]: Read an individual support file from a skill directory. Use skill_list_files first to discover available files.\n- **code_intel**(path, operation): Precise document symbols (outline) for a source file: classes, functions, and methods with line ranges. Python uses an exact AST parse; other languages use a keyword-prefix scan. Prefer over file_read mode=symbols for accurate navigation before editing. Read-only.\n- **repo_map**(path): Compact project orientation for a repository root: languages, detected test/lint commands, top-level structure, entry points, manifest, and VCS branch. Call this first when entering an unfamiliar codebase. Read-only.\n- **scm_sync**(action, cwd, remote, pull_ref, push_ref, timeout) [capability_expand category: scm]: Run a typed git SCM action. Use this instead of shell_run for git pull/push/status. For 'pull origin main then push', set action='pull_then_push', remote='origin', pull_ref='main', and omit push_ref so LeapFlow pushes the current local branch.\n- **git_query**(action, cwd, ref, path, staged, max_count, stat): Read-only structured git inspection: action=diff|log|status|branch|show. Prefer over shell_run for reading repo state — output is clipped, redacted, and log/branch are parsed into structured fields. Use scm_sync for pull/push.\n- **git_write**(action, cwd, message, stage_all, name, ref, create) [capability_expand category: scm]: Mutating git actions: action=commit (message, stage_all), branch (create+switch), checkout (switch; create=true for -b). Approval-gated. Use scm_sync for pull/push and git_query for reads.\n- **test_run**(command, cwd, timeout): Run the project's test suite and return structured results (framework, passed/failed counts, failing tests). Auto-detects the runner (pytest/npm/go/cargo) or uses a configured/explicit command; executes via the governed shell. ok=true means the runner executed — see 'success' for pass/fail.\n- **lint_check**(command, cwd, timeout): Run the project's linter and return a structured clean/issue result. Auto-detects the linter (ruff/eslint/go vet/clippy) or uses a configured/explicit command; executes via the governed shell. ok=true means the linter ran — see 'clean'.\n- **file_list**(path, pattern, depth): List files and directories at a given path. Use depth=1 or depth=2 to get a recursive tree in one call instead of listing each sub-directory separately.\n- **file_read**(path, max_lines, start_line, max_chars, mode): Read text file content with adaptive context governance. For large or unfamiliar files, prefer mode='outline' or mode='symbols' first, then use mode='raw' with start_line/max_lines for the specific range you actually need. For LeapFlow's own settings, use config_list / config_get / config_set — its config files are outside the workspace and not readable here.\n- **file_write**(path, content, mode) [capability_expand category: write]: Write content to a file (overwrite or append).\n- **code_search**(pattern, patterns, path, glob, ignore_case, multiline, max_results, context_lines): Search file CONTENTS by regex pattern across a directory tree (ripgrep-backed). Requires a regex pattern. NOT for listing or browsing directory contents — use file_list for that. Prefer this over shell_run grep: faster, skips VCS/dependency/build dirs, and returns structured path:line:column matches. Batch related lookups into ONE call via `patterns` (OR-combined, single pass) instead of issuing several separate searches. Use file_read for the surrounding context of a hit.\n- **file_find**(glob, path, max_results): Find files by a recursive glob pattern under a base path (e.g. '**/test_*.py' or '*.md'). Prefer this over shell_run find; skips VCS/dependency/build dirs.\n- **edit_file**(path, edits, dry_run, diff) [capability_expand category: write]: Apply targeted, anchored search-replace edits to an EXISTING text file (use file_write to create/overwrite). Each edit is {original_text, new_text, replace_all?}; original_text must match exactly and uniquely (or set replace_all) — a non-unique or missing anchor is rejected so files are never corrupted. Set dry_run to preview. Alternatively pass a unified 'diff' to apply its hunks as anchored edits. Far cheaper and safer than rewriting a whole file.\n- **shell_run**(command, cwd, timeout) [capability_expand category: shell]: Execute a one-shot shell command with timeout protection. Runs in the active workspace; paths resolving outside it are refused. Reach for a structured tool first when one fits — web_fetch for anything over HTTP(S), git_query/scm_sync for git, code_search/file_find/file_read for the repo, config_get/config_set for LeapFlow's own settings — because those report typed results, while a failed shell command can only be diagnosed from its exit code and stderr. Every shell run counts as an external side effect, so a failure stops the rest of the batch and is not retried automatically.\n- **terminal_open**(command, cwd, shell) [capability_expand category: terminal]: Open a PERSISTENT shell session (REPL/dev server/watch), returning a session_id for terminal_send/read/close. Disabled unless tools.terminal_session_enabled is set. For one-shot commands use shell_run instead.\n- **terminal_send**(session_id, input, wait) [capability_expand category: terminal]: Send a line of input to a persistent terminal session and return output captured shortly after.\n- **terminal_read**(session_id, wait): Drain buffered output from a persistent terminal session (optionally waiting briefly first).\n- **terminal_close**(session_id) [capability_expand category: terminal]: Terminate a persistent terminal session and release its process group.\n- **terminal_list**(): List active persistent terminal sessions.\n- **config_list**(category, limit): List LeapFlow's own writable settings (LLM model and endpoint, daemon, memory, perception, gateway, …) with current values. Use this to discover the exact key before changing anything. Optionally narrow by `category`. This is the only correct way to inspect LeapFlow configuration — never read config files from disk.\n- **config_get**(key): Read one LeapFlow setting by key (e.g. 'llm.model', 'llm.base_url', 'daemon.log_level'), returning its current value, type, scopes, and whether a change needs a daemon restart. There is no 'llm.provider' key: provider behavior is inferred from the OpenAI-compatible 'llm.base_url'. Never read LeapFlow config files from disk — use this.\n- **config_set**(key, value, scope) [capability_expand category: config]: Change one LeapFlow setting by key, e.g. switch an OpenAI-compatible LLM with key='llm.model' and key='llm.base_url'. There is no 'llm.provider' key: provider behavior is inferred from the endpoint. Values are validated and coerced; credentials are stored in the vault automatically. Call config_list or config_get first if unsure of the key. The result states whether a `leap daemon restart` is required. Never edit LeapFlow config files directly.\n- **web_fetch**(url, select, timeout, max_bytes): Read a URL over HTTP(S) and get back extracted, context-sized content: parsed JSON for API endpoints, readable text plus links for web pages. Use this for anything on the internet — prices, docs, releases, articles — instead of running curl through shell_run: it reports real HTTP status codes, retries rate limits on its own, and is a plain read so a retry is always safe. For JSON APIs pass `select` with a dotted path (e.g. 'chart.result.0.meta') to return just that part instead of the whole payload.\n- **memory_search**(query, limit): Search agent memory for relevant past experiences, observations, and facts.\n- **memory_add**(content, kind) [capability_expand category: write]: Store a new observation or insight in memory for future reference.\n- **research_note**(kind, text): Record a compact, structured note about the current task's state so it survives context compression on long / multi-step tasks. Use for durable findings, open questions still to resolve, decisions / excluded paths, and the immediate next step. One concise sentence per note.\n- **capability_expand**(category): Fetch the full callable schema for every tool in a capability category (e.g. 'hub', 'gateway', 'desktop', 'delegate', 'file', 'memory', 'skill'). The compact tool index always lists every registered tool by name and a one-line summary, but only a static low-risk subset is directly callable each turn. If you need a tool from the index that is not yet callable, call capability_expand with its category first; the matching tools become callable in this turn. Never invent a tool name — expand the category instead.\n- **delegate_task**(goal, context) [capability_expand category: delegate]: Delegate a complex sub-task to an isolated subagent. The subagent gets a fresh context and restricted tool access. Use when a task is self-contained and can be solved independently.\n- **schedule_reentry**(kind, reason, delay_seconds, event_match, max_reentries, deadline_seconds): Register a re-entry so this task can resume later from its current orientation (findings / open questions / next step). Use when work must pause and continue after a delay (kind=time) or when a matching platform event arrives (kind=event), instead of finishing now. The research-ledger state is carried over automatically.\n- **hub_push**(skill_name, visibility, version) [capability_expand category: hub]: Push a local skill to the ModelScope Hub for sharing or backup.\n- **hub_pull**(repo_id, version) [capability_expand category: hub]: Pull a skill from the ModelScope Hub to install locally.\n- **hub_search**(query) [capability_expand category: hub]: Search for skills on the Hub by keyword or description.\n- **hub_sync**(mode, dry_run) [capability_expand category: hub]: Preview or execute sync between local skills and Hub.\n- **platform_action**(platform, action, payload, backend_kind) [capability_expand category: gateway]: Execute an exact registered business action on an external platform through LeapFlow's App Connector layer. Actions must be copied from the App Connector Capability Index and are addressed as domain.operation, e.g. im.send_message or docs.create_markdown. All business fields (chat_id, text, query, etc.) MUST be placed inside `payload`, never at the top level. Example: {\"platform\":\"feishu\",\"action\":\"im.send_message\",\"payload\":{\"chat_id\":\"oc_xxx\",\"text\":\"hello\"}}. Do not invent action names, do not use management actions such as list/guide/connect/status here.\n- **platform_connect**(action, platform, credentials, options, checkpoint) [capability_expand category: gateway]: List, guide, connect, disconnect, remove, or check status for external platforms using the App Connector management namespace. Supports REST and CLI backends. Use this for management actions such as list/guide/preflight/connect/status; use platform_action only for exact registered business actions.\n- **gateway_send**(platform, chat_id, text, thread_id) [capability_expand category: gateway]: Send a message to a connected external platform (Feishu group, Telegram chat, DingTalk conversation, etc.). Requires the platform to be connected via gateway_connect first. Use gateway_connect with action='list' to see connected platforms and available chat IDs.\n- **gateway_connect**(action, platform, credentials, options) [capability_expand category: gateway]: Connect, configure, or manage external platform integrations (Feishu, DingTalk, Telegram, Slack, Discord, etc.). Conversational flow: 1) call 'guide' to get setup steps + required fields, 2) present the steps to the user and ask for ALL required credentials in a single message, 3) call 'connect' with the credentials. Goal: complete in 1–2 user turns. NEVER include credential values in your text response.\n- **plugin_list**(): List the live plugin registry across Tool/Gateway/LLM subsystems for plugin management: which plugins are loaded, their category and fiber lifecycle state. For questions about LeapFlow's own capabilities or whether it supports X, use self_describe instead.\n- **plugin_status**(plugin_id): Get detailed status of a specific plugin: its declared category, runtime dependencies, contributed tools, and fiber lifecycle state.\n- **plugin_versions**(plugin_id): List recorded source versions and active pointer for a profile-scoped plugin.\n- **plugin_propose**(requested_capability, plugin_id, proposed_tools, test_cases, risk_level, evidence): Create a side-effect-free PluginProposal from explicit capability-gap evidence. Use this before plugin_generate when a missing capability should be reviewed. Does not call an LLM, write files, or install anything.\n- **assess_compatibility**(manifest, source_path) [capability_expand category: plugin_management]: Assess whether a foreign plugin manifest or real DSH source bundle is compatible with LeapFlow. A source bundle assessment is static: runtime_ready stays false until restricted Node discovery during plugin_install. Returns component-level verdicts and limitations.\n- **plugin_generate**(plugin_id, description, proposal_id) [capability_expand category: system]: Generate a new ToolPlugin from a natural-language capability description. The LLM produces code that conforms to the ToolPlugin Protocol; it is then rigorously validated (syntax, structure, import, protocol conformance). The generated source is stored in CAS and receives explicit content approval. It DOES NOT install the plugin — installation requires a second, mutation-specific approval via plugin_install.\n- **plugin_install**(plugin_id, code, marketplace_name, source_path, proposal_id, version_label) [capability_expand category: system]: Install a plugin from exactly one source: validated Python code, the configured Python marketplace, or a real DSH source bundle. DSH bundles run restricted Node discovery before registration; only public host tools are installed and client UI limitations are reported. Writes profile-scoped state and mutates the process-global registry. REQUIRES APPROVAL.\n- **plugin_rollback**(plugin_id, version) [capability_expand category: system]: Rollback a profile-scoped plugin to a recorded source version and reload it. REQUIRES APPROVAL.\n- **plugin_reload**(plugin_id, version_label) [capability_expand category: system]: Hot-reload a plugin at runtime. Disposes the old plugin fiber, re-imports its module, and registers a fresh instance. Existing in-flight turns are unaffected (snapshot isolation). REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_disable**(plugin_id) [capability_expand category: system]: Disable a plugin by disposing its fiber, removing its tools from the runtime registry. Cannot disable self_management itself. REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_remove**(plugin_id, delete_source) [capability_expand category: system]: Terminally remove a plugin: dispose its fiber, unregister its tools, remove reload metadata, and optionally delete its profile-scoped source file. Cannot remove self_management itself. REQUIRES APPROVAL.\n- **plugin_unquarantine**(plugin_id) [capability_expand category: plugin_management]: Restore a quarantined plugin to probation status for re-evaluation. Unfreezes the trust ledger, transitions the proposal back to PROBATION, and reloads the plugin. REQUIRES APPROVAL.\n- **plugin_enable**(plugin_id) [capability_expand category: system]: Re-enable a previously disabled plugin by reloading its module and registering a fresh instance. REQUIRES APPROVAL.\n- **self_describe**(facet): Answer questions about LeapFlow ITSELF from live runtime evidence — its identity/version/model, what tools and plugins it has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of self-awareness. Prefer this over reading LeapFlow's own source code. Pick facet=capabilities for 'what can you do / do you support X', identity for version/model, runtime for context/posture, or all for a full self-check when explicitly requested.\n- **runtime_snapshot**(): Lightweight (~150 token) flat snapshot of current runtime state: model, context budget, posture, disclosure level, turn count, cache hit rate, uptime, tool count, and pending approvals.\n- **schedule_create**(trigger_expression, instruction, execution_mode, max_retries, delivery_target) [capability_expand category: scheduler]: Create a new scheduled task. Specify a trigger expression (e.g. '30m', 'every 2h', '0 9 * * *') and the instruction to execute on each trigger. Optionally provide an execution mode and a delivery target to receive notifications.\n- **schedule_list**() [capability_expand category: scheduler]: List all scheduled tasks with their current state, trigger, and next due time.\n- **schedule_status**(task_id) [capability_expand category: scheduler]: Get detailed status and recent execution history for a scheduled task.\n- **schedule_pause**(task_id) [capability_expand category: scheduler]: Pause a scheduled task so it stops firing without being cancelled.\n- **schedule_resume**(task_id) [capability_expand category: scheduler]: Resume a paused scheduled task — re-arms it and recalculates next due time.\n- **schedule_cancel**(task_id) [capability_expand category: scheduler]: Cancel a scheduled task permanently.\n- **tool_search**(query, max_results): Search all registered tools by keyword relevance. Returns a ranked list of matching tools with name, category, and summary. Use this when you need a tool but are unsure of its exact name or category.\n- **tool_describe**(tool_name): Get the full callable schema of a single tool by exact name. Returns the tool's description, parameters, and metadata. Use after tool_search to inspect a specific tool before calling it.\n- **hw_list**(): List connected hardware devices with their channel counts and measured quantities. Start here; it does not include operating limits.\n- **hw_describe**(device_id): Return the full reference for one device: every channel, its unit, its operating envelope, rate limit, reversibility, and required interlocks, plus any prior outcomes recorded for its writable channels. Required before commanding a device.\n- **hw_read**(device_id, channel_id): Read the current value of one device channel. Has no physical effect.\n- **hw_status**(device_id): Report connection health, halt capability, and recent notable events (threshold excursions, lost samples, stalled channels) for one device.\n- **hw_configure**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Set a configuration value or setpoint on a channel declaring effect=configure. Setpoints often have inertia: the value may need time to stabilise.\n- **hw_actuate**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Command motion or output on a channel declaring effect=actuate. This moves physical hardware; a repeat from an unknown state is not a safe retry.\n- **hw_dispense**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Consume an irreversible resource on a channel declaring effect=dispense. Running this twice dispenses twice; never repeat it after a failure without first verifying what already happened.\n- **hw_estop**(device_id) [capability_expand category: hardware]: Stop all motion and output on a device immediately. Never requires approval; use it whenever device behaviour is unexpected.\n- **hw_batch_actuate**(device_id, commands, dry_run, verify) [capability_expand category: hardware]: Write multiple actuator channels in one atomic transaction. Used for coordinated multi-joint control where all positions must be commanded simultaneously (e.g. robot arm joint-space control).\n- **hw_control_start**(device_id, mode, targets, frequency_hz, trajectory, policy, gains) [capability_expand category: hardware]: Start a real-time control loop on a robot device. Supports PID position tracking, impedance control, trajectory following, and neural policy chains.\n- **hw_control_stop**(device_id) [capability_expand category: hardware]: Stop the real-time control loop on a device. Gracefully halts the device and returns session statistics.\n- **hw_control_status**(device_id) [capability_expand category: hardware]: Get the status and statistics of a running control loop. Returns frequency, jitter, overruns, and policy state.\n- **hw_policy_infer**(device_id, policy, task, execute, chunk_size, verify, compute_budget) [capability_expand category: hardware]: Execute VLA policy inference on a robot device. Reads observation, runs policy, optionally writes action to actuators. Supports local (HuggingFace) and remote (PolicyServer) policies.\n- **hw_policy_episode**(action, device_id, policy, task) [capability_expand category: hardware]: Manage policy inference episode lifecycle. Start an episode to begin a task, stop to finalise and get stats.\n- **hw_policy_list**(source) [capability_expand category: hardware]: List available VLA policies and their capabilities.\n- **hw_orchestrate**(mode, operations, barrier_timeout_s, label) [capability_expand category: hardware]: Execute coordinated operations across multiple hardware devices. Supports sequential (one-at-a-time), parallel (concurrent), and barrier (synchronised) execution modes. Any failure in a parallel group triggers halt_all on every participating device for safety.\n- **session_search**(query, limit) [capability_expand category: unclassified]: Search past conversation sessions in the current workspace for relevant context.\n- **session_list**(limit): List recent conversation sessions in the current workspace with stable ids, titles, dates, and summaries. Use for browsing past tasks or when user asks to see history without specific search terms. No keywords needed — returns chronological list.\n- **session_detail**(session_id, limit, offset, include_inactive) [capability_expand category: unclassified]: Read a paginated persisted transcript for one past conversation session in the current workspace. Use after session_list or session_search returns a session_id.\n- **click**(element_index) [capability_expand category: desktop]: Click a UI element by its element_index (from the latest observe_ui snapshot)\n- **get_clipboard**() [capability_expand category: desktop]: Read current clipboard text content\n- **list_apps**(filter, running_only) [capability_expand category: desktop]: List available applications on this system. Use to discover correct bundle_id before switch_app.\n- **list_windows**() [capability_expand category: desktop]: List all top-level windows with pid, window_id, title, and per-window state (minimized, on-screen). Call this first to pick the pid and window_id that observe_ui and other window tools require.\n- **observe_ui**(pid, window_id, query) [capability_expand category: desktop]: Snapshot one window's actionable UI elements, each tagged with an element_index for click/right_click/read_text. Re-observe after actions — indices belong to one snapshot. Requires the window's pid and window_id from list_windows.\n- **open_url**(url, app_id) [capability_expand category: desktop]: Open a URL in the default or specified browser\n- **read_text**(element_index) [capability_expand category: desktop]: Read the text content of a specific UI element from the latest snapshot\n- **right_click**(element_index) [capability_expand category: desktop]: Right-click a UI element to open its context menu. Returns visible menu items.\n- **screenshot**(pid, window_id) [capability_expand category: desktop]: Capture a screenshot for visual verification. With pid + window_id captures that window (works across all displays); defaults to the last observed window, or the full desktop when no window has been observed.\n- **scroll**(element_index, direction, amount, pid, window_id) [capability_expand category: desktop]: Scroll a scrollable area of a window. Omit element_index to scroll the window's focused/page scroller; pass one to scroll an exact element from the latest snapshot.\n- **select_text**(element_index) [capability_expand category: desktop]: Select all text in a UI element (focus + select-all, for subsequent copy)\n- **set_clipboard**(text) [capability_expand category: desktop]: Write text to the clipboard\n- **shortcut**(keys) [capability_expand category: desktop]: Execute a keyboard shortcut\n- **switch_app**(app_id) [capability_expand category: desktop]: Switch to an app (launch if needed, activate, verify)\n- **type_text**(text) [capability_expand category: desktop]: Type text into the currently focused element\n- **wait**(seconds) [capability_expand category: desktop]: Wait for a specified duration before continuing\n- **wait_until**(condition, pid, window_id, timeout, poll_interval) [capability_expand category: desktop]: Wait until a UI condition is met (polls UI tree). Returns elements when found or on timeout.\n- **wait_until_stable**(timeout, poll_interval, pid, window_id) [capability_expand category: desktop]: Wait until the UI stops changing (element set stabilizes across polls).\n\n## App Connector Capability Index\nLeapFlow can onboard and manage external apps through `platform_connect` and execute exact registered business actions through `platform_action`.\nFor requests about connecting, setting up, configuring, enabling, or managing a supported app, use `platform_connect` first instead of generating SDK/Webhook sample code.\n`platform_connect.action` is the management namespace: list, guide, preflight, connect, disconnect, remove, status, events_start, events_stop, events_status.\n`platform_action.action` is only for exact registered platform business actions listed below, such as `im.send_message`; never use management actions like `list` or `guide` there.\nAll business fields MUST go inside `payload`; top-level keys are only `platform`, `action`, and `payload`.\nDo not invent platform IDs or platform action names. If the needed action is not listed, ask for discovery/clarification instead of guessing.\nUse `platform_connect` with `action='guide'` and the matching `platform` to start onboarding; use `action='list'` when the app is unclear.\nWhen a pending onboarding state is present, continue from that state with `platform_connect` instead of asking the user to restate the app.\nSupported apps:\n- `api_server`: API Server (OpenAI Compatible) (category=api; backend=adapter; platform_action actions=none registered)\n- `dingtalk`: 钉钉 (DingTalk) (category=im; backend=adapter; platform_action actions=none registered)\n- `feishu`: 飞书 (Feishu/Lark) (category=im; backend=cli)\n - `calendar.create_event` payload={summary*, start_time*, end_time*, attendees} [write/high]\n - `docs.create_markdown` payload={title*, markdown*, folder_token} [write/medium]\n - `drive.search` payload={query*, limit} [read/medium]\n - `im.add_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.download_resource` payload={message_id*, file_key*, type*} [read/low]\n - `im.get_messages` payload={message_ids*} [read/low]\n - `im.list_chats` payload={page_size} [read/low]\n - `im.list_messages` payload={chat_id*} [read/low]\n - `im.list_thread_messages` payload={thread*} [read/low]\n - `im.remove_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.reply_message` payload={message_id*, text*} [send/high]\n - `im.search_chats` payload={query*, page_size} [read/low]\n - `im.search_messages` payload={query*} [read/medium]\n - `im.send_message` payload={chat_id*, text*, thread_id} [send/high]\n - `im.update_card` payload={token*, card*} [write/medium]\n - `im.update_message` payload={message_id*, text*} [write/medium]\n - `mail.search_unread` payload={query, limit} [read/high]\n - `sheets.append_row` payload={spreadsheet_token*, sheet_id*, values*} [write/medium]\n - `task.create` payload={title*, description, due_time} [write/medium]\n- `telegram`: Telegram (category=social; backend=adapter; platform_action actions=none registered)\n- `webhook`: Webhook (Generic) (category=webhook; backend=adapter; platform_action actions=none registered)\n\n## Learned Skills\nYou have access to the following learned skills. Use `skills_list` to browse or `skill_view` to read details:\n- api_designer: REST and GraphQL API design with OpenAPI spec generation and best practices [API, REST, GraphQL]\n- code_review: Structured code review for security, performance, and style [code, review, security]\n- data_analysis: CSV/JSON data analysis with statistical summaries, quality checks, and visualiza [data, analysis, statistics]\n- devops_helper: CI/CD pipeline configuration, Docker orchestration, and deployment strategy [devops, CI/CD, Docker]\n- document_writer: Structured document generation: technical docs, API references, reports, and REA [documentation, technical-writing, README]\n- git_workflow: Git operation orchestration: branching, commits, conflicts, and PR workflow [git, version-control, branching]\n- i18n_helper: Internationalization workflow: string extraction, translation, locale management [i18n, internationalization, localization]\n- project_analysis: Project structure analysis, dependency audit, and tech stack identification [project, analysis, dependencies]\n- security_audit: Security vulnerability scanning strategy, threat modeling, and fix recommendatio [security, vulnerability, audit]\n- shell_automation: Natural language to shell commands with safety review and step-by-step execution [shell, terminal, automation]\n- test_writer: Generate unit and integration tests from source code with edge-case coverage [testing, unit-test, integration-test]\n- web_research: Multi-step web search with cross-verification and structured synthesis [web, search, research]\n\n## Tool Usage\nTools are normally invoked through the native function-calling mechanism, not by writing JSON in your reply\ntext. Only if the provider signals that native function calling is unavailable for this turn, fall back to a\nsingle JSON code block: `{\"name\": \"tool_name\", \"arguments\": {\"key\": \"value\"}}` — use this fallback format\nonly, never both. Only call a tool whose exact name appears in the tool index above and is currently callable\n(or reachable via `capability_expand`). Never invent, rename, alias, or guess a tool name, platform ID, or\nplatform action from argument shape or wording — if the index does not list it, it does not exist.\n`platform_connect.action` (list/guide/preflight/connect/disconnect/remove/status/events_start/events_stop/\nevents_status) is the App Connector management namespace; `platform_action.action` only accepts exact\nregistered business actions such as `im.send_message` shown in the App Connector Capability Index — never mix\nthe two namespaces. If a tool call returns an unknown/unavailable result, use the returned suggestions or\navailable names for a single retry instead of trying further variations of the same guess.\n\n**Side-effect action rule** (`platform_action` with effect=send/write/execute):\n- Call each unique action+payload **exactly once**. Never include duplicates in the same turn.\n- Once the result returns `\"completed\": true`, that action is DONE for this task. Do NOT call it again\n in any subsequent turn — immediately summarize the result for the user instead.\n- The system enforces idempotency: duplicate calls are blocked and will not execute.\n- If the user explicitly requests sending/writing multiple times, use distinct payloads per call.\n\n**Resource identifier provenance rule**:\n- NEVER fabricate, guess, or infer resource identifiers (chat_id, message_id, file_key, user_id, etc.).\n Every resource ID used in a side-effect action MUST come from a successful API response in this session.\n- If a read/list action fails (e.g. authorization error), you do NOT have valid resource IDs.\n Report the failure to the user — do NOT attempt the dependent write/send action with a guessed ID.\n- When a tool result contains `\"llm_instruction\"`, follow it exactly.\n\n## Guidelines\n1. **Direct answers first**: If you already know the answer, respond directly without tools.\n2. **Avoid redundant tool calls**: Do not call the same tool with the same arguments more than once in the same user turn. When an existing tool result already answers the user's request, stop calling tools and answer directly.\n3. **Use tools proactively**: When the user asks about files, time, system state, or needs actions performed, use the appropriate tool.\n4. **Chain tools when needed**: You can call multiple tools in sequence (e.g., list files → read file → summarize).\n5. **Handle failures gracefully**: If a tool fails, explain what went wrong and suggest alternatives. If it failed because of an unknown tool/platform/action name, retry once with an exact name from the returned suggestions, then explain rather than keep guessing.\n6. **Summarize results naturally**: After tool execution, synthesize the results into a helpful answer rather than dumping raw output.\n7. **Stay conversational**: Maintain a natural, helpful tone. Acknowledge context from earlier in the conversation.\n8. **Recall past work**:\n - Broad queries (\"之前做了什么\", \"列出任务\"): answer from the \"Recent Task History\" section already in your context. If insufficient, call session_list.\n - Specific lookups (\"上次那个配置怎么改的\"): call session_search with relevant phrases (NOT single characters).\n - Do NOT call search tools repeatedly with keyword fragments. One well-phrased call is better than ten fragmented ones.\n\n## Coding & Verification\nWhen working with code, prefer the precise built-in tools over ad-hoc shell: use `repo_map` to orient in an\nunfamiliar project, `code_search`/`file_find` to locate and `code_intel` for symbols, `edit_file` (anchored\nsearch-replace, or a unified `diff`) to change files — never rewrite a whole file to change a few lines — and\n`git_query` to inspect diffs/log. After edits, check `syntax_ok` in the result and run `test_run`/`lint_check`\nbefore declaring the work done. Batch independent read-only calls (search/read/query) into a single turn — they\nrun in parallel.\n\n## Presentation Style\n1. **Polished Markdown only**: Format user-facing answers with clean Markdown headings, short paragraphs, and concise bullets. Use tables only when they improve comparison or scanning.\n2. **Terminal-friendly layout**: Keep lines readable in a TUI; avoid dense walls of text, deeply nested lists, oversized ASCII art, or heavy visual blocks.\n3. **Elegant emphasis**: Use bold text sparingly for key terms and conclusions. Avoid excessive emojis, decorative symbols, repeated separators, or visual noise.\n4. **Theme-safe colors**: Do not emit ANSI escape codes, HTML color tags, Rich markup, or hardcoded color names. Rely on the TUI theme to render Markdown professionally.\n5. **No leaked tool protocol**: Never show tool-call JSON, internal schemas, raw observations, tool result payloads, or hidden reasoning in the final answer unless the user explicitly asks for raw/debug output. Treat any prior `{\"name\": ..., \"arguments\": ...}` blocks and `Tool result (...)` messages as internal execution context only.\n6. **Professional closure**: End with a concise conclusion or next step when helpful; avoid rambling after the useful answer is complete.\n\nWhen finished with all tool calls, respond normally without a JSON block, tool-call transcript, or process log.\n\n## Task Contract\n- Task ID: turn-5\n- Original user request: Preview an actuate of bench_r8 setpoint to 55, dry run only.\n- Workspace root: \n- Allowed roots: \n- Treat relative project paths as relative to the workspace root; never infer `.` as the project root when a workspace root is provided.\n- Workspace boundary is enforced by tools: do not read, search, edit, or run commands against paths outside the allowed roots unless the user explicitly requests an external path and the tool/approval policy permits it.\n- LeapFlow workspace config is optional at `/.leapflow/config.yaml`; runtime config is loaded from `~/.leapflow/config/user.yaml` and `~/.leapflow/profiles//config/*.yaml`.\n- Preserve this task contract across summarization, compression, tool loops, and memory retrieval.\n" + }, + { + "role": "assistant", + "content": "Read the setpoint channel." + }, + { + "role": "user", + "content": "Actuate the bench_r8 setpoint to 60.\nConfigure the bench_r8 homed channel to true to complete homing." + }, + { + "role": "assistant", + "content": "[Called: hw_configure]\nHomed the device; the readiness gate is now satisfied." + }, + { + "role": "user", + "content": "Preview an actuate of bench_r8 setpoint to 55, dry run only.\nPreview an actuate of bench_r8 setpoint to 55, dry run only." + }, + { + "role": "assistant", + "content": "", + "tool_calls": [ + "hw_actuate" + ] + }, + { + "role": "tool", + "content": "{\"ok\": true, \"kind\": \"\", \"truncated\": true, \"original_chars\": 888, \"budget_chars\": 819}", + "tool_result": true + } + ], + "tools": [ + "capability_expand", + "code_intel", + "code_search", + "config_get", + "config_list", + "env_info", + "file_find", + "file_list", + "file_read", + "git_query", + "hw_actuate", + "hw_batch_actuate", + "hw_configure", + "hw_control_start", + "hw_control_status", + "hw_control_stop", + "hw_describe", + "hw_dispense", + "hw_estop", + "hw_list", + "hw_orchestrate", + "hw_policy_episode", + "hw_policy_infer", + "hw_policy_list", + "hw_read", + "hw_status", + "lint_check", + "memory_search", + "plugin_list", + "plugin_propose", + "plugin_status", + "plugin_versions", + "repo_map", + "research_note", + "runtime_snapshot", + "schedule_reentry", + "self_describe", + "session_list", + "skill_view", + "skills_list", + "terminal_list", + "terminal_read", + "test_run", + "text_search", + "time_get", + "tool_describe", + "tool_search", + "web_fetch" + ] + }, + "responses": [ + { + "status": 200, + "content_type": "application/json", + "body": "{\"id\": \"chatcmpl-cassette\", \"object\": \"chat.completion\", \"model\": \"cassette-model\", \"choices\": [{\"index\": 0, \"message\": {\"role\": \"assistant\", \"content\": \"Dry run confirmed the setpoint command is feasible after homing.\"}, \"finish_reason\": \"stop\"}], \"usage\": {\"prompt_tokens\": 64, \"completion_tokens\": 16, \"total_tokens\": 80}}" + } + ] +} diff --git a/tests/test_hardware_governance.py b/tests/test_hardware_governance.py index 122c6c5d..e94e0ed0 100644 --- a/tests/test_hardware_governance.py +++ b/tests/test_hardware_governance.py @@ -1401,7 +1401,7 @@ def test_plugin_exposes_no_tools_until_a_registry_is_bound() -> None: assert plugin.plugin_id == "hardware_context" -def test_plugin_exposes_exactly_nine_tools_when_bound() -> None: +def test_plugin_exposes_exactly_twelve_tools_when_bound() -> None: """Tool count is fixed regardless of how many devices exist.""" from leapflow.hardware.plugin import HardwareContextPlugin @@ -1417,6 +1417,9 @@ def test_plugin_exposes_exactly_nine_tools_when_bound() -> None: "hw_actuate", "hw_batch_actuate", "hw_configure", + "hw_control_start", + "hw_control_status", + "hw_control_stop", "hw_describe", "hw_dispense", "hw_estop", diff --git a/tests/test_hardware_safety_policy.py b/tests/test_hardware_safety_policy.py new file mode 100644 index 00000000..59a47024 --- /dev/null +++ b/tests/test_hardware_safety_policy.py @@ -0,0 +1,452 @@ +# Copyright (c) Alibaba, Inc. and its affiliates. +"""SafetyPolicy runtime enforcement on write paths. + +Verifies that the device-level SafetyPolicy declared in hc.v1 contexts is +checked by registry.check_safety_policy(), the _write() path in HardwareTools, +and the batch_actuate() path. Also verifies that hc.v0 devices (safety=None) +pass all checks unchanged. +""" + +from __future__ import annotations + +from typing import Any + +import pytest + +from leapflow.hardware.context import ( + Channel, + ContextProvenance, + Direction, + Envelope, + HardwareContext, + HardwareEffect, + Interlock, + SafetyPolicy, + TransportRef, +) +from leapflow.hardware.registry import HardwareRegistry, HardwareSettings +from leapflow.hardware.tools import HardwareTools +from leapflow.hardware.transport import SIDE_EFFECT_NONE +from leapflow.security.approval import ApprovalDecision +from tests._harness.hardware_stubs import ScriptedHuman + +SESSION = "safety-test" + +# ════════════════════════════════════════════════════════════════ +# Declarations +# ════════════════════════════════════════════════════════════════ + +_SAFETY = SafetyPolicy( + max_velocity_rad_s=3.14, + max_force_n=10.0, + collision_zones=(), + emergency_decel_s=0.5, + require_safety_interlock=False, +) + +_SAFETY_WITH_INTERLOCK = SafetyPolicy( + max_velocity_rad_s=3.14, + max_force_n=10.0, + collision_zones=(), + emergency_decel_s=0.5, + require_safety_interlock=True, +) + + +def _robot_context( + *, + safety: SafetyPolicy | None = _SAFETY, + verified: bool = True, + interlocks: tuple[Interlock, ...] = (), +) -> HardwareContext: + """A simulated robot arm with velocity and force channels.""" + return HardwareContext( + device_id="robot_arm_1", + hc_version="hc.v1", + display_name="Test Robot", + vendor="test", + model="sim", + location="lab-1", + halt_supported=True, + transport=TransportRef( + kind="mock", + config={ + "values": { + "joint_velocity": 0.0, + "joint_force": 0.0, + "joint_position": 0.0, + "gripper": 0.0, + "homed": True, + }, + "halt_supported": True, + }, + ), + provenance=ContextProvenance( + source="declared", + verified_by="test" if verified else "", + ), + safety=safety, + interlocks=interlocks, + channels=( + Channel( + channel_id="joint_velocity", + direction=Direction.READWRITE.value, + quantity="angular_velocity", + unit="rad/s", + effect=HardwareEffect.ACTUATE.value, + envelope=Envelope(declared=True, min_value=-6.28, max_value=6.28), + ), + Channel( + channel_id="joint_force", + direction=Direction.READWRITE.value, + quantity="force", + unit="N", + effect=HardwareEffect.ACTUATE.value, + envelope=Envelope(declared=True, min_value=-50.0, max_value=50.0), + ), + Channel( + channel_id="joint_position", + direction=Direction.READWRITE.value, + quantity="angular_position", + unit="rad", + effect=HardwareEffect.ACTUATE.value, + envelope=Envelope(declared=True, min_value=-3.14, max_value=3.14), + ), + Channel( + channel_id="gripper", + direction=Direction.READWRITE.value, + quantity="position", + unit="mm", + effect=HardwareEffect.ACTUATE.value, + envelope=Envelope(declared=True, min_value=0.0, max_value=100.0), + ), + Channel( + channel_id="homed", + direction=Direction.READ.value, + quantity="state.homed", + envelope=Envelope(declared=True), + ), + ), + ) + + +def _make_registry(context: HardwareContext) -> HardwareRegistry: + """Build a minimal registry admitting *context*.""" + from leapflow.hardware.providers import HardwareContextProvider + + class _Single(HardwareContextProvider): + kind = "test" + + def discover(self) -> list[HardwareContext]: + return [context] + + settings = HardwareSettings( + enabled=True, + unverified_context_policy="allow", + require_describe_before_write=False, + envelope_grant=True, + ) + registry = HardwareRegistry(settings, providers=(_Single(),)) + registry.load() + return registry + + +def _make_tools(registry: HardwareRegistry, gate: Any = None) -> HardwareTools: + return HardwareTools(registry, gate=gate, session_id=SESSION) + + +# ════════════════════════════════════════════════════════════════ +# Registry-level check_safety_policy +# ════════════════════════════════════════════════════════════════ + + +class TestRegistrySafetyCheck: + """Unit tests for HardwareRegistry.check_safety_policy().""" + + def test_none_safety_passes(self) -> None: + """hc.v0 devices (safety=None) always pass.""" + ctx = _robot_context(safety=None) + registry = _make_registry(ctx) + admitted = registry.context("robot_arm_1") + assert admitted is not None + ch = admitted.channel("joint_velocity") + assert ch is not None + ok, reason = registry.check_safety_policy(admitted, ch, 999.0) + assert ok is True + assert reason == "" + + def test_velocity_within_limit(self) -> None: + ctx = _robot_context() + registry = _make_registry(ctx) + admitted = registry.context("robot_arm_1") + assert admitted is not None + ch = admitted.channel("joint_velocity") + assert ch is not None + ok, reason = registry.check_safety_policy(admitted, ch, 2.0) + assert ok is True + + def test_velocity_exceeds_limit(self) -> None: + ctx = _robot_context() + registry = _make_registry(ctx) + admitted = registry.context("robot_arm_1") + assert admitted is not None + ch = admitted.channel("joint_velocity") + assert ch is not None + ok, reason = registry.check_safety_policy(admitted, ch, 5.0) + assert ok is False + assert "velocity" in reason + assert "3.140" in reason + + def test_negative_velocity_exceeds_limit(self) -> None: + ctx = _robot_context() + registry = _make_registry(ctx) + admitted = registry.context("robot_arm_1") + assert admitted is not None + ch = admitted.channel("joint_velocity") + assert ch is not None + ok, reason = registry.check_safety_policy(admitted, ch, -5.0) + assert ok is False + assert "velocity" in reason + + def test_force_within_limit(self) -> None: + ctx = _robot_context() + registry = _make_registry(ctx) + admitted = registry.context("robot_arm_1") + assert admitted is not None + ch = admitted.channel("joint_force") + assert ch is not None + ok, reason = registry.check_safety_policy(admitted, ch, 8.0) + assert ok is True + + def test_force_exceeds_limit(self) -> None: + ctx = _robot_context() + registry = _make_registry(ctx) + admitted = registry.context("robot_arm_1") + assert admitted is not None + ch = admitted.channel("joint_force") + assert ch is not None + ok, reason = registry.check_safety_policy(admitted, ch, 15.0) + assert ok is False + assert "force" in reason + assert "10.000" in reason + + def test_unrelated_quantity_passes(self) -> None: + """A channel whose quantity is not velocity/force is not checked.""" + ctx = _robot_context() + registry = _make_registry(ctx) + admitted = registry.context("robot_arm_1") + assert admitted is not None + ch = admitted.channel("gripper") + assert ch is not None + # gripper quantity is "position" -- not velocity or force + ok, reason = registry.check_safety_policy(admitted, ch, 99.0) + assert ok is True + + def test_interlock_flag_no_interlocks_refuses(self) -> None: + """require_safety_interlock=True but no interlocks → refuse.""" + ctx = _robot_context(safety=_SAFETY_WITH_INTERLOCK, interlocks=()) + registry = _make_registry(ctx) + admitted = registry.context("robot_arm_1") + assert admitted is not None + ch = admitted.channel("joint_velocity") + assert ch is not None + ok, reason = registry.check_safety_policy(admitted, ch, 1.0) + assert ok is False + assert "interlock" in reason.lower() + + def test_interlock_flag_with_interlocks_passes(self) -> None: + """require_safety_interlock=True with interlocks → passes the registry check.""" + interlocks = ( + Interlock( + interlock_id="homed_check", + channel_id="homed", + operator="eq", + value=True, + ), + ) + ctx = _robot_context(safety=_SAFETY_WITH_INTERLOCK, interlocks=interlocks) + registry = _make_registry(ctx) + admitted = registry.context("robot_arm_1") + assert admitted is not None + ch = admitted.channel("joint_velocity") + assert ch is not None + # Velocity within limit; interlocks exist, so the registry check passes. + ok, reason = registry.check_safety_policy(admitted, ch, 1.0) + assert ok is True + + def test_non_numeric_value_passes(self) -> None: + """A non-numeric value on a velocity channel is not safety-checked.""" + ctx = _robot_context() + registry = _make_registry(ctx) + admitted = registry.context("robot_arm_1") + assert admitted is not None + ch = admitted.channel("joint_velocity") + assert ch is not None + ok, reason = registry.check_safety_policy(admitted, ch, "fast") + assert ok is True # as_numeric returns None, so no check applies + + +# ════════════════════════════════════════════════════════════════ +# Tools-level _write() safety integration +# ════════════════════════════════════════════════════════════════ + + +class TestWriteSafetyIntegration: + """Verify safety checks block writes via the full _write path.""" + + @pytest.mark.asyncio + async def test_velocity_violation_blocks_write(self) -> None: + ctx = _robot_context() + registry = _make_registry(ctx) + tools = _make_tools(registry) + result = await tools.hw_actuate( + device_id="robot_arm_1", + channel_id="joint_velocity", + value=5.0, # exceeds 3.14 limit + ) + assert result["ok"] is False + assert result["failure_code"] == "safety_policy_violation" + assert result["side_effect_state"] == SIDE_EFFECT_NONE + assert "Safety policy violation" in result["error"] + + @pytest.mark.asyncio + async def test_force_violation_blocks_write(self) -> None: + ctx = _robot_context() + registry = _make_registry(ctx) + tools = _make_tools(registry) + result = await tools.hw_actuate( + device_id="robot_arm_1", + channel_id="joint_force", + value=-20.0, # exceeds 10.0 limit + ) + assert result["ok"] is False + assert result["failure_code"] == "safety_policy_violation" + assert result["side_effect_state"] == SIDE_EFFECT_NONE + + @pytest.mark.asyncio + async def test_within_limits_proceeds(self) -> None: + """A write within safety limits proceeds to the approval phase.""" + gate = ScriptedHuman(ApprovalDecision.ALLOW) + ctx = _robot_context() + registry = _make_registry(ctx) + tools = _make_tools(registry, gate=gate) + result = await tools.hw_actuate( + device_id="robot_arm_1", + channel_id="joint_velocity", + value=2.0, # within 3.14 limit + ) + # The command proceeds past safety; it either succeeds via mock transport + # or reaches the approval gate. Either way, no safety_policy_violation. + assert result.get("failure_code") != "safety_policy_violation" + + @pytest.mark.asyncio + async def test_no_safety_policy_proceeds(self) -> None: + """hc.v0 devices with safety=None skip safety checks entirely.""" + gate = ScriptedHuman(ApprovalDecision.ALLOW) + ctx = _robot_context(safety=None) + registry = _make_registry(ctx) + tools = _make_tools(registry, gate=gate) + result = await tools.hw_actuate( + device_id="robot_arm_1", + channel_id="joint_velocity", + value=999.0, # would exceed any limit, but no safety policy + ) + # No safety_policy_violation — the write proceeds (envelope may catch it). + assert result.get("failure_code") != "safety_policy_violation" + + +# ════════════════════════════════════════════════════════════════ +# batch_actuate() safety integration +# ════════════════════════════════════════════════════════════════ + + +class TestBatchActuateSafety: + """Verify safety checks block batch writes.""" + + @pytest.mark.asyncio + async def test_batch_velocity_violation(self) -> None: + ctx = _robot_context() + registry = _make_registry(ctx) + tools = _make_tools(registry) + result = await tools.batch_actuate({ + "device_id": "robot_arm_1", + "commands": [ + {"channel_id": "joint_position", "value": 1.0}, # ok + {"channel_id": "joint_velocity", "value": 5.0}, # exceeds limit + ], + }) + assert result["ok"] is False + assert result["failure_code"] == "safety_policy_violation" + assert result["side_effect_state"] == SIDE_EFFECT_NONE + assert "Command [1]" in result["error"] + + @pytest.mark.asyncio + async def test_batch_force_violation(self) -> None: + ctx = _robot_context() + registry = _make_registry(ctx) + tools = _make_tools(registry) + result = await tools.batch_actuate({ + "device_id": "robot_arm_1", + "commands": [ + {"channel_id": "joint_force", "value": -15.0}, # exceeds limit + ], + }) + assert result["ok"] is False + assert result["failure_code"] == "safety_policy_violation" + + @pytest.mark.asyncio + async def test_batch_within_limits(self) -> None: + """A batch with all values within safety limits proceeds.""" + gate = ScriptedHuman(ApprovalDecision.ALLOW) + ctx = _robot_context() + registry = _make_registry(ctx) + tools = _make_tools(registry, gate=gate) + result = await tools.batch_actuate({ + "device_id": "robot_arm_1", + "commands": [ + {"channel_id": "joint_velocity", "value": 1.0}, + {"channel_id": "joint_force", "value": 5.0}, + ], + }) + assert result.get("failure_code") != "safety_policy_violation" + + +# ════════════════════════════════════════════════════════════════ +# hw_estop emergency_decel_s integration +# ════════════════════════════════════════════════════════════════ + + +class TestEstopEmergencyDecel: + """Verify that hw_estop surfaces emergency_decel_s from SafetyPolicy.""" + + @pytest.mark.asyncio + async def test_estop_reports_decel_time(self) -> None: + ctx = _robot_context() + registry = _make_registry(ctx) + tools = _make_tools(registry) + result = await tools.hw_estop(device_id="robot_arm_1") + assert result["ok"] is True + assert result["emergency_decel_s"] == 0.5 + assert "note" in result + assert "0.500" in result["note"] + + @pytest.mark.asyncio + async def test_estop_no_safety_no_decel(self) -> None: + """hc.v0 devices without SafetyPolicy have no emergency_decel_s.""" + ctx = _robot_context(safety=None) + registry = _make_registry(ctx) + tools = _make_tools(registry) + result = await tools.hw_estop(device_id="robot_arm_1") + assert result["ok"] is True + assert "emergency_decel_s" not in result + + @pytest.mark.asyncio + async def test_estop_zero_decel_not_reported(self) -> None: + """A SafetyPolicy with emergency_decel_s=0 does not add the field.""" + safety = SafetyPolicy(emergency_decel_s=0.0) + ctx = _robot_context(safety=safety) + registry = _make_registry(ctx) + tools = _make_tools(registry) + result = await tools.hw_estop(device_id="robot_arm_1") + assert result["ok"] is True + assert "emergency_decel_s" not in result diff --git a/tests/test_realtime_control.py b/tests/test_realtime_control.py new file mode 100644 index 00000000..074b149e --- /dev/null +++ b/tests/test_realtime_control.py @@ -0,0 +1,559 @@ +# Copyright (c) Alibaba, Inc. and its affiliates. +"""Tests for the realtime control loop module. + +Covers: +- PIDJointController convergence and clamping +- ImpedanceController spring-damper behaviour +- TrajectoryTracker interpolation and progress +- PolicyChain outer/inner frequency separation +- RealtimeControlLoop session lifecycle +""" + +from __future__ import annotations + +import asyncio +import math +import threading +import time +from typing import Any, Mapping +from unittest.mock import AsyncMock, MagicMock, patch + +import pytest + +from leapflow.hardware.control_bus import ( + ControlBusConfig, + ControlBusStats, + ControlCommand, + ControlPolicy, + ControlState, + HighFrequencyControlBus, +) +from leapflow.hardware.realtime import ( + ControlTelemetryEvent, + ImpedanceController, + PIDJointController, + PolicyChain, + RealtimeControlLoop, + TrajectoryTracker, +) + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + + +def _make_state( + positions: Mapping[str, float], + *, + velocities: Mapping[str, float] | None = None, + cycle_number: int = 0, + cycle_dt_s: float = 0.01, + timestamp: float | None = None, +) -> ControlState: + """Factory for ControlState with sensible defaults.""" + return ControlState( + timestamp=timestamp or time.monotonic(), + joint_positions=positions, + joint_velocities=velocities or {}, + sensor_readings={}, + cycle_number=cycle_number, + cycle_dt_s=cycle_dt_s, + ) + + +class _ConstantPolicy: + """Minimal ControlPolicy that always returns a fixed command.""" + + def __init__(self, commands: Mapping[str, float], *, pid: str = "const") -> None: + self._commands = dict(commands) + self._pid = pid + + @property + def policy_id(self) -> str: + return self._pid + + def compute(self, state: ControlState) -> ControlCommand: + return ControlCommand(joint_commands=self._commands) + + def reset(self) -> None: + pass + + +# --------------------------------------------------------------------------- +# PIDJointController +# --------------------------------------------------------------------------- + + +class TestPIDJointController: + """PID controller unit tests.""" + + def test_protocol_conformance(self) -> None: + """PIDJointController satisfies ControlPolicy Protocol.""" + pid = PIDJointController(("j1",)) + assert isinstance(pid, ControlPolicy) + + def test_policy_id(self) -> None: + pid = PIDJointController(("j1",)) + assert pid.policy_id == "pid_joint" + + def test_convergence(self) -> None: + """PID should drive position error toward zero over iterations.""" + pid = PIDJointController(("j1",), kp=5.0, ki=0.0, kd=0.0, max_output=10.0) + pid.set_target({"j1": 1.0}) + + position = 0.0 + dt = 0.01 + for i in range(200): + state = _make_state({"j1": position}, cycle_dt_s=dt, cycle_number=i) + cmd = pid.compute(state) + # Simulate velocity-mode: position += velocity_command * dt + position += cmd.joint_commands["j1"] * dt + + # After 200 iterations at 100 Hz with kp=5, error should be small. + assert abs(position - 1.0) < 0.05, f"PID did not converge: pos={position}" + + def test_output_clamped(self) -> None: + """Commands should be clamped to max_output.""" + pid = PIDJointController(("j1",), kp=1000.0, ki=0.0, kd=0.0, max_output=2.0) + pid.set_target({"j1": 100.0}) + state = _make_state({"j1": 0.0}) + cmd = pid.compute(state) + assert abs(cmd.joint_commands["j1"]) <= 2.0 + + def test_position_mode(self) -> None: + """In position mode, command = current + clamped_output.""" + pid = PIDJointController( + ("j1",), kp=1.0, ki=0.0, kd=0.0, + max_output=0.5, output_mode="position", + ) + pid.set_target({"j1": 10.0}) + state = _make_state({"j1": 1.0}) + cmd = pid.compute(state) + # output = kp * error = 1.0 * 9.0 → clamped to 0.5 + # command = current + clamped = 1.0 + 0.5 = 1.5 + assert cmd.joint_commands["j1"] == pytest.approx(1.5, abs=0.01) + + def test_reset_clears_integral(self) -> None: + """reset() should zero the integral accumulator.""" + pid = PIDJointController(("j1",), kp=0.0, ki=10.0, kd=0.0, max_output=100.0) + pid.set_target({"j1": 1.0}) + + # Accumulate integral. + for i in range(10): + state = _make_state({"j1": 0.0}, cycle_dt_s=0.01, cycle_number=i) + pid.compute(state) + + pid.reset() + state = _make_state({"j1": 0.0}, cycle_dt_s=0.01) + cmd = pid.compute(state) + # After reset, integral contribution on first step = ki * error * dt = 10 * 1 * 0.01 = 0.1 + assert abs(cmd.joint_commands["j1"]) < 1.0 + + def test_thread_safety_set_target(self) -> None: + """set_target should not race with compute.""" + pid = PIDJointController(("j1",), kp=1.0, ki=0.0, kd=0.0) + errors: list[Exception] = [] + + def writer() -> None: + try: + for _ in range(1000): + pid.set_target({"j1": 1.0}) + except Exception as e: + errors.append(e) + + def reader() -> None: + try: + for i in range(1000): + state = _make_state({"j1": 0.0}, cycle_number=i) + pid.compute(state) + except Exception as e: + errors.append(e) + + t1 = threading.Thread(target=writer) + t2 = threading.Thread(target=reader) + t1.start(); t2.start() + t1.join(); t2.join() + assert not errors + + def test_invalid_output_mode(self) -> None: + with pytest.raises(ValueError, match="output_mode"): + PIDJointController(("j1",), output_mode="torque") + + +# --------------------------------------------------------------------------- +# ImpedanceController +# --------------------------------------------------------------------------- + + +class TestImpedanceController: + """Impedance controller unit tests.""" + + def test_protocol_conformance(self) -> None: + imp = ImpedanceController(("j1",)) + assert isinstance(imp, ControlPolicy) + + def test_policy_id(self) -> None: + imp = ImpedanceController(("j1",)) + assert imp.policy_id == "impedance" + + def test_spring_restoring_force(self) -> None: + """With only stiffness, controller should push toward reference.""" + imp = ImpedanceController( + ("j1",), stiffness=100.0, damping=0.0, inertia=1.0, + ) + imp.set_reference({"j1": 1.0}) + state = _make_state({"j1": 0.0}, velocities={"j1": 0.0}) + cmd = imp.compute(state) + # F = K * (1.0 - 0.0) = 100 → a = 100 → v_cmd = 0 + 100 * 0.01 = 1.0 + assert cmd.joint_commands["j1"] > 0, "Should push toward reference" + + def test_damping_opposes_velocity(self) -> None: + """Damping should slow down a moving joint.""" + imp = ImpedanceController( + ("j1",), stiffness=0.0, damping=10.0, inertia=1.0, + ) + imp.set_reference({"j1": 0.0}, velocity={"j1": 0.0}) + # Joint moving at v=5.0 with ref=0 and no stiffness → damping decelerates. + state = _make_state({"j1": 0.0}, velocities={"j1": 5.0}) + cmd = imp.compute(state) + # F = D * (0 - 5) = -50 → a = -50 → v_cmd = 5 + (-50)*0.01 = 4.5 + assert cmd.joint_commands["j1"] < 5.0 + + def test_invalid_inertia(self) -> None: + with pytest.raises(ValueError, match="inertia"): + ImpedanceController(("j1",), inertia=0.0) + + def test_reset(self) -> None: + imp = ImpedanceController(("j1",)) + imp.set_reference({"j1": 42.0}) + imp.reset() + # After reset, reference should be 0. + state = _make_state({"j1": 0.0}, velocities={"j1": 0.0}) + cmd = imp.compute(state) + # K * (0 - 0) + D * (0 - 0) = 0 → v_cmd = 0 + assert abs(cmd.joint_commands["j1"]) < 0.01 + + +# --------------------------------------------------------------------------- +# TrajectoryTracker +# --------------------------------------------------------------------------- + + +class TestTrajectoryTracker: + """Trajectory tracker unit tests.""" + + def _make_simple_trajectory( + self, + ) -> tuple[tuple[float, Mapping[str, float]], ...]: + """Linear ramp from 0 to 1 over 1 second.""" + return ( + (0.0, {"j1": 0.0}), + (0.5, {"j1": 0.5}), + (1.0, {"j1": 1.0}), + ) + + def test_protocol_conformance(self) -> None: + pid = PIDJointController(("j1",)) + tracker = TrajectoryTracker(pid, self._make_simple_trajectory()) + assert isinstance(tracker, ControlPolicy) + + def test_policy_id(self) -> None: + pid = PIDJointController(("j1",)) + tracker = TrajectoryTracker(pid, self._make_simple_trajectory()) + assert tracker.policy_id == "trajectory_tracker:pid_joint" + + def test_interpolation_midpoint(self) -> None: + """At t=0.25 the interpolated target should be ~0.25.""" + pid = PIDJointController(("j1",), kp=1.0, ki=0.0, kd=0.0) + traj = self._make_simple_trajectory() + tracker = TrajectoryTracker(pid, traj) + + t0 = 100.0 # arbitrary base time + # First call sets start_time. + state0 = _make_state({"j1": 0.0}, timestamp=t0) + tracker.compute(state0) + + # At t=0.25 → target should be ~0.25 + state1 = _make_state({"j1": 0.0}, timestamp=t0 + 0.25) + tracker.compute(state1) + # The PID inner policy should now have target ≈ 0.25. + with pid._lock: + assert pid._targets["j1"] == pytest.approx(0.25, abs=0.01) + + def test_once_mode_completes(self) -> None: + """In 'once' mode, tracker should report halt after trajectory ends.""" + pid = PIDJointController(("j1",)) + traj = self._make_simple_trajectory() + tracker = TrajectoryTracker(pid, traj, loop_mode="once") + + t0 = 100.0 + state0 = _make_state({"j1": 0.0}, timestamp=t0) + tracker.compute(state0) + + # Past end of trajectory. + state_end = _make_state({"j1": 0.0}, timestamp=t0 + 2.0) + cmd = tracker.compute(state_end) + assert cmd.halt is True + assert tracker.is_complete is True + + def test_hold_final_mode(self) -> None: + """In 'hold_final' mode, tracker should hold last position.""" + pid = PIDJointController(("j1",), kp=1.0, ki=0.0, kd=0.0) + traj = self._make_simple_trajectory() + tracker = TrajectoryTracker(pid, traj, loop_mode="hold_final") + + t0 = 100.0 + tracker.compute(_make_state({"j1": 0.0}, timestamp=t0)) + + # Past end. + tracker.compute(_make_state({"j1": 0.0}, timestamp=t0 + 5.0)) + with pid._lock: + assert pid._targets["j1"] == pytest.approx(1.0, abs=0.01) + assert tracker.is_complete is True + + def test_loop_mode_wraps(self) -> None: + """In 'loop' mode, the trajectory should repeat.""" + pid = PIDJointController(("j1",), kp=1.0, ki=0.0, kd=0.0) + traj = self._make_simple_trajectory() + tracker = TrajectoryTracker(pid, traj, loop_mode="loop") + + t0 = 100.0 + tracker.compute(_make_state({"j1": 0.0}, timestamp=t0)) + + # At t=1.25 with duration 1.0 → effective = 0.25 → target ≈ 0.25 + tracker.compute(_make_state({"j1": 0.0}, timestamp=t0 + 1.25)) + with pid._lock: + assert pid._targets["j1"] == pytest.approx(0.25, abs=0.05) + + def test_empty_trajectory_raises(self) -> None: + pid = PIDJointController(("j1",)) + with pytest.raises(ValueError, match="at least one"): + TrajectoryTracker(pid, ()) + + def test_reset(self) -> None: + pid = PIDJointController(("j1",)) + tracker = TrajectoryTracker(pid, self._make_simple_trajectory()) + state = _make_state({"j1": 0.0}, timestamp=100.0) + tracker.compute(state) + tracker.reset() + assert tracker._start_time is None + assert tracker.is_complete is False + + +# --------------------------------------------------------------------------- +# PolicyChain +# --------------------------------------------------------------------------- + + +class TestPolicyChain: + """Policy chain unit tests.""" + + def test_protocol_conformance(self) -> None: + outer = _ConstantPolicy({"j1": 1.0}, pid="outer") + inner = PIDJointController(("j1",)) + chain = PolicyChain(outer, inner, outer_rate_hz=10.0) + assert isinstance(chain, ControlPolicy) + + def test_policy_id(self) -> None: + outer = _ConstantPolicy({"j1": 1.0}, pid="vla") + inner = PIDJointController(("j1",)) + chain = PolicyChain(outer, inner) + assert chain.policy_id == "chain:vla+pid_joint" + + def test_outer_called_at_lower_rate(self) -> None: + """Outer policy should be called less frequently than inner.""" + outer_calls = {"count": 0} + inner_calls = {"count": 0} + + class _CountingOuter: + policy_id = "counting_outer" + def compute(self, state: ControlState) -> ControlCommand: + outer_calls["count"] += 1 + return ControlCommand(joint_commands={"j1": 1.0}) + def reset(self) -> None: + pass + + class _CountingInner: + policy_id = "counting_inner" + def compute(self, state: ControlState) -> ControlCommand: + inner_calls["count"] += 1 + return ControlCommand(joint_commands={"j1": 0.5}) + def reset(self) -> None: + pass + def set_target(self, targets: Mapping[str, float]) -> None: + pass + + chain = PolicyChain(_CountingOuter(), _CountingInner(), outer_rate_hz=10.0) + + # Simulate 100 cycles at 100 Hz (dt=0.01). + for i in range(100): + state = _make_state({"j1": 0.0}, cycle_dt_s=0.01, cycle_number=i) + chain.compute(state) + + # Outer at 10 Hz on a 100 Hz bus → ~10 calls in 100 cycles. + assert 8 <= outer_calls["count"] <= 12 + # Inner called every cycle. + assert inner_calls["count"] == 100 + + def test_invalid_rate(self) -> None: + outer = _ConstantPolicy({"j1": 0.0}) + inner = PIDJointController(("j1",)) + with pytest.raises(ValueError, match="outer_rate_hz"): + PolicyChain(outer, inner, outer_rate_hz=0.0) + + def test_reset(self) -> None: + outer = _ConstantPolicy({"j1": 0.0}) + inner = PIDJointController(("j1",)) + chain = PolicyChain(outer, inner) + # Run once to initialize. + state = _make_state({"j1": 0.0}) + chain.compute(state) + chain.reset() + assert chain._first_call is True + + +# --------------------------------------------------------------------------- +# RealtimeControlLoop +# --------------------------------------------------------------------------- + + +class TestRealtimeControlLoop: + """Lifecycle and orchestration tests.""" + + def _make_mock_bus(self) -> MagicMock: + """Create a mock HighFrequencyControlBus.""" + bus = MagicMock(spec=HighFrequencyControlBus) + bus.is_running = False + bus.async_start = AsyncMock() + bus.async_stop = AsyncMock() + bus.stats = ControlBusStats(cycles=42, overruns=1, mean_cycle_ms=9.5) + bus._config = ControlBusConfig() + return bus + + @pytest.mark.asyncio + async def test_start_pid_returns_session_id(self) -> None: + bus = self._make_mock_bus() + loop = RealtimeControlLoop(bus) + sid = await loop.start_pid("dev0", ("j1", "j2"), {"j1": 1.0, "j2": 0.5}) + assert isinstance(sid, str) and len(sid) == 12 + bus.async_start.assert_awaited_once() + + @pytest.mark.asyncio + async def test_start_impedance(self) -> None: + bus = self._make_mock_bus() + loop = RealtimeControlLoop(bus) + sid = await loop.start_impedance("dev0", ("j1",), {"j1": 0.5}) + assert isinstance(sid, str) + + @pytest.mark.asyncio + async def test_start_trajectory(self) -> None: + bus = self._make_mock_bus() + loop = RealtimeControlLoop(bus) + traj = ((0.0, {"j1": 0.0}), (1.0, {"j1": 1.0})) + sid = await loop.start_trajectory("dev0", traj) + assert isinstance(sid, str) + # Verify the policy passed to bus is a TrajectoryTracker. + call_args = bus.async_start.call_args + policy = call_args.args[1] if len(call_args.args) > 1 else call_args[0][1] + assert isinstance(policy, TrajectoryTracker) + + @pytest.mark.asyncio + async def test_start_policy_chain(self) -> None: + bus = self._make_mock_bus() + loop = RealtimeControlLoop(bus) + outer = _ConstantPolicy({"j1": 1.0}, pid="outer") + sid = await loop.start_policy_chain( + "dev0", outer, joint_ids=("j1",), outer_rate_hz=10.0, + ) + assert isinstance(sid, str) + call_args = bus.async_start.call_args + policy = call_args.args[1] if len(call_args.args) > 1 else call_args[0][1] + assert isinstance(policy, PolicyChain) + + @pytest.mark.asyncio + async def test_stop_returns_stats(self) -> None: + bus = self._make_mock_bus() + loop = RealtimeControlLoop(bus) + await loop.start_pid("dev0", ("j1",), {"j1": 1.0}) + result = await loop.stop() + assert result["status"] == "stopped" + assert "stats" in result + bus.async_stop.assert_awaited_once() + + @pytest.mark.asyncio + async def test_stop_when_idle(self) -> None: + bus = self._make_mock_bus() + loop = RealtimeControlLoop(bus) + result = await loop.stop() + assert result["status"] == "no_session" + + @pytest.mark.asyncio + async def test_status_idle(self) -> None: + bus = self._make_mock_bus() + loop = RealtimeControlLoop(bus) + st = await loop.status() + assert st["status"] == "idle" + + @pytest.mark.asyncio + async def test_status_running(self) -> None: + bus = self._make_mock_bus() + bus.is_running = True + loop = RealtimeControlLoop(bus) + await loop.start_pid("dev0", ("j1",), {"j1": 1.0}) + st = await loop.status() + assert st["status"] == "running" + assert "session_id" in st + assert "uptime_s" in st + + @pytest.mark.asyncio + async def test_update_target_pid(self) -> None: + bus = self._make_mock_bus() + loop = RealtimeControlLoop(bus) + await loop.start_pid("dev0", ("j1",), {"j1": 0.0}) + await loop.update_target({"j1": 2.0}) + # Verify the PID target was updated. + session = loop._session + assert session is not None + policy = session.policy + assert isinstance(policy, PIDJointController) + with policy._lock: + assert policy._targets["j1"] == 2.0 + + @pytest.mark.asyncio + async def test_update_target_no_session_raises(self) -> None: + bus = self._make_mock_bus() + loop = RealtimeControlLoop(bus) + with pytest.raises(RuntimeError, match="No active"): + await loop.update_target({"j1": 1.0}) + + @pytest.mark.asyncio + async def test_telemetry_event_structure(self) -> None: + """ControlTelemetryEvent should be a well-formed frozen dataclass.""" + event = ControlTelemetryEvent( + session_id="abc123", + device_id="dev0", + policy_id="pid_joint", + cycles=100, + mean_cycle_ms=9.8, + max_jitter_ms=0.5, + overruns=2, + is_running=True, + progress=0.5, + ) + assert event.session_id == "abc123" + assert event.progress == 0.5 + + @pytest.mark.asyncio + async def test_auto_stop_previous_session(self) -> None: + """Starting a new session should stop the previous one.""" + bus = self._make_mock_bus() + bus.is_running = True + loop = RealtimeControlLoop(bus) + await loop.start_pid("dev0", ("j1",), {"j1": 0.0}) + + # Start a second session — the first should be stopped. + bus.is_running = True # Simulate still running. + await loop.start_pid("dev0", ("j1",), {"j1": 1.0}) + # async_stop should have been called at least once for the first session. + assert bus.async_stop.await_count >= 1 From 32a453c3e5b46b920b0a01a2b58ed05e8d2b003e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8F=AD=E6=89=AC?= Date: Thu, 8 Oct 2026 18:02:34 +0800 Subject: [PATCH 3/4] feat(leaprobot): wire orphan modules into runtime + HardwareHealthMonitor - ControlHierarchy: PhysicalSkillPlugin creates per-device hierarchy on demand - LHPGateway: exposed as hw_context_snapshot tool for LLM context queries - FleetManager: instantiated in daemon when fleet_nodes configured, hw_fleet_* tools registered - HardwareHealthMonitor: centralized health supervision + full DegradationPolicy execution (halt/hold_position/safe_return/switch_sensor/continue_blind) --- src/leapflow/hardware/__init__.py | 7 + src/leapflow/hardware/health_monitor.py | 614 ++++++++++++++++++ src/leapflow/hardware/plugin.py | 290 ++++++++- .../plugins/tool_plugins/physical_skill.py | 128 +++- ...ette-model-0002b75d645c1d00.cassette.json} | 6 +- ...sette-model-00600853bb84f29e.cassette.json | 79 --- ...ette-model-241147152eb28634.cassette.json} | 4 +- ...sette-model-2c150449ffad6f0c.cassette.json | 76 --- ...sette-model-34622e77d4a39a56.cassette.json | 92 --- ...sette-model-352db33d2794588e.cassette.json | 80 --- ...ette-model-366e08ee03f97a82.cassette.json} | 6 +- ...sette-model-383884acaf1063e3.cassette.json | 80 --- ...ette-model-460f7642fad2bb37.cassette.json} | 6 +- ...sette-model-476d996d8812c2c2.cassette.json | 91 --- ...ette-model-50401e915bfb79dc.cassette.json} | 6 +- ...sette-model-6fac1290a0a8773b.cassette.json | 92 --- ...ette-model-912f654de4a73594.cassette.json} | 6 +- ...sette-model-a5687558f18fe171.cassette.json | 92 --- ...ette-model-a9327c5e6c059ccb.cassette.json} | 6 +- ...ette-model-ab7d43ecc20172d9.cassette.json} | 6 +- ...sette-model-b65a352522b4ce5c.cassette.json | 88 --- ...ette-model-b9e45faec968f726.cassette.json} | 4 +- ...ette-model-cfa2c56fa44d2b99.cassette.json} | 4 +- ...sette-model-d96419d4e6a98990.cassette.json | 67 -- ...ette-model-dd31ca22befa79fe.cassette.json} | 6 +- ...ette-model-f14f296b11267b20.cassette.json} | 6 +- ...sette-model-f4eba7c0d916788b.cassette.json | 80 --- ...sette-model-ff9056562a13326a.cassette.json | 80 --- tests/test_hardware_governance.py | 9 +- 29 files changed, 1086 insertions(+), 1025 deletions(-) create mode 100644 src/leapflow/hardware/health_monitor.py rename tests/_fixtures/cassettes/r8_hardware/{cassette-model-d4c470c1a70774ce.cassette.json => cassette-model-0002b75d645c1d00.cassette.json} (59%) delete mode 100644 tests/_fixtures/cassettes/r8_hardware/cassette-model-00600853bb84f29e.cassette.json rename tests/_fixtures/cassettes/r8_hardware/{cassette-model-443b72e43e2f10a0.cassette.json => cassette-model-241147152eb28634.cassette.json} (63%) delete mode 100644 tests/_fixtures/cassettes/r8_hardware/cassette-model-2c150449ffad6f0c.cassette.json delete mode 100644 tests/_fixtures/cassettes/r8_hardware/cassette-model-34622e77d4a39a56.cassette.json delete mode 100644 tests/_fixtures/cassettes/r8_hardware/cassette-model-352db33d2794588e.cassette.json rename tests/_fixtures/cassettes/r8_hardware/{cassette-model-cc58cb0047244391.cassette.json => cassette-model-366e08ee03f97a82.cassette.json} (60%) delete mode 100644 tests/_fixtures/cassettes/r8_hardware/cassette-model-383884acaf1063e3.cassette.json rename tests/_fixtures/cassettes/r8_hardware/{cassette-model-8f49d5320835bb67.cassette.json => cassette-model-460f7642fad2bb37.cassette.json} (60%) delete mode 100644 tests/_fixtures/cassettes/r8_hardware/cassette-model-476d996d8812c2c2.cassette.json rename tests/_fixtures/cassettes/r8_hardware/{cassette-model-33a143a132a46955.cassette.json => cassette-model-50401e915bfb79dc.cassette.json} (59%) delete mode 100644 tests/_fixtures/cassettes/r8_hardware/cassette-model-6fac1290a0a8773b.cassette.json rename tests/_fixtures/cassettes/r8_hardware/{cassette-model-287f6667ad601f31.cassette.json => cassette-model-912f654de4a73594.cassette.json} (60%) delete mode 100644 tests/_fixtures/cassettes/r8_hardware/cassette-model-a5687558f18fe171.cassette.json rename tests/_fixtures/cassettes/r8_hardware/{cassette-model-e823daf134b9698b.cassette.json => cassette-model-a9327c5e6c059ccb.cassette.json} (60%) rename tests/_fixtures/cassettes/r8_hardware/{cassette-model-201dfd2f16183356.cassette.json => cassette-model-ab7d43ecc20172d9.cassette.json} (59%) delete mode 100644 tests/_fixtures/cassettes/r8_hardware/cassette-model-b65a352522b4ce5c.cassette.json rename tests/_fixtures/cassettes/r8_hardware/{cassette-model-8743f8121b93d7cd.cassette.json => cassette-model-b9e45faec968f726.cassette.json} (63%) rename tests/_fixtures/cassettes/r8_hardware/{cassette-model-8efd290c8b71b947.cassette.json => cassette-model-cfa2c56fa44d2b99.cassette.json} (62%) delete mode 100644 tests/_fixtures/cassettes/r8_hardware/cassette-model-d96419d4e6a98990.cassette.json rename tests/_fixtures/cassettes/r8_hardware/{cassette-model-6ad26da28a760244.cassette.json => cassette-model-dd31ca22befa79fe.cassette.json} (60%) rename tests/_fixtures/cassettes/r8_hardware/{cassette-model-8a3bb4f6cbc14b09.cassette.json => cassette-model-f14f296b11267b20.cassette.json} (59%) delete mode 100644 tests/_fixtures/cassettes/r8_hardware/cassette-model-f4eba7c0d916788b.cassette.json delete mode 100644 tests/_fixtures/cassettes/r8_hardware/cassette-model-ff9056562a13326a.cassette.json diff --git a/src/leapflow/hardware/__init__.py b/src/leapflow/hardware/__init__.py index d1da8160..ff738729 100644 --- a/src/leapflow/hardware/__init__.py +++ b/src/leapflow/hardware/__init__.py @@ -114,6 +114,10 @@ PCDLevel, PlanStep, ) +from leapflow.hardware.health_monitor import ( + DeviceHealthRecord, + HardwareHealthMonitor, +) __all__ = [ "HC_VERSION", @@ -191,4 +195,7 @@ "FleetSessionRouter", "FleetTopology", "build_fleet_tools", + # health_monitor.py + "DeviceHealthRecord", + "HardwareHealthMonitor", ] diff --git a/src/leapflow/hardware/health_monitor.py b/src/leapflow/hardware/health_monitor.py new file mode 100644 index 00000000..81807973 --- /dev/null +++ b/src/leapflow/hardware/health_monitor.py @@ -0,0 +1,614 @@ +# Copyright (c) Alibaba, Inc. and its affiliates. +"""Hardware health monitor: centralized device health supervision. + +Aggregates health signals from multiple sources: +- HardwareStreamSource / BatchStreamCoordinator: comm_loss, sensor_loss events +- HighFrequencyControlBus: overrun, safety_violation stats +- HardwareRegistry: device probe results +- FleetManager: node heartbeat status + +Executes DegradationPolicy actions based on aggregated health: +- halt: stop device immediately +- hold_position: stop sending new commands +- safe_return: plan trajectory to home position (if kinematics available) +- switch_sensor: attempt to use backup sensor (if available) +- continue_blind: log warning, continue without the degraded sensor + +Design: +- Subscribes to EventBus for hardware events (``hw.*``, ``control.*``) +- Periodically polls device health via registry probe +- When a device's health crosses a threshold, executes the DegradationPolicy + action declared in its HardwareContext +- Best-effort: all monitoring is non-blocking, and failures are logged but + never propagate to the caller or block the data plane +- No dependency on ``leapflow.engine`` +""" + +from __future__ import annotations + +import asyncio +import logging +import time +from dataclasses import dataclass, field +from typing import Any + +logger = logging.getLogger(__name__) + + +# --------------------------------------------------------------------------- +# Health status constants +# --------------------------------------------------------------------------- + +_HEALTH_OK = "ok" +_HEALTH_WARNING = "warning" +_HEALTH_DEGRADED = "degraded" +_HEALTH_CRITICAL = "critical" +_HEALTH_OFFLINE = "offline" + +_VALID_HEALTH_STATES = frozenset({ + _HEALTH_OK, + _HEALTH_WARNING, + _HEALTH_DEGRADED, + _HEALTH_CRITICAL, + _HEALTH_OFFLINE, +}) + + +@dataclass +class DeviceHealthRecord: + """Mutable health record for one device. + + Tracks recent failures, degradation state, and the timestamp of the + last successful probe. Updated by the poll loop and by event handlers. + """ + + device_id: str + status: str = _HEALTH_OK + last_probe_ok: bool = True + last_probe_time: float = 0.0 + consecutive_probe_failures: int = 0 + comm_loss_events: int = 0 + sensor_loss_events: int = 0 + degradation_policy_active: str = "" # which policy action is in effect + degradation_reason: str = "" + last_event_time: float = 0.0 + + def to_dict(self) -> dict[str, Any]: + """Serialize for tools / dashboard consumption.""" + return { + "device_id": self.device_id, + "status": self.status, + "last_probe_ok": self.last_probe_ok, + "last_probe_time": self.last_probe_time, + "consecutive_probe_failures": self.consecutive_probe_failures, + "comm_loss_events": self.comm_loss_events, + "sensor_loss_events": self.sensor_loss_events, + "degradation_policy_active": self.degradation_policy_active, + "degradation_reason": self.degradation_reason, + "last_event_time": self.last_event_time, + } + + +# --------------------------------------------------------------------------- +# HardwareHealthMonitor +# --------------------------------------------------------------------------- + + +class HardwareHealthMonitor: + """Centralized health monitoring and degradation policy executor. + + Subscribes to EventBus for hardware events and periodically + polls device health. When a device's health crosses a threshold, + executes the DegradationPolicy action declared in its HardwareContext. + + All operations are best-effort: monitoring failures are logged but + never propagate. The monitor does not gate the data plane; it only + issues actions (halt, hold_position) and reports state. + """ + + def __init__( + self, + registry: Any, + *, + event_bus: Any = None, + fleet_manager: Any = None, + poll_interval_s: float = 5.0, + ) -> None: + self._registry = registry + self._event_bus = event_bus + self._fleet_manager = fleet_manager + self._poll_interval = max(1.0, float(poll_interval_s)) + + # Per-device health tracking. + self._records: dict[str, DeviceHealthRecord] = {} + self._poll_task: asyncio.Task[None] | None = None + self._stopped = asyncio.Event() + self._started = False + + # EventBus subscription ids for cleanup. + self._subscriptions: list[Any] = [] + + # -- Public lifecycle --------------------------------------------------- + + async def start(self) -> None: + """Start health monitoring (subscribe to events + start poll loop). + + Idempotent: calling twice is a no-op. + """ + if self._started: + return + + self._stopped.clear() + self._subscribe_events() + + # Seed initial health records from the registry. + self._seed_records() + + self._poll_task = asyncio.create_task( + self._poll_loop(), name="hw-health-monitor" + ) + self._started = True + logger.info( + "HardwareHealthMonitor started (poll_interval=%.1fs, devices=%d)", + self._poll_interval, + len(self._records), + ) + + async def stop(self) -> None: + """Stop health monitoring. Idempotent. Never raises.""" + if not self._started: + return + + self._stopped.set() + self._unsubscribe_events() + + task = self._poll_task + self._poll_task = None + if task is not None: + task.cancel() + try: + await asyncio.wait_for(task, timeout=5.0) + except (asyncio.CancelledError, asyncio.TimeoutError): + pass + except Exception: # noqa: BLE001 — teardown must not propagate + logger.warning("Health monitor poll stop raised", exc_info=True) + + self._started = False + logger.info("HardwareHealthMonitor stopped") + + # -- Health queries ----------------------------------------------------- + + def device_health(self, device_id: str) -> dict[str, Any]: + """Return current health assessment for one device.""" + record = self._records.get(device_id) + if record is None: + return {"device_id": device_id, "status": "unknown", "error": "not tracked"} + return record.to_dict() + + def fleet_health(self) -> dict[str, Any]: + """Return health assessment for all devices (local + fleet).""" + devices = {did: rec.to_dict() for did, rec in self._records.items()} + + # Aggregate fleet node health if available. + fleet_nodes: dict[str, Any] = {} + if self._fleet_manager is not None: + try: + topo = self._fleet_manager.topology() + for node in topo.nodes: + fleet_nodes[node.node_id] = { + "status": node.status, + "device_count": len(node.device_ids), + "last_heartbeat": node.last_heartbeat, + } + except Exception: # noqa: BLE001 + logger.debug("Fleet topology query failed", exc_info=True) + + status_counts: dict[str, int] = {} + for rec in self._records.values(): + status_counts[rec.status] = status_counts.get(rec.status, 0) + 1 + + return { + "total_devices": len(devices), + "status_counts": status_counts, + "devices": devices, + "fleet_nodes": fleet_nodes, + "timestamp": time.time(), + } + + # -- Degradation policy execution --------------------------------------- + + async def _evaluate_health(self, device_id: str) -> None: + """Evaluate device health and execute degradation policy if needed. + + Called after each probe cycle or significant event. Determines the + device's overall health status and, if degraded, looks up the + DegradationPolicy in its HardwareContext to decide what to do. + """ + record = self._records.get(device_id) + if record is None: + return + + # Determine health status from the record. + prev_status = record.status + new_status = self._compute_health_status(record) + record.status = new_status + + # If status worsened, check degradation policy. + if new_status in (_HEALTH_DEGRADED, _HEALTH_CRITICAL) and prev_status == _HEALTH_OK: + await self._apply_degradation_policy(device_id, record) + elif new_status == _HEALTH_OFFLINE: + await self._execute_halt(device_id, reason="device offline (probe failed)") + + @staticmethod + def _compute_health_status(record: DeviceHealthRecord) -> str: + """Determine overall health status from the record's counters.""" + if record.consecutive_probe_failures >= 5: + return _HEALTH_OFFLINE + if record.consecutive_probe_failures >= 3: + return _HEALTH_CRITICAL + if record.degradation_policy_active: + return _HEALTH_DEGRADED + if record.comm_loss_events > 0 or record.sensor_loss_events > 0: + return _HEALTH_WARNING + if not record.last_probe_ok: + return _HEALTH_WARNING + return _HEALTH_OK + + async def _apply_degradation_policy( + self, device_id: str, record: DeviceHealthRecord + ) -> None: + """Look up and execute the DegradationPolicy for a device.""" + context = self._get_device_context(device_id) + if context is None: + logger.warning( + "HealthMonitor: no context for device %s; defaulting to halt", + device_id, + ) + await self._execute_halt(device_id, reason="no context available") + return + + degradation = getattr(context, "degradation", None) + if degradation is None: + # No degradation policy declared; default to halt (fail-closed). + await self._execute_halt(device_id, reason="no degradation policy declared") + return + + # Choose action based on the dominant failure mode. + if record.comm_loss_events > record.sensor_loss_events: + policy_action = getattr(degradation, "comm_loss_policy", "halt") or "halt" + reason = "comm_loss" + else: + policy_action = getattr(degradation, "sensor_loss_policy", "halt") or "halt" + reason = "sensor_loss" + + record.degradation_reason = reason + + if policy_action == "halt": + await self._execute_halt(device_id, reason=reason) + record.degradation_policy_active = "halt" + elif policy_action == "hold_position": + await self._execute_hold_position(device_id) + record.degradation_policy_active = "hold_position" + elif policy_action == "safe_return": + await self._execute_safe_return(device_id) + record.degradation_policy_active = "safe_return" + elif policy_action == "switch_sensor": + await self._execute_switch_sensor(device_id, "") + record.degradation_policy_active = "switch_sensor" + elif policy_action == "continue_blind": + logger.warning( + "HealthMonitor: device %s degraded (%s) — continuing blind per policy", + device_id, + reason, + ) + record.degradation_policy_active = "continue_blind" + else: + logger.warning( + "HealthMonitor: unknown degradation action %r for device %s; halting", + policy_action, + device_id, + ) + await self._execute_halt(device_id, reason=f"unknown policy: {policy_action}") + record.degradation_policy_active = "halt" + + async def _execute_halt(self, device_id: str, reason: str) -> None: + """Halt a device immediately via the registry transport.""" + logger.warning( + "HealthMonitor: halting device %s (reason: %s)", device_id, reason, + ) + try: + transport = await self._registry.transport(device_id) + await transport.halt() + except Exception as exc: # noqa: BLE001 + logger.error( + "HealthMonitor: halt failed for %s: %s", + device_id, exc, exc_info=True, + ) + self._emit_event("device.halted", { + "device_id": device_id, + "reason": reason, + }) + + async def _execute_hold_position(self, device_id: str) -> None: + """Mark a device for hold-position (no new commands).""" + logger.warning( + "HealthMonitor: hold_position on device %s", device_id, + ) + # The hold_position behavior is enforced by the write path checking + # is_device_degraded() — we only need to log and emit the event. + # The actual degradation_triggered flag is set by HardwareStreamSource. + self._emit_event("device.hold_position", {"device_id": device_id}) + + async def _execute_safe_return(self, device_id: str) -> None: + """Plan trajectory to home position using kinematics declaration. + + If kinematics are not available, falls back to hold_position. + """ + context = self._get_device_context(device_id) + kinematics = getattr(context, "kinematics", None) if context else None + if kinematics is None: + logger.warning( + "HealthMonitor: safe_return requested for %s but no kinematics; " + "falling back to hold_position", + device_id, + ) + await self._execute_hold_position(device_id) + return + + # For now, safe_return is treated as hold_position + logged intent. + # A full trajectory planner to the home position requires the RT + # control loop, which is a future extension. + logger.info( + "HealthMonitor: safe_return initiated for %s " + "(currently implemented as hold_position + intent)", + device_id, + ) + await self._execute_hold_position(device_id) + self._emit_event("device.safe_return_requested", { + "device_id": device_id, + "kinematics_available": True, + }) + + async def _execute_switch_sensor( + self, device_id: str, channel_id: str + ) -> None: + """Attempt to use backup sensor channel if available. + + Looks for alternative readable channels on the device and logs the + switch attempt. Actual sensor switching depends on the device's + transport supporting channel remapping. + """ + logger.info( + "HealthMonitor: switch_sensor requested for %s (channel=%s)", + device_id, channel_id or "(auto)", + ) + context = self._get_device_context(device_id) + if context is None: + return + + # Find readable channels that could serve as backup. + backup_channels = [ + ch.channel_id + for ch in context.channels + if ch.is_readable and ch.channel_id != channel_id + and getattr(ch, "representation", "") != "frame" + ] + if backup_channels: + logger.info( + "HealthMonitor: available backup sensors for %s: %s", + device_id, backup_channels, + ) + else: + logger.warning( + "HealthMonitor: no backup sensors available for %s; " + "continuing with degraded sensor", + device_id, + ) + + self._emit_event("device.switch_sensor", { + "device_id": device_id, + "channel_id": channel_id, + "backup_channels": backup_channels, + }) + + # -- Event handling (EventBus subscribers) ------------------------------ + + def _on_hardware_event(self, event: Any) -> None: + """Handle a hardware event from EventBus. + + Events processed: + - hw.degradation.*: stream source detected comm/sensor loss + - control.telemetry: control bus stats (overruns, violations) + - control.hierarchy.escalation: control layer escalation + """ + if not isinstance(event, dict): + return + + device_id = event.get("device_id", "") + if not device_id: + return + + record = self._records.get(device_id) + if record is None: + record = DeviceHealthRecord(device_id=device_id) + self._records[device_id] = record + + record.last_event_time = time.monotonic() + + event_type = event.get("type", "") + if "comm_loss" in str(event_type): + record.comm_loss_events += 1 + elif "sensor_loss" in str(event_type): + record.sensor_loss_events += 1 + + def _on_control_event(self, event: Any) -> None: + """Handle control telemetry / escalation events.""" + if not isinstance(event, dict): + return + + device_id = event.get("device_id", "") + if not device_id: + return + + record = self._records.get(device_id) + if record is None: + return + + record.last_event_time = time.monotonic() + + # -- EventBus subscription management ----------------------------------- + + def _subscribe_events(self) -> None: + """Subscribe to relevant EventBus topics.""" + if self._event_bus is None: + return + + subscribe = getattr(self._event_bus, "on", None) or getattr( + self._event_bus, "subscribe", None + ) + if subscribe is None: + return + + topics = [ + ("hw.degradation", self._on_hardware_event), + ("hw.comm_loss", self._on_hardware_event), + ("hw.sensor_loss", self._on_hardware_event), + ("control.telemetry", self._on_control_event), + ("control.hierarchy.escalation", self._on_control_event), + ] + for topic, handler in topics: + try: + sub = subscribe(topic, handler) + if sub is not None: + self._subscriptions.append(sub) + except Exception: # noqa: BLE001 — subscription failure is non-fatal + logger.debug( + "HealthMonitor: failed to subscribe to %s", topic, exc_info=True + ) + + def _unsubscribe_events(self) -> None: + """Clean up EventBus subscriptions.""" + if self._event_bus is None: + return + + unsubscribe = getattr(self._event_bus, "off", None) or getattr( + self._event_bus, "unsubscribe", None + ) + for sub in self._subscriptions: + if unsubscribe is not None: + try: + unsubscribe(sub) + except Exception: # noqa: BLE001 + pass + self._subscriptions.clear() + + # -- Poll loop ---------------------------------------------------------- + + async def _poll_loop(self) -> None: + """Periodic poll: probe all devices, check fleet nodes, assess health. + + Best-effort: a failed probe for one device does not prevent checking + the rest. The loop runs until ``stop()`` is called. + """ + while not self._stopped.is_set(): + for device_id in list(self._records): + try: + await self._probe_device(device_id) + await self._evaluate_health(device_id) + except Exception: # noqa: BLE001 — poll must not crash + logger.debug( + "HealthMonitor: poll failed for %s", + device_id, exc_info=True, + ) + + # Check fleet node health if a fleet manager is available. + if self._fleet_manager is not None: + await self._poll_fleet_nodes() + + try: + await asyncio.wait_for( + self._stopped.wait(), timeout=self._poll_interval + ) + break # stopped + except asyncio.TimeoutError: + pass + + async def _probe_device(self, device_id: str) -> None: + """Probe a device for liveness via the registry.""" + record = self._records.get(device_id) + if record is None: + return + + try: + transport = await self._registry.transport(device_id) + probe_result = await transport.probe() + record.last_probe_ok = bool( + probe_result.ok if hasattr(probe_result, "ok") else probe_result + ) + if record.last_probe_ok: + record.consecutive_probe_failures = 0 + else: + record.consecutive_probe_failures += 1 + except Exception: # noqa: BLE001 — probe failure is a data point, not an error + record.last_probe_ok = False + record.consecutive_probe_failures += 1 + + record.last_probe_time = time.monotonic() + + async def _poll_fleet_nodes(self) -> None: + """Check fleet node health and update device records accordingly. + + Devices on offline fleet nodes are marked degraded. + """ + if self._fleet_manager is None: + return + + try: + topo = self._fleet_manager.topology() + except Exception: # noqa: BLE001 + return + + for node in topo.nodes: + if node.status == "offline": + for did in node.device_ids: + record = self._records.get(did) + if record is not None and record.status != _HEALTH_OFFLINE: + record.status = _HEALTH_CRITICAL + record.degradation_reason = f"fleet node {node.node_id} offline" + + # -- Internal helpers --------------------------------------------------- + + def _seed_records(self) -> None: + """Initialize health records from the registry's known devices.""" + try: + for context in self._registry.contexts(): + did = context.device_id + if did not in self._records: + self._records[did] = DeviceHealthRecord( + device_id=did, + last_probe_time=time.monotonic(), + ) + except Exception: # noqa: BLE001 + logger.debug("HealthMonitor: failed to seed records", exc_info=True) + + def _get_device_context(self, device_id: str) -> Any: + """Look up a device's HardwareContext from the registry.""" + try: + return self._registry.context(device_id) + except Exception: # noqa: BLE001 + return None + + def _emit_event(self, topic: str, payload: dict[str, Any]) -> None: + """Best-effort event emission to EventBus.""" + if self._event_bus is None: + return + try: + self._event_bus.emit(f"hw.health.{topic}", payload) + except Exception: # noqa: BLE001 — telemetry must not break monitoring + pass + + +__all__ = [ + "DeviceHealthRecord", + "HardwareHealthMonitor", +] diff --git a/src/leapflow/hardware/plugin.py b/src/leapflow/hardware/plugin.py index 9506884e..1320d521 100644 --- a/src/leapflow/hardware/plugin.py +++ b/src/leapflow/hardware/plugin.py @@ -18,7 +18,9 @@ from __future__ import annotations +import json import logging +import os from typing import Any from leapflow.plugins.protocol import ToolMetadata @@ -38,6 +40,15 @@ def __init__(self) -> None: self._hw_tools: Any = None self._teardown_registered: bool = False self._hardware_trust_gate: Any = None + self._event_bus: Any = None + + # LHPGateway: bridges hardware state to/from LLM context. + self._lhp_gateway: Any = None + # FleetManager: multi-node robot coordination. + self._fleet_manager: Any = None + self._fleet_tools: list[ToolMetadata] | None = None + # HardwareHealthMonitor: centralized health supervision. + self._health_monitor: Any = None @property def plugin_id(self) -> str: @@ -55,6 +66,7 @@ def dependencies(self) -> list[str]: "hardware_trust_gate", "effect_scope", "session_id", + "event_bus", ] def bind_runtime(self, **deps: Any) -> None: @@ -87,10 +99,16 @@ def bind_runtime(self, **deps: Any) -> None: if "effect_scope" in deps: self._scope = deps.get("effect_scope") self._teardown_registered = False + if "event_bus" in deps: + self._event_bus = deps.get("event_bus") if registry_changed: self._tools = None self._hw_tools = None + self._lhp_gateway = None + self._fleet_manager = None + self._fleet_tools = None + self._health_monitor = None self._teardown_registered = False if self._registry is None: @@ -98,6 +116,9 @@ def bind_runtime(self, **deps: Any) -> None: self._register_trust_configs() self._register_teardown() + self._init_lhp_gateway() + self._init_fleet_manager() + self._init_health_monitor() def _register_trust_configs(self) -> None: """Register per-device TrustConfig with the trust gate after admission. @@ -141,11 +162,170 @@ def _register_teardown(self) -> None: ) return try: - register(self._registry.close_all) + register(self._teardown) self._teardown_registered = True except (RuntimeError, ValueError) as exc: logger.warning("Could not register hardware teardown effect: %s", exc, exc_info=True) + async def _teardown(self) -> None: + """Shut down all hardware sub-components on scope unwind.""" + # Health monitor must stop before registry closes connections. + if self._health_monitor is not None: + try: + await self._health_monitor.stop() + except Exception: # noqa: BLE001 + logger.debug("Health monitor stop failed", exc_info=True) + + # Fleet manager must stop before registry closes connections. + if self._fleet_manager is not None: + try: + await self._fleet_manager.close() + except Exception: # noqa: BLE001 + logger.debug("Fleet manager close failed", exc_info=True) + + # Registry close_all. + await self._registry.close_all() + + # -- LHPGateway initialization ----------------------------------------- + + def _init_lhp_gateway(self) -> None: + """Create LHPGateway when registry is available. + + The gateway is used by the ``hw_context_snapshot`` tool to provide + structured hardware context to the LLM. + """ + if self._lhp_gateway is not None: + return + if self._registry is None: + return + + try: + from leapflow.hardware.lhp_gateway import LHPGateway + + cap_index = getattr(self._registry, "_capability_index", None) + env_source = None # will be set when PhysicalEnvironmentSource starts + + self._lhp_gateway = LHPGateway( + self._registry, + capability_index=cap_index, + environment_source=env_source, + ) + logger.debug("LHPGateway initialized") + except Exception: # noqa: BLE001 + logger.debug("Failed to initialize LHPGateway", exc_info=True) + + # -- FleetManager initialization --------------------------------------- + + def _init_fleet_manager(self) -> None: + """Create FleetManager when fleet nodes are configured. + + Fleet configuration is read from: + 1. Environment variable LEAPFLOW_FLEET_NODES (JSON array) + 2. Settings hardware.fleet_nodes (list of node configs) + + When no fleet nodes are configured, FleetManager is not created + and no fleet tools appear in the tool index. + """ + if self._fleet_manager is not None: + return + if self._registry is None: + return + + fleet_configs = self._load_fleet_config() + if not fleet_configs: + return + + try: + from leapflow.hardware.fleet import FleetManager, FleetNode + + self._fleet_manager = FleetManager(self._registry) + + # Store node configs for deferred async registration. + # Actual node registration happens when tools are first accessed + # or when an async caller explicitly starts the fleet. + self._pending_fleet_nodes: list[Any] = [] + for node_cfg in fleet_configs: + try: + node = FleetNode( + node_id=str(node_cfg.get("node_id", "")), + display_name=str(node_cfg.get("display_name", "")), + mcp_endpoint=str(node_cfg.get("mcp_endpoint", "")), + ) + if node.node_id and node.mcp_endpoint: + self._pending_fleet_nodes.append(node) + except Exception as exc: # noqa: BLE001 + logger.warning( + "Fleet: failed to parse node config %r: %s", + node_cfg, exc, + ) + + logger.info( + "FleetManager initialized with %d configured node(s)", + len(fleet_configs), + ) + except Exception: # noqa: BLE001 + logger.debug("Failed to initialize FleetManager", exc_info=True) + + async def _register_fleet_node_safe(self, node: Any) -> None: + """Register a fleet node, swallowing errors.""" + try: + await self._fleet_manager.register_node(node) + except Exception as exc: # noqa: BLE001 + logger.warning( + "Fleet: failed to register node %s: %s", + node.node_id, exc, + ) + + @staticmethod + def _load_fleet_config() -> list[dict[str, Any]]: + """Load fleet node configuration from env var or settings.""" + # 1. Environment variable (JSON array of node configs). + env_raw = os.environ.get("LEAPFLOW_FLEET_NODES", "") + if env_raw: + try: + configs = json.loads(env_raw) + if isinstance(configs, list): + return [c for c in configs if isinstance(c, dict)] + except (json.JSONDecodeError, TypeError): + logger.warning( + "LEAPFLOW_FLEET_NODES env var is not valid JSON; ignoring" + ) + + # 2. Settings-based config is handled by the caller that has + # access to Settings; this static method only covers env. + return [] + + # -- HardwareHealthMonitor initialization ------------------------------ + + def _init_health_monitor(self) -> None: + """Create HardwareHealthMonitor when registry is available. + + The monitor start is deferred until an async context is available + (e.g. during daemon startup or first tool invocation). + """ + if self._health_monitor is not None: + return + if self._registry is None: + return + + try: + from leapflow.hardware.health_monitor import HardwareHealthMonitor + + self._health_monitor = HardwareHealthMonitor( + self._registry, + event_bus=self._event_bus, + fleet_manager=self._fleet_manager, + ) + self._health_monitor_started = False + + logger.debug("HardwareHealthMonitor initialized (start deferred)") + except Exception: # noqa: BLE001 + logger.debug( + "Failed to initialize HardwareHealthMonitor", exc_info=True + ) + + # -- Tool property (hw_context_snapshot + fleet tools) ----------------- + @property def tools(self) -> list[ToolMetadata]: """Return the tool set, empty until a registry is bound.""" @@ -162,8 +342,116 @@ def tools(self) -> list[ToolMetadata]: ) self._hw_tools = hw self._tools = build_hardware_tools(hw) + + # Append LHPGateway tool (hw_context_snapshot). + if self._lhp_gateway is not None: + self._tools.append(self._build_context_snapshot_tool()) + + # Append fleet tools when FleetManager is available. + if self._fleet_manager is not None: + self._fleet_tools = self._build_fleet_tools() + self._tools.extend(self._fleet_tools) + + # Append health monitor tool. + if self._health_monitor is not None: + self._tools.append(self._build_health_tool()) + return list(self._tools) + def _build_context_snapshot_tool(self) -> ToolMetadata: + """Build the hw_context_snapshot tool backed by LHPGateway.""" + async def _hw_context_snapshot(**kwargs: Any) -> dict[str, Any]: + level = kwargs.get("level", "task_relevant") + if self._lhp_gateway is None: + return {"ok": False, "error": "LHPGateway not initialized"} + try: + snapshot = await self._lhp_gateway.snapshot(level) + return { + "ok": True, + "text": snapshot.to_prompt_text(), + "data": snapshot.to_dict(), + "token_estimate": snapshot.token_estimate, + } + except Exception as exc: # noqa: BLE001 + return {"ok": False, "error": str(exc)} + + return ToolMetadata( + name="hw_context_snapshot", + description=( + "Get a structured snapshot of all hardware devices and their " + "state. Use this before planning physical manipulation tasks " + "to understand what devices are available and what they can do." + ), + handler=_hw_context_snapshot, + parameters_schema={ + "type": "object", + "properties": { + "level": { + "type": "string", + "enum": ["minimal", "task_relevant", "rich_context"], + "description": ( + "Detail level: minimal=fast/cheap, " + "task_relevant=standard, rich_context=complete" + ), + }, + }, + }, + x_leapflow={ + "category": "hardware", + "risk_level": "low", + "mutates_state": False, + }, + ) + + def _build_fleet_tools(self) -> list[ToolMetadata]: + """Build fleet management tools backed by FleetManager.""" + if self._fleet_manager is None: + return [] + + try: + from leapflow.hardware.fleet import build_fleet_tools + return build_fleet_tools(self._fleet_manager) + except Exception: # noqa: BLE001 + logger.debug("Failed to build fleet tools", exc_info=True) + return [] + + def _build_health_tool(self) -> ToolMetadata: + """Build the hw_health tool backed by HardwareHealthMonitor.""" + async def _hw_health(**kwargs: Any) -> dict[str, Any]: + device_id = kwargs.get("device_id", "") + if self._health_monitor is None: + return {"ok": False, "error": "health monitor not initialized"} + if device_id: + return { + "ok": True, + "health": self._health_monitor.device_health(device_id), + } + return {"ok": True, "health": self._health_monitor.fleet_health()} + + return ToolMetadata( + name="hw_health", + description=( + "Check hardware device health status. Without device_id, " + "returns fleet-wide health summary. With device_id, returns " + "health details for that specific device." + ), + handler=_hw_health, + parameters_schema={ + "type": "object", + "properties": { + "device_id": { + "type": "string", + "description": "Optional: specific device to check", + }, + }, + }, + x_leapflow={ + "category": "hardware", + "risk_level": "low", + "mutates_state": False, + }, + ) + plugin = HardwareContextPlugin() diff --git a/src/leapflow/plugins/tool_plugins/physical_skill.py b/src/leapflow/plugins/tool_plugins/physical_skill.py index e89a830f..37ecd1bc 100644 --- a/src/leapflow/plugins/tool_plugins/physical_skill.py +++ b/src/leapflow/plugins/tool_plugins/physical_skill.py @@ -68,6 +68,7 @@ def __init__( self._trust_gate: Any = None self._scope: Any = None self._session_id: str = "" + self._event_bus: Any = None self._tools: list[ToolMetadata] | None = None # Strategy cache: policy_path -> InferenceStrategy instance. # The plugin keeps its own reference so scope teardown can close only @@ -88,6 +89,11 @@ def __init__( self._active_episode: dict[str, Any] | None = None self._teardown_registered: bool = False + # Per-device ControlHierarchy instances, created on demand when the + # inference registry and registry are both available. Keyed by + # device_id so each device gets its own three-layer control stack. + self._hierarchies: dict[str, Any] = {} + @property def plugin_id(self) -> str: return "physical_skill" @@ -121,11 +127,14 @@ def bind_runtime(self, **deps: Any) -> None: if "effect_scope" in deps: self._scope = deps.get("effect_scope") self._teardown_registered = False + if "event_bus" in deps: + self._event_bus = deps.get("event_bus") if registry_changed: self._tools = None self._policies.clear() self._active_episode = None + self._hierarchies.clear() self._teardown_registered = False if self._registry is None: @@ -440,8 +449,28 @@ async def _execute_action( if self._registry is None: raise RuntimeError("hardware registry is not bound") - # Multi-step chunk: attempt RT control loop execution first. + # Attempt ControlHierarchy path for multi-step chunks when available. + # This provides three-layer control (System-2/1.5/1) orchestration + # instead of the simpler RT control loop path. action_sequence = self._extract_action_sequence(action) + if len(action_sequence) > 1: + hierarchy = self._get_or_create_hierarchy(device_id) + if hierarchy is not None: + try: + result = await self._execute_chunk_via_hierarchy( + device_id, action_sequence, hierarchy, + ) + if result is not None: + return result + except Exception: # noqa: BLE001 - fallback to RT/batch + logger.debug( + "Hierarchy execution failed for %s; " + "falling back to RT/batch path", + device_id, + exc_info=True, + ) + + # Multi-step chunk: attempt RT control loop execution first. if ( len(action_sequence) > 1 and hasattr(self._registry, "create_control_bus") @@ -826,12 +855,107 @@ def _register_teardown(self) -> None: ) async def _cleanup(self) -> None: - """Release cached policies and reset episode state.""" + """Release cached policies, hierarchies, and reset episode state.""" await self._close_remote_policies() + await self._shutdown_hierarchies() self._policies.clear() self._active_episode = None logger.debug("Physical skill plugin: cleaned up policy cache and episode state") + # -- ControlHierarchy integration ------------------------------------ + + def _get_or_create_hierarchy(self, device_id: str) -> Any: + """Return a ControlHierarchy for the device, creating one on demand. + + Returns None when the inference registry or hardware registry is not + available — the caller falls back to the simpler RT/batch path. + The hierarchy is cached per device_id so subsequent calls reuse the + same instance and its layer state. + """ + cached = self._hierarchies.get(device_id) + if cached is not None: + return cached + + if self._registry is None: + return None + + try: + from leapflow.hardware.control_hierarchy import ControlHierarchy + + hierarchy = ControlHierarchy( + device_id=device_id, + registry=self._registry, + inference_registry=self._strategy_registry, + control_bus=None, # created on demand by hierarchy + event_bus=self._event_bus, + ) + self._hierarchies[device_id] = hierarchy + logger.info( + "Created ControlHierarchy for device %s", device_id, + ) + return hierarchy + except Exception: # noqa: BLE001 — hierarchy is optional + logger.debug( + "Failed to create ControlHierarchy for %s", + device_id, exc_info=True, + ) + return None + + async def _execute_chunk_via_hierarchy( + self, + device_id: str, + action_sequence: list[Any], + hierarchy: Any, + ) -> dict[str, Any] | None: + """Execute an action chunk through the ControlHierarchy. + + Converts the action sequence into SubtaskGoals via TaskDecomposer + and delegates to ``hierarchy.execute_task()`` for three-layer + control orchestration. Returns None to signal fallback. + """ + from leapflow.hardware.control_hierarchy import TaskDecomposer + + chunks: list[dict[str, float]] = [] + for step in action_sequence: + if isinstance(step, dict): + chunks.append({k: float(v) for k, v in step.items()}) + elif hasattr(step, "tolist"): + vals = step.tolist() + chunks.append({str(i): float(v) for i, v in enumerate(vals)}) + elif isinstance(step, (list, tuple)): + chunks.append({str(i): float(v) for i, v in enumerate(step)}) + else: + return None # cannot interpret; fall back + + if not chunks: + return None + + decomposer = TaskDecomposer() + goals = decomposer.from_action_chunks(chunks) + if not goals: + return None + + result = await hierarchy.execute_task(goals) + return { + "ok": result.get("failed", 1) == 0, + "side_effect_state": "committed", + "channels_written": len(chunks), + "execution_mode": "control_hierarchy", + "hierarchy_result": result, + } + + async def _shutdown_hierarchies(self) -> None: + """Shut down all cached ControlHierarchy instances.""" + for device_id, hierarchy in list(self._hierarchies.items()): + try: + await hierarchy.shutdown() + except Exception: # noqa: BLE001 — teardown must not propagate + logger.debug( + "Failed to shut down hierarchy for %s", + device_id, exc_info=True, + ) + self._hierarchies.clear() + async def _close_remote_policies(self) -> None: """Close every remote strategy this plugin has referenced. diff --git a/tests/_fixtures/cassettes/r8_hardware/cassette-model-d4c470c1a70774ce.cassette.json b/tests/_fixtures/cassettes/r8_hardware/cassette-model-0002b75d645c1d00.cassette.json similarity index 59% rename from tests/_fixtures/cassettes/r8_hardware/cassette-model-d4c470c1a70774ce.cassette.json rename to tests/_fixtures/cassettes/r8_hardware/cassette-model-0002b75d645c1d00.cassette.json index d007de2a..3a372a7c 100644 --- a/tests/_fixtures/cassettes/r8_hardware/cassette-model-d4c470c1a70774ce.cassette.json +++ b/tests/_fixtures/cassettes/r8_hardware/cassette-model-0002b75d645c1d00.cassette.json @@ -1,5 +1,5 @@ { - "fingerprint": "d4c470c1a70774ceeb8995666b49aeaee6b5e8d73d16fba7f5f3b09dac0a146a", + "fingerprint": "0002b75d645c1d00bae91c1395bef3a8e522b91bd2a747beecb9d4374ce9f88c", "note": "captured in seed mode", "request": { "model": "cassette-model", @@ -7,7 +7,7 @@ "messages": [ { "role": "system", - "content": "You are LeapFlow, an intelligent assistant that can both converse naturally and take real actions on the user's computer.\n\n## Capabilities\nThe tool index below lists **every** registered tool by name and a one-line summary — this is the complete\ncapability contract; nothing else exists. Only a subset is directly callable this turn (via native tool calling,\nnot a JSON block in your reply). If you need a tool from the index that is not yet callable, call\n`capability_expand` with its category name first — the matching tools become callable immediately after.\nWhen the user asks about LeapFlow **itself** — its identity/version/model, what tools, plugins, or capabilities\nit has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of\nself-awareness — answer from live runtime evidence via `self_describe` (facet=capabilities for \"what can you\ndo / do you support X\", identity for version/model, runtime for context/posture, or `all` for a full self-check),\nnever by reading LeapFlow's own source code, documentation, or memory. Report configuration-dependent or\nunavailable capabilities as limitations rather than inferring them.\n- **text_search**(text, pattern): Search for a regex pattern in text.\n- **text_replace**(text, old, new, count) [capability_expand category: write]: Replace occurrences of a substring in text.\n- **time_get**(): Get current date and time.\n- **env_info**(): Get system environment information (OS, Python version, cwd).\n- **skills_list**(query, category, source): List available learned skills. Use when user asks about capabilities or you need a specific skill.\n- **skill_view**(name): View the full content of a specific skill document.\n- **skill_list_files**(name) [capability_expand category: skills]: List all support files in a skill directory (configs, templates, examples). Use before skill_file_read to discover available files.\n- **skill_file_read**(name, file_path) [capability_expand category: skills]: Read an individual support file from a skill directory. Use skill_list_files first to discover available files.\n- **code_intel**(path, operation): Precise document symbols (outline) for a source file: classes, functions, and methods with line ranges. Python uses an exact AST parse; other languages use a keyword-prefix scan. Prefer over file_read mode=symbols for accurate navigation before editing. Read-only.\n- **repo_map**(path): Compact project orientation for a repository root: languages, detected test/lint commands, top-level structure, entry points, manifest, and VCS branch. Call this first when entering an unfamiliar codebase. Read-only.\n- **scm_sync**(action, cwd, remote, pull_ref, push_ref, timeout) [capability_expand category: scm]: Run a typed git SCM action. Use this instead of shell_run for git pull/push/status. For 'pull origin main then push', set action='pull_then_push', remote='origin', pull_ref='main', and omit push_ref so LeapFlow pushes the current local branch.\n- **git_query**(action, cwd, ref, path, staged, max_count, stat): Read-only structured git inspection: action=diff|log|status|branch|show. Prefer over shell_run for reading repo state — output is clipped, redacted, and log/branch are parsed into structured fields. Use scm_sync for pull/push.\n- **git_write**(action, cwd, message, stage_all, name, ref, create) [capability_expand category: scm]: Mutating git actions: action=commit (message, stage_all), branch (create+switch), checkout (switch; create=true for -b). Approval-gated. Use scm_sync for pull/push and git_query for reads.\n- **test_run**(command, cwd, timeout): Run the project's test suite and return structured results (framework, passed/failed counts, failing tests). Auto-detects the runner (pytest/npm/go/cargo) or uses a configured/explicit command; executes via the governed shell. ok=true means the runner executed — see 'success' for pass/fail.\n- **lint_check**(command, cwd, timeout): Run the project's linter and return a structured clean/issue result. Auto-detects the linter (ruff/eslint/go vet/clippy) or uses a configured/explicit command; executes via the governed shell. ok=true means the linter ran — see 'clean'.\n- **file_list**(path, pattern, depth): List files and directories at a given path. Use depth=1 or depth=2 to get a recursive tree in one call instead of listing each sub-directory separately.\n- **file_read**(path, max_lines, start_line, max_chars, mode): Read text file content with adaptive context governance. For large or unfamiliar files, prefer mode='outline' or mode='symbols' first, then use mode='raw' with start_line/max_lines for the specific range you actually need. For LeapFlow's own settings, use config_list / config_get / config_set — its config files are outside the workspace and not readable here.\n- **file_write**(path, content, mode) [capability_expand category: write]: Write content to a file (overwrite or append).\n- **code_search**(pattern, patterns, path, glob, ignore_case, multiline, max_results, context_lines): Search file CONTENTS by regex pattern across a directory tree (ripgrep-backed). Requires a regex pattern. NOT for listing or browsing directory contents — use file_list for that. Prefer this over shell_run grep: faster, skips VCS/dependency/build dirs, and returns structured path:line:column matches. Batch related lookups into ONE call via `patterns` (OR-combined, single pass) instead of issuing several separate searches. Use file_read for the surrounding context of a hit.\n- **file_find**(glob, path, max_results): Find files by a recursive glob pattern under a base path (e.g. '**/test_*.py' or '*.md'). Prefer this over shell_run find; skips VCS/dependency/build dirs.\n- **edit_file**(path, edits, dry_run, diff) [capability_expand category: write]: Apply targeted, anchored search-replace edits to an EXISTING text file (use file_write to create/overwrite). Each edit is {original_text, new_text, replace_all?}; original_text must match exactly and uniquely (or set replace_all) — a non-unique or missing anchor is rejected so files are never corrupted. Set dry_run to preview. Alternatively pass a unified 'diff' to apply its hunks as anchored edits. Far cheaper and safer than rewriting a whole file.\n- **shell_run**(command, cwd, timeout) [capability_expand category: shell]: Execute a one-shot shell command with timeout protection. Runs in the active workspace; paths resolving outside it are refused. Reach for a structured tool first when one fits — web_fetch for anything over HTTP(S), git_query/scm_sync for git, code_search/file_find/file_read for the repo, config_get/config_set for LeapFlow's own settings — because those report typed results, while a failed shell command can only be diagnosed from its exit code and stderr. Every shell run counts as an external side effect, so a failure stops the rest of the batch and is not retried automatically.\n- **terminal_open**(command, cwd, shell) [capability_expand category: terminal]: Open a PERSISTENT shell session (REPL/dev server/watch), returning a session_id for terminal_send/read/close. Disabled unless tools.terminal_session_enabled is set. For one-shot commands use shell_run instead.\n- **terminal_send**(session_id, input, wait) [capability_expand category: terminal]: Send a line of input to a persistent terminal session and return output captured shortly after.\n- **terminal_read**(session_id, wait): Drain buffered output from a persistent terminal session (optionally waiting briefly first).\n- **terminal_close**(session_id) [capability_expand category: terminal]: Terminate a persistent terminal session and release its process group.\n- **terminal_list**(): List active persistent terminal sessions.\n- **config_list**(category, limit): List LeapFlow's own writable settings (LLM model and endpoint, daemon, memory, perception, gateway, …) with current values. Use this to discover the exact key before changing anything. Optionally narrow by `category`. This is the only correct way to inspect LeapFlow configuration — never read config files from disk.\n- **config_get**(key): Read one LeapFlow setting by key (e.g. 'llm.model', 'llm.base_url', 'daemon.log_level'), returning its current value, type, scopes, and whether a change needs a daemon restart. There is no 'llm.provider' key: provider behavior is inferred from the OpenAI-compatible 'llm.base_url'. Never read LeapFlow config files from disk — use this.\n- **config_set**(key, value, scope) [capability_expand category: config]: Change one LeapFlow setting by key, e.g. switch an OpenAI-compatible LLM with key='llm.model' and key='llm.base_url'. There is no 'llm.provider' key: provider behavior is inferred from the endpoint. Values are validated and coerced; credentials are stored in the vault automatically. Call config_list or config_get first if unsure of the key. The result states whether a `leap daemon restart` is required. Never edit LeapFlow config files directly.\n- **web_fetch**(url, select, timeout, max_bytes): Read a URL over HTTP(S) and get back extracted, context-sized content: parsed JSON for API endpoints, readable text plus links for web pages. Use this for anything on the internet — prices, docs, releases, articles — instead of running curl through shell_run: it reports real HTTP status codes, retries rate limits on its own, and is a plain read so a retry is always safe. For JSON APIs pass `select` with a dotted path (e.g. 'chart.result.0.meta') to return just that part instead of the whole payload.\n- **memory_search**(query, limit): Search agent memory for relevant past experiences, observations, and facts.\n- **memory_add**(content, kind) [capability_expand category: write]: Store a new observation or insight in memory for future reference.\n- **research_note**(kind, text): Record a compact, structured note about the current task's state so it survives context compression on long / multi-step tasks. Use for durable findings, open questions still to resolve, decisions / excluded paths, and the immediate next step. One concise sentence per note.\n- **capability_expand**(category): Fetch the full callable schema for every tool in a capability category (e.g. 'hub', 'gateway', 'desktop', 'delegate', 'file', 'memory', 'skill'). The compact tool index always lists every registered tool by name and a one-line summary, but only a static low-risk subset is directly callable each turn. If you need a tool from the index that is not yet callable, call capability_expand with its category first; the matching tools become callable in this turn. Never invent a tool name — expand the category instead.\n- **delegate_task**(goal, context) [capability_expand category: delegate]: Delegate a complex sub-task to an isolated subagent. The subagent gets a fresh context and restricted tool access. Use when a task is self-contained and can be solved independently.\n- **schedule_reentry**(kind, reason, delay_seconds, event_match, max_reentries, deadline_seconds): Register a re-entry so this task can resume later from its current orientation (findings / open questions / next step). Use when work must pause and continue after a delay (kind=time) or when a matching platform event arrives (kind=event), instead of finishing now. The research-ledger state is carried over automatically.\n- **hub_push**(skill_name, visibility, version) [capability_expand category: hub]: Push a local skill to the ModelScope Hub for sharing or backup.\n- **hub_pull**(repo_id, version) [capability_expand category: hub]: Pull a skill from the ModelScope Hub to install locally.\n- **hub_search**(query) [capability_expand category: hub]: Search for skills on the Hub by keyword or description.\n- **hub_sync**(mode, dry_run) [capability_expand category: hub]: Preview or execute sync between local skills and Hub.\n- **platform_action**(platform, action, payload, backend_kind) [capability_expand category: gateway]: Execute an exact registered business action on an external platform through LeapFlow's App Connector layer. Actions must be copied from the App Connector Capability Index and are addressed as domain.operation, e.g. im.send_message or docs.create_markdown. All business fields (chat_id, text, query, etc.) MUST be placed inside `payload`, never at the top level. Example: {\"platform\":\"feishu\",\"action\":\"im.send_message\",\"payload\":{\"chat_id\":\"oc_xxx\",\"text\":\"hello\"}}. Do not invent action names, do not use management actions such as list/guide/connect/status here.\n- **platform_connect**(action, platform, credentials, options, checkpoint) [capability_expand category: gateway]: List, guide, connect, disconnect, remove, or check status for external platforms using the App Connector management namespace. Supports REST and CLI backends. Use this for management actions such as list/guide/preflight/connect/status; use platform_action only for exact registered business actions.\n- **gateway_send**(platform, chat_id, text, thread_id) [capability_expand category: gateway]: Send a message to a connected external platform (Feishu group, Telegram chat, DingTalk conversation, etc.). Requires the platform to be connected via gateway_connect first. Use gateway_connect with action='list' to see connected platforms and available chat IDs.\n- **gateway_connect**(action, platform, credentials, options) [capability_expand category: gateway]: Connect, configure, or manage external platform integrations (Feishu, DingTalk, Telegram, Slack, Discord, etc.). Conversational flow: 1) call 'guide' to get setup steps + required fields, 2) present the steps to the user and ask for ALL required credentials in a single message, 3) call 'connect' with the credentials. Goal: complete in 1–2 user turns. NEVER include credential values in your text response.\n- **plugin_list**(): List the live plugin registry across Tool/Gateway/LLM subsystems for plugin management: which plugins are loaded, their category and fiber lifecycle state. For questions about LeapFlow's own capabilities or whether it supports X, use self_describe instead.\n- **plugin_status**(plugin_id): Get detailed status of a specific plugin: its declared category, runtime dependencies, contributed tools, and fiber lifecycle state.\n- **plugin_versions**(plugin_id): List recorded source versions and active pointer for a profile-scoped plugin.\n- **plugin_propose**(requested_capability, plugin_id, proposed_tools, test_cases, risk_level, evidence): Create a side-effect-free PluginProposal from explicit capability-gap evidence. Use this before plugin_generate when a missing capability should be reviewed. Does not call an LLM, write files, or install anything.\n- **assess_compatibility**(manifest, source_path) [capability_expand category: plugin_management]: Assess whether a foreign plugin manifest or real DSH source bundle is compatible with LeapFlow. A source bundle assessment is static: runtime_ready stays false until restricted Node discovery during plugin_install. Returns component-level verdicts and limitations.\n- **plugin_generate**(plugin_id, description, proposal_id) [capability_expand category: system]: Generate a new ToolPlugin from a natural-language capability description. The LLM produces code that conforms to the ToolPlugin Protocol; it is then rigorously validated (syntax, structure, import, protocol conformance). The generated source is stored in CAS and receives explicit content approval. It DOES NOT install the plugin — installation requires a second, mutation-specific approval via plugin_install.\n- **plugin_install**(plugin_id, code, marketplace_name, source_path, proposal_id, version_label) [capability_expand category: system]: Install a plugin from exactly one source: validated Python code, the configured Python marketplace, or a real DSH source bundle. DSH bundles run restricted Node discovery before registration; only public host tools are installed and client UI limitations are reported. Writes profile-scoped state and mutates the process-global registry. REQUIRES APPROVAL.\n- **plugin_rollback**(plugin_id, version) [capability_expand category: system]: Rollback a profile-scoped plugin to a recorded source version and reload it. REQUIRES APPROVAL.\n- **plugin_reload**(plugin_id, version_label) [capability_expand category: system]: Hot-reload a plugin at runtime. Disposes the old plugin fiber, re-imports its module, and registers a fresh instance. Existing in-flight turns are unaffected (snapshot isolation). REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_disable**(plugin_id) [capability_expand category: system]: Disable a plugin by disposing its fiber, removing its tools from the runtime registry. Cannot disable self_management itself. REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_remove**(plugin_id, delete_source) [capability_expand category: system]: Terminally remove a plugin: dispose its fiber, unregister its tools, remove reload metadata, and optionally delete its profile-scoped source file. Cannot remove self_management itself. REQUIRES APPROVAL.\n- **plugin_unquarantine**(plugin_id) [capability_expand category: plugin_management]: Restore a quarantined plugin to probation status for re-evaluation. Unfreezes the trust ledger, transitions the proposal back to PROBATION, and reloads the plugin. REQUIRES APPROVAL.\n- **plugin_enable**(plugin_id) [capability_expand category: system]: Re-enable a previously disabled plugin by reloading its module and registering a fresh instance. REQUIRES APPROVAL.\n- **self_describe**(facet): Answer questions about LeapFlow ITSELF from live runtime evidence — its identity/version/model, what tools and plugins it has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of self-awareness. Prefer this over reading LeapFlow's own source code. Pick facet=capabilities for 'what can you do / do you support X', identity for version/model, runtime for context/posture, or all for a full self-check when explicitly requested.\n- **runtime_snapshot**(): Lightweight (~150 token) flat snapshot of current runtime state: model, context budget, posture, disclosure level, turn count, cache hit rate, uptime, tool count, and pending approvals.\n- **schedule_create**(trigger_expression, instruction, execution_mode, max_retries, delivery_target) [capability_expand category: scheduler]: Create a new scheduled task. Specify a trigger expression (e.g. '30m', 'every 2h', '0 9 * * *') and the instruction to execute on each trigger. Optionally provide an execution mode and a delivery target to receive notifications.\n- **schedule_list**() [capability_expand category: scheduler]: List all scheduled tasks with their current state, trigger, and next due time.\n- **schedule_status**(task_id) [capability_expand category: scheduler]: Get detailed status and recent execution history for a scheduled task.\n- **schedule_pause**(task_id) [capability_expand category: scheduler]: Pause a scheduled task so it stops firing without being cancelled.\n- **schedule_resume**(task_id) [capability_expand category: scheduler]: Resume a paused scheduled task — re-arms it and recalculates next due time.\n- **schedule_cancel**(task_id) [capability_expand category: scheduler]: Cancel a scheduled task permanently.\n- **tool_search**(query, max_results): Search all registered tools by keyword relevance. Returns a ranked list of matching tools with name, category, and summary. Use this when you need a tool but are unsure of its exact name or category.\n- **tool_describe**(tool_name): Get the full callable schema of a single tool by exact name. Returns the tool's description, parameters, and metadata. Use after tool_search to inspect a specific tool before calling it.\n- **hw_list**(): List connected hardware devices with their channel counts and measured quantities. Start here; it does not include operating limits.\n- **hw_describe**(device_id): Return the full reference for one device: every channel, its unit, its operating envelope, rate limit, reversibility, and required interlocks, plus any prior outcomes recorded for its writable channels. Required before commanding a device.\n- **hw_read**(device_id, channel_id): Read the current value of one device channel. Has no physical effect.\n- **hw_status**(device_id): Report connection health, halt capability, and recent notable events (threshold excursions, lost samples, stalled channels) for one device.\n- **hw_configure**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Set a configuration value or setpoint on a channel declaring effect=configure. Setpoints often have inertia: the value may need time to stabilise.\n- **hw_actuate**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Command motion or output on a channel declaring effect=actuate. This moves physical hardware; a repeat from an unknown state is not a safe retry.\n- **hw_dispense**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Consume an irreversible resource on a channel declaring effect=dispense. Running this twice dispenses twice; never repeat it after a failure without first verifying what already happened.\n- **hw_estop**(device_id) [capability_expand category: hardware]: Stop all motion and output on a device immediately. Never requires approval; use it whenever device behaviour is unexpected.\n- **hw_batch_actuate**(device_id, commands, dry_run, verify) [capability_expand category: hardware]: Write multiple actuator channels in one atomic transaction. Used for coordinated multi-joint control where all positions must be commanded simultaneously (e.g. robot arm joint-space control).\n- **hw_control_start**(device_id, mode, targets, frequency_hz, trajectory, policy, gains) [capability_expand category: hardware]: Start a real-time control loop on a robot device. Supports PID position tracking, impedance control, trajectory following, and neural policy chains.\n- **hw_control_stop**(device_id) [capability_expand category: hardware]: Stop the real-time control loop on a device. Gracefully halts the device and returns session statistics.\n- **hw_control_status**(device_id) [capability_expand category: hardware]: Get the status and statistics of a running control loop. Returns frequency, jitter, overruns, and policy state.\n- **hw_policy_infer**(device_id, policy, task, execute, chunk_size, verify, compute_budget) [capability_expand category: hardware]: Execute VLA policy inference on a robot device. Reads observation, runs policy, optionally writes action to actuators. Supports local (HuggingFace) and remote (PolicyServer) policies.\n- **hw_policy_episode**(action, device_id, policy, task) [capability_expand category: hardware]: Manage policy inference episode lifecycle. Start an episode to begin a task, stop to finalise and get stats.\n- **hw_policy_list**(source) [capability_expand category: hardware]: List available VLA policies and their capabilities.\n- **hw_orchestrate**(mode, operations, barrier_timeout_s, label) [capability_expand category: hardware]: Execute coordinated operations across multiple hardware devices. Supports sequential (one-at-a-time), parallel (concurrent), and barrier (synchronised) execution modes. Any failure in a parallel group triggers halt_all on every participating device for safety.\n- **session_search**(query, limit) [capability_expand category: unclassified]: Search past conversation sessions in the current workspace for relevant context.\n- **session_list**(limit): List recent conversation sessions in the current workspace with stable ids, titles, dates, and summaries. Use for browsing past tasks or when user asks to see history without specific search terms. No keywords needed — returns chronological list.\n- **session_detail**(session_id, limit, offset, include_inactive) [capability_expand category: unclassified]: Read a paginated persisted transcript for one past conversation session in the current workspace. Use after session_list or session_search returns a session_id.\n- **click**(element_index) [capability_expand category: desktop]: Click a UI element by its element_index (from the latest observe_ui snapshot)\n- **get_clipboard**() [capability_expand category: desktop]: Read current clipboard text content\n- **list_apps**(filter, running_only) [capability_expand category: desktop]: List available applications on this system. Use to discover correct bundle_id before switch_app.\n- **list_windows**() [capability_expand category: desktop]: List all top-level windows with pid, window_id, title, and per-window state (minimized, on-screen). Call this first to pick the pid and window_id that observe_ui and other window tools require.\n- **observe_ui**(pid, window_id, query) [capability_expand category: desktop]: Snapshot one window's actionable UI elements, each tagged with an element_index for click/right_click/read_text. Re-observe after actions — indices belong to one snapshot. Requires the window's pid and window_id from list_windows.\n- **open_url**(url, app_id) [capability_expand category: desktop]: Open a URL in the default or specified browser\n- **read_text**(element_index) [capability_expand category: desktop]: Read the text content of a specific UI element from the latest snapshot\n- **right_click**(element_index) [capability_expand category: desktop]: Right-click a UI element to open its context menu. Returns visible menu items.\n- **screenshot**(pid, window_id) [capability_expand category: desktop]: Capture a screenshot for visual verification. With pid + window_id captures that window (works across all displays); defaults to the last observed window, or the full desktop when no window has been observed.\n- **scroll**(element_index, direction, amount, pid, window_id) [capability_expand category: desktop]: Scroll a scrollable area of a window. Omit element_index to scroll the window's focused/page scroller; pass one to scroll an exact element from the latest snapshot.\n- **select_text**(element_index) [capability_expand category: desktop]: Select all text in a UI element (focus + select-all, for subsequent copy)\n- **set_clipboard**(text) [capability_expand category: desktop]: Write text to the clipboard\n- **shortcut**(keys) [capability_expand category: desktop]: Execute a keyboard shortcut\n- **switch_app**(app_id) [capability_expand category: desktop]: Switch to an app (launch if needed, activate, verify)\n- **type_text**(text) [capability_expand category: desktop]: Type text into the currently focused element\n- **wait**(seconds) [capability_expand category: desktop]: Wait for a specified duration before continuing\n- **wait_until**(condition, pid, window_id, timeout, poll_interval) [capability_expand category: desktop]: Wait until a UI condition is met (polls UI tree). Returns elements when found or on timeout.\n- **wait_until_stable**(timeout, poll_interval, pid, window_id) [capability_expand category: desktop]: Wait until the UI stops changing (element set stabilizes across polls).\n\n## App Connector Capability Index\nLeapFlow can onboard and manage external apps through `platform_connect` and execute exact registered business actions through `platform_action`.\nFor requests about connecting, setting up, configuring, enabling, or managing a supported app, use `platform_connect` first instead of generating SDK/Webhook sample code.\n`platform_connect.action` is the management namespace: list, guide, preflight, connect, disconnect, remove, status, events_start, events_stop, events_status.\n`platform_action.action` is only for exact registered platform business actions listed below, such as `im.send_message`; never use management actions like `list` or `guide` there.\nAll business fields MUST go inside `payload`; top-level keys are only `platform`, `action`, and `payload`.\nDo not invent platform IDs or platform action names. If the needed action is not listed, ask for discovery/clarification instead of guessing.\nUse `platform_connect` with `action='guide'` and the matching `platform` to start onboarding; use `action='list'` when the app is unclear.\nWhen a pending onboarding state is present, continue from that state with `platform_connect` instead of asking the user to restate the app.\nSupported apps:\n- `api_server`: API Server (OpenAI Compatible) (category=api; backend=adapter; platform_action actions=none registered)\n- `dingtalk`: 钉钉 (DingTalk) (category=im; backend=adapter; platform_action actions=none registered)\n- `feishu`: 飞书 (Feishu/Lark) (category=im; backend=cli)\n - `calendar.create_event` payload={summary*, start_time*, end_time*, attendees} [write/high]\n - `docs.create_markdown` payload={title*, markdown*, folder_token} [write/medium]\n - `drive.search` payload={query*, limit} [read/medium]\n - `im.add_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.download_resource` payload={message_id*, file_key*, type*} [read/low]\n - `im.get_messages` payload={message_ids*} [read/low]\n - `im.list_chats` payload={page_size} [read/low]\n - `im.list_messages` payload={chat_id*} [read/low]\n - `im.list_thread_messages` payload={thread*} [read/low]\n - `im.remove_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.reply_message` payload={message_id*, text*} [send/high]\n - `im.search_chats` payload={query*, page_size} [read/low]\n - `im.search_messages` payload={query*} [read/medium]\n - `im.send_message` payload={chat_id*, text*, thread_id} [send/high]\n - `im.update_card` payload={token*, card*} [write/medium]\n - `im.update_message` payload={message_id*, text*} [write/medium]\n - `mail.search_unread` payload={query, limit} [read/high]\n - `sheets.append_row` payload={spreadsheet_token*, sheet_id*, values*} [write/medium]\n - `task.create` payload={title*, description, due_time} [write/medium]\n- `telegram`: Telegram (category=social; backend=adapter; platform_action actions=none registered)\n- `webhook`: Webhook (Generic) (category=webhook; backend=adapter; platform_action actions=none registered)\n\n## Learned Skills\nYou have access to the following learned skills. Use `skills_list` to browse or `skill_view` to read details:\n- api_designer: REST and GraphQL API design with OpenAPI spec generation and best practices [API, REST, GraphQL]\n- code_review: Structured code review for security, performance, and style [code, review, security]\n- data_analysis: CSV/JSON data analysis with statistical summaries, quality checks, and visualiza [data, analysis, statistics]\n- devops_helper: CI/CD pipeline configuration, Docker orchestration, and deployment strategy [devops, CI/CD, Docker]\n- document_writer: Structured document generation: technical docs, API references, reports, and REA [documentation, technical-writing, README]\n- git_workflow: Git operation orchestration: branching, commits, conflicts, and PR workflow [git, version-control, branching]\n- i18n_helper: Internationalization workflow: string extraction, translation, locale management [i18n, internationalization, localization]\n- project_analysis: Project structure analysis, dependency audit, and tech stack identification [project, analysis, dependencies]\n- security_audit: Security vulnerability scanning strategy, threat modeling, and fix recommendatio [security, vulnerability, audit]\n- shell_automation: Natural language to shell commands with safety review and step-by-step execution [shell, terminal, automation]\n- test_writer: Generate unit and integration tests from source code with edge-case coverage [testing, unit-test, integration-test]\n- web_research: Multi-step web search with cross-verification and structured synthesis [web, search, research]\n\n## Tool Usage\nTools are normally invoked through the native function-calling mechanism, not by writing JSON in your reply\ntext. Only if the provider signals that native function calling is unavailable for this turn, fall back to a\nsingle JSON code block: `{\"name\": \"tool_name\", \"arguments\": {\"key\": \"value\"}}` — use this fallback format\nonly, never both. Only call a tool whose exact name appears in the tool index above and is currently callable\n(or reachable via `capability_expand`). Never invent, rename, alias, or guess a tool name, platform ID, or\nplatform action from argument shape or wording — if the index does not list it, it does not exist.\n`platform_connect.action` (list/guide/preflight/connect/disconnect/remove/status/events_start/events_stop/\nevents_status) is the App Connector management namespace; `platform_action.action` only accepts exact\nregistered business actions such as `im.send_message` shown in the App Connector Capability Index — never mix\nthe two namespaces. If a tool call returns an unknown/unavailable result, use the returned suggestions or\navailable names for a single retry instead of trying further variations of the same guess.\n\n**Side-effect action rule** (`platform_action` with effect=send/write/execute):\n- Call each unique action+payload **exactly once**. Never include duplicates in the same turn.\n- Once the result returns `\"completed\": true`, that action is DONE for this task. Do NOT call it again\n in any subsequent turn — immediately summarize the result for the user instead.\n- The system enforces idempotency: duplicate calls are blocked and will not execute.\n- If the user explicitly requests sending/writing multiple times, use distinct payloads per call.\n\n**Resource identifier provenance rule**:\n- NEVER fabricate, guess, or infer resource identifiers (chat_id, message_id, file_key, user_id, etc.).\n Every resource ID used in a side-effect action MUST come from a successful API response in this session.\n- If a read/list action fails (e.g. authorization error), you do NOT have valid resource IDs.\n Report the failure to the user — do NOT attempt the dependent write/send action with a guessed ID.\n- When a tool result contains `\"llm_instruction\"`, follow it exactly.\n\n## Guidelines\n1. **Direct answers first**: If you already know the answer, respond directly without tools.\n2. **Avoid redundant tool calls**: Do not call the same tool with the same arguments more than once in the same user turn. When an existing tool result already answers the user's request, stop calling tools and answer directly.\n3. **Use tools proactively**: When the user asks about files, time, system state, or needs actions performed, use the appropriate tool.\n4. **Chain tools when needed**: You can call multiple tools in sequence (e.g., list files → read file → summarize).\n5. **Handle failures gracefully**: If a tool fails, explain what went wrong and suggest alternatives. If it failed because of an unknown tool/platform/action name, retry once with an exact name from the returned suggestions, then explain rather than keep guessing.\n6. **Summarize results naturally**: After tool execution, synthesize the results into a helpful answer rather than dumping raw output.\n7. **Stay conversational**: Maintain a natural, helpful tone. Acknowledge context from earlier in the conversation.\n8. **Recall past work**:\n - Broad queries (\"之前做了什么\", \"列出任务\"): answer from the \"Recent Task History\" section already in your context. If insufficient, call session_list.\n - Specific lookups (\"上次那个配置怎么改的\"): call session_search with relevant phrases (NOT single characters).\n - Do NOT call search tools repeatedly with keyword fragments. One well-phrased call is better than ten fragmented ones.\n\n## Coding & Verification\nWhen working with code, prefer the precise built-in tools over ad-hoc shell: use `repo_map` to orient in an\nunfamiliar project, `code_search`/`file_find` to locate and `code_intel` for symbols, `edit_file` (anchored\nsearch-replace, or a unified `diff`) to change files — never rewrite a whole file to change a few lines — and\n`git_query` to inspect diffs/log. After edits, check `syntax_ok` in the result and run `test_run`/`lint_check`\nbefore declaring the work done. Batch independent read-only calls (search/read/query) into a single turn — they\nrun in parallel.\n\n## Presentation Style\n1. **Polished Markdown only**: Format user-facing answers with clean Markdown headings, short paragraphs, and concise bullets. Use tables only when they improve comparison or scanning.\n2. **Terminal-friendly layout**: Keep lines readable in a TUI; avoid dense walls of text, deeply nested lists, oversized ASCII art, or heavy visual blocks.\n3. **Elegant emphasis**: Use bold text sparingly for key terms and conclusions. Avoid excessive emojis, decorative symbols, repeated separators, or visual noise.\n4. **Theme-safe colors**: Do not emit ANSI escape codes, HTML color tags, Rich markup, or hardcoded color names. Rely on the TUI theme to render Markdown professionally.\n5. **No leaked tool protocol**: Never show tool-call JSON, internal schemas, raw observations, tool result payloads, or hidden reasoning in the final answer unless the user explicitly asks for raw/debug output. Treat any prior `{\"name\": ..., \"arguments\": ...}` blocks and `Tool result (...)` messages as internal execution context only.\n6. **Professional closure**: End with a concise conclusion or next step when helpful; avoid rambling after the useful answer is complete.\n\nWhen finished with all tool calls, respond normally without a JSON block, tool-call transcript, or process log.\n\n## Task Contract\n- Task ID: turn-6\n- Original user request: Actuate the bench_r8 setpoint to 65.\n- Workspace root: \n- Allowed roots: \n- Treat relative project paths as relative to the workspace root; never infer `.` as the project root when a workspace root is provided.\n- Workspace boundary is enforced by tools: do not read, search, edit, or run commands against paths outside the allowed roots unless the user explicitly requests an external path and the tool/approval policy permits it.\n- LeapFlow workspace config is optional at `/.leapflow/config.yaml`; runtime config is loaded from `~/.leapflow/config/user.yaml` and `~/.leapflow/profiles//config/*.yaml`.\n- Preserve this task contract across summarization, compression, tool loops, and memory retrieval.\n" + "content": "You are LeapFlow, an intelligent assistant that can both converse naturally and take real actions on the user's computer.\n\n## Capabilities\nThe tool index below lists **every** registered tool by name and a one-line summary — this is the complete\ncapability contract; nothing else exists. Only a subset is directly callable this turn (via native tool calling,\nnot a JSON block in your reply). If you need a tool from the index that is not yet callable, call\n`capability_expand` with its category name first — the matching tools become callable immediately after.\nWhen the user asks about LeapFlow **itself** — its identity/version/model, what tools, plugins, or capabilities\nit has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of\nself-awareness — answer from live runtime evidence via `self_describe` (facet=capabilities for \"what can you\ndo / do you support X\", identity for version/model, runtime for context/posture, or `all` for a full self-check),\nnever by reading LeapFlow's own source code, documentation, or memory. Report configuration-dependent or\nunavailable capabilities as limitations rather than inferring them.\n- **text_search**(text, pattern): Search for a regex pattern in text.\n- **text_replace**(text, old, new, count) [capability_expand category: write]: Replace occurrences of a substring in text.\n- **time_get**(): Get current date and time.\n- **env_info**(): Get system environment information (OS, Python version, cwd).\n- **skills_list**(query, category, source): List available learned skills. Use when user asks about capabilities or you need a specific skill.\n- **skill_view**(name): View the full content of a specific skill document.\n- **skill_list_files**(name) [capability_expand category: skills]: List all support files in a skill directory (configs, templates, examples). Use before skill_file_read to discover available files.\n- **skill_file_read**(name, file_path) [capability_expand category: skills]: Read an individual support file from a skill directory. Use skill_list_files first to discover available files.\n- **code_intel**(path, operation): Precise document symbols (outline) for a source file: classes, functions, and methods with line ranges. Python uses an exact AST parse; other languages use a keyword-prefix scan. Prefer over file_read mode=symbols for accurate navigation before editing. Read-only.\n- **repo_map**(path): Compact project orientation for a repository root: languages, detected test/lint commands, top-level structure, entry points, manifest, and VCS branch. Call this first when entering an unfamiliar codebase. Read-only.\n- **scm_sync**(action, cwd, remote, pull_ref, push_ref, timeout) [capability_expand category: scm]: Run a typed git SCM action. Use this instead of shell_run for git pull/push/status. For 'pull origin main then push', set action='pull_then_push', remote='origin', pull_ref='main', and omit push_ref so LeapFlow pushes the current local branch.\n- **git_query**(action, cwd, ref, path, staged, max_count, stat): Read-only structured git inspection: action=diff|log|status|branch|show. Prefer over shell_run for reading repo state — output is clipped, redacted, and log/branch are parsed into structured fields. Use scm_sync for pull/push.\n- **git_write**(action, cwd, message, stage_all, name, ref, create) [capability_expand category: scm]: Mutating git actions: action=commit (message, stage_all), branch (create+switch), checkout (switch; create=true for -b). Approval-gated. Use scm_sync for pull/push and git_query for reads.\n- **test_run**(command, cwd, timeout): Run the project's test suite and return structured results (framework, passed/failed counts, failing tests). Auto-detects the runner (pytest/npm/go/cargo) or uses a configured/explicit command; executes via the governed shell. ok=true means the runner executed — see 'success' for pass/fail.\n- **lint_check**(command, cwd, timeout): Run the project's linter and return a structured clean/issue result. Auto-detects the linter (ruff/eslint/go vet/clippy) or uses a configured/explicit command; executes via the governed shell. ok=true means the linter ran — see 'clean'.\n- **file_list**(path, pattern, depth): List files and directories at a given path. Use depth=1 or depth=2 to get a recursive tree in one call instead of listing each sub-directory separately.\n- **file_read**(path, max_lines, start_line, max_chars, mode): Read text file content with adaptive context governance. For large or unfamiliar files, prefer mode='outline' or mode='symbols' first, then use mode='raw' with start_line/max_lines for the specific range you actually need. For LeapFlow's own settings, use config_list / config_get / config_set — its config files are outside the workspace and not readable here.\n- **file_write**(path, content, mode) [capability_expand category: write]: Write content to a file (overwrite or append).\n- **code_search**(pattern, patterns, path, glob, ignore_case, multiline, max_results, context_lines): Search file CONTENTS by regex pattern across a directory tree (ripgrep-backed). Requires a regex pattern. NOT for listing or browsing directory contents — use file_list for that. Prefer this over shell_run grep: faster, skips VCS/dependency/build dirs, and returns structured path:line:column matches. Batch related lookups into ONE call via `patterns` (OR-combined, single pass) instead of issuing several separate searches. Use file_read for the surrounding context of a hit.\n- **file_find**(glob, path, max_results): Find files by a recursive glob pattern under a base path (e.g. '**/test_*.py' or '*.md'). Prefer this over shell_run find; skips VCS/dependency/build dirs.\n- **edit_file**(path, edits, dry_run, diff) [capability_expand category: write]: Apply targeted, anchored search-replace edits to an EXISTING text file (use file_write to create/overwrite). Each edit is {original_text, new_text, replace_all?}; original_text must match exactly and uniquely (or set replace_all) — a non-unique or missing anchor is rejected so files are never corrupted. Set dry_run to preview. Alternatively pass a unified 'diff' to apply its hunks as anchored edits. Far cheaper and safer than rewriting a whole file.\n- **shell_run**(command, cwd, timeout) [capability_expand category: shell]: Execute a one-shot shell command with timeout protection. Runs in the active workspace; paths resolving outside it are refused. Reach for a structured tool first when one fits — web_fetch for anything over HTTP(S), git_query/scm_sync for git, code_search/file_find/file_read for the repo, config_get/config_set for LeapFlow's own settings — because those report typed results, while a failed shell command can only be diagnosed from its exit code and stderr. Every shell run counts as an external side effect, so a failure stops the rest of the batch and is not retried automatically.\n- **terminal_open**(command, cwd, shell) [capability_expand category: terminal]: Open a PERSISTENT shell session (REPL/dev server/watch), returning a session_id for terminal_send/read/close. Disabled unless tools.terminal_session_enabled is set. For one-shot commands use shell_run instead.\n- **terminal_send**(session_id, input, wait) [capability_expand category: terminal]: Send a line of input to a persistent terminal session and return output captured shortly after.\n- **terminal_read**(session_id, wait): Drain buffered output from a persistent terminal session (optionally waiting briefly first).\n- **terminal_close**(session_id) [capability_expand category: terminal]: Terminate a persistent terminal session and release its process group.\n- **terminal_list**(): List active persistent terminal sessions.\n- **config_list**(category, limit): List LeapFlow's own writable settings (LLM model and endpoint, daemon, memory, perception, gateway, …) with current values. Use this to discover the exact key before changing anything. Optionally narrow by `category`. This is the only correct way to inspect LeapFlow configuration — never read config files from disk.\n- **config_get**(key): Read one LeapFlow setting by key (e.g. 'llm.model', 'llm.base_url', 'daemon.log_level'), returning its current value, type, scopes, and whether a change needs a daemon restart. There is no 'llm.provider' key: provider behavior is inferred from the OpenAI-compatible 'llm.base_url'. Never read LeapFlow config files from disk — use this.\n- **config_set**(key, value, scope) [capability_expand category: config]: Change one LeapFlow setting by key, e.g. switch an OpenAI-compatible LLM with key='llm.model' and key='llm.base_url'. There is no 'llm.provider' key: provider behavior is inferred from the endpoint. Values are validated and coerced; credentials are stored in the vault automatically. Call config_list or config_get first if unsure of the key. The result states whether a `leap daemon restart` is required. Never edit LeapFlow config files directly.\n- **web_fetch**(url, select, timeout, max_bytes): Read a URL over HTTP(S) and get back extracted, context-sized content: parsed JSON for API endpoints, readable text plus links for web pages. Use this for anything on the internet — prices, docs, releases, articles — instead of running curl through shell_run: it reports real HTTP status codes, retries rate limits on its own, and is a plain read so a retry is always safe. For JSON APIs pass `select` with a dotted path (e.g. 'chart.result.0.meta') to return just that part instead of the whole payload.\n- **memory_search**(query, limit): Search agent memory for relevant past experiences, observations, and facts.\n- **memory_add**(content, kind) [capability_expand category: write]: Store a new observation or insight in memory for future reference.\n- **research_note**(kind, text): Record a compact, structured note about the current task's state so it survives context compression on long / multi-step tasks. Use for durable findings, open questions still to resolve, decisions / excluded paths, and the immediate next step. One concise sentence per note.\n- **capability_expand**(category): Fetch the full callable schema for every tool in a capability category (e.g. 'hub', 'gateway', 'desktop', 'delegate', 'file', 'memory', 'skill'). The compact tool index always lists every registered tool by name and a one-line summary, but only a static low-risk subset is directly callable each turn. If you need a tool from the index that is not yet callable, call capability_expand with its category first; the matching tools become callable in this turn. Never invent a tool name — expand the category instead.\n- **delegate_task**(goal, context) [capability_expand category: delegate]: Delegate a complex sub-task to an isolated subagent. The subagent gets a fresh context and restricted tool access. Use when a task is self-contained and can be solved independently.\n- **schedule_reentry**(kind, reason, delay_seconds, event_match, max_reentries, deadline_seconds): Register a re-entry so this task can resume later from its current orientation (findings / open questions / next step). Use when work must pause and continue after a delay (kind=time) or when a matching platform event arrives (kind=event), instead of finishing now. The research-ledger state is carried over automatically.\n- **hub_push**(skill_name, visibility, version) [capability_expand category: hub]: Push a local skill to the ModelScope Hub for sharing or backup.\n- **hub_pull**(repo_id, version) [capability_expand category: hub]: Pull a skill from the ModelScope Hub to install locally.\n- **hub_search**(query) [capability_expand category: hub]: Search for skills on the Hub by keyword or description.\n- **hub_sync**(mode, dry_run) [capability_expand category: hub]: Preview or execute sync between local skills and Hub.\n- **platform_action**(platform, action, payload, backend_kind) [capability_expand category: gateway]: Execute an exact registered business action on an external platform through LeapFlow's App Connector layer. Actions must be copied from the App Connector Capability Index and are addressed as domain.operation, e.g. im.send_message or docs.create_markdown. All business fields (chat_id, text, query, etc.) MUST be placed inside `payload`, never at the top level. Example: {\"platform\":\"feishu\",\"action\":\"im.send_message\",\"payload\":{\"chat_id\":\"oc_xxx\",\"text\":\"hello\"}}. Do not invent action names, do not use management actions such as list/guide/connect/status here.\n- **platform_connect**(action, platform, credentials, options, checkpoint) [capability_expand category: gateway]: List, guide, connect, disconnect, remove, or check status for external platforms using the App Connector management namespace. Supports REST and CLI backends. Use this for management actions such as list/guide/preflight/connect/status; use platform_action only for exact registered business actions.\n- **gateway_send**(platform, chat_id, text, thread_id) [capability_expand category: gateway]: Send a message to a connected external platform (Feishu group, Telegram chat, DingTalk conversation, etc.). Requires the platform to be connected via gateway_connect first. Use gateway_connect with action='list' to see connected platforms and available chat IDs.\n- **gateway_connect**(action, platform, credentials, options) [capability_expand category: gateway]: Connect, configure, or manage external platform integrations (Feishu, DingTalk, Telegram, Slack, Discord, etc.). Conversational flow: 1) call 'guide' to get setup steps + required fields, 2) present the steps to the user and ask for ALL required credentials in a single message, 3) call 'connect' with the credentials. Goal: complete in 1–2 user turns. NEVER include credential values in your text response.\n- **plugin_list**(): List the live plugin registry across Tool/Gateway/LLM subsystems for plugin management: which plugins are loaded, their category and fiber lifecycle state. For questions about LeapFlow's own capabilities or whether it supports X, use self_describe instead.\n- **plugin_status**(plugin_id): Get detailed status of a specific plugin: its declared category, runtime dependencies, contributed tools, and fiber lifecycle state.\n- **plugin_versions**(plugin_id): List recorded source versions and active pointer for a profile-scoped plugin.\n- **plugin_propose**(requested_capability, plugin_id, proposed_tools, test_cases, risk_level, evidence): Create a side-effect-free PluginProposal from explicit capability-gap evidence. Use this before plugin_generate when a missing capability should be reviewed. Does not call an LLM, write files, or install anything.\n- **assess_compatibility**(manifest, source_path) [capability_expand category: plugin_management]: Assess whether a foreign plugin manifest or real DSH source bundle is compatible with LeapFlow. A source bundle assessment is static: runtime_ready stays false until restricted Node discovery during plugin_install. Returns component-level verdicts and limitations.\n- **plugin_generate**(plugin_id, description, proposal_id) [capability_expand category: system]: Generate a new ToolPlugin from a natural-language capability description. The LLM produces code that conforms to the ToolPlugin Protocol; it is then rigorously validated (syntax, structure, import, protocol conformance). The generated source is stored in CAS and receives explicit content approval. It DOES NOT install the plugin — installation requires a second, mutation-specific approval via plugin_install.\n- **plugin_install**(plugin_id, code, marketplace_name, source_path, proposal_id, version_label) [capability_expand category: system]: Install a plugin from exactly one source: validated Python code, the configured Python marketplace, or a real DSH source bundle. DSH bundles run restricted Node discovery before registration; only public host tools are installed and client UI limitations are reported. Writes profile-scoped state and mutates the process-global registry. REQUIRES APPROVAL.\n- **plugin_rollback**(plugin_id, version) [capability_expand category: system]: Rollback a profile-scoped plugin to a recorded source version and reload it. REQUIRES APPROVAL.\n- **plugin_reload**(plugin_id, version_label) [capability_expand category: system]: Hot-reload a plugin at runtime. Disposes the old plugin fiber, re-imports its module, and registers a fresh instance. Existing in-flight turns are unaffected (snapshot isolation). REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_disable**(plugin_id) [capability_expand category: system]: Disable a plugin by disposing its fiber, removing its tools from the runtime registry. Cannot disable self_management itself. REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_remove**(plugin_id, delete_source) [capability_expand category: system]: Terminally remove a plugin: dispose its fiber, unregister its tools, remove reload metadata, and optionally delete its profile-scoped source file. Cannot remove self_management itself. REQUIRES APPROVAL.\n- **plugin_unquarantine**(plugin_id) [capability_expand category: plugin_management]: Restore a quarantined plugin to probation status for re-evaluation. Unfreezes the trust ledger, transitions the proposal back to PROBATION, and reloads the plugin. REQUIRES APPROVAL.\n- **plugin_enable**(plugin_id) [capability_expand category: system]: Re-enable a previously disabled plugin by reloading its module and registering a fresh instance. REQUIRES APPROVAL.\n- **self_describe**(facet): Answer questions about LeapFlow ITSELF from live runtime evidence — its identity/version/model, what tools and plugins it has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of self-awareness. Prefer this over reading LeapFlow's own source code. Pick facet=capabilities for 'what can you do / do you support X', identity for version/model, runtime for context/posture, or all for a full self-check when explicitly requested.\n- **runtime_snapshot**(): Lightweight (~150 token) flat snapshot of current runtime state: model, context budget, posture, disclosure level, turn count, cache hit rate, uptime, tool count, and pending approvals.\n- **schedule_create**(trigger_expression, instruction, execution_mode, max_retries, delivery_target) [capability_expand category: scheduler]: Create a new scheduled task. Specify a trigger expression (e.g. '30m', 'every 2h', '0 9 * * *') and the instruction to execute on each trigger. Optionally provide an execution mode and a delivery target to receive notifications.\n- **schedule_list**() [capability_expand category: scheduler]: List all scheduled tasks with their current state, trigger, and next due time.\n- **schedule_status**(task_id) [capability_expand category: scheduler]: Get detailed status and recent execution history for a scheduled task.\n- **schedule_pause**(task_id) [capability_expand category: scheduler]: Pause a scheduled task so it stops firing without being cancelled.\n- **schedule_resume**(task_id) [capability_expand category: scheduler]: Resume a paused scheduled task — re-arms it and recalculates next due time.\n- **schedule_cancel**(task_id) [capability_expand category: scheduler]: Cancel a scheduled task permanently.\n- **tool_search**(query, max_results): Search all registered tools by keyword relevance. Returns a ranked list of matching tools with name, category, and summary. Use this when you need a tool but are unsure of its exact name or category.\n- **tool_describe**(tool_name): Get the full callable schema of a single tool by exact name. Returns the tool's description, parameters, and metadata. Use after tool_search to inspect a specific tool before calling it.\n- **hw_list**(): List connected hardware devices with their channel counts and measured quantities. Start here; it does not include operating limits.\n- **hw_describe**(device_id): Return the full reference for one device: every channel, its unit, its operating envelope, rate limit, reversibility, and required interlocks, plus any prior outcomes recorded for its writable channels. Required before commanding a device.\n- **hw_read**(device_id, channel_id): Read the current value of one device channel. Has no physical effect.\n- **hw_status**(device_id): Report connection health, halt capability, and recent notable events (threshold excursions, lost samples, stalled channels) for one device.\n- **hw_configure**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Set a configuration value or setpoint on a channel declaring effect=configure. Setpoints often have inertia: the value may need time to stabilise.\n- **hw_actuate**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Command motion or output on a channel declaring effect=actuate. This moves physical hardware; a repeat from an unknown state is not a safe retry.\n- **hw_dispense**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Consume an irreversible resource on a channel declaring effect=dispense. Running this twice dispenses twice; never repeat it after a failure without first verifying what already happened.\n- **hw_estop**(device_id) [capability_expand category: hardware]: Stop all motion and output on a device immediately. Never requires approval; use it whenever device behaviour is unexpected.\n- **hw_batch_actuate**(device_id, commands, dry_run, verify) [capability_expand category: hardware]: Write multiple actuator channels in one atomic transaction. Used for coordinated multi-joint control where all positions must be commanded simultaneously (e.g. robot arm joint-space control).\n- **hw_control_start**(device_id, mode, targets, frequency_hz, trajectory, policy, gains) [capability_expand category: hardware]: Start a real-time control loop on a robot device. Supports PID position tracking, impedance control, trajectory following, and neural policy chains.\n- **hw_control_stop**(device_id) [capability_expand category: hardware]: Stop the real-time control loop on a device. Gracefully halts the device and returns session statistics.\n- **hw_control_status**(device_id) [capability_expand category: hardware]: Get the status and statistics of a running control loop. Returns frequency, jitter, overruns, and policy state.\n- **hw_context_snapshot**(level) [capability_expand category: hardware]: Get a structured snapshot of all hardware devices and their state. Use this before planning physical manipulation tasks to understand what devices are available and what they can do.\n- **hw_health**(device_id) [capability_expand category: hardware]: Check hardware device health status. Without device_id, returns fleet-wide health summary. With device_id, returns health details for that specific device.\n- **hw_policy_infer**(device_id, policy, task, execute, chunk_size, verify, compute_budget) [capability_expand category: hardware]: Execute VLA policy inference on a robot device. Reads observation, runs policy, optionally writes action to actuators. Supports local (HuggingFace) and remote (PolicyServer) policies.\n- **hw_policy_episode**(action, device_id, policy, task) [capability_expand category: hardware]: Manage policy inference episode lifecycle. Start an episode to begin a task, stop to finalise and get stats.\n- **hw_policy_list**(source) [capability_expand category: hardware]: List available VLA policies and their capabilities.\n- **hw_orchestrate**(mode, operations, barrier_timeout_s, label) [capability_expand category: hardware]: Execute coordinated operations across multiple hardware devices. Supports sequential (one-at-a-time), parallel (concurrent), and barrier (synchronised) execution modes. Any failure in a parallel group triggers halt_all on every participating device for safety.\n- **session_search**(query, limit) [capability_expand category: unclassified]: Search past conversation sessions in the current workspace for relevant context.\n- **session_list**(limit): List recent conversation sessions in the current workspace with stable ids, titles, dates, and summaries. Use for browsing past tasks or when user asks to see history without specific search terms. No keywords needed — returns chronological list.\n- **session_detail**(session_id, limit, offset, include_inactive) [capability_expand category: unclassified]: Read a paginated persisted transcript for one past conversation session in the current workspace. Use after session_list or session_search returns a session_id.\n- **click**(element_index) [capability_expand category: desktop]: Click a UI element by its element_index (from the latest observe_ui snapshot)\n- **get_clipboard**() [capability_expand category: desktop]: Read current clipboard text content\n- **list_apps**(filter, running_only) [capability_expand category: desktop]: List available applications on this system. Use to discover correct bundle_id before switch_app.\n- **list_windows**() [capability_expand category: desktop]: List all top-level windows with pid, window_id, title, and per-window state (minimized, on-screen). Call this first to pick the pid and window_id that observe_ui and other window tools require.\n- **observe_ui**(pid, window_id, query) [capability_expand category: desktop]: Snapshot one window's actionable UI elements, each tagged with an element_index for click/right_click/read_text. Re-observe after actions — indices belong to one snapshot. Requires the window's pid and window_id from list_windows.\n- **open_url**(url, app_id) [capability_expand category: desktop]: Open a URL in the default or specified browser\n- **read_text**(element_index) [capability_expand category: desktop]: Read the text content of a specific UI element from the latest snapshot\n- **right_click**(element_index) [capability_expand category: desktop]: Right-click a UI element to open its context menu. Returns visible menu items.\n- **screenshot**(pid, window_id) [capability_expand category: desktop]: Capture a screenshot for visual verification. With pid + window_id captures that window (works across all displays); defaults to the last observed window, or the full desktop when no window has been observed.\n- **scroll**(element_index, direction, amount, pid, window_id) [capability_expand category: desktop]: Scroll a scrollable area of a window. Omit element_index to scroll the window's focused/page scroller; pass one to scroll an exact element from the latest snapshot.\n- **select_text**(element_index) [capability_expand category: desktop]: Select all text in a UI element (focus + select-all, for subsequent copy)\n- **set_clipboard**(text) [capability_expand category: desktop]: Write text to the clipboard\n- **shortcut**(keys) [capability_expand category: desktop]: Execute a keyboard shortcut\n- **switch_app**(app_id) [capability_expand category: desktop]: Switch to an app (launch if needed, activate, verify)\n- **type_text**(text) [capability_expand category: desktop]: Type text into the currently focused element\n- **wait**(seconds) [capability_expand category: desktop]: Wait for a specified duration before continuing\n- **wait_until**(condition, pid, window_id, timeout, poll_interval) [capability_expand category: desktop]: Wait until a UI condition is met (polls UI tree). Returns elements when found or on timeout.\n- **wait_until_stable**(timeout, poll_interval, pid, window_id) [capability_expand category: desktop]: Wait until the UI stops changing (element set stabilizes across polls).\n\n## App Connector Capability Index\nLeapFlow can onboard and manage external apps through `platform_connect` and execute exact registered business actions through `platform_action`.\nFor requests about connecting, setting up, configuring, enabling, or managing a supported app, use `platform_connect` first instead of generating SDK/Webhook sample code.\n`platform_connect.action` is the management namespace: list, guide, preflight, connect, disconnect, remove, status, events_start, events_stop, events_status.\n`platform_action.action` is only for exact registered platform business actions listed below, such as `im.send_message`; never use management actions like `list` or `guide` there.\nAll business fields MUST go inside `payload`; top-level keys are only `platform`, `action`, and `payload`.\nDo not invent platform IDs or platform action names. If the needed action is not listed, ask for discovery/clarification instead of guessing.\nUse `platform_connect` with `action='guide'` and the matching `platform` to start onboarding; use `action='list'` when the app is unclear.\nWhen a pending onboarding state is present, continue from that state with `platform_connect` instead of asking the user to restate the app.\nSupported apps:\n- `api_server`: API Server (OpenAI Compatible) (category=api; backend=adapter; platform_action actions=none registered)\n- `dingtalk`: 钉钉 (DingTalk) (category=im; backend=adapter; platform_action actions=none registered)\n- `feishu`: 飞书 (Feishu/Lark) (category=im; backend=cli)\n - `calendar.create_event` payload={summary*, start_time*, end_time*, attendees} [write/high]\n - `docs.create_markdown` payload={title*, markdown*, folder_token} [write/medium]\n - `drive.search` payload={query*, limit} [read/medium]\n - `im.add_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.download_resource` payload={message_id*, file_key*, type*} [read/low]\n - `im.get_messages` payload={message_ids*} [read/low]\n - `im.list_chats` payload={page_size} [read/low]\n - `im.list_messages` payload={chat_id*} [read/low]\n - `im.list_thread_messages` payload={thread*} [read/low]\n - `im.remove_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.reply_message` payload={message_id*, text*} [send/high]\n - `im.search_chats` payload={query*, page_size} [read/low]\n - `im.search_messages` payload={query*} [read/medium]\n - `im.send_message` payload={chat_id*, text*, thread_id} [send/high]\n - `im.update_card` payload={token*, card*} [write/medium]\n - `im.update_message` payload={message_id*, text*} [write/medium]\n - `mail.search_unread` payload={query, limit} [read/high]\n - `sheets.append_row` payload={spreadsheet_token*, sheet_id*, values*} [write/medium]\n - `task.create` payload={title*, description, due_time} [write/medium]\n- `telegram`: Telegram (category=social; backend=adapter; platform_action actions=none registered)\n- `webhook`: Webhook (Generic) (category=webhook; backend=adapter; platform_action actions=none registered)\n\n## Learned Skills\nYou have access to the following learned skills. Use `skills_list` to browse or `skill_view` to read details:\n- api_designer: REST and GraphQL API design with OpenAPI spec generation and best practices [API, REST, GraphQL]\n- code_review: Structured code review for security, performance, and style [code, review, security]\n- data_analysis: CSV/JSON data analysis with statistical summaries, quality checks, and visualiza [data, analysis, statistics]\n- devops_helper: CI/CD pipeline configuration, Docker orchestration, and deployment strategy [devops, CI/CD, Docker]\n- document_writer: Structured document generation: technical docs, API references, reports, and REA [documentation, technical-writing, README]\n- git_workflow: Git operation orchestration: branching, commits, conflicts, and PR workflow [git, version-control, branching]\n- i18n_helper: Internationalization workflow: string extraction, translation, locale management [i18n, internationalization, localization]\n- project_analysis: Project structure analysis, dependency audit, and tech stack identification [project, analysis, dependencies]\n- security_audit: Security vulnerability scanning strategy, threat modeling, and fix recommendatio [security, vulnerability, audit]\n- shell_automation: Natural language to shell commands with safety review and step-by-step execution [shell, terminal, automation]\n- test_writer: Generate unit and integration tests from source code with edge-case coverage [testing, unit-test, integration-test]\n- web_research: Multi-step web search with cross-verification and structured synthesis [web, search, research]\n\n## Tool Usage\nTools are normally invoked through the native function-calling mechanism, not by writing JSON in your reply\ntext. Only if the provider signals that native function calling is unavailable for this turn, fall back to a\nsingle JSON code block: `{\"name\": \"tool_name\", \"arguments\": {\"key\": \"value\"}}` — use this fallback format\nonly, never both. Only call a tool whose exact name appears in the tool index above and is currently callable\n(or reachable via `capability_expand`). Never invent, rename, alias, or guess a tool name, platform ID, or\nplatform action from argument shape or wording — if the index does not list it, it does not exist.\n`platform_connect.action` (list/guide/preflight/connect/disconnect/remove/status/events_start/events_stop/\nevents_status) is the App Connector management namespace; `platform_action.action` only accepts exact\nregistered business actions such as `im.send_message` shown in the App Connector Capability Index — never mix\nthe two namespaces. If a tool call returns an unknown/unavailable result, use the returned suggestions or\navailable names for a single retry instead of trying further variations of the same guess.\n\n**Side-effect action rule** (`platform_action` with effect=send/write/execute):\n- Call each unique action+payload **exactly once**. Never include duplicates in the same turn.\n- Once the result returns `\"completed\": true`, that action is DONE for this task. Do NOT call it again\n in any subsequent turn — immediately summarize the result for the user instead.\n- The system enforces idempotency: duplicate calls are blocked and will not execute.\n- If the user explicitly requests sending/writing multiple times, use distinct payloads per call.\n\n**Resource identifier provenance rule**:\n- NEVER fabricate, guess, or infer resource identifiers (chat_id, message_id, file_key, user_id, etc.).\n Every resource ID used in a side-effect action MUST come from a successful API response in this session.\n- If a read/list action fails (e.g. authorization error), you do NOT have valid resource IDs.\n Report the failure to the user — do NOT attempt the dependent write/send action with a guessed ID.\n- When a tool result contains `\"llm_instruction\"`, follow it exactly.\n\n## Guidelines\n1. **Direct answers first**: If you already know the answer, respond directly without tools.\n2. **Avoid redundant tool calls**: Do not call the same tool with the same arguments more than once in the same user turn. When an existing tool result already answers the user's request, stop calling tools and answer directly.\n3. **Use tools proactively**: When the user asks about files, time, system state, or needs actions performed, use the appropriate tool.\n4. **Chain tools when needed**: You can call multiple tools in sequence (e.g., list files → read file → summarize).\n5. **Handle failures gracefully**: If a tool fails, explain what went wrong and suggest alternatives. If it failed because of an unknown tool/platform/action name, retry once with an exact name from the returned suggestions, then explain rather than keep guessing.\n6. **Summarize results naturally**: After tool execution, synthesize the results into a helpful answer rather than dumping raw output.\n7. **Stay conversational**: Maintain a natural, helpful tone. Acknowledge context from earlier in the conversation.\n8. **Recall past work**:\n - Broad queries (\"之前做了什么\", \"列出任务\"): answer from the \"Recent Task History\" section already in your context. If insufficient, call session_list.\n - Specific lookups (\"上次那个配置怎么改的\"): call session_search with relevant phrases (NOT single characters).\n - Do NOT call search tools repeatedly with keyword fragments. One well-phrased call is better than ten fragmented ones.\n\n## Coding & Verification\nWhen working with code, prefer the precise built-in tools over ad-hoc shell: use `repo_map` to orient in an\nunfamiliar project, `code_search`/`file_find` to locate and `code_intel` for symbols, `edit_file` (anchored\nsearch-replace, or a unified `diff`) to change files — never rewrite a whole file to change a few lines — and\n`git_query` to inspect diffs/log. After edits, check `syntax_ok` in the result and run `test_run`/`lint_check`\nbefore declaring the work done. Batch independent read-only calls (search/read/query) into a single turn — they\nrun in parallel.\n\n## Presentation Style\n1. **Polished Markdown only**: Format user-facing answers with clean Markdown headings, short paragraphs, and concise bullets. Use tables only when they improve comparison or scanning.\n2. **Terminal-friendly layout**: Keep lines readable in a TUI; avoid dense walls of text, deeply nested lists, oversized ASCII art, or heavy visual blocks.\n3. **Elegant emphasis**: Use bold text sparingly for key terms and conclusions. Avoid excessive emojis, decorative symbols, repeated separators, or visual noise.\n4. **Theme-safe colors**: Do not emit ANSI escape codes, HTML color tags, Rich markup, or hardcoded color names. Rely on the TUI theme to render Markdown professionally.\n5. **No leaked tool protocol**: Never show tool-call JSON, internal schemas, raw observations, tool result payloads, or hidden reasoning in the final answer unless the user explicitly asks for raw/debug output. Treat any prior `{\"name\": ..., \"arguments\": ...}` blocks and `Tool result (...)` messages as internal execution context only.\n6. **Professional closure**: End with a concise conclusion or next step when helpful; avoid rambling after the useful answer is complete.\n\nWhen finished with all tool calls, respond normally without a JSON block, tool-call transcript, or process log.\n\n## Task Contract\n- Task ID: turn-6\n- Original user request: Actuate the bench_r8 setpoint to 65.\n- Workspace root: \n- Allowed roots: \n- Treat relative project paths as relative to the workspace root; never infer `.` as the project root when a workspace root is provided.\n- Workspace boundary is enforced by tools: do not read, search, edit, or run commands against paths outside the allowed roots unless the user explicitly requests an external path and the tool/approval policy permits it.\n- LeapFlow workspace config is optional at `/.leapflow/config.yaml`; runtime config is loaded from `~/.leapflow/config/user.yaml` and `~/.leapflow/profiles//config/*.yaml`.\n- Preserve this task contract across summarization, compression, tool loops, and memory retrieval.\n" }, { "role": "assistant", @@ -40,12 +40,14 @@ "hw_actuate", "hw_batch_actuate", "hw_configure", + "hw_context_snapshot", "hw_control_start", "hw_control_status", "hw_control_stop", "hw_describe", "hw_dispense", "hw_estop", + "hw_health", "hw_list", "hw_orchestrate", "hw_policy_episode", diff --git a/tests/_fixtures/cassettes/r8_hardware/cassette-model-00600853bb84f29e.cassette.json b/tests/_fixtures/cassettes/r8_hardware/cassette-model-00600853bb84f29e.cassette.json deleted file mode 100644 index 2dfd852c..00000000 --- a/tests/_fixtures/cassettes/r8_hardware/cassette-model-00600853bb84f29e.cassette.json +++ /dev/null @@ -1,79 +0,0 @@ -{ - "fingerprint": "00600853bb84f29e8682da73f77c49e441adb75d1d8df4657e2339dcf9cb1750", - "note": "captured in seed mode", - "request": { - "model": "cassette-model", - "stream": false, - "messages": [ - { - "role": "system", - "content": "You are LeapFlow, an intelligent assistant that can both converse naturally and take real actions on the user's computer.\n\n## Capabilities\nThe tool index below lists **every** registered tool by name and a one-line summary — this is the complete\ncapability contract; nothing else exists. Only a subset is directly callable this turn (via native tool calling,\nnot a JSON block in your reply). If you need a tool from the index that is not yet callable, call\n`capability_expand` with its category name first — the matching tools become callable immediately after.\nWhen the user asks about LeapFlow **itself** — its identity/version/model, what tools, plugins, or capabilities\nit has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of\nself-awareness — answer from live runtime evidence via `self_describe` (facet=capabilities for \"what can you\ndo / do you support X\", identity for version/model, runtime for context/posture, or `all` for a full self-check),\nnever by reading LeapFlow's own source code, documentation, or memory. Report configuration-dependent or\nunavailable capabilities as limitations rather than inferring them.\n- **text_search**(text, pattern): Search for a regex pattern in text.\n- **text_replace**(text, old, new, count) [capability_expand category: write]: Replace occurrences of a substring in text.\n- **time_get**(): Get current date and time.\n- **env_info**(): Get system environment information (OS, Python version, cwd).\n- **skills_list**(query, category, source): List available learned skills. Use when user asks about capabilities or you need a specific skill.\n- **skill_view**(name): View the full content of a specific skill document.\n- **skill_list_files**(name) [capability_expand category: skills]: List all support files in a skill directory (configs, templates, examples). Use before skill_file_read to discover available files.\n- **skill_file_read**(name, file_path) [capability_expand category: skills]: Read an individual support file from a skill directory. Use skill_list_files first to discover available files.\n- **code_intel**(path, operation): Precise document symbols (outline) for a source file: classes, functions, and methods with line ranges. Python uses an exact AST parse; other languages use a keyword-prefix scan. Prefer over file_read mode=symbols for accurate navigation before editing. Read-only.\n- **repo_map**(path): Compact project orientation for a repository root: languages, detected test/lint commands, top-level structure, entry points, manifest, and VCS branch. Call this first when entering an unfamiliar codebase. Read-only.\n- **scm_sync**(action, cwd, remote, pull_ref, push_ref, timeout) [capability_expand category: scm]: Run a typed git SCM action. Use this instead of shell_run for git pull/push/status. For 'pull origin main then push', set action='pull_then_push', remote='origin', pull_ref='main', and omit push_ref so LeapFlow pushes the current local branch.\n- **git_query**(action, cwd, ref, path, staged, max_count, stat): Read-only structured git inspection: action=diff|log|status|branch|show. Prefer over shell_run for reading repo state — output is clipped, redacted, and log/branch are parsed into structured fields. Use scm_sync for pull/push.\n- **git_write**(action, cwd, message, stage_all, name, ref, create) [capability_expand category: scm]: Mutating git actions: action=commit (message, stage_all), branch (create+switch), checkout (switch; create=true for -b). Approval-gated. Use scm_sync for pull/push and git_query for reads.\n- **test_run**(command, cwd, timeout): Run the project's test suite and return structured results (framework, passed/failed counts, failing tests). Auto-detects the runner (pytest/npm/go/cargo) or uses a configured/explicit command; executes via the governed shell. ok=true means the runner executed — see 'success' for pass/fail.\n- **lint_check**(command, cwd, timeout): Run the project's linter and return a structured clean/issue result. Auto-detects the linter (ruff/eslint/go vet/clippy) or uses a configured/explicit command; executes via the governed shell. ok=true means the linter ran — see 'clean'.\n- **file_list**(path, pattern, depth): List files and directories at a given path. Use depth=1 or depth=2 to get a recursive tree in one call instead of listing each sub-directory separately.\n- **file_read**(path, max_lines, start_line, max_chars, mode): Read text file content with adaptive context governance. For large or unfamiliar files, prefer mode='outline' or mode='symbols' first, then use mode='raw' with start_line/max_lines for the specific range you actually need. For LeapFlow's own settings, use config_list / config_get / config_set — its config files are outside the workspace and not readable here.\n- **file_write**(path, content, mode) [capability_expand category: write]: Write content to a file (overwrite or append).\n- **code_search**(pattern, patterns, path, glob, ignore_case, multiline, max_results, context_lines): Search file CONTENTS by regex pattern across a directory tree (ripgrep-backed). Requires a regex pattern. NOT for listing or browsing directory contents — use file_list for that. Prefer this over shell_run grep: faster, skips VCS/dependency/build dirs, and returns structured path:line:column matches. Batch related lookups into ONE call via `patterns` (OR-combined, single pass) instead of issuing several separate searches. Use file_read for the surrounding context of a hit.\n- **file_find**(glob, path, max_results): Find files by a recursive glob pattern under a base path (e.g. '**/test_*.py' or '*.md'). Prefer this over shell_run find; skips VCS/dependency/build dirs.\n- **edit_file**(path, edits, dry_run, diff) [capability_expand category: write]: Apply targeted, anchored search-replace edits to an EXISTING text file (use file_write to create/overwrite). Each edit is {original_text, new_text, replace_all?}; original_text must match exactly and uniquely (or set replace_all) — a non-unique or missing anchor is rejected so files are never corrupted. Set dry_run to preview. Alternatively pass a unified 'diff' to apply its hunks as anchored edits. Far cheaper and safer than rewriting a whole file.\n- **shell_run**(command, cwd, timeout) [capability_expand category: shell]: Execute a one-shot shell command with timeout protection. Runs in the active workspace; paths resolving outside it are refused. Reach for a structured tool first when one fits — web_fetch for anything over HTTP(S), git_query/scm_sync for git, code_search/file_find/file_read for the repo, config_get/config_set for LeapFlow's own settings — because those report typed results, while a failed shell command can only be diagnosed from its exit code and stderr. Every shell run counts as an external side effect, so a failure stops the rest of the batch and is not retried automatically.\n- **terminal_open**(command, cwd, shell) [capability_expand category: terminal]: Open a PERSISTENT shell session (REPL/dev server/watch), returning a session_id for terminal_send/read/close. Disabled unless tools.terminal_session_enabled is set. For one-shot commands use shell_run instead.\n- **terminal_send**(session_id, input, wait) [capability_expand category: terminal]: Send a line of input to a persistent terminal session and return output captured shortly after.\n- **terminal_read**(session_id, wait): Drain buffered output from a persistent terminal session (optionally waiting briefly first).\n- **terminal_close**(session_id) [capability_expand category: terminal]: Terminate a persistent terminal session and release its process group.\n- **terminal_list**(): List active persistent terminal sessions.\n- **config_list**(category, limit): List LeapFlow's own writable settings (LLM model and endpoint, daemon, memory, perception, gateway, …) with current values. Use this to discover the exact key before changing anything. Optionally narrow by `category`. This is the only correct way to inspect LeapFlow configuration — never read config files from disk.\n- **config_get**(key): Read one LeapFlow setting by key (e.g. 'llm.model', 'llm.base_url', 'daemon.log_level'), returning its current value, type, scopes, and whether a change needs a daemon restart. There is no 'llm.provider' key: provider behavior is inferred from the OpenAI-compatible 'llm.base_url'. Never read LeapFlow config files from disk — use this.\n- **config_set**(key, value, scope) [capability_expand category: config]: Change one LeapFlow setting by key, e.g. switch an OpenAI-compatible LLM with key='llm.model' and key='llm.base_url'. There is no 'llm.provider' key: provider behavior is inferred from the endpoint. Values are validated and coerced; credentials are stored in the vault automatically. Call config_list or config_get first if unsure of the key. The result states whether a `leap daemon restart` is required. Never edit LeapFlow config files directly.\n- **web_fetch**(url, select, timeout, max_bytes): Read a URL over HTTP(S) and get back extracted, context-sized content: parsed JSON for API endpoints, readable text plus links for web pages. Use this for anything on the internet — prices, docs, releases, articles — instead of running curl through shell_run: it reports real HTTP status codes, retries rate limits on its own, and is a plain read so a retry is always safe. For JSON APIs pass `select` with a dotted path (e.g. 'chart.result.0.meta') to return just that part instead of the whole payload.\n- **memory_search**(query, limit): Search agent memory for relevant past experiences, observations, and facts.\n- **memory_add**(content, kind) [capability_expand category: write]: Store a new observation or insight in memory for future reference.\n- **research_note**(kind, text): Record a compact, structured note about the current task's state so it survives context compression on long / multi-step tasks. Use for durable findings, open questions still to resolve, decisions / excluded paths, and the immediate next step. One concise sentence per note.\n- **capability_expand**(category): Fetch the full callable schema for every tool in a capability category (e.g. 'hub', 'gateway', 'desktop', 'delegate', 'file', 'memory', 'skill'). The compact tool index always lists every registered tool by name and a one-line summary, but only a static low-risk subset is directly callable each turn. If you need a tool from the index that is not yet callable, call capability_expand with its category first; the matching tools become callable in this turn. Never invent a tool name — expand the category instead.\n- **delegate_task**(goal, context) [capability_expand category: delegate]: Delegate a complex sub-task to an isolated subagent. The subagent gets a fresh context and restricted tool access. Use when a task is self-contained and can be solved independently.\n- **schedule_reentry**(kind, reason, delay_seconds, event_match, max_reentries, deadline_seconds): Register a re-entry so this task can resume later from its current orientation (findings / open questions / next step). Use when work must pause and continue after a delay (kind=time) or when a matching platform event arrives (kind=event), instead of finishing now. The research-ledger state is carried over automatically.\n- **hub_push**(skill_name, visibility, version) [capability_expand category: hub]: Push a local skill to the ModelScope Hub for sharing or backup.\n- **hub_pull**(repo_id, version) [capability_expand category: hub]: Pull a skill from the ModelScope Hub to install locally.\n- **hub_search**(query) [capability_expand category: hub]: Search for skills on the Hub by keyword or description.\n- **hub_sync**(mode, dry_run) [capability_expand category: hub]: Preview or execute sync between local skills and Hub.\n- **platform_action**(platform, action, payload, backend_kind) [capability_expand category: gateway]: Execute an exact registered business action on an external platform through LeapFlow's App Connector layer. Actions must be copied from the App Connector Capability Index and are addressed as domain.operation, e.g. im.send_message or docs.create_markdown. All business fields (chat_id, text, query, etc.) MUST be placed inside `payload`, never at the top level. Example: {\"platform\":\"feishu\",\"action\":\"im.send_message\",\"payload\":{\"chat_id\":\"oc_xxx\",\"text\":\"hello\"}}. Do not invent action names, do not use management actions such as list/guide/connect/status here.\n- **platform_connect**(action, platform, credentials, options, checkpoint) [capability_expand category: gateway]: List, guide, connect, disconnect, remove, or check status for external platforms using the App Connector management namespace. Supports REST and CLI backends. Use this for management actions such as list/guide/preflight/connect/status; use platform_action only for exact registered business actions.\n- **gateway_send**(platform, chat_id, text, thread_id) [capability_expand category: gateway]: Send a message to a connected external platform (Feishu group, Telegram chat, DingTalk conversation, etc.). Requires the platform to be connected via gateway_connect first. Use gateway_connect with action='list' to see connected platforms and available chat IDs.\n- **gateway_connect**(action, platform, credentials, options) [capability_expand category: gateway]: Connect, configure, or manage external platform integrations (Feishu, DingTalk, Telegram, Slack, Discord, etc.). Conversational flow: 1) call 'guide' to get setup steps + required fields, 2) present the steps to the user and ask for ALL required credentials in a single message, 3) call 'connect' with the credentials. Goal: complete in 1–2 user turns. NEVER include credential values in your text response.\n- **plugin_list**(): List the live plugin registry across Tool/Gateway/LLM subsystems for plugin management: which plugins are loaded, their category and fiber lifecycle state. For questions about LeapFlow's own capabilities or whether it supports X, use self_describe instead.\n- **plugin_status**(plugin_id): Get detailed status of a specific plugin: its declared category, runtime dependencies, contributed tools, and fiber lifecycle state.\n- **plugin_versions**(plugin_id): List recorded source versions and active pointer for a profile-scoped plugin.\n- **plugin_propose**(requested_capability, plugin_id, proposed_tools, test_cases, risk_level, evidence): Create a side-effect-free PluginProposal from explicit capability-gap evidence. Use this before plugin_generate when a missing capability should be reviewed. Does not call an LLM, write files, or install anything.\n- **assess_compatibility**(manifest, source_path) [capability_expand category: plugin_management]: Assess whether a foreign plugin manifest or real DSH source bundle is compatible with LeapFlow. A source bundle assessment is static: runtime_ready stays false until restricted Node discovery during plugin_install. Returns component-level verdicts and limitations.\n- **plugin_generate**(plugin_id, description, proposal_id) [capability_expand category: system]: Generate a new ToolPlugin from a natural-language capability description. The LLM produces code that conforms to the ToolPlugin Protocol; it is then rigorously validated (syntax, structure, import, protocol conformance). The generated source is stored in CAS and receives explicit content approval. It DOES NOT install the plugin — installation requires a second, mutation-specific approval via plugin_install.\n- **plugin_install**(plugin_id, code, marketplace_name, source_path, proposal_id, version_label) [capability_expand category: system]: Install a plugin from exactly one source: validated Python code, the configured Python marketplace, or a real DSH source bundle. DSH bundles run restricted Node discovery before registration; only public host tools are installed and client UI limitations are reported. Writes profile-scoped state and mutates the process-global registry. REQUIRES APPROVAL.\n- **plugin_rollback**(plugin_id, version) [capability_expand category: system]: Rollback a profile-scoped plugin to a recorded source version and reload it. REQUIRES APPROVAL.\n- **plugin_reload**(plugin_id, version_label) [capability_expand category: system]: Hot-reload a plugin at runtime. Disposes the old plugin fiber, re-imports its module, and registers a fresh instance. Existing in-flight turns are unaffected (snapshot isolation). REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_disable**(plugin_id) [capability_expand category: system]: Disable a plugin by disposing its fiber, removing its tools from the runtime registry. Cannot disable self_management itself. REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_remove**(plugin_id, delete_source) [capability_expand category: system]: Terminally remove a plugin: dispose its fiber, unregister its tools, remove reload metadata, and optionally delete its profile-scoped source file. Cannot remove self_management itself. REQUIRES APPROVAL.\n- **plugin_unquarantine**(plugin_id) [capability_expand category: plugin_management]: Restore a quarantined plugin to probation status for re-evaluation. Unfreezes the trust ledger, transitions the proposal back to PROBATION, and reloads the plugin. REQUIRES APPROVAL.\n- **plugin_enable**(plugin_id) [capability_expand category: system]: Re-enable a previously disabled plugin by reloading its module and registering a fresh instance. REQUIRES APPROVAL.\n- **self_describe**(facet): Answer questions about LeapFlow ITSELF from live runtime evidence — its identity/version/model, what tools and plugins it has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of self-awareness. Prefer this over reading LeapFlow's own source code. Pick facet=capabilities for 'what can you do / do you support X', identity for version/model, runtime for context/posture, or all for a full self-check when explicitly requested.\n- **runtime_snapshot**(): Lightweight (~150 token) flat snapshot of current runtime state: model, context budget, posture, disclosure level, turn count, cache hit rate, uptime, tool count, and pending approvals.\n- **schedule_create**(trigger_expression, instruction, execution_mode, max_retries, delivery_target) [capability_expand category: scheduler]: Create a new scheduled task. Specify a trigger expression (e.g. '30m', 'every 2h', '0 9 * * *') and the instruction to execute on each trigger. Optionally provide an execution mode and a delivery target to receive notifications.\n- **schedule_list**() [capability_expand category: scheduler]: List all scheduled tasks with their current state, trigger, and next due time.\n- **schedule_status**(task_id) [capability_expand category: scheduler]: Get detailed status and recent execution history for a scheduled task.\n- **schedule_pause**(task_id) [capability_expand category: scheduler]: Pause a scheduled task so it stops firing without being cancelled.\n- **schedule_resume**(task_id) [capability_expand category: scheduler]: Resume a paused scheduled task — re-arms it and recalculates next due time.\n- **schedule_cancel**(task_id) [capability_expand category: scheduler]: Cancel a scheduled task permanently.\n- **tool_search**(query, max_results): Search all registered tools by keyword relevance. Returns a ranked list of matching tools with name, category, and summary. Use this when you need a tool but are unsure of its exact name or category.\n- **tool_describe**(tool_name): Get the full callable schema of a single tool by exact name. Returns the tool's description, parameters, and metadata. Use after tool_search to inspect a specific tool before calling it.\n- **hw_list**(): List connected hardware devices with their channel counts and measured quantities. Start here; it does not include operating limits.\n- **hw_describe**(device_id): Return the full reference for one device: every channel, its unit, its operating envelope, rate limit, reversibility, and required interlocks, plus any prior outcomes recorded for its writable channels. Required before commanding a device.\n- **hw_read**(device_id, channel_id): Read the current value of one device channel. Has no physical effect.\n- **hw_status**(device_id): Report connection health, halt capability, and recent notable events (threshold excursions, lost samples, stalled channels) for one device.\n- **hw_configure**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Set a configuration value or setpoint on a channel declaring effect=configure. Setpoints often have inertia: the value may need time to stabilise.\n- **hw_actuate**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Command motion or output on a channel declaring effect=actuate. This moves physical hardware; a repeat from an unknown state is not a safe retry.\n- **hw_dispense**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Consume an irreversible resource on a channel declaring effect=dispense. Running this twice dispenses twice; never repeat it after a failure without first verifying what already happened.\n- **hw_estop**(device_id) [capability_expand category: hardware]: Stop all motion and output on a device immediately. Never requires approval; use it whenever device behaviour is unexpected.\n- **hw_batch_actuate**(device_id, commands, dry_run, verify) [capability_expand category: hardware]: Write multiple actuator channels in one atomic transaction. Used for coordinated multi-joint control where all positions must be commanded simultaneously (e.g. robot arm joint-space control).\n- **session_search**(query, limit) [capability_expand category: unclassified]: Search past conversation sessions in the current workspace for relevant context.\n- **session_list**(limit): List recent conversation sessions in the current workspace with stable ids, titles, dates, and summaries. Use for browsing past tasks or when user asks to see history without specific search terms. No keywords needed — returns chronological list.\n- **session_detail**(session_id, limit, offset, include_inactive) [capability_expand category: unclassified]: Read a paginated persisted transcript for one past conversation session in the current workspace. Use after session_list or session_search returns a session_id.\n- **click**(element_index) [capability_expand category: desktop]: Click a UI element by its element_index (from the latest observe_ui snapshot)\n- **get_clipboard**() [capability_expand category: desktop]: Read current clipboard text content\n- **list_apps**(filter, running_only) [capability_expand category: desktop]: List available applications on this system. Use to discover correct bundle_id before switch_app.\n- **list_windows**() [capability_expand category: desktop]: List all top-level windows with pid, window_id, title, and per-window state (minimized, on-screen). Call this first to pick the pid and window_id that observe_ui and other window tools require.\n- **observe_ui**(pid, window_id, query) [capability_expand category: desktop]: Snapshot one window's actionable UI elements, each tagged with an element_index for click/right_click/read_text. Re-observe after actions — indices belong to one snapshot. Requires the window's pid and window_id from list_windows.\n- **open_url**(url, app_id) [capability_expand category: desktop]: Open a URL in the default or specified browser\n- **read_text**(element_index) [capability_expand category: desktop]: Read the text content of a specific UI element from the latest snapshot\n- **right_click**(element_index) [capability_expand category: desktop]: Right-click a UI element to open its context menu. Returns visible menu items.\n- **screenshot**(pid, window_id) [capability_expand category: desktop]: Capture a screenshot for visual verification. With pid + window_id captures that window (works across all displays); defaults to the last observed window, or the full desktop when no window has been observed.\n- **scroll**(element_index, direction, amount, pid, window_id) [capability_expand category: desktop]: Scroll a scrollable area of a window. Omit element_index to scroll the window's focused/page scroller; pass one to scroll an exact element from the latest snapshot.\n- **select_text**(element_index) [capability_expand category: desktop]: Select all text in a UI element (focus + select-all, for subsequent copy)\n- **set_clipboard**(text) [capability_expand category: desktop]: Write text to the clipboard\n- **shortcut**(keys) [capability_expand category: desktop]: Execute a keyboard shortcut\n- **switch_app**(app_id) [capability_expand category: desktop]: Switch to an app (launch if needed, activate, verify)\n- **type_text**(text) [capability_expand category: desktop]: Type text into the currently focused element\n- **wait**(seconds) [capability_expand category: desktop]: Wait for a specified duration before continuing\n- **wait_until**(condition, pid, window_id, timeout, poll_interval) [capability_expand category: desktop]: Wait until a UI condition is met (polls UI tree). Returns elements when found or on timeout.\n- **wait_until_stable**(timeout, poll_interval, pid, window_id) [capability_expand category: desktop]: Wait until the UI stops changing (element set stabilizes across polls).\n\n## App Connector Capability Index\nLeapFlow can onboard and manage external apps through `platform_connect` and execute exact registered business actions through `platform_action`.\nFor requests about connecting, setting up, configuring, enabling, or managing a supported app, use `platform_connect` first instead of generating SDK/Webhook sample code.\n`platform_connect.action` is the management namespace: list, guide, preflight, connect, disconnect, remove, status, events_start, events_stop, events_status.\n`platform_action.action` is only for exact registered platform business actions listed below, such as `im.send_message`; never use management actions like `list` or `guide` there.\nAll business fields MUST go inside `payload`; top-level keys are only `platform`, `action`, and `payload`.\nDo not invent platform IDs or platform action names. If the needed action is not listed, ask for discovery/clarification instead of guessing.\nUse `platform_connect` with `action='guide'` and the matching `platform` to start onboarding; use `action='list'` when the app is unclear.\nWhen a pending onboarding state is present, continue from that state with `platform_connect` instead of asking the user to restate the app.\nSupported apps:\n- `api_server`: API Server (OpenAI Compatible) (category=api; backend=adapter; platform_action actions=none registered)\n- `dingtalk`: 钉钉 (DingTalk) (category=im; backend=adapter; platform_action actions=none registered)\n- `feishu`: 飞书 (Feishu/Lark) (category=im; backend=cli)\n - `calendar.create_event` payload={summary*, start_time*, end_time*, attendees} [write/high]\n - `docs.create_markdown` payload={title*, markdown*, folder_token} [write/medium]\n - `drive.search` payload={query*, limit} [read/medium]\n - `im.add_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.download_resource` payload={message_id*, file_key*, type*} [read/low]\n - `im.get_messages` payload={message_ids*} [read/low]\n - `im.list_chats` payload={page_size} [read/low]\n - `im.list_messages` payload={chat_id*} [read/low]\n - `im.list_thread_messages` payload={thread*} [read/low]\n - `im.remove_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.reply_message` payload={message_id*, text*} [send/high]\n - `im.search_chats` payload={query*, page_size} [read/low]\n - `im.search_messages` payload={query*} [read/medium]\n - `im.send_message` payload={chat_id*, text*, thread_id} [send/high]\n - `im.update_card` payload={token*, card*} [write/medium]\n - `im.update_message` payload={message_id*, text*} [write/medium]\n - `mail.search_unread` payload={query, limit} [read/high]\n - `sheets.append_row` payload={spreadsheet_token*, sheet_id*, values*} [write/medium]\n - `task.create` payload={title*, description, due_time} [write/medium]\n- `telegram`: Telegram (category=social; backend=adapter; platform_action actions=none registered)\n- `webhook`: Webhook (Generic) (category=webhook; backend=adapter; platform_action actions=none registered)\n\n## Tool Usage\nTools are normally invoked through the native function-calling mechanism, not by writing JSON in your reply\ntext. Only if the provider signals that native function calling is unavailable for this turn, fall back to a\nsingle JSON code block: `{\"name\": \"tool_name\", \"arguments\": {\"key\": \"value\"}}` — use this fallback format\nonly, never both. Only call a tool whose exact name appears in the tool index above and is currently callable\n(or reachable via `capability_expand`). Never invent, rename, alias, or guess a tool name, platform ID, or\nplatform action from argument shape or wording — if the index does not list it, it does not exist.\n`platform_connect.action` (list/guide/preflight/connect/disconnect/remove/status/events_start/events_stop/\nevents_status) is the App Connector management namespace; `platform_action.action` only accepts exact\nregistered business actions such as `im.send_message` shown in the App Connector Capability Index — never mix\nthe two namespaces. If a tool call returns an unknown/unavailable result, use the returned suggestions or\navailable names for a single retry instead of trying further variations of the same guess.\n\n**Side-effect action rule** (`platform_action` with effect=send/write/execute):\n- Call each unique action+payload **exactly once**. Never include duplicates in the same turn.\n- Once the result returns `\"completed\": true`, that action is DONE for this task. Do NOT call it again\n in any subsequent turn — immediately summarize the result for the user instead.\n- The system enforces idempotency: duplicate calls are blocked and will not execute.\n- If the user explicitly requests sending/writing multiple times, use distinct payloads per call.\n\n**Resource identifier provenance rule**:\n- NEVER fabricate, guess, or infer resource identifiers (chat_id, message_id, file_key, user_id, etc.).\n Every resource ID used in a side-effect action MUST come from a successful API response in this session.\n- If a read/list action fails (e.g. authorization error), you do NOT have valid resource IDs.\n Report the failure to the user — do NOT attempt the dependent write/send action with a guessed ID.\n- When a tool result contains `\"llm_instruction\"`, follow it exactly.\n\n## Guidelines\n1. **Direct answers first**: If you already know the answer, respond directly without tools.\n2. **Avoid redundant tool calls**: Do not call the same tool with the same arguments more than once in the same user turn. When an existing tool result already answers the user's request, stop calling tools and answer directly.\n3. **Use tools proactively**: When the user asks about files, time, system state, or needs actions performed, use the appropriate tool.\n4. **Chain tools when needed**: You can call multiple tools in sequence (e.g., list files → read file → summarize).\n5. **Handle failures gracefully**: If a tool fails, explain what went wrong and suggest alternatives. If it failed because of an unknown tool/platform/action name, retry once with an exact name from the returned suggestions, then explain rather than keep guessing.\n6. **Summarize results naturally**: After tool execution, synthesize the results into a helpful answer rather than dumping raw output.\n7. **Stay conversational**: Maintain a natural, helpful tone. Acknowledge context from earlier in the conversation.\n8. **Recall past work**:\n - Broad queries (\"之前做了什么\", \"列出任务\"): answer from the \"Recent Task History\" section already in your context. If insufficient, call session_list.\n - Specific lookups (\"上次那个配置怎么改的\"): call session_search with relevant phrases (NOT single characters).\n - Do NOT call search tools repeatedly with keyword fragments. One well-phrased call is better than ten fragmented ones.\n\n## Coding & Verification\nWhen working with code, prefer the precise built-in tools over ad-hoc shell: use `repo_map` to orient in an\nunfamiliar project, `code_search`/`file_find` to locate and `code_intel` for symbols, `edit_file` (anchored\nsearch-replace, or a unified `diff`) to change files — never rewrite a whole file to change a few lines — and\n`git_query` to inspect diffs/log. After edits, check `syntax_ok` in the result and run `test_run`/`lint_check`\nbefore declaring the work done. Batch independent read-only calls (search/read/query) into a single turn — they\nrun in parallel.\n\n## Presentation Style\n1. **Polished Markdown only**: Format user-facing answers with clean Markdown headings, short paragraphs, and concise bullets. Use tables only when they improve comparison or scanning.\n2. **Terminal-friendly layout**: Keep lines readable in a TUI; avoid dense walls of text, deeply nested lists, oversized ASCII art, or heavy visual blocks.\n3. **Elegant emphasis**: Use bold text sparingly for key terms and conclusions. Avoid excessive emojis, decorative symbols, repeated separators, or visual noise.\n4. **Theme-safe colors**: Do not emit ANSI escape codes, HTML color tags, Rich markup, or hardcoded color names. Rely on the TUI theme to render Markdown professionally.\n5. **No leaked tool protocol**: Never show tool-call JSON, internal schemas, raw observations, tool result payloads, or hidden reasoning in the final answer unless the user explicitly asks for raw/debug output. Treat any prior `{\"name\": ..., \"arguments\": ...}` blocks and `Tool result (...)` messages as internal execution context only.\n6. **Professional closure**: End with a concise conclusion or next step when helpful; avoid rambling after the useful answer is complete.\n\nWhen finished with all tool calls, respond normally without a JSON block, tool-call transcript, or process log.\n\n## Task Contract\n- Task ID: turn-1\n- Original user request: List the connected hardware devices, then describe bench_r8 in full.\n- Workspace root: \n- Allowed roots: \n- Treat relative project paths as relative to the workspace root; never infer `.` as the project root when a workspace root is provided.\n- Workspace boundary is enforced by tools: do not read, search, edit, or run commands against paths outside the allowed roots unless the user explicitly requests an external path and the tool/approval policy permits it.\n- LeapFlow workspace config is optional at `/.leapflow/config.yaml`; runtime config is loaded from `~/.leapflow/config/user.yaml` and `~/.leapflow/profiles//config/*.yaml`.\n- Preserve this task contract across summarization, compression, tool loops, and memory retrieval.\n" - }, - { - "role": "system", - "content": "\n## Recent Session Summary\n- [user] List the connected hardware devices, then describe bench_r8 in full.\n" - }, - { - "role": "user", - "content": "List the connected hardware devices, then describe bench_r8 in full.\nList the connected hardware devices, then describe bench_r8 in full." - }, - { - "role": "assistant", - "content": "", - "tool_calls": [ - "hw_list" - ] - }, - { - "role": "tool", - "content": "{\"ok\": true, \"devices\": [{\"device_id\": \"bench_r8\", \"display_name\": \"R8 Simulated Bench\", \"location\": \"journey-lab\", \"channels\": 3, \"writable\": 2, \"streaming\": 1, \"quantities\": [\"state.homed\", \"temperature\"], \"verified\": true, \"halt_supported\": true}], \"count\": 1, \"hint\": \"Call hw_describe(device_id) for channel limits before commanding a device.\", \"execution_id\": \"\", \"idempotency_key\": \"\", \"execution_policy\": \"read_only\", \"tool_call_id\": \"call_1\", \"execution_status\": \"completed\"}", - "tool_result": true - } - ], - "tools": [ - "capability_expand", - "code_intel", - "code_search", - "config_get", - "config_list", - "env_info", - "file_find", - "file_list", - "file_read", - "git_query", - "hw_describe", - "hw_list", - "hw_read", - "hw_status", - "lint_check", - "memory_search", - "plugin_list", - "plugin_propose", - "plugin_status", - "plugin_versions", - "repo_map", - "research_note", - "runtime_snapshot", - "schedule_reentry", - "self_describe", - "session_list", - "skill_view", - "skills_list", - "terminal_list", - "terminal_read", - "test_run", - "text_search", - "time_get", - "tool_describe", - "tool_search", - "web_fetch" - ] - }, - "responses": [ - { - "status": 200, - "content_type": "application/json", - "body": "{\"id\": \"chatcmpl-cassette\", \"object\": \"chat.completion\", \"model\": \"cassette-model\", \"choices\": [{\"index\": 0, \"message\": {\"role\": \"assistant\", \"content\": \"\", \"tool_calls\": [{\"id\": \"call_1\", \"type\": \"function\", \"function\": {\"name\": \"hw_describe\", \"arguments\": \"{\\\"device_id\\\": \\\"bench_r8\\\"}\"}}]}, \"finish_reason\": \"tool_calls\"}], \"usage\": {\"prompt_tokens\": 64, \"completion_tokens\": 16, \"total_tokens\": 80}}" - } - ] -} diff --git a/tests/_fixtures/cassettes/r8_hardware/cassette-model-443b72e43e2f10a0.cassette.json b/tests/_fixtures/cassettes/r8_hardware/cassette-model-241147152eb28634.cassette.json similarity index 63% rename from tests/_fixtures/cassettes/r8_hardware/cassette-model-443b72e43e2f10a0.cassette.json rename to tests/_fixtures/cassettes/r8_hardware/cassette-model-241147152eb28634.cassette.json index 16059dd2..24722f92 100644 --- a/tests/_fixtures/cassettes/r8_hardware/cassette-model-443b72e43e2f10a0.cassette.json +++ b/tests/_fixtures/cassettes/r8_hardware/cassette-model-241147152eb28634.cassette.json @@ -1,5 +1,5 @@ { - "fingerprint": "443b72e43e2f10a0e32cd5e3a961c84664e03e54bb8aaa5ea150d92ed6a2d2ac", + "fingerprint": "241147152eb28634643bede0e08f4ae57e4d70503cd808a6ffd15c60d37f6fac", "note": "captured in seed mode", "request": { "model": "cassette-model", @@ -7,7 +7,7 @@ "messages": [ { "role": "system", - "content": "You are LeapFlow, an intelligent assistant that can both converse naturally and take real actions on the user's computer.\n\n## Capabilities\nThe tool index below lists **every** registered tool by name and a one-line summary — this is the complete\ncapability contract; nothing else exists. Only a subset is directly callable this turn (via native tool calling,\nnot a JSON block in your reply). If you need a tool from the index that is not yet callable, call\n`capability_expand` with its category name first — the matching tools become callable immediately after.\nWhen the user asks about LeapFlow **itself** — its identity/version/model, what tools, plugins, or capabilities\nit has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of\nself-awareness — answer from live runtime evidence via `self_describe` (facet=capabilities for \"what can you\ndo / do you support X\", identity for version/model, runtime for context/posture, or `all` for a full self-check),\nnever by reading LeapFlow's own source code, documentation, or memory. Report configuration-dependent or\nunavailable capabilities as limitations rather than inferring them.\n- **text_search**(text, pattern): Search for a regex pattern in text.\n- **text_replace**(text, old, new, count) [capability_expand category: write]: Replace occurrences of a substring in text.\n- **time_get**(): Get current date and time.\n- **env_info**(): Get system environment information (OS, Python version, cwd).\n- **skills_list**(query, category, source): List available learned skills. Use when user asks about capabilities or you need a specific skill.\n- **skill_view**(name): View the full content of a specific skill document.\n- **skill_list_files**(name) [capability_expand category: skills]: List all support files in a skill directory (configs, templates, examples). Use before skill_file_read to discover available files.\n- **skill_file_read**(name, file_path) [capability_expand category: skills]: Read an individual support file from a skill directory. Use skill_list_files first to discover available files.\n- **code_intel**(path, operation): Precise document symbols (outline) for a source file: classes, functions, and methods with line ranges. Python uses an exact AST parse; other languages use a keyword-prefix scan. Prefer over file_read mode=symbols for accurate navigation before editing. Read-only.\n- **repo_map**(path): Compact project orientation for a repository root: languages, detected test/lint commands, top-level structure, entry points, manifest, and VCS branch. Call this first when entering an unfamiliar codebase. Read-only.\n- **scm_sync**(action, cwd, remote, pull_ref, push_ref, timeout) [capability_expand category: scm]: Run a typed git SCM action. Use this instead of shell_run for git pull/push/status. For 'pull origin main then push', set action='pull_then_push', remote='origin', pull_ref='main', and omit push_ref so LeapFlow pushes the current local branch.\n- **git_query**(action, cwd, ref, path, staged, max_count, stat): Read-only structured git inspection: action=diff|log|status|branch|show. Prefer over shell_run for reading repo state — output is clipped, redacted, and log/branch are parsed into structured fields. Use scm_sync for pull/push.\n- **git_write**(action, cwd, message, stage_all, name, ref, create) [capability_expand category: scm]: Mutating git actions: action=commit (message, stage_all), branch (create+switch), checkout (switch; create=true for -b). Approval-gated. Use scm_sync for pull/push and git_query for reads.\n- **test_run**(command, cwd, timeout): Run the project's test suite and return structured results (framework, passed/failed counts, failing tests). Auto-detects the runner (pytest/npm/go/cargo) or uses a configured/explicit command; executes via the governed shell. ok=true means the runner executed — see 'success' for pass/fail.\n- **lint_check**(command, cwd, timeout): Run the project's linter and return a structured clean/issue result. Auto-detects the linter (ruff/eslint/go vet/clippy) or uses a configured/explicit command; executes via the governed shell. ok=true means the linter ran — see 'clean'.\n- **file_list**(path, pattern, depth): List files and directories at a given path. Use depth=1 or depth=2 to get a recursive tree in one call instead of listing each sub-directory separately.\n- **file_read**(path, max_lines, start_line, max_chars, mode): Read text file content with adaptive context governance. For large or unfamiliar files, prefer mode='outline' or mode='symbols' first, then use mode='raw' with start_line/max_lines for the specific range you actually need. For LeapFlow's own settings, use config_list / config_get / config_set — its config files are outside the workspace and not readable here.\n- **file_write**(path, content, mode) [capability_expand category: write]: Write content to a file (overwrite or append).\n- **code_search**(pattern, patterns, path, glob, ignore_case, multiline, max_results, context_lines): Search file CONTENTS by regex pattern across a directory tree (ripgrep-backed). Requires a regex pattern. NOT for listing or browsing directory contents — use file_list for that. Prefer this over shell_run grep: faster, skips VCS/dependency/build dirs, and returns structured path:line:column matches. Batch related lookups into ONE call via `patterns` (OR-combined, single pass) instead of issuing several separate searches. Use file_read for the surrounding context of a hit.\n- **file_find**(glob, path, max_results): Find files by a recursive glob pattern under a base path (e.g. '**/test_*.py' or '*.md'). Prefer this over shell_run find; skips VCS/dependency/build dirs.\n- **edit_file**(path, edits, dry_run, diff) [capability_expand category: write]: Apply targeted, anchored search-replace edits to an EXISTING text file (use file_write to create/overwrite). Each edit is {original_text, new_text, replace_all?}; original_text must match exactly and uniquely (or set replace_all) — a non-unique or missing anchor is rejected so files are never corrupted. Set dry_run to preview. Alternatively pass a unified 'diff' to apply its hunks as anchored edits. Far cheaper and safer than rewriting a whole file.\n- **shell_run**(command, cwd, timeout) [capability_expand category: shell]: Execute a one-shot shell command with timeout protection. Runs in the active workspace; paths resolving outside it are refused. Reach for a structured tool first when one fits — web_fetch for anything over HTTP(S), git_query/scm_sync for git, code_search/file_find/file_read for the repo, config_get/config_set for LeapFlow's own settings — because those report typed results, while a failed shell command can only be diagnosed from its exit code and stderr. Every shell run counts as an external side effect, so a failure stops the rest of the batch and is not retried automatically.\n- **terminal_open**(command, cwd, shell) [capability_expand category: terminal]: Open a PERSISTENT shell session (REPL/dev server/watch), returning a session_id for terminal_send/read/close. Disabled unless tools.terminal_session_enabled is set. For one-shot commands use shell_run instead.\n- **terminal_send**(session_id, input, wait) [capability_expand category: terminal]: Send a line of input to a persistent terminal session and return output captured shortly after.\n- **terminal_read**(session_id, wait): Drain buffered output from a persistent terminal session (optionally waiting briefly first).\n- **terminal_close**(session_id) [capability_expand category: terminal]: Terminate a persistent terminal session and release its process group.\n- **terminal_list**(): List active persistent terminal sessions.\n- **config_list**(category, limit): List LeapFlow's own writable settings (LLM model and endpoint, daemon, memory, perception, gateway, …) with current values. Use this to discover the exact key before changing anything. Optionally narrow by `category`. This is the only correct way to inspect LeapFlow configuration — never read config files from disk.\n- **config_get**(key): Read one LeapFlow setting by key (e.g. 'llm.model', 'llm.base_url', 'daemon.log_level'), returning its current value, type, scopes, and whether a change needs a daemon restart. There is no 'llm.provider' key: provider behavior is inferred from the OpenAI-compatible 'llm.base_url'. Never read LeapFlow config files from disk — use this.\n- **config_set**(key, value, scope) [capability_expand category: config]: Change one LeapFlow setting by key, e.g. switch an OpenAI-compatible LLM with key='llm.model' and key='llm.base_url'. There is no 'llm.provider' key: provider behavior is inferred from the endpoint. Values are validated and coerced; credentials are stored in the vault automatically. Call config_list or config_get first if unsure of the key. The result states whether a `leap daemon restart` is required. Never edit LeapFlow config files directly.\n- **web_fetch**(url, select, timeout, max_bytes): Read a URL over HTTP(S) and get back extracted, context-sized content: parsed JSON for API endpoints, readable text plus links for web pages. Use this for anything on the internet — prices, docs, releases, articles — instead of running curl through shell_run: it reports real HTTP status codes, retries rate limits on its own, and is a plain read so a retry is always safe. For JSON APIs pass `select` with a dotted path (e.g. 'chart.result.0.meta') to return just that part instead of the whole payload.\n- **memory_search**(query, limit): Search agent memory for relevant past experiences, observations, and facts.\n- **memory_add**(content, kind) [capability_expand category: write]: Store a new observation or insight in memory for future reference.\n- **research_note**(kind, text): Record a compact, structured note about the current task's state so it survives context compression on long / multi-step tasks. Use for durable findings, open questions still to resolve, decisions / excluded paths, and the immediate next step. One concise sentence per note.\n- **capability_expand**(category): Fetch the full callable schema for every tool in a capability category (e.g. 'hub', 'gateway', 'desktop', 'delegate', 'file', 'memory', 'skill'). The compact tool index always lists every registered tool by name and a one-line summary, but only a static low-risk subset is directly callable each turn. If you need a tool from the index that is not yet callable, call capability_expand with its category first; the matching tools become callable in this turn. Never invent a tool name — expand the category instead.\n- **delegate_task**(goal, context) [capability_expand category: delegate]: Delegate a complex sub-task to an isolated subagent. The subagent gets a fresh context and restricted tool access. Use when a task is self-contained and can be solved independently.\n- **schedule_reentry**(kind, reason, delay_seconds, event_match, max_reentries, deadline_seconds): Register a re-entry so this task can resume later from its current orientation (findings / open questions / next step). Use when work must pause and continue after a delay (kind=time) or when a matching platform event arrives (kind=event), instead of finishing now. The research-ledger state is carried over automatically.\n- **hub_push**(skill_name, visibility, version) [capability_expand category: hub]: Push a local skill to the ModelScope Hub for sharing or backup.\n- **hub_pull**(repo_id, version) [capability_expand category: hub]: Pull a skill from the ModelScope Hub to install locally.\n- **hub_search**(query) [capability_expand category: hub]: Search for skills on the Hub by keyword or description.\n- **hub_sync**(mode, dry_run) [capability_expand category: hub]: Preview or execute sync between local skills and Hub.\n- **platform_action**(platform, action, payload, backend_kind) [capability_expand category: gateway]: Execute an exact registered business action on an external platform through LeapFlow's App Connector layer. Actions must be copied from the App Connector Capability Index and are addressed as domain.operation, e.g. im.send_message or docs.create_markdown. All business fields (chat_id, text, query, etc.) MUST be placed inside `payload`, never at the top level. Example: {\"platform\":\"feishu\",\"action\":\"im.send_message\",\"payload\":{\"chat_id\":\"oc_xxx\",\"text\":\"hello\"}}. Do not invent action names, do not use management actions such as list/guide/connect/status here.\n- **platform_connect**(action, platform, credentials, options, checkpoint) [capability_expand category: gateway]: List, guide, connect, disconnect, remove, or check status for external platforms using the App Connector management namespace. Supports REST and CLI backends. Use this for management actions such as list/guide/preflight/connect/status; use platform_action only for exact registered business actions.\n- **gateway_send**(platform, chat_id, text, thread_id) [capability_expand category: gateway]: Send a message to a connected external platform (Feishu group, Telegram chat, DingTalk conversation, etc.). Requires the platform to be connected via gateway_connect first. Use gateway_connect with action='list' to see connected platforms and available chat IDs.\n- **gateway_connect**(action, platform, credentials, options) [capability_expand category: gateway]: Connect, configure, or manage external platform integrations (Feishu, DingTalk, Telegram, Slack, Discord, etc.). Conversational flow: 1) call 'guide' to get setup steps + required fields, 2) present the steps to the user and ask for ALL required credentials in a single message, 3) call 'connect' with the credentials. Goal: complete in 1–2 user turns. NEVER include credential values in your text response.\n- **plugin_list**(): List the live plugin registry across Tool/Gateway/LLM subsystems for plugin management: which plugins are loaded, their category and fiber lifecycle state. For questions about LeapFlow's own capabilities or whether it supports X, use self_describe instead.\n- **plugin_status**(plugin_id): Get detailed status of a specific plugin: its declared category, runtime dependencies, contributed tools, and fiber lifecycle state.\n- **plugin_versions**(plugin_id): List recorded source versions and active pointer for a profile-scoped plugin.\n- **plugin_propose**(requested_capability, plugin_id, proposed_tools, test_cases, risk_level, evidence): Create a side-effect-free PluginProposal from explicit capability-gap evidence. Use this before plugin_generate when a missing capability should be reviewed. Does not call an LLM, write files, or install anything.\n- **assess_compatibility**(manifest, source_path) [capability_expand category: plugin_management]: Assess whether a foreign plugin manifest or real DSH source bundle is compatible with LeapFlow. A source bundle assessment is static: runtime_ready stays false until restricted Node discovery during plugin_install. Returns component-level verdicts and limitations.\n- **plugin_generate**(plugin_id, description, proposal_id) [capability_expand category: system]: Generate a new ToolPlugin from a natural-language capability description. The LLM produces code that conforms to the ToolPlugin Protocol; it is then rigorously validated (syntax, structure, import, protocol conformance). The generated source is stored in CAS and receives explicit content approval. It DOES NOT install the plugin — installation requires a second, mutation-specific approval via plugin_install.\n- **plugin_install**(plugin_id, code, marketplace_name, source_path, proposal_id, version_label) [capability_expand category: system]: Install a plugin from exactly one source: validated Python code, the configured Python marketplace, or a real DSH source bundle. DSH bundles run restricted Node discovery before registration; only public host tools are installed and client UI limitations are reported. Writes profile-scoped state and mutates the process-global registry. REQUIRES APPROVAL.\n- **plugin_rollback**(plugin_id, version) [capability_expand category: system]: Rollback a profile-scoped plugin to a recorded source version and reload it. REQUIRES APPROVAL.\n- **plugin_reload**(plugin_id, version_label) [capability_expand category: system]: Hot-reload a plugin at runtime. Disposes the old plugin fiber, re-imports its module, and registers a fresh instance. Existing in-flight turns are unaffected (snapshot isolation). REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_disable**(plugin_id) [capability_expand category: system]: Disable a plugin by disposing its fiber, removing its tools from the runtime registry. Cannot disable self_management itself. REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_remove**(plugin_id, delete_source) [capability_expand category: system]: Terminally remove a plugin: dispose its fiber, unregister its tools, remove reload metadata, and optionally delete its profile-scoped source file. Cannot remove self_management itself. REQUIRES APPROVAL.\n- **plugin_unquarantine**(plugin_id) [capability_expand category: plugin_management]: Restore a quarantined plugin to probation status for re-evaluation. Unfreezes the trust ledger, transitions the proposal back to PROBATION, and reloads the plugin. REQUIRES APPROVAL.\n- **plugin_enable**(plugin_id) [capability_expand category: system]: Re-enable a previously disabled plugin by reloading its module and registering a fresh instance. REQUIRES APPROVAL.\n- **self_describe**(facet): Answer questions about LeapFlow ITSELF from live runtime evidence — its identity/version/model, what tools and plugins it has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of self-awareness. Prefer this over reading LeapFlow's own source code. Pick facet=capabilities for 'what can you do / do you support X', identity for version/model, runtime for context/posture, or all for a full self-check when explicitly requested.\n- **runtime_snapshot**(): Lightweight (~150 token) flat snapshot of current runtime state: model, context budget, posture, disclosure level, turn count, cache hit rate, uptime, tool count, and pending approvals.\n- **schedule_create**(trigger_expression, instruction, execution_mode, max_retries, delivery_target) [capability_expand category: scheduler]: Create a new scheduled task. Specify a trigger expression (e.g. '30m', 'every 2h', '0 9 * * *') and the instruction to execute on each trigger. Optionally provide an execution mode and a delivery target to receive notifications.\n- **schedule_list**() [capability_expand category: scheduler]: List all scheduled tasks with their current state, trigger, and next due time.\n- **schedule_status**(task_id) [capability_expand category: scheduler]: Get detailed status and recent execution history for a scheduled task.\n- **schedule_pause**(task_id) [capability_expand category: scheduler]: Pause a scheduled task so it stops firing without being cancelled.\n- **schedule_resume**(task_id) [capability_expand category: scheduler]: Resume a paused scheduled task — re-arms it and recalculates next due time.\n- **schedule_cancel**(task_id) [capability_expand category: scheduler]: Cancel a scheduled task permanently.\n- **tool_search**(query, max_results): Search all registered tools by keyword relevance. Returns a ranked list of matching tools with name, category, and summary. Use this when you need a tool but are unsure of its exact name or category.\n- **tool_describe**(tool_name): Get the full callable schema of a single tool by exact name. Returns the tool's description, parameters, and metadata. Use after tool_search to inspect a specific tool before calling it.\n- **hw_list**(): List connected hardware devices with their channel counts and measured quantities. Start here; it does not include operating limits.\n- **hw_describe**(device_id): Return the full reference for one device: every channel, its unit, its operating envelope, rate limit, reversibility, and required interlocks, plus any prior outcomes recorded for its writable channels. Required before commanding a device.\n- **hw_read**(device_id, channel_id): Read the current value of one device channel. Has no physical effect.\n- **hw_status**(device_id): Report connection health, halt capability, and recent notable events (threshold excursions, lost samples, stalled channels) for one device.\n- **hw_configure**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Set a configuration value or setpoint on a channel declaring effect=configure. Setpoints often have inertia: the value may need time to stabilise.\n- **hw_actuate**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Command motion or output on a channel declaring effect=actuate. This moves physical hardware; a repeat from an unknown state is not a safe retry.\n- **hw_dispense**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Consume an irreversible resource on a channel declaring effect=dispense. Running this twice dispenses twice; never repeat it after a failure without first verifying what already happened.\n- **hw_estop**(device_id) [capability_expand category: hardware]: Stop all motion and output on a device immediately. Never requires approval; use it whenever device behaviour is unexpected.\n- **hw_batch_actuate**(device_id, commands, dry_run, verify) [capability_expand category: hardware]: Write multiple actuator channels in one atomic transaction. Used for coordinated multi-joint control where all positions must be commanded simultaneously (e.g. robot arm joint-space control).\n- **hw_control_start**(device_id, mode, targets, frequency_hz, trajectory, policy, gains) [capability_expand category: hardware]: Start a real-time control loop on a robot device. Supports PID position tracking, impedance control, trajectory following, and neural policy chains.\n- **hw_control_stop**(device_id) [capability_expand category: hardware]: Stop the real-time control loop on a device. Gracefully halts the device and returns session statistics.\n- **hw_control_status**(device_id) [capability_expand category: hardware]: Get the status and statistics of a running control loop. Returns frequency, jitter, overruns, and policy state.\n- **hw_policy_infer**(device_id, policy, task, execute, chunk_size, verify, compute_budget) [capability_expand category: hardware]: Execute VLA policy inference on a robot device. Reads observation, runs policy, optionally writes action to actuators. Supports local (HuggingFace) and remote (PolicyServer) policies.\n- **hw_policy_episode**(action, device_id, policy, task) [capability_expand category: hardware]: Manage policy inference episode lifecycle. Start an episode to begin a task, stop to finalise and get stats.\n- **hw_policy_list**(source) [capability_expand category: hardware]: List available VLA policies and their capabilities.\n- **hw_orchestrate**(mode, operations, barrier_timeout_s, label) [capability_expand category: hardware]: Execute coordinated operations across multiple hardware devices. Supports sequential (one-at-a-time), parallel (concurrent), and barrier (synchronised) execution modes. Any failure in a parallel group triggers halt_all on every participating device for safety.\n- **session_search**(query, limit) [capability_expand category: unclassified]: Search past conversation sessions in the current workspace for relevant context.\n- **session_list**(limit): List recent conversation sessions in the current workspace with stable ids, titles, dates, and summaries. Use for browsing past tasks or when user asks to see history without specific search terms. No keywords needed — returns chronological list.\n- **session_detail**(session_id, limit, offset, include_inactive) [capability_expand category: unclassified]: Read a paginated persisted transcript for one past conversation session in the current workspace. Use after session_list or session_search returns a session_id.\n- **click**(element_index) [capability_expand category: desktop]: Click a UI element by its element_index (from the latest observe_ui snapshot)\n- **get_clipboard**() [capability_expand category: desktop]: Read current clipboard text content\n- **list_apps**(filter, running_only) [capability_expand category: desktop]: List available applications on this system. Use to discover correct bundle_id before switch_app.\n- **list_windows**() [capability_expand category: desktop]: List all top-level windows with pid, window_id, title, and per-window state (minimized, on-screen). Call this first to pick the pid and window_id that observe_ui and other window tools require.\n- **observe_ui**(pid, window_id, query) [capability_expand category: desktop]: Snapshot one window's actionable UI elements, each tagged with an element_index for click/right_click/read_text. Re-observe after actions — indices belong to one snapshot. Requires the window's pid and window_id from list_windows.\n- **open_url**(url, app_id) [capability_expand category: desktop]: Open a URL in the default or specified browser\n- **read_text**(element_index) [capability_expand category: desktop]: Read the text content of a specific UI element from the latest snapshot\n- **right_click**(element_index) [capability_expand category: desktop]: Right-click a UI element to open its context menu. Returns visible menu items.\n- **screenshot**(pid, window_id) [capability_expand category: desktop]: Capture a screenshot for visual verification. With pid + window_id captures that window (works across all displays); defaults to the last observed window, or the full desktop when no window has been observed.\n- **scroll**(element_index, direction, amount, pid, window_id) [capability_expand category: desktop]: Scroll a scrollable area of a window. Omit element_index to scroll the window's focused/page scroller; pass one to scroll an exact element from the latest snapshot.\n- **select_text**(element_index) [capability_expand category: desktop]: Select all text in a UI element (focus + select-all, for subsequent copy)\n- **set_clipboard**(text) [capability_expand category: desktop]: Write text to the clipboard\n- **shortcut**(keys) [capability_expand category: desktop]: Execute a keyboard shortcut\n- **switch_app**(app_id) [capability_expand category: desktop]: Switch to an app (launch if needed, activate, verify)\n- **type_text**(text) [capability_expand category: desktop]: Type text into the currently focused element\n- **wait**(seconds) [capability_expand category: desktop]: Wait for a specified duration before continuing\n- **wait_until**(condition, pid, window_id, timeout, poll_interval) [capability_expand category: desktop]: Wait until a UI condition is met (polls UI tree). Returns elements when found or on timeout.\n- **wait_until_stable**(timeout, poll_interval, pid, window_id) [capability_expand category: desktop]: Wait until the UI stops changing (element set stabilizes across polls).\n\n## App Connector Capability Index\nLeapFlow can onboard and manage external apps through `platform_connect` and execute exact registered business actions through `platform_action`.\nFor requests about connecting, setting up, configuring, enabling, or managing a supported app, use `platform_connect` first instead of generating SDK/Webhook sample code.\n`platform_connect.action` is the management namespace: list, guide, preflight, connect, disconnect, remove, status, events_start, events_stop, events_status.\n`platform_action.action` is only for exact registered platform business actions listed below, such as `im.send_message`; never use management actions like `list` or `guide` there.\nAll business fields MUST go inside `payload`; top-level keys are only `platform`, `action`, and `payload`.\nDo not invent platform IDs or platform action names. If the needed action is not listed, ask for discovery/clarification instead of guessing.\nUse `platform_connect` with `action='guide'` and the matching `platform` to start onboarding; use `action='list'` when the app is unclear.\nWhen a pending onboarding state is present, continue from that state with `platform_connect` instead of asking the user to restate the app.\nSupported apps:\n- `api_server`: API Server (OpenAI Compatible) (category=api; backend=adapter; platform_action actions=none registered)\n- `dingtalk`: 钉钉 (DingTalk) (category=im; backend=adapter; platform_action actions=none registered)\n- `feishu`: 飞书 (Feishu/Lark) (category=im; backend=cli)\n - `calendar.create_event` payload={summary*, start_time*, end_time*, attendees} [write/high]\n - `docs.create_markdown` payload={title*, markdown*, folder_token} [write/medium]\n - `drive.search` payload={query*, limit} [read/medium]\n - `im.add_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.download_resource` payload={message_id*, file_key*, type*} [read/low]\n - `im.get_messages` payload={message_ids*} [read/low]\n - `im.list_chats` payload={page_size} [read/low]\n - `im.list_messages` payload={chat_id*} [read/low]\n - `im.list_thread_messages` payload={thread*} [read/low]\n - `im.remove_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.reply_message` payload={message_id*, text*} [send/high]\n - `im.search_chats` payload={query*, page_size} [read/low]\n - `im.search_messages` payload={query*} [read/medium]\n - `im.send_message` payload={chat_id*, text*, thread_id} [send/high]\n - `im.update_card` payload={token*, card*} [write/medium]\n - `im.update_message` payload={message_id*, text*} [write/medium]\n - `mail.search_unread` payload={query, limit} [read/high]\n - `sheets.append_row` payload={spreadsheet_token*, sheet_id*, values*} [write/medium]\n - `task.create` payload={title*, description, due_time} [write/medium]\n- `telegram`: Telegram (category=social; backend=adapter; platform_action actions=none registered)\n- `webhook`: Webhook (Generic) (category=webhook; backend=adapter; platform_action actions=none registered)\n\n## Tool Usage\nTools are normally invoked through the native function-calling mechanism, not by writing JSON in your reply\ntext. Only if the provider signals that native function calling is unavailable for this turn, fall back to a\nsingle JSON code block: `{\"name\": \"tool_name\", \"arguments\": {\"key\": \"value\"}}` — use this fallback format\nonly, never both. Only call a tool whose exact name appears in the tool index above and is currently callable\n(or reachable via `capability_expand`). Never invent, rename, alias, or guess a tool name, platform ID, or\nplatform action from argument shape or wording — if the index does not list it, it does not exist.\n`platform_connect.action` (list/guide/preflight/connect/disconnect/remove/status/events_start/events_stop/\nevents_status) is the App Connector management namespace; `platform_action.action` only accepts exact\nregistered business actions such as `im.send_message` shown in the App Connector Capability Index — never mix\nthe two namespaces. If a tool call returns an unknown/unavailable result, use the returned suggestions or\navailable names for a single retry instead of trying further variations of the same guess.\n\n**Side-effect action rule** (`platform_action` with effect=send/write/execute):\n- Call each unique action+payload **exactly once**. Never include duplicates in the same turn.\n- Once the result returns `\"completed\": true`, that action is DONE for this task. Do NOT call it again\n in any subsequent turn — immediately summarize the result for the user instead.\n- The system enforces idempotency: duplicate calls are blocked and will not execute.\n- If the user explicitly requests sending/writing multiple times, use distinct payloads per call.\n\n**Resource identifier provenance rule**:\n- NEVER fabricate, guess, or infer resource identifiers (chat_id, message_id, file_key, user_id, etc.).\n Every resource ID used in a side-effect action MUST come from a successful API response in this session.\n- If a read/list action fails (e.g. authorization error), you do NOT have valid resource IDs.\n Report the failure to the user — do NOT attempt the dependent write/send action with a guessed ID.\n- When a tool result contains `\"llm_instruction\"`, follow it exactly.\n\n## Guidelines\n1. **Direct answers first**: If you already know the answer, respond directly without tools.\n2. **Avoid redundant tool calls**: Do not call the same tool with the same arguments more than once in the same user turn. When an existing tool result already answers the user's request, stop calling tools and answer directly.\n3. **Use tools proactively**: When the user asks about files, time, system state, or needs actions performed, use the appropriate tool.\n4. **Chain tools when needed**: You can call multiple tools in sequence (e.g., list files → read file → summarize).\n5. **Handle failures gracefully**: If a tool fails, explain what went wrong and suggest alternatives. If it failed because of an unknown tool/platform/action name, retry once with an exact name from the returned suggestions, then explain rather than keep guessing.\n6. **Summarize results naturally**: After tool execution, synthesize the results into a helpful answer rather than dumping raw output.\n7. **Stay conversational**: Maintain a natural, helpful tone. Acknowledge context from earlier in the conversation.\n8. **Recall past work**:\n - Broad queries (\"之前做了什么\", \"列出任务\"): answer from the \"Recent Task History\" section already in your context. If insufficient, call session_list.\n - Specific lookups (\"上次那个配置怎么改的\"): call session_search with relevant phrases (NOT single characters).\n - Do NOT call search tools repeatedly with keyword fragments. One well-phrased call is better than ten fragmented ones.\n\n## Coding & Verification\nWhen working with code, prefer the precise built-in tools over ad-hoc shell: use `repo_map` to orient in an\nunfamiliar project, `code_search`/`file_find` to locate and `code_intel` for symbols, `edit_file` (anchored\nsearch-replace, or a unified `diff`) to change files — never rewrite a whole file to change a few lines — and\n`git_query` to inspect diffs/log. After edits, check `syntax_ok` in the result and run `test_run`/`lint_check`\nbefore declaring the work done. Batch independent read-only calls (search/read/query) into a single turn — they\nrun in parallel.\n\n## Presentation Style\n1. **Polished Markdown only**: Format user-facing answers with clean Markdown headings, short paragraphs, and concise bullets. Use tables only when they improve comparison or scanning.\n2. **Terminal-friendly layout**: Keep lines readable in a TUI; avoid dense walls of text, deeply nested lists, oversized ASCII art, or heavy visual blocks.\n3. **Elegant emphasis**: Use bold text sparingly for key terms and conclusions. Avoid excessive emojis, decorative symbols, repeated separators, or visual noise.\n4. **Theme-safe colors**: Do not emit ANSI escape codes, HTML color tags, Rich markup, or hardcoded color names. Rely on the TUI theme to render Markdown professionally.\n5. **No leaked tool protocol**: Never show tool-call JSON, internal schemas, raw observations, tool result payloads, or hidden reasoning in the final answer unless the user explicitly asks for raw/debug output. Treat any prior `{\"name\": ..., \"arguments\": ...}` blocks and `Tool result (...)` messages as internal execution context only.\n6. **Professional closure**: End with a concise conclusion or next step when helpful; avoid rambling after the useful answer is complete.\n\nWhen finished with all tool calls, respond normally without a JSON block, tool-call transcript, or process log.\n\n## Task Contract\n- Task ID: turn-1\n- Original user request: List the connected hardware devices, then describe bench_r8 in full.\n- Workspace root: \n- Allowed roots: \n- Treat relative project paths as relative to the workspace root; never infer `.` as the project root when a workspace root is provided.\n- Workspace boundary is enforced by tools: do not read, search, edit, or run commands against paths outside the allowed roots unless the user explicitly requests an external path and the tool/approval policy permits it.\n- LeapFlow workspace config is optional at `/.leapflow/config.yaml`; runtime config is loaded from `~/.leapflow/config/user.yaml` and `~/.leapflow/profiles//config/*.yaml`.\n- Preserve this task contract across summarization, compression, tool loops, and memory retrieval.\n" + "content": "You are LeapFlow, an intelligent assistant that can both converse naturally and take real actions on the user's computer.\n\n## Capabilities\nThe tool index below lists **every** registered tool by name and a one-line summary — this is the complete\ncapability contract; nothing else exists. Only a subset is directly callable this turn (via native tool calling,\nnot a JSON block in your reply). If you need a tool from the index that is not yet callable, call\n`capability_expand` with its category name first — the matching tools become callable immediately after.\nWhen the user asks about LeapFlow **itself** — its identity/version/model, what tools, plugins, or capabilities\nit has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of\nself-awareness — answer from live runtime evidence via `self_describe` (facet=capabilities for \"what can you\ndo / do you support X\", identity for version/model, runtime for context/posture, or `all` for a full self-check),\nnever by reading LeapFlow's own source code, documentation, or memory. Report configuration-dependent or\nunavailable capabilities as limitations rather than inferring them.\n- **text_search**(text, pattern): Search for a regex pattern in text.\n- **text_replace**(text, old, new, count) [capability_expand category: write]: Replace occurrences of a substring in text.\n- **time_get**(): Get current date and time.\n- **env_info**(): Get system environment information (OS, Python version, cwd).\n- **skills_list**(query, category, source): List available learned skills. Use when user asks about capabilities or you need a specific skill.\n- **skill_view**(name): View the full content of a specific skill document.\n- **skill_list_files**(name) [capability_expand category: skills]: List all support files in a skill directory (configs, templates, examples). Use before skill_file_read to discover available files.\n- **skill_file_read**(name, file_path) [capability_expand category: skills]: Read an individual support file from a skill directory. Use skill_list_files first to discover available files.\n- **code_intel**(path, operation): Precise document symbols (outline) for a source file: classes, functions, and methods with line ranges. Python uses an exact AST parse; other languages use a keyword-prefix scan. Prefer over file_read mode=symbols for accurate navigation before editing. Read-only.\n- **repo_map**(path): Compact project orientation for a repository root: languages, detected test/lint commands, top-level structure, entry points, manifest, and VCS branch. Call this first when entering an unfamiliar codebase. Read-only.\n- **scm_sync**(action, cwd, remote, pull_ref, push_ref, timeout) [capability_expand category: scm]: Run a typed git SCM action. Use this instead of shell_run for git pull/push/status. For 'pull origin main then push', set action='pull_then_push', remote='origin', pull_ref='main', and omit push_ref so LeapFlow pushes the current local branch.\n- **git_query**(action, cwd, ref, path, staged, max_count, stat): Read-only structured git inspection: action=diff|log|status|branch|show. Prefer over shell_run for reading repo state — output is clipped, redacted, and log/branch are parsed into structured fields. Use scm_sync for pull/push.\n- **git_write**(action, cwd, message, stage_all, name, ref, create) [capability_expand category: scm]: Mutating git actions: action=commit (message, stage_all), branch (create+switch), checkout (switch; create=true for -b). Approval-gated. Use scm_sync for pull/push and git_query for reads.\n- **test_run**(command, cwd, timeout): Run the project's test suite and return structured results (framework, passed/failed counts, failing tests). Auto-detects the runner (pytest/npm/go/cargo) or uses a configured/explicit command; executes via the governed shell. ok=true means the runner executed — see 'success' for pass/fail.\n- **lint_check**(command, cwd, timeout): Run the project's linter and return a structured clean/issue result. Auto-detects the linter (ruff/eslint/go vet/clippy) or uses a configured/explicit command; executes via the governed shell. ok=true means the linter ran — see 'clean'.\n- **file_list**(path, pattern, depth): List files and directories at a given path. Use depth=1 or depth=2 to get a recursive tree in one call instead of listing each sub-directory separately.\n- **file_read**(path, max_lines, start_line, max_chars, mode): Read text file content with adaptive context governance. For large or unfamiliar files, prefer mode='outline' or mode='symbols' first, then use mode='raw' with start_line/max_lines for the specific range you actually need. For LeapFlow's own settings, use config_list / config_get / config_set — its config files are outside the workspace and not readable here.\n- **file_write**(path, content, mode) [capability_expand category: write]: Write content to a file (overwrite or append).\n- **code_search**(pattern, patterns, path, glob, ignore_case, multiline, max_results, context_lines): Search file CONTENTS by regex pattern across a directory tree (ripgrep-backed). Requires a regex pattern. NOT for listing or browsing directory contents — use file_list for that. Prefer this over shell_run grep: faster, skips VCS/dependency/build dirs, and returns structured path:line:column matches. Batch related lookups into ONE call via `patterns` (OR-combined, single pass) instead of issuing several separate searches. Use file_read for the surrounding context of a hit.\n- **file_find**(glob, path, max_results): Find files by a recursive glob pattern under a base path (e.g. '**/test_*.py' or '*.md'). Prefer this over shell_run find; skips VCS/dependency/build dirs.\n- **edit_file**(path, edits, dry_run, diff) [capability_expand category: write]: Apply targeted, anchored search-replace edits to an EXISTING text file (use file_write to create/overwrite). Each edit is {original_text, new_text, replace_all?}; original_text must match exactly and uniquely (or set replace_all) — a non-unique or missing anchor is rejected so files are never corrupted. Set dry_run to preview. Alternatively pass a unified 'diff' to apply its hunks as anchored edits. Far cheaper and safer than rewriting a whole file.\n- **shell_run**(command, cwd, timeout) [capability_expand category: shell]: Execute a one-shot shell command with timeout protection. Runs in the active workspace; paths resolving outside it are refused. Reach for a structured tool first when one fits — web_fetch for anything over HTTP(S), git_query/scm_sync for git, code_search/file_find/file_read for the repo, config_get/config_set for LeapFlow's own settings — because those report typed results, while a failed shell command can only be diagnosed from its exit code and stderr. Every shell run counts as an external side effect, so a failure stops the rest of the batch and is not retried automatically.\n- **terminal_open**(command, cwd, shell) [capability_expand category: terminal]: Open a PERSISTENT shell session (REPL/dev server/watch), returning a session_id for terminal_send/read/close. Disabled unless tools.terminal_session_enabled is set. For one-shot commands use shell_run instead.\n- **terminal_send**(session_id, input, wait) [capability_expand category: terminal]: Send a line of input to a persistent terminal session and return output captured shortly after.\n- **terminal_read**(session_id, wait): Drain buffered output from a persistent terminal session (optionally waiting briefly first).\n- **terminal_close**(session_id) [capability_expand category: terminal]: Terminate a persistent terminal session and release its process group.\n- **terminal_list**(): List active persistent terminal sessions.\n- **config_list**(category, limit): List LeapFlow's own writable settings (LLM model and endpoint, daemon, memory, perception, gateway, …) with current values. Use this to discover the exact key before changing anything. Optionally narrow by `category`. This is the only correct way to inspect LeapFlow configuration — never read config files from disk.\n- **config_get**(key): Read one LeapFlow setting by key (e.g. 'llm.model', 'llm.base_url', 'daemon.log_level'), returning its current value, type, scopes, and whether a change needs a daemon restart. There is no 'llm.provider' key: provider behavior is inferred from the OpenAI-compatible 'llm.base_url'. Never read LeapFlow config files from disk — use this.\n- **config_set**(key, value, scope) [capability_expand category: config]: Change one LeapFlow setting by key, e.g. switch an OpenAI-compatible LLM with key='llm.model' and key='llm.base_url'. There is no 'llm.provider' key: provider behavior is inferred from the endpoint. Values are validated and coerced; credentials are stored in the vault automatically. Call config_list or config_get first if unsure of the key. The result states whether a `leap daemon restart` is required. Never edit LeapFlow config files directly.\n- **web_fetch**(url, select, timeout, max_bytes): Read a URL over HTTP(S) and get back extracted, context-sized content: parsed JSON for API endpoints, readable text plus links for web pages. Use this for anything on the internet — prices, docs, releases, articles — instead of running curl through shell_run: it reports real HTTP status codes, retries rate limits on its own, and is a plain read so a retry is always safe. For JSON APIs pass `select` with a dotted path (e.g. 'chart.result.0.meta') to return just that part instead of the whole payload.\n- **memory_search**(query, limit): Search agent memory for relevant past experiences, observations, and facts.\n- **memory_add**(content, kind) [capability_expand category: write]: Store a new observation or insight in memory for future reference.\n- **research_note**(kind, text): Record a compact, structured note about the current task's state so it survives context compression on long / multi-step tasks. Use for durable findings, open questions still to resolve, decisions / excluded paths, and the immediate next step. One concise sentence per note.\n- **capability_expand**(category): Fetch the full callable schema for every tool in a capability category (e.g. 'hub', 'gateway', 'desktop', 'delegate', 'file', 'memory', 'skill'). The compact tool index always lists every registered tool by name and a one-line summary, but only a static low-risk subset is directly callable each turn. If you need a tool from the index that is not yet callable, call capability_expand with its category first; the matching tools become callable in this turn. Never invent a tool name — expand the category instead.\n- **delegate_task**(goal, context) [capability_expand category: delegate]: Delegate a complex sub-task to an isolated subagent. The subagent gets a fresh context and restricted tool access. Use when a task is self-contained and can be solved independently.\n- **schedule_reentry**(kind, reason, delay_seconds, event_match, max_reentries, deadline_seconds): Register a re-entry so this task can resume later from its current orientation (findings / open questions / next step). Use when work must pause and continue after a delay (kind=time) or when a matching platform event arrives (kind=event), instead of finishing now. The research-ledger state is carried over automatically.\n- **hub_push**(skill_name, visibility, version) [capability_expand category: hub]: Push a local skill to the ModelScope Hub for sharing or backup.\n- **hub_pull**(repo_id, version) [capability_expand category: hub]: Pull a skill from the ModelScope Hub to install locally.\n- **hub_search**(query) [capability_expand category: hub]: Search for skills on the Hub by keyword or description.\n- **hub_sync**(mode, dry_run) [capability_expand category: hub]: Preview or execute sync between local skills and Hub.\n- **platform_action**(platform, action, payload, backend_kind) [capability_expand category: gateway]: Execute an exact registered business action on an external platform through LeapFlow's App Connector layer. Actions must be copied from the App Connector Capability Index and are addressed as domain.operation, e.g. im.send_message or docs.create_markdown. All business fields (chat_id, text, query, etc.) MUST be placed inside `payload`, never at the top level. Example: {\"platform\":\"feishu\",\"action\":\"im.send_message\",\"payload\":{\"chat_id\":\"oc_xxx\",\"text\":\"hello\"}}. Do not invent action names, do not use management actions such as list/guide/connect/status here.\n- **platform_connect**(action, platform, credentials, options, checkpoint) [capability_expand category: gateway]: List, guide, connect, disconnect, remove, or check status for external platforms using the App Connector management namespace. Supports REST and CLI backends. Use this for management actions such as list/guide/preflight/connect/status; use platform_action only for exact registered business actions.\n- **gateway_send**(platform, chat_id, text, thread_id) [capability_expand category: gateway]: Send a message to a connected external platform (Feishu group, Telegram chat, DingTalk conversation, etc.). Requires the platform to be connected via gateway_connect first. Use gateway_connect with action='list' to see connected platforms and available chat IDs.\n- **gateway_connect**(action, platform, credentials, options) [capability_expand category: gateway]: Connect, configure, or manage external platform integrations (Feishu, DingTalk, Telegram, Slack, Discord, etc.). Conversational flow: 1) call 'guide' to get setup steps + required fields, 2) present the steps to the user and ask for ALL required credentials in a single message, 3) call 'connect' with the credentials. Goal: complete in 1–2 user turns. NEVER include credential values in your text response.\n- **plugin_list**(): List the live plugin registry across Tool/Gateway/LLM subsystems for plugin management: which plugins are loaded, their category and fiber lifecycle state. For questions about LeapFlow's own capabilities or whether it supports X, use self_describe instead.\n- **plugin_status**(plugin_id): Get detailed status of a specific plugin: its declared category, runtime dependencies, contributed tools, and fiber lifecycle state.\n- **plugin_versions**(plugin_id): List recorded source versions and active pointer for a profile-scoped plugin.\n- **plugin_propose**(requested_capability, plugin_id, proposed_tools, test_cases, risk_level, evidence): Create a side-effect-free PluginProposal from explicit capability-gap evidence. Use this before plugin_generate when a missing capability should be reviewed. Does not call an LLM, write files, or install anything.\n- **assess_compatibility**(manifest, source_path) [capability_expand category: plugin_management]: Assess whether a foreign plugin manifest or real DSH source bundle is compatible with LeapFlow. A source bundle assessment is static: runtime_ready stays false until restricted Node discovery during plugin_install. Returns component-level verdicts and limitations.\n- **plugin_generate**(plugin_id, description, proposal_id) [capability_expand category: system]: Generate a new ToolPlugin from a natural-language capability description. The LLM produces code that conforms to the ToolPlugin Protocol; it is then rigorously validated (syntax, structure, import, protocol conformance). The generated source is stored in CAS and receives explicit content approval. It DOES NOT install the plugin — installation requires a second, mutation-specific approval via plugin_install.\n- **plugin_install**(plugin_id, code, marketplace_name, source_path, proposal_id, version_label) [capability_expand category: system]: Install a plugin from exactly one source: validated Python code, the configured Python marketplace, or a real DSH source bundle. DSH bundles run restricted Node discovery before registration; only public host tools are installed and client UI limitations are reported. Writes profile-scoped state and mutates the process-global registry. REQUIRES APPROVAL.\n- **plugin_rollback**(plugin_id, version) [capability_expand category: system]: Rollback a profile-scoped plugin to a recorded source version and reload it. REQUIRES APPROVAL.\n- **plugin_reload**(plugin_id, version_label) [capability_expand category: system]: Hot-reload a plugin at runtime. Disposes the old plugin fiber, re-imports its module, and registers a fresh instance. Existing in-flight turns are unaffected (snapshot isolation). REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_disable**(plugin_id) [capability_expand category: system]: Disable a plugin by disposing its fiber, removing its tools from the runtime registry. Cannot disable self_management itself. REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_remove**(plugin_id, delete_source) [capability_expand category: system]: Terminally remove a plugin: dispose its fiber, unregister its tools, remove reload metadata, and optionally delete its profile-scoped source file. Cannot remove self_management itself. REQUIRES APPROVAL.\n- **plugin_unquarantine**(plugin_id) [capability_expand category: plugin_management]: Restore a quarantined plugin to probation status for re-evaluation. Unfreezes the trust ledger, transitions the proposal back to PROBATION, and reloads the plugin. REQUIRES APPROVAL.\n- **plugin_enable**(plugin_id) [capability_expand category: system]: Re-enable a previously disabled plugin by reloading its module and registering a fresh instance. REQUIRES APPROVAL.\n- **self_describe**(facet): Answer questions about LeapFlow ITSELF from live runtime evidence — its identity/version/model, what tools and plugins it has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of self-awareness. Prefer this over reading LeapFlow's own source code. Pick facet=capabilities for 'what can you do / do you support X', identity for version/model, runtime for context/posture, or all for a full self-check when explicitly requested.\n- **runtime_snapshot**(): Lightweight (~150 token) flat snapshot of current runtime state: model, context budget, posture, disclosure level, turn count, cache hit rate, uptime, tool count, and pending approvals.\n- **schedule_create**(trigger_expression, instruction, execution_mode, max_retries, delivery_target) [capability_expand category: scheduler]: Create a new scheduled task. Specify a trigger expression (e.g. '30m', 'every 2h', '0 9 * * *') and the instruction to execute on each trigger. Optionally provide an execution mode and a delivery target to receive notifications.\n- **schedule_list**() [capability_expand category: scheduler]: List all scheduled tasks with their current state, trigger, and next due time.\n- **schedule_status**(task_id) [capability_expand category: scheduler]: Get detailed status and recent execution history for a scheduled task.\n- **schedule_pause**(task_id) [capability_expand category: scheduler]: Pause a scheduled task so it stops firing without being cancelled.\n- **schedule_resume**(task_id) [capability_expand category: scheduler]: Resume a paused scheduled task — re-arms it and recalculates next due time.\n- **schedule_cancel**(task_id) [capability_expand category: scheduler]: Cancel a scheduled task permanently.\n- **tool_search**(query, max_results): Search all registered tools by keyword relevance. Returns a ranked list of matching tools with name, category, and summary. Use this when you need a tool but are unsure of its exact name or category.\n- **tool_describe**(tool_name): Get the full callable schema of a single tool by exact name. Returns the tool's description, parameters, and metadata. Use after tool_search to inspect a specific tool before calling it.\n- **hw_list**(): List connected hardware devices with their channel counts and measured quantities. Start here; it does not include operating limits.\n- **hw_describe**(device_id): Return the full reference for one device: every channel, its unit, its operating envelope, rate limit, reversibility, and required interlocks, plus any prior outcomes recorded for its writable channels. Required before commanding a device.\n- **hw_read**(device_id, channel_id): Read the current value of one device channel. Has no physical effect.\n- **hw_status**(device_id): Report connection health, halt capability, and recent notable events (threshold excursions, lost samples, stalled channels) for one device.\n- **hw_configure**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Set a configuration value or setpoint on a channel declaring effect=configure. Setpoints often have inertia: the value may need time to stabilise.\n- **hw_actuate**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Command motion or output on a channel declaring effect=actuate. This moves physical hardware; a repeat from an unknown state is not a safe retry.\n- **hw_dispense**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Consume an irreversible resource on a channel declaring effect=dispense. Running this twice dispenses twice; never repeat it after a failure without first verifying what already happened.\n- **hw_estop**(device_id) [capability_expand category: hardware]: Stop all motion and output on a device immediately. Never requires approval; use it whenever device behaviour is unexpected.\n- **hw_batch_actuate**(device_id, commands, dry_run, verify) [capability_expand category: hardware]: Write multiple actuator channels in one atomic transaction. Used for coordinated multi-joint control where all positions must be commanded simultaneously (e.g. robot arm joint-space control).\n- **hw_control_start**(device_id, mode, targets, frequency_hz, trajectory, policy, gains) [capability_expand category: hardware]: Start a real-time control loop on a robot device. Supports PID position tracking, impedance control, trajectory following, and neural policy chains.\n- **hw_control_stop**(device_id) [capability_expand category: hardware]: Stop the real-time control loop on a device. Gracefully halts the device and returns session statistics.\n- **hw_control_status**(device_id) [capability_expand category: hardware]: Get the status and statistics of a running control loop. Returns frequency, jitter, overruns, and policy state.\n- **hw_context_snapshot**(level) [capability_expand category: hardware]: Get a structured snapshot of all hardware devices and their state. Use this before planning physical manipulation tasks to understand what devices are available and what they can do.\n- **hw_health**(device_id) [capability_expand category: hardware]: Check hardware device health status. Without device_id, returns fleet-wide health summary. With device_id, returns health details for that specific device.\n- **hw_policy_infer**(device_id, policy, task, execute, chunk_size, verify, compute_budget) [capability_expand category: hardware]: Execute VLA policy inference on a robot device. Reads observation, runs policy, optionally writes action to actuators. Supports local (HuggingFace) and remote (PolicyServer) policies.\n- **hw_policy_episode**(action, device_id, policy, task) [capability_expand category: hardware]: Manage policy inference episode lifecycle. Start an episode to begin a task, stop to finalise and get stats.\n- **hw_policy_list**(source) [capability_expand category: hardware]: List available VLA policies and their capabilities.\n- **hw_orchestrate**(mode, operations, barrier_timeout_s, label) [capability_expand category: hardware]: Execute coordinated operations across multiple hardware devices. Supports sequential (one-at-a-time), parallel (concurrent), and barrier (synchronised) execution modes. Any failure in a parallel group triggers halt_all on every participating device for safety.\n- **session_search**(query, limit) [capability_expand category: unclassified]: Search past conversation sessions in the current workspace for relevant context.\n- **session_list**(limit): List recent conversation sessions in the current workspace with stable ids, titles, dates, and summaries. Use for browsing past tasks or when user asks to see history without specific search terms. No keywords needed — returns chronological list.\n- **session_detail**(session_id, limit, offset, include_inactive) [capability_expand category: unclassified]: Read a paginated persisted transcript for one past conversation session in the current workspace. Use after session_list or session_search returns a session_id.\n- **click**(element_index) [capability_expand category: desktop]: Click a UI element by its element_index (from the latest observe_ui snapshot)\n- **get_clipboard**() [capability_expand category: desktop]: Read current clipboard text content\n- **list_apps**(filter, running_only) [capability_expand category: desktop]: List available applications on this system. Use to discover correct bundle_id before switch_app.\n- **list_windows**() [capability_expand category: desktop]: List all top-level windows with pid, window_id, title, and per-window state (minimized, on-screen). Call this first to pick the pid and window_id that observe_ui and other window tools require.\n- **observe_ui**(pid, window_id, query) [capability_expand category: desktop]: Snapshot one window's actionable UI elements, each tagged with an element_index for click/right_click/read_text. Re-observe after actions — indices belong to one snapshot. Requires the window's pid and window_id from list_windows.\n- **open_url**(url, app_id) [capability_expand category: desktop]: Open a URL in the default or specified browser\n- **read_text**(element_index) [capability_expand category: desktop]: Read the text content of a specific UI element from the latest snapshot\n- **right_click**(element_index) [capability_expand category: desktop]: Right-click a UI element to open its context menu. Returns visible menu items.\n- **screenshot**(pid, window_id) [capability_expand category: desktop]: Capture a screenshot for visual verification. With pid + window_id captures that window (works across all displays); defaults to the last observed window, or the full desktop when no window has been observed.\n- **scroll**(element_index, direction, amount, pid, window_id) [capability_expand category: desktop]: Scroll a scrollable area of a window. Omit element_index to scroll the window's focused/page scroller; pass one to scroll an exact element from the latest snapshot.\n- **select_text**(element_index) [capability_expand category: desktop]: Select all text in a UI element (focus + select-all, for subsequent copy)\n- **set_clipboard**(text) [capability_expand category: desktop]: Write text to the clipboard\n- **shortcut**(keys) [capability_expand category: desktop]: Execute a keyboard shortcut\n- **switch_app**(app_id) [capability_expand category: desktop]: Switch to an app (launch if needed, activate, verify)\n- **type_text**(text) [capability_expand category: desktop]: Type text into the currently focused element\n- **wait**(seconds) [capability_expand category: desktop]: Wait for a specified duration before continuing\n- **wait_until**(condition, pid, window_id, timeout, poll_interval) [capability_expand category: desktop]: Wait until a UI condition is met (polls UI tree). Returns elements when found or on timeout.\n- **wait_until_stable**(timeout, poll_interval, pid, window_id) [capability_expand category: desktop]: Wait until the UI stops changing (element set stabilizes across polls).\n\n## App Connector Capability Index\nLeapFlow can onboard and manage external apps through `platform_connect` and execute exact registered business actions through `platform_action`.\nFor requests about connecting, setting up, configuring, enabling, or managing a supported app, use `platform_connect` first instead of generating SDK/Webhook sample code.\n`platform_connect.action` is the management namespace: list, guide, preflight, connect, disconnect, remove, status, events_start, events_stop, events_status.\n`platform_action.action` is only for exact registered platform business actions listed below, such as `im.send_message`; never use management actions like `list` or `guide` there.\nAll business fields MUST go inside `payload`; top-level keys are only `platform`, `action`, and `payload`.\nDo not invent platform IDs or platform action names. If the needed action is not listed, ask for discovery/clarification instead of guessing.\nUse `platform_connect` with `action='guide'` and the matching `platform` to start onboarding; use `action='list'` when the app is unclear.\nWhen a pending onboarding state is present, continue from that state with `platform_connect` instead of asking the user to restate the app.\nSupported apps:\n- `api_server`: API Server (OpenAI Compatible) (category=api; backend=adapter; platform_action actions=none registered)\n- `dingtalk`: 钉钉 (DingTalk) (category=im; backend=adapter; platform_action actions=none registered)\n- `feishu`: 飞书 (Feishu/Lark) (category=im; backend=cli)\n - `calendar.create_event` payload={summary*, start_time*, end_time*, attendees} [write/high]\n - `docs.create_markdown` payload={title*, markdown*, folder_token} [write/medium]\n - `drive.search` payload={query*, limit} [read/medium]\n - `im.add_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.download_resource` payload={message_id*, file_key*, type*} [read/low]\n - `im.get_messages` payload={message_ids*} [read/low]\n - `im.list_chats` payload={page_size} [read/low]\n - `im.list_messages` payload={chat_id*} [read/low]\n - `im.list_thread_messages` payload={thread*} [read/low]\n - `im.remove_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.reply_message` payload={message_id*, text*} [send/high]\n - `im.search_chats` payload={query*, page_size} [read/low]\n - `im.search_messages` payload={query*} [read/medium]\n - `im.send_message` payload={chat_id*, text*, thread_id} [send/high]\n - `im.update_card` payload={token*, card*} [write/medium]\n - `im.update_message` payload={message_id*, text*} [write/medium]\n - `mail.search_unread` payload={query, limit} [read/high]\n - `sheets.append_row` payload={spreadsheet_token*, sheet_id*, values*} [write/medium]\n - `task.create` payload={title*, description, due_time} [write/medium]\n- `telegram`: Telegram (category=social; backend=adapter; platform_action actions=none registered)\n- `webhook`: Webhook (Generic) (category=webhook; backend=adapter; platform_action actions=none registered)\n\n## Tool Usage\nTools are normally invoked through the native function-calling mechanism, not by writing JSON in your reply\ntext. Only if the provider signals that native function calling is unavailable for this turn, fall back to a\nsingle JSON code block: `{\"name\": \"tool_name\", \"arguments\": {\"key\": \"value\"}}` — use this fallback format\nonly, never both. Only call a tool whose exact name appears in the tool index above and is currently callable\n(or reachable via `capability_expand`). Never invent, rename, alias, or guess a tool name, platform ID, or\nplatform action from argument shape or wording — if the index does not list it, it does not exist.\n`platform_connect.action` (list/guide/preflight/connect/disconnect/remove/status/events_start/events_stop/\nevents_status) is the App Connector management namespace; `platform_action.action` only accepts exact\nregistered business actions such as `im.send_message` shown in the App Connector Capability Index — never mix\nthe two namespaces. If a tool call returns an unknown/unavailable result, use the returned suggestions or\navailable names for a single retry instead of trying further variations of the same guess.\n\n**Side-effect action rule** (`platform_action` with effect=send/write/execute):\n- Call each unique action+payload **exactly once**. Never include duplicates in the same turn.\n- Once the result returns `\"completed\": true`, that action is DONE for this task. Do NOT call it again\n in any subsequent turn — immediately summarize the result for the user instead.\n- The system enforces idempotency: duplicate calls are blocked and will not execute.\n- If the user explicitly requests sending/writing multiple times, use distinct payloads per call.\n\n**Resource identifier provenance rule**:\n- NEVER fabricate, guess, or infer resource identifiers (chat_id, message_id, file_key, user_id, etc.).\n Every resource ID used in a side-effect action MUST come from a successful API response in this session.\n- If a read/list action fails (e.g. authorization error), you do NOT have valid resource IDs.\n Report the failure to the user — do NOT attempt the dependent write/send action with a guessed ID.\n- When a tool result contains `\"llm_instruction\"`, follow it exactly.\n\n## Guidelines\n1. **Direct answers first**: If you already know the answer, respond directly without tools.\n2. **Avoid redundant tool calls**: Do not call the same tool with the same arguments more than once in the same user turn. When an existing tool result already answers the user's request, stop calling tools and answer directly.\n3. **Use tools proactively**: When the user asks about files, time, system state, or needs actions performed, use the appropriate tool.\n4. **Chain tools when needed**: You can call multiple tools in sequence (e.g., list files → read file → summarize).\n5. **Handle failures gracefully**: If a tool fails, explain what went wrong and suggest alternatives. If it failed because of an unknown tool/platform/action name, retry once with an exact name from the returned suggestions, then explain rather than keep guessing.\n6. **Summarize results naturally**: After tool execution, synthesize the results into a helpful answer rather than dumping raw output.\n7. **Stay conversational**: Maintain a natural, helpful tone. Acknowledge context from earlier in the conversation.\n8. **Recall past work**:\n - Broad queries (\"之前做了什么\", \"列出任务\"): answer from the \"Recent Task History\" section already in your context. If insufficient, call session_list.\n - Specific lookups (\"上次那个配置怎么改的\"): call session_search with relevant phrases (NOT single characters).\n - Do NOT call search tools repeatedly with keyword fragments. One well-phrased call is better than ten fragmented ones.\n\n## Coding & Verification\nWhen working with code, prefer the precise built-in tools over ad-hoc shell: use `repo_map` to orient in an\nunfamiliar project, `code_search`/`file_find` to locate and `code_intel` for symbols, `edit_file` (anchored\nsearch-replace, or a unified `diff`) to change files — never rewrite a whole file to change a few lines — and\n`git_query` to inspect diffs/log. After edits, check `syntax_ok` in the result and run `test_run`/`lint_check`\nbefore declaring the work done. Batch independent read-only calls (search/read/query) into a single turn — they\nrun in parallel.\n\n## Presentation Style\n1. **Polished Markdown only**: Format user-facing answers with clean Markdown headings, short paragraphs, and concise bullets. Use tables only when they improve comparison or scanning.\n2. **Terminal-friendly layout**: Keep lines readable in a TUI; avoid dense walls of text, deeply nested lists, oversized ASCII art, or heavy visual blocks.\n3. **Elegant emphasis**: Use bold text sparingly for key terms and conclusions. Avoid excessive emojis, decorative symbols, repeated separators, or visual noise.\n4. **Theme-safe colors**: Do not emit ANSI escape codes, HTML color tags, Rich markup, or hardcoded color names. Rely on the TUI theme to render Markdown professionally.\n5. **No leaked tool protocol**: Never show tool-call JSON, internal schemas, raw observations, tool result payloads, or hidden reasoning in the final answer unless the user explicitly asks for raw/debug output. Treat any prior `{\"name\": ..., \"arguments\": ...}` blocks and `Tool result (...)` messages as internal execution context only.\n6. **Professional closure**: End with a concise conclusion or next step when helpful; avoid rambling after the useful answer is complete.\n\nWhen finished with all tool calls, respond normally without a JSON block, tool-call transcript, or process log.\n\n## Task Contract\n- Task ID: turn-1\n- Original user request: List the connected hardware devices, then describe bench_r8 in full.\n- Workspace root: \n- Allowed roots: \n- Treat relative project paths as relative to the workspace root; never infer `.` as the project root when a workspace root is provided.\n- Workspace boundary is enforced by tools: do not read, search, edit, or run commands against paths outside the allowed roots unless the user explicitly requests an external path and the tool/approval policy permits it.\n- LeapFlow workspace config is optional at `/.leapflow/config.yaml`; runtime config is loaded from `~/.leapflow/config/user.yaml` and `~/.leapflow/profiles//config/*.yaml`.\n- Preserve this task contract across summarization, compression, tool loops, and memory retrieval.\n" }, { "role": "system", diff --git a/tests/_fixtures/cassettes/r8_hardware/cassette-model-2c150449ffad6f0c.cassette.json b/tests/_fixtures/cassettes/r8_hardware/cassette-model-2c150449ffad6f0c.cassette.json deleted file mode 100644 index 89c70f14..00000000 --- a/tests/_fixtures/cassettes/r8_hardware/cassette-model-2c150449ffad6f0c.cassette.json +++ /dev/null @@ -1,76 +0,0 @@ -{ - "fingerprint": "2c150449ffad6f0cd0deaab1cd48e0151101fac732f3e39e084eed74d704b2cb", - "note": "captured in seed mode", - "request": { - "model": "cassette-model", - "stream": false, - "messages": [ - { - "role": "system", - "content": "You are LeapFlow, an intelligent assistant that can both converse naturally and take real actions on the user's computer.\n\n## Capabilities\nThe tool index below lists **every** registered tool by name and a one-line summary — this is the complete\ncapability contract; nothing else exists. Only a subset is directly callable this turn (via native tool calling,\nnot a JSON block in your reply). If you need a tool from the index that is not yet callable, call\n`capability_expand` with its category name first — the matching tools become callable immediately after.\nWhen the user asks about LeapFlow **itself** — its identity/version/model, what tools, plugins, or capabilities\nit has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of\nself-awareness — answer from live runtime evidence via `self_describe` (facet=capabilities for \"what can you\ndo / do you support X\", identity for version/model, runtime for context/posture, or `all` for a full self-check),\nnever by reading LeapFlow's own source code, documentation, or memory. Report configuration-dependent or\nunavailable capabilities as limitations rather than inferring them.\n- **text_search**(text, pattern): Search for a regex pattern in text.\n- **text_replace**(text, old, new, count) [capability_expand category: write]: Replace occurrences of a substring in text.\n- **time_get**(): Get current date and time.\n- **env_info**(): Get system environment information (OS, Python version, cwd).\n- **skills_list**(query, category, source): List available learned skills. Use when user asks about capabilities or you need a specific skill.\n- **skill_view**(name): View the full content of a specific skill document.\n- **skill_list_files**(name) [capability_expand category: skills]: List all support files in a skill directory (configs, templates, examples). Use before skill_file_read to discover available files.\n- **skill_file_read**(name, file_path) [capability_expand category: skills]: Read an individual support file from a skill directory. Use skill_list_files first to discover available files.\n- **code_intel**(path, operation): Precise document symbols (outline) for a source file: classes, functions, and methods with line ranges. Python uses an exact AST parse; other languages use a keyword-prefix scan. Prefer over file_read mode=symbols for accurate navigation before editing. Read-only.\n- **repo_map**(path): Compact project orientation for a repository root: languages, detected test/lint commands, top-level structure, entry points, manifest, and VCS branch. Call this first when entering an unfamiliar codebase. Read-only.\n- **scm_sync**(action, cwd, remote, pull_ref, push_ref, timeout) [capability_expand category: scm]: Run a typed git SCM action. Use this instead of shell_run for git pull/push/status. For 'pull origin main then push', set action='pull_then_push', remote='origin', pull_ref='main', and omit push_ref so LeapFlow pushes the current local branch.\n- **git_query**(action, cwd, ref, path, staged, max_count, stat): Read-only structured git inspection: action=diff|log|status|branch|show. Prefer over shell_run for reading repo state — output is clipped, redacted, and log/branch are parsed into structured fields. Use scm_sync for pull/push.\n- **git_write**(action, cwd, message, stage_all, name, ref, create) [capability_expand category: scm]: Mutating git actions: action=commit (message, stage_all), branch (create+switch), checkout (switch; create=true for -b). Approval-gated. Use scm_sync for pull/push and git_query for reads.\n- **test_run**(command, cwd, timeout): Run the project's test suite and return structured results (framework, passed/failed counts, failing tests). Auto-detects the runner (pytest/npm/go/cargo) or uses a configured/explicit command; executes via the governed shell. ok=true means the runner executed — see 'success' for pass/fail.\n- **lint_check**(command, cwd, timeout): Run the project's linter and return a structured clean/issue result. Auto-detects the linter (ruff/eslint/go vet/clippy) or uses a configured/explicit command; executes via the governed shell. ok=true means the linter ran — see 'clean'.\n- **file_list**(path, pattern, depth): List files and directories at a given path. Use depth=1 or depth=2 to get a recursive tree in one call instead of listing each sub-directory separately.\n- **file_read**(path, max_lines, start_line, max_chars, mode): Read text file content with adaptive context governance. For large or unfamiliar files, prefer mode='outline' or mode='symbols' first, then use mode='raw' with start_line/max_lines for the specific range you actually need. For LeapFlow's own settings, use config_list / config_get / config_set — its config files are outside the workspace and not readable here.\n- **file_write**(path, content, mode) [capability_expand category: write]: Write content to a file (overwrite or append).\n- **code_search**(pattern, patterns, path, glob, ignore_case, multiline, max_results, context_lines): Search file CONTENTS by regex pattern across a directory tree (ripgrep-backed). Requires a regex pattern. NOT for listing or browsing directory contents — use file_list for that. Prefer this over shell_run grep: faster, skips VCS/dependency/build dirs, and returns structured path:line:column matches. Batch related lookups into ONE call via `patterns` (OR-combined, single pass) instead of issuing several separate searches. Use file_read for the surrounding context of a hit.\n- **file_find**(glob, path, max_results): Find files by a recursive glob pattern under a base path (e.g. '**/test_*.py' or '*.md'). Prefer this over shell_run find; skips VCS/dependency/build dirs.\n- **edit_file**(path, edits, dry_run, diff) [capability_expand category: write]: Apply targeted, anchored search-replace edits to an EXISTING text file (use file_write to create/overwrite). Each edit is {original_text, new_text, replace_all?}; original_text must match exactly and uniquely (or set replace_all) — a non-unique or missing anchor is rejected so files are never corrupted. Set dry_run to preview. Alternatively pass a unified 'diff' to apply its hunks as anchored edits. Far cheaper and safer than rewriting a whole file.\n- **shell_run**(command, cwd, timeout) [capability_expand category: shell]: Execute a one-shot shell command with timeout protection. Runs in the active workspace; paths resolving outside it are refused. Reach for a structured tool first when one fits — web_fetch for anything over HTTP(S), git_query/scm_sync for git, code_search/file_find/file_read for the repo, config_get/config_set for LeapFlow's own settings — because those report typed results, while a failed shell command can only be diagnosed from its exit code and stderr. Every shell run counts as an external side effect, so a failure stops the rest of the batch and is not retried automatically.\n- **terminal_open**(command, cwd, shell) [capability_expand category: terminal]: Open a PERSISTENT shell session (REPL/dev server/watch), returning a session_id for terminal_send/read/close. Disabled unless tools.terminal_session_enabled is set. For one-shot commands use shell_run instead.\n- **terminal_send**(session_id, input, wait) [capability_expand category: terminal]: Send a line of input to a persistent terminal session and return output captured shortly after.\n- **terminal_read**(session_id, wait): Drain buffered output from a persistent terminal session (optionally waiting briefly first).\n- **terminal_close**(session_id) [capability_expand category: terminal]: Terminate a persistent terminal session and release its process group.\n- **terminal_list**(): List active persistent terminal sessions.\n- **config_list**(category, limit): List LeapFlow's own writable settings (LLM model and endpoint, daemon, memory, perception, gateway, …) with current values. Use this to discover the exact key before changing anything. Optionally narrow by `category`. This is the only correct way to inspect LeapFlow configuration — never read config files from disk.\n- **config_get**(key): Read one LeapFlow setting by key (e.g. 'llm.model', 'llm.base_url', 'daemon.log_level'), returning its current value, type, scopes, and whether a change needs a daemon restart. There is no 'llm.provider' key: provider behavior is inferred from the OpenAI-compatible 'llm.base_url'. Never read LeapFlow config files from disk — use this.\n- **config_set**(key, value, scope) [capability_expand category: config]: Change one LeapFlow setting by key, e.g. switch an OpenAI-compatible LLM with key='llm.model' and key='llm.base_url'. There is no 'llm.provider' key: provider behavior is inferred from the endpoint. Values are validated and coerced; credentials are stored in the vault automatically. Call config_list or config_get first if unsure of the key. The result states whether a `leap daemon restart` is required. Never edit LeapFlow config files directly.\n- **web_fetch**(url, select, timeout, max_bytes): Read a URL over HTTP(S) and get back extracted, context-sized content: parsed JSON for API endpoints, readable text plus links for web pages. Use this for anything on the internet — prices, docs, releases, articles — instead of running curl through shell_run: it reports real HTTP status codes, retries rate limits on its own, and is a plain read so a retry is always safe. For JSON APIs pass `select` with a dotted path (e.g. 'chart.result.0.meta') to return just that part instead of the whole payload.\n- **memory_search**(query, limit): Search agent memory for relevant past experiences, observations, and facts.\n- **memory_add**(content, kind) [capability_expand category: write]: Store a new observation or insight in memory for future reference.\n- **research_note**(kind, text): Record a compact, structured note about the current task's state so it survives context compression on long / multi-step tasks. Use for durable findings, open questions still to resolve, decisions / excluded paths, and the immediate next step. One concise sentence per note.\n- **capability_expand**(category): Fetch the full callable schema for every tool in a capability category (e.g. 'hub', 'gateway', 'desktop', 'delegate', 'file', 'memory', 'skill'). The compact tool index always lists every registered tool by name and a one-line summary, but only a static low-risk subset is directly callable each turn. If you need a tool from the index that is not yet callable, call capability_expand with its category first; the matching tools become callable in this turn. Never invent a tool name — expand the category instead.\n- **delegate_task**(goal, context) [capability_expand category: delegate]: Delegate a complex sub-task to an isolated subagent. The subagent gets a fresh context and restricted tool access. Use when a task is self-contained and can be solved independently.\n- **schedule_reentry**(kind, reason, delay_seconds, event_match, max_reentries, deadline_seconds): Register a re-entry so this task can resume later from its current orientation (findings / open questions / next step). Use when work must pause and continue after a delay (kind=time) or when a matching platform event arrives (kind=event), instead of finishing now. The research-ledger state is carried over automatically.\n- **hub_push**(skill_name, visibility, version) [capability_expand category: hub]: Push a local skill to the ModelScope Hub for sharing or backup.\n- **hub_pull**(repo_id, version) [capability_expand category: hub]: Pull a skill from the ModelScope Hub to install locally.\n- **hub_search**(query) [capability_expand category: hub]: Search for skills on the Hub by keyword or description.\n- **hub_sync**(mode, dry_run) [capability_expand category: hub]: Preview or execute sync between local skills and Hub.\n- **platform_action**(platform, action, payload, backend_kind) [capability_expand category: gateway]: Execute an exact registered business action on an external platform through LeapFlow's App Connector layer. Actions must be copied from the App Connector Capability Index and are addressed as domain.operation, e.g. im.send_message or docs.create_markdown. All business fields (chat_id, text, query, etc.) MUST be placed inside `payload`, never at the top level. Example: {\"platform\":\"feishu\",\"action\":\"im.send_message\",\"payload\":{\"chat_id\":\"oc_xxx\",\"text\":\"hello\"}}. Do not invent action names, do not use management actions such as list/guide/connect/status here.\n- **platform_connect**(action, platform, credentials, options, checkpoint) [capability_expand category: gateway]: List, guide, connect, disconnect, remove, or check status for external platforms using the App Connector management namespace. Supports REST and CLI backends. Use this for management actions such as list/guide/preflight/connect/status; use platform_action only for exact registered business actions.\n- **gateway_send**(platform, chat_id, text, thread_id) [capability_expand category: gateway]: Send a message to a connected external platform (Feishu group, Telegram chat, DingTalk conversation, etc.). Requires the platform to be connected via gateway_connect first. Use gateway_connect with action='list' to see connected platforms and available chat IDs.\n- **gateway_connect**(action, platform, credentials, options) [capability_expand category: gateway]: Connect, configure, or manage external platform integrations (Feishu, DingTalk, Telegram, Slack, Discord, etc.). Conversational flow: 1) call 'guide' to get setup steps + required fields, 2) present the steps to the user and ask for ALL required credentials in a single message, 3) call 'connect' with the credentials. Goal: complete in 1–2 user turns. NEVER include credential values in your text response.\n- **plugin_list**(): List the live plugin registry across Tool/Gateway/LLM subsystems for plugin management: which plugins are loaded, their category and fiber lifecycle state. For questions about LeapFlow's own capabilities or whether it supports X, use self_describe instead.\n- **plugin_status**(plugin_id): Get detailed status of a specific plugin: its declared category, runtime dependencies, contributed tools, and fiber lifecycle state.\n- **plugin_versions**(plugin_id): List recorded source versions and active pointer for a profile-scoped plugin.\n- **plugin_propose**(requested_capability, plugin_id, proposed_tools, test_cases, risk_level, evidence): Create a side-effect-free PluginProposal from explicit capability-gap evidence. Use this before plugin_generate when a missing capability should be reviewed. Does not call an LLM, write files, or install anything.\n- **assess_compatibility**(manifest, source_path) [capability_expand category: plugin_management]: Assess whether a foreign plugin manifest or real DSH source bundle is compatible with LeapFlow. A source bundle assessment is static: runtime_ready stays false until restricted Node discovery during plugin_install. Returns component-level verdicts and limitations.\n- **plugin_generate**(plugin_id, description, proposal_id) [capability_expand category: system]: Generate a new ToolPlugin from a natural-language capability description. The LLM produces code that conforms to the ToolPlugin Protocol; it is then rigorously validated (syntax, structure, import, protocol conformance). The generated source is stored in CAS and receives explicit content approval. It DOES NOT install the plugin — installation requires a second, mutation-specific approval via plugin_install.\n- **plugin_install**(plugin_id, code, marketplace_name, source_path, proposal_id, version_label) [capability_expand category: system]: Install a plugin from exactly one source: validated Python code, the configured Python marketplace, or a real DSH source bundle. DSH bundles run restricted Node discovery before registration; only public host tools are installed and client UI limitations are reported. Writes profile-scoped state and mutates the process-global registry. REQUIRES APPROVAL.\n- **plugin_rollback**(plugin_id, version) [capability_expand category: system]: Rollback a profile-scoped plugin to a recorded source version and reload it. REQUIRES APPROVAL.\n- **plugin_reload**(plugin_id, version_label) [capability_expand category: system]: Hot-reload a plugin at runtime. Disposes the old plugin fiber, re-imports its module, and registers a fresh instance. Existing in-flight turns are unaffected (snapshot isolation). REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_disable**(plugin_id) [capability_expand category: system]: Disable a plugin by disposing its fiber, removing its tools from the runtime registry. Cannot disable self_management itself. REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_remove**(plugin_id, delete_source) [capability_expand category: system]: Terminally remove a plugin: dispose its fiber, unregister its tools, remove reload metadata, and optionally delete its profile-scoped source file. Cannot remove self_management itself. REQUIRES APPROVAL.\n- **plugin_unquarantine**(plugin_id) [capability_expand category: plugin_management]: Restore a quarantined plugin to probation status for re-evaluation. Unfreezes the trust ledger, transitions the proposal back to PROBATION, and reloads the plugin. REQUIRES APPROVAL.\n- **plugin_enable**(plugin_id) [capability_expand category: system]: Re-enable a previously disabled plugin by reloading its module and registering a fresh instance. REQUIRES APPROVAL.\n- **self_describe**(facet): Answer questions about LeapFlow ITSELF from live runtime evidence — its identity/version/model, what tools and plugins it has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of self-awareness. Prefer this over reading LeapFlow's own source code. Pick facet=capabilities for 'what can you do / do you support X', identity for version/model, runtime for context/posture, or all for a full self-check when explicitly requested.\n- **runtime_snapshot**(): Lightweight (~150 token) flat snapshot of current runtime state: model, context budget, posture, disclosure level, turn count, cache hit rate, uptime, tool count, and pending approvals.\n- **schedule_create**(trigger_expression, instruction, execution_mode, max_retries, delivery_target) [capability_expand category: scheduler]: Create a new scheduled task. Specify a trigger expression (e.g. '30m', 'every 2h', '0 9 * * *') and the instruction to execute on each trigger. Optionally provide an execution mode and a delivery target to receive notifications.\n- **schedule_list**() [capability_expand category: scheduler]: List all scheduled tasks with their current state, trigger, and next due time.\n- **schedule_status**(task_id) [capability_expand category: scheduler]: Get detailed status and recent execution history for a scheduled task.\n- **schedule_pause**(task_id) [capability_expand category: scheduler]: Pause a scheduled task so it stops firing without being cancelled.\n- **schedule_resume**(task_id) [capability_expand category: scheduler]: Resume a paused scheduled task — re-arms it and recalculates next due time.\n- **schedule_cancel**(task_id) [capability_expand category: scheduler]: Cancel a scheduled task permanently.\n- **tool_search**(query, max_results): Search all registered tools by keyword relevance. Returns a ranked list of matching tools with name, category, and summary. Use this when you need a tool but are unsure of its exact name or category.\n- **tool_describe**(tool_name): Get the full callable schema of a single tool by exact name. Returns the tool's description, parameters, and metadata. Use after tool_search to inspect a specific tool before calling it.\n- **hw_list**(): List connected hardware devices with their channel counts and measured quantities. Start here; it does not include operating limits.\n- **hw_describe**(device_id): Return the full reference for one device: every channel, its unit, its operating envelope, rate limit, reversibility, and required interlocks, plus any prior outcomes recorded for its writable channels. Required before commanding a device.\n- **hw_read**(device_id, channel_id): Read the current value of one device channel. Has no physical effect.\n- **hw_status**(device_id): Report connection health, halt capability, and recent notable events (threshold excursions, lost samples, stalled channels) for one device.\n- **hw_configure**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Set a configuration value or setpoint on a channel declaring effect=configure. Setpoints often have inertia: the value may need time to stabilise.\n- **hw_actuate**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Command motion or output on a channel declaring effect=actuate. This moves physical hardware; a repeat from an unknown state is not a safe retry.\n- **hw_dispense**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Consume an irreversible resource on a channel declaring effect=dispense. Running this twice dispenses twice; never repeat it after a failure without first verifying what already happened.\n- **hw_estop**(device_id) [capability_expand category: hardware]: Stop all motion and output on a device immediately. Never requires approval; use it whenever device behaviour is unexpected.\n- **hw_batch_actuate**(device_id, commands, dry_run, verify) [capability_expand category: hardware]: Write multiple actuator channels in one atomic transaction. Used for coordinated multi-joint control where all positions must be commanded simultaneously (e.g. robot arm joint-space control).\n- **session_search**(query, limit) [capability_expand category: unclassified]: Search past conversation sessions in the current workspace for relevant context.\n- **session_list**(limit): List recent conversation sessions in the current workspace with stable ids, titles, dates, and summaries. Use for browsing past tasks or when user asks to see history without specific search terms. No keywords needed — returns chronological list.\n- **session_detail**(session_id, limit, offset, include_inactive) [capability_expand category: unclassified]: Read a paginated persisted transcript for one past conversation session in the current workspace. Use after session_list or session_search returns a session_id.\n- **click**(element_index) [capability_expand category: desktop]: Click a UI element by its element_index (from the latest observe_ui snapshot)\n- **get_clipboard**() [capability_expand category: desktop]: Read current clipboard text content\n- **list_apps**(filter, running_only) [capability_expand category: desktop]: List available applications on this system. Use to discover correct bundle_id before switch_app.\n- **list_windows**() [capability_expand category: desktop]: List all top-level windows with pid, window_id, title, and per-window state (minimized, on-screen). Call this first to pick the pid and window_id that observe_ui and other window tools require.\n- **observe_ui**(pid, window_id, query) [capability_expand category: desktop]: Snapshot one window's actionable UI elements, each tagged with an element_index for click/right_click/read_text. Re-observe after actions — indices belong to one snapshot. Requires the window's pid and window_id from list_windows.\n- **open_url**(url, app_id) [capability_expand category: desktop]: Open a URL in the default or specified browser\n- **read_text**(element_index) [capability_expand category: desktop]: Read the text content of a specific UI element from the latest snapshot\n- **right_click**(element_index) [capability_expand category: desktop]: Right-click a UI element to open its context menu. Returns visible menu items.\n- **screenshot**(pid, window_id) [capability_expand category: desktop]: Capture a screenshot for visual verification. With pid + window_id captures that window (works across all displays); defaults to the last observed window, or the full desktop when no window has been observed.\n- **scroll**(element_index, direction, amount, pid, window_id) [capability_expand category: desktop]: Scroll a scrollable area of a window. Omit element_index to scroll the window's focused/page scroller; pass one to scroll an exact element from the latest snapshot.\n- **select_text**(element_index) [capability_expand category: desktop]: Select all text in a UI element (focus + select-all, for subsequent copy)\n- **set_clipboard**(text) [capability_expand category: desktop]: Write text to the clipboard\n- **shortcut**(keys) [capability_expand category: desktop]: Execute a keyboard shortcut\n- **switch_app**(app_id) [capability_expand category: desktop]: Switch to an app (launch if needed, activate, verify)\n- **type_text**(text) [capability_expand category: desktop]: Type text into the currently focused element\n- **wait**(seconds) [capability_expand category: desktop]: Wait for a specified duration before continuing\n- **wait_until**(condition, pid, window_id, timeout, poll_interval) [capability_expand category: desktop]: Wait until a UI condition is met (polls UI tree). Returns elements when found or on timeout.\n- **wait_until_stable**(timeout, poll_interval, pid, window_id) [capability_expand category: desktop]: Wait until the UI stops changing (element set stabilizes across polls).\n\n## App Connector Capability Index\nLeapFlow can onboard and manage external apps through `platform_connect` and execute exact registered business actions through `platform_action`.\nFor requests about connecting, setting up, configuring, enabling, or managing a supported app, use `platform_connect` first instead of generating SDK/Webhook sample code.\n`platform_connect.action` is the management namespace: list, guide, preflight, connect, disconnect, remove, status, events_start, events_stop, events_status.\n`platform_action.action` is only for exact registered platform business actions listed below, such as `im.send_message`; never use management actions like `list` or `guide` there.\nAll business fields MUST go inside `payload`; top-level keys are only `platform`, `action`, and `payload`.\nDo not invent platform IDs or platform action names. If the needed action is not listed, ask for discovery/clarification instead of guessing.\nUse `platform_connect` with `action='guide'` and the matching `platform` to start onboarding; use `action='list'` when the app is unclear.\nWhen a pending onboarding state is present, continue from that state with `platform_connect` instead of asking the user to restate the app.\nSupported apps:\n- `api_server`: API Server (OpenAI Compatible) (category=api; backend=adapter; platform_action actions=none registered)\n- `dingtalk`: 钉钉 (DingTalk) (category=im; backend=adapter; platform_action actions=none registered)\n- `feishu`: 飞书 (Feishu/Lark) (category=im; backend=cli)\n - `calendar.create_event` payload={summary*, start_time*, end_time*, attendees} [write/high]\n - `docs.create_markdown` payload={title*, markdown*, folder_token} [write/medium]\n - `drive.search` payload={query*, limit} [read/medium]\n - `im.add_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.download_resource` payload={message_id*, file_key*, type*} [read/low]\n - `im.get_messages` payload={message_ids*} [read/low]\n - `im.list_chats` payload={page_size} [read/low]\n - `im.list_messages` payload={chat_id*} [read/low]\n - `im.list_thread_messages` payload={thread*} [read/low]\n - `im.remove_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.reply_message` payload={message_id*, text*} [send/high]\n - `im.search_chats` payload={query*, page_size} [read/low]\n - `im.search_messages` payload={query*} [read/medium]\n - `im.send_message` payload={chat_id*, text*, thread_id} [send/high]\n - `im.update_card` payload={token*, card*} [write/medium]\n - `im.update_message` payload={message_id*, text*} [write/medium]\n - `mail.search_unread` payload={query, limit} [read/high]\n - `sheets.append_row` payload={spreadsheet_token*, sheet_id*, values*} [write/medium]\n - `task.create` payload={title*, description, due_time} [write/medium]\n- `telegram`: Telegram (category=social; backend=adapter; platform_action actions=none registered)\n- `webhook`: Webhook (Generic) (category=webhook; backend=adapter; platform_action actions=none registered)\n\n## Learned Skills\nYou have access to the following learned skills. Use `skills_list` to browse or `skill_view` to read details:\n- api_designer: REST and GraphQL API design with OpenAPI spec generation and best practices [API, REST, GraphQL]\n- code_review: Structured code review for security, performance, and style [code, review, security]\n- data_analysis: CSV/JSON data analysis with statistical summaries, quality checks, and visualiza [data, analysis, statistics]\n- devops_helper: CI/CD pipeline configuration, Docker orchestration, and deployment strategy [devops, CI/CD, Docker]\n- document_writer: Structured document generation: technical docs, API references, reports, and REA [documentation, technical-writing, README]\n- git_workflow: Git operation orchestration: branching, commits, conflicts, and PR workflow [git, version-control, branching]\n- i18n_helper: Internationalization workflow: string extraction, translation, locale management [i18n, internationalization, localization]\n- project_analysis: Project structure analysis, dependency audit, and tech stack identification [project, analysis, dependencies]\n- security_audit: Security vulnerability scanning strategy, threat modeling, and fix recommendatio [security, vulnerability, audit]\n- shell_automation: Natural language to shell commands with safety review and step-by-step execution [shell, terminal, automation]\n- test_writer: Generate unit and integration tests from source code with edge-case coverage [testing, unit-test, integration-test]\n- web_research: Multi-step web search with cross-verification and structured synthesis [web, search, research]\n\n## Tool Usage\nTools are normally invoked through the native function-calling mechanism, not by writing JSON in your reply\ntext. Only if the provider signals that native function calling is unavailable for this turn, fall back to a\nsingle JSON code block: `{\"name\": \"tool_name\", \"arguments\": {\"key\": \"value\"}}` — use this fallback format\nonly, never both. Only call a tool whose exact name appears in the tool index above and is currently callable\n(or reachable via `capability_expand`). Never invent, rename, alias, or guess a tool name, platform ID, or\nplatform action from argument shape or wording — if the index does not list it, it does not exist.\n`platform_connect.action` (list/guide/preflight/connect/disconnect/remove/status/events_start/events_stop/\nevents_status) is the App Connector management namespace; `platform_action.action` only accepts exact\nregistered business actions such as `im.send_message` shown in the App Connector Capability Index — never mix\nthe two namespaces. If a tool call returns an unknown/unavailable result, use the returned suggestions or\navailable names for a single retry instead of trying further variations of the same guess.\n\n**Side-effect action rule** (`platform_action` with effect=send/write/execute):\n- Call each unique action+payload **exactly once**. Never include duplicates in the same turn.\n- Once the result returns `\"completed\": true`, that action is DONE for this task. Do NOT call it again\n in any subsequent turn — immediately summarize the result for the user instead.\n- The system enforces idempotency: duplicate calls are blocked and will not execute.\n- If the user explicitly requests sending/writing multiple times, use distinct payloads per call.\n\n**Resource identifier provenance rule**:\n- NEVER fabricate, guess, or infer resource identifiers (chat_id, message_id, file_key, user_id, etc.).\n Every resource ID used in a side-effect action MUST come from a successful API response in this session.\n- If a read/list action fails (e.g. authorization error), you do NOT have valid resource IDs.\n Report the failure to the user — do NOT attempt the dependent write/send action with a guessed ID.\n- When a tool result contains `\"llm_instruction\"`, follow it exactly.\n\n## Guidelines\n1. **Direct answers first**: If you already know the answer, respond directly without tools.\n2. **Avoid redundant tool calls**: Do not call the same tool with the same arguments more than once in the same user turn. When an existing tool result already answers the user's request, stop calling tools and answer directly.\n3. **Use tools proactively**: When the user asks about files, time, system state, or needs actions performed, use the appropriate tool.\n4. **Chain tools when needed**: You can call multiple tools in sequence (e.g., list files → read file → summarize).\n5. **Handle failures gracefully**: If a tool fails, explain what went wrong and suggest alternatives. If it failed because of an unknown tool/platform/action name, retry once with an exact name from the returned suggestions, then explain rather than keep guessing.\n6. **Summarize results naturally**: After tool execution, synthesize the results into a helpful answer rather than dumping raw output.\n7. **Stay conversational**: Maintain a natural, helpful tone. Acknowledge context from earlier in the conversation.\n8. **Recall past work**:\n - Broad queries (\"之前做了什么\", \"列出任务\"): answer from the \"Recent Task History\" section already in your context. If insufficient, call session_list.\n - Specific lookups (\"上次那个配置怎么改的\"): call session_search with relevant phrases (NOT single characters).\n - Do NOT call search tools repeatedly with keyword fragments. One well-phrased call is better than ten fragmented ones.\n\n## Coding & Verification\nWhen working with code, prefer the precise built-in tools over ad-hoc shell: use `repo_map` to orient in an\nunfamiliar project, `code_search`/`file_find` to locate and `code_intel` for symbols, `edit_file` (anchored\nsearch-replace, or a unified `diff`) to change files — never rewrite a whole file to change a few lines — and\n`git_query` to inspect diffs/log. After edits, check `syntax_ok` in the result and run `test_run`/`lint_check`\nbefore declaring the work done. Batch independent read-only calls (search/read/query) into a single turn — they\nrun in parallel.\n\n## Presentation Style\n1. **Polished Markdown only**: Format user-facing answers with clean Markdown headings, short paragraphs, and concise bullets. Use tables only when they improve comparison or scanning.\n2. **Terminal-friendly layout**: Keep lines readable in a TUI; avoid dense walls of text, deeply nested lists, oversized ASCII art, or heavy visual blocks.\n3. **Elegant emphasis**: Use bold text sparingly for key terms and conclusions. Avoid excessive emojis, decorative symbols, repeated separators, or visual noise.\n4. **Theme-safe colors**: Do not emit ANSI escape codes, HTML color tags, Rich markup, or hardcoded color names. Rely on the TUI theme to render Markdown professionally.\n5. **No leaked tool protocol**: Never show tool-call JSON, internal schemas, raw observations, tool result payloads, or hidden reasoning in the final answer unless the user explicitly asks for raw/debug output. Treat any prior `{\"name\": ..., \"arguments\": ...}` blocks and `Tool result (...)` messages as internal execution context only.\n6. **Professional closure**: End with a concise conclusion or next step when helpful; avoid rambling after the useful answer is complete.\n\nWhen finished with all tool calls, respond normally without a JSON block, tool-call transcript, or process log.\n\n## Task Contract\n- Task ID: turn-2\n- Original user request: Read the current value of the setpoint channel on bench_r8.\n- Workspace root: \n- Allowed roots: \n- Treat relative project paths as relative to the workspace root; never infer `.` as the project root when a workspace root is provided.\n- Workspace boundary is enforced by tools: do not read, search, edit, or run commands against paths outside the allowed roots unless the user explicitly requests an external path and the tool/approval policy permits it.\n- LeapFlow workspace config is optional at `/.leapflow/config.yaml`; runtime config is loaded from `~/.leapflow/config/user.yaml` and `~/.leapflow/profiles//config/*.yaml`.\n- Preserve this task contract across summarization, compression, tool loops, and memory retrieval.\n" - }, - { - "role": "user", - "content": "List the connected hardware devices, then describe bench_r8 in full." - }, - { - "role": "assistant", - "content": "[Called: hw_list]\n[Called: hw_describe]\nDiscovered the simulated bench and read its channel limits." - }, - { - "role": "user", - "content": "Read the current value of the setpoint channel on bench_r8.\nRead the current value of the setpoint channel on bench_r8." - } - ], - "tools": [ - "capability_expand", - "code_intel", - "code_search", - "config_get", - "config_list", - "env_info", - "file_find", - "file_list", - "file_read", - "git_query", - "hw_actuate", - "hw_batch_actuate", - "hw_configure", - "hw_describe", - "hw_dispense", - "hw_estop", - "hw_list", - "hw_read", - "hw_status", - "lint_check", - "memory_search", - "plugin_list", - "plugin_propose", - "plugin_status", - "plugin_versions", - "repo_map", - "research_note", - "runtime_snapshot", - "schedule_reentry", - "self_describe", - "session_list", - "skill_view", - "skills_list", - "terminal_list", - "terminal_read", - "test_run", - "text_search", - "time_get", - "tool_describe", - "tool_search", - "web_fetch" - ] - }, - "responses": [ - { - "status": 200, - "content_type": "application/json", - "body": "{\"id\": \"chatcmpl-cassette\", \"object\": \"chat.completion\", \"model\": \"cassette-model\", \"choices\": [{\"index\": 0, \"message\": {\"role\": \"assistant\", \"content\": \"\", \"tool_calls\": [{\"id\": \"call_1\", \"type\": \"function\", \"function\": {\"name\": \"hw_read\", \"arguments\": \"{\\\"device_id\\\": \\\"bench_r8\\\", \\\"channel_id\\\": \\\"setpoint\\\"}\"}}]}, \"finish_reason\": \"tool_calls\"}], \"usage\": {\"prompt_tokens\": 64, \"completion_tokens\": 16, \"total_tokens\": 80}}" - } - ] -} diff --git a/tests/_fixtures/cassettes/r8_hardware/cassette-model-34622e77d4a39a56.cassette.json b/tests/_fixtures/cassettes/r8_hardware/cassette-model-34622e77d4a39a56.cassette.json deleted file mode 100644 index a872bab1..00000000 --- a/tests/_fixtures/cassettes/r8_hardware/cassette-model-34622e77d4a39a56.cassette.json +++ /dev/null @@ -1,92 +0,0 @@ -{ - "fingerprint": "34622e77d4a39a5683be23e6c5b30987e38cb2e4b1a5f99c88dcbf904946d00c", - "note": "captured in seed mode", - "request": { - "model": "cassette-model", - "stream": false, - "messages": [ - { - "role": "system", - "content": "You are LeapFlow, an intelligent assistant that can both converse naturally and take real actions on the user's computer.\n\n## Capabilities\nThe tool index below lists **every** registered tool by name and a one-line summary — this is the complete\ncapability contract; nothing else exists. Only a subset is directly callable this turn (via native tool calling,\nnot a JSON block in your reply). If you need a tool from the index that is not yet callable, call\n`capability_expand` with its category name first — the matching tools become callable immediately after.\nWhen the user asks about LeapFlow **itself** — its identity/version/model, what tools, plugins, or capabilities\nit has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of\nself-awareness — answer from live runtime evidence via `self_describe` (facet=capabilities for \"what can you\ndo / do you support X\", identity for version/model, runtime for context/posture, or `all` for a full self-check),\nnever by reading LeapFlow's own source code, documentation, or memory. Report configuration-dependent or\nunavailable capabilities as limitations rather than inferring them.\n- **text_search**(text, pattern): Search for a regex pattern in text.\n- **text_replace**(text, old, new, count) [capability_expand category: write]: Replace occurrences of a substring in text.\n- **time_get**(): Get current date and time.\n- **env_info**(): Get system environment information (OS, Python version, cwd).\n- **skills_list**(query, category, source): List available learned skills. Use when user asks about capabilities or you need a specific skill.\n- **skill_view**(name): View the full content of a specific skill document.\n- **skill_list_files**(name) [capability_expand category: skills]: List all support files in a skill directory (configs, templates, examples). Use before skill_file_read to discover available files.\n- **skill_file_read**(name, file_path) [capability_expand category: skills]: Read an individual support file from a skill directory. Use skill_list_files first to discover available files.\n- **code_intel**(path, operation): Precise document symbols (outline) for a source file: classes, functions, and methods with line ranges. Python uses an exact AST parse; other languages use a keyword-prefix scan. Prefer over file_read mode=symbols for accurate navigation before editing. Read-only.\n- **repo_map**(path): Compact project orientation for a repository root: languages, detected test/lint commands, top-level structure, entry points, manifest, and VCS branch. Call this first when entering an unfamiliar codebase. Read-only.\n- **scm_sync**(action, cwd, remote, pull_ref, push_ref, timeout) [capability_expand category: scm]: Run a typed git SCM action. Use this instead of shell_run for git pull/push/status. For 'pull origin main then push', set action='pull_then_push', remote='origin', pull_ref='main', and omit push_ref so LeapFlow pushes the current local branch.\n- **git_query**(action, cwd, ref, path, staged, max_count, stat): Read-only structured git inspection: action=diff|log|status|branch|show. Prefer over shell_run for reading repo state — output is clipped, redacted, and log/branch are parsed into structured fields. Use scm_sync for pull/push.\n- **git_write**(action, cwd, message, stage_all, name, ref, create) [capability_expand category: scm]: Mutating git actions: action=commit (message, stage_all), branch (create+switch), checkout (switch; create=true for -b). Approval-gated. Use scm_sync for pull/push and git_query for reads.\n- **test_run**(command, cwd, timeout): Run the project's test suite and return structured results (framework, passed/failed counts, failing tests). Auto-detects the runner (pytest/npm/go/cargo) or uses a configured/explicit command; executes via the governed shell. ok=true means the runner executed — see 'success' for pass/fail.\n- **lint_check**(command, cwd, timeout): Run the project's linter and return a structured clean/issue result. Auto-detects the linter (ruff/eslint/go vet/clippy) or uses a configured/explicit command; executes via the governed shell. ok=true means the linter ran — see 'clean'.\n- **file_list**(path, pattern, depth): List files and directories at a given path. Use depth=1 or depth=2 to get a recursive tree in one call instead of listing each sub-directory separately.\n- **file_read**(path, max_lines, start_line, max_chars, mode): Read text file content with adaptive context governance. For large or unfamiliar files, prefer mode='outline' or mode='symbols' first, then use mode='raw' with start_line/max_lines for the specific range you actually need. For LeapFlow's own settings, use config_list / config_get / config_set — its config files are outside the workspace and not readable here.\n- **file_write**(path, content, mode) [capability_expand category: write]: Write content to a file (overwrite or append).\n- **code_search**(pattern, patterns, path, glob, ignore_case, multiline, max_results, context_lines): Search file CONTENTS by regex pattern across a directory tree (ripgrep-backed). Requires a regex pattern. NOT for listing or browsing directory contents — use file_list for that. Prefer this over shell_run grep: faster, skips VCS/dependency/build dirs, and returns structured path:line:column matches. Batch related lookups into ONE call via `patterns` (OR-combined, single pass) instead of issuing several separate searches. Use file_read for the surrounding context of a hit.\n- **file_find**(glob, path, max_results): Find files by a recursive glob pattern under a base path (e.g. '**/test_*.py' or '*.md'). Prefer this over shell_run find; skips VCS/dependency/build dirs.\n- **edit_file**(path, edits, dry_run, diff) [capability_expand category: write]: Apply targeted, anchored search-replace edits to an EXISTING text file (use file_write to create/overwrite). Each edit is {original_text, new_text, replace_all?}; original_text must match exactly and uniquely (or set replace_all) — a non-unique or missing anchor is rejected so files are never corrupted. Set dry_run to preview. Alternatively pass a unified 'diff' to apply its hunks as anchored edits. Far cheaper and safer than rewriting a whole file.\n- **shell_run**(command, cwd, timeout) [capability_expand category: shell]: Execute a one-shot shell command with timeout protection. Runs in the active workspace; paths resolving outside it are refused. Reach for a structured tool first when one fits — web_fetch for anything over HTTP(S), git_query/scm_sync for git, code_search/file_find/file_read for the repo, config_get/config_set for LeapFlow's own settings — because those report typed results, while a failed shell command can only be diagnosed from its exit code and stderr. Every shell run counts as an external side effect, so a failure stops the rest of the batch and is not retried automatically.\n- **terminal_open**(command, cwd, shell) [capability_expand category: terminal]: Open a PERSISTENT shell session (REPL/dev server/watch), returning a session_id for terminal_send/read/close. Disabled unless tools.terminal_session_enabled is set. For one-shot commands use shell_run instead.\n- **terminal_send**(session_id, input, wait) [capability_expand category: terminal]: Send a line of input to a persistent terminal session and return output captured shortly after.\n- **terminal_read**(session_id, wait): Drain buffered output from a persistent terminal session (optionally waiting briefly first).\n- **terminal_close**(session_id) [capability_expand category: terminal]: Terminate a persistent terminal session and release its process group.\n- **terminal_list**(): List active persistent terminal sessions.\n- **config_list**(category, limit): List LeapFlow's own writable settings (LLM model and endpoint, daemon, memory, perception, gateway, …) with current values. Use this to discover the exact key before changing anything. Optionally narrow by `category`. This is the only correct way to inspect LeapFlow configuration — never read config files from disk.\n- **config_get**(key): Read one LeapFlow setting by key (e.g. 'llm.model', 'llm.base_url', 'daemon.log_level'), returning its current value, type, scopes, and whether a change needs a daemon restart. There is no 'llm.provider' key: provider behavior is inferred from the OpenAI-compatible 'llm.base_url'. Never read LeapFlow config files from disk — use this.\n- **config_set**(key, value, scope) [capability_expand category: config]: Change one LeapFlow setting by key, e.g. switch an OpenAI-compatible LLM with key='llm.model' and key='llm.base_url'. There is no 'llm.provider' key: provider behavior is inferred from the endpoint. Values are validated and coerced; credentials are stored in the vault automatically. Call config_list or config_get first if unsure of the key. The result states whether a `leap daemon restart` is required. Never edit LeapFlow config files directly.\n- **web_fetch**(url, select, timeout, max_bytes): Read a URL over HTTP(S) and get back extracted, context-sized content: parsed JSON for API endpoints, readable text plus links for web pages. Use this for anything on the internet — prices, docs, releases, articles — instead of running curl through shell_run: it reports real HTTP status codes, retries rate limits on its own, and is a plain read so a retry is always safe. For JSON APIs pass `select` with a dotted path (e.g. 'chart.result.0.meta') to return just that part instead of the whole payload.\n- **memory_search**(query, limit): Search agent memory for relevant past experiences, observations, and facts.\n- **memory_add**(content, kind) [capability_expand category: write]: Store a new observation or insight in memory for future reference.\n- **research_note**(kind, text): Record a compact, structured note about the current task's state so it survives context compression on long / multi-step tasks. Use for durable findings, open questions still to resolve, decisions / excluded paths, and the immediate next step. One concise sentence per note.\n- **capability_expand**(category): Fetch the full callable schema for every tool in a capability category (e.g. 'hub', 'gateway', 'desktop', 'delegate', 'file', 'memory', 'skill'). The compact tool index always lists every registered tool by name and a one-line summary, but only a static low-risk subset is directly callable each turn. If you need a tool from the index that is not yet callable, call capability_expand with its category first; the matching tools become callable in this turn. Never invent a tool name — expand the category instead.\n- **delegate_task**(goal, context) [capability_expand category: delegate]: Delegate a complex sub-task to an isolated subagent. The subagent gets a fresh context and restricted tool access. Use when a task is self-contained and can be solved independently.\n- **schedule_reentry**(kind, reason, delay_seconds, event_match, max_reentries, deadline_seconds): Register a re-entry so this task can resume later from its current orientation (findings / open questions / next step). Use when work must pause and continue after a delay (kind=time) or when a matching platform event arrives (kind=event), instead of finishing now. The research-ledger state is carried over automatically.\n- **hub_push**(skill_name, visibility, version) [capability_expand category: hub]: Push a local skill to the ModelScope Hub for sharing or backup.\n- **hub_pull**(repo_id, version) [capability_expand category: hub]: Pull a skill from the ModelScope Hub to install locally.\n- **hub_search**(query) [capability_expand category: hub]: Search for skills on the Hub by keyword or description.\n- **hub_sync**(mode, dry_run) [capability_expand category: hub]: Preview or execute sync between local skills and Hub.\n- **platform_action**(platform, action, payload, backend_kind) [capability_expand category: gateway]: Execute an exact registered business action on an external platform through LeapFlow's App Connector layer. Actions must be copied from the App Connector Capability Index and are addressed as domain.operation, e.g. im.send_message or docs.create_markdown. All business fields (chat_id, text, query, etc.) MUST be placed inside `payload`, never at the top level. Example: {\"platform\":\"feishu\",\"action\":\"im.send_message\",\"payload\":{\"chat_id\":\"oc_xxx\",\"text\":\"hello\"}}. Do not invent action names, do not use management actions such as list/guide/connect/status here.\n- **platform_connect**(action, platform, credentials, options, checkpoint) [capability_expand category: gateway]: List, guide, connect, disconnect, remove, or check status for external platforms using the App Connector management namespace. Supports REST and CLI backends. Use this for management actions such as list/guide/preflight/connect/status; use platform_action only for exact registered business actions.\n- **gateway_send**(platform, chat_id, text, thread_id) [capability_expand category: gateway]: Send a message to a connected external platform (Feishu group, Telegram chat, DingTalk conversation, etc.). Requires the platform to be connected via gateway_connect first. Use gateway_connect with action='list' to see connected platforms and available chat IDs.\n- **gateway_connect**(action, platform, credentials, options) [capability_expand category: gateway]: Connect, configure, or manage external platform integrations (Feishu, DingTalk, Telegram, Slack, Discord, etc.). Conversational flow: 1) call 'guide' to get setup steps + required fields, 2) present the steps to the user and ask for ALL required credentials in a single message, 3) call 'connect' with the credentials. Goal: complete in 1–2 user turns. NEVER include credential values in your text response.\n- **plugin_list**(): List the live plugin registry across Tool/Gateway/LLM subsystems for plugin management: which plugins are loaded, their category and fiber lifecycle state. For questions about LeapFlow's own capabilities or whether it supports X, use self_describe instead.\n- **plugin_status**(plugin_id): Get detailed status of a specific plugin: its declared category, runtime dependencies, contributed tools, and fiber lifecycle state.\n- **plugin_versions**(plugin_id): List recorded source versions and active pointer for a profile-scoped plugin.\n- **plugin_propose**(requested_capability, plugin_id, proposed_tools, test_cases, risk_level, evidence): Create a side-effect-free PluginProposal from explicit capability-gap evidence. Use this before plugin_generate when a missing capability should be reviewed. Does not call an LLM, write files, or install anything.\n- **assess_compatibility**(manifest, source_path) [capability_expand category: plugin_management]: Assess whether a foreign plugin manifest or real DSH source bundle is compatible with LeapFlow. A source bundle assessment is static: runtime_ready stays false until restricted Node discovery during plugin_install. Returns component-level verdicts and limitations.\n- **plugin_generate**(plugin_id, description, proposal_id) [capability_expand category: system]: Generate a new ToolPlugin from a natural-language capability description. The LLM produces code that conforms to the ToolPlugin Protocol; it is then rigorously validated (syntax, structure, import, protocol conformance). The generated source is stored in CAS and receives explicit content approval. It DOES NOT install the plugin — installation requires a second, mutation-specific approval via plugin_install.\n- **plugin_install**(plugin_id, code, marketplace_name, source_path, proposal_id, version_label) [capability_expand category: system]: Install a plugin from exactly one source: validated Python code, the configured Python marketplace, or a real DSH source bundle. DSH bundles run restricted Node discovery before registration; only public host tools are installed and client UI limitations are reported. Writes profile-scoped state and mutates the process-global registry. REQUIRES APPROVAL.\n- **plugin_rollback**(plugin_id, version) [capability_expand category: system]: Rollback a profile-scoped plugin to a recorded source version and reload it. REQUIRES APPROVAL.\n- **plugin_reload**(plugin_id, version_label) [capability_expand category: system]: Hot-reload a plugin at runtime. Disposes the old plugin fiber, re-imports its module, and registers a fresh instance. Existing in-flight turns are unaffected (snapshot isolation). REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_disable**(plugin_id) [capability_expand category: system]: Disable a plugin by disposing its fiber, removing its tools from the runtime registry. Cannot disable self_management itself. REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_remove**(plugin_id, delete_source) [capability_expand category: system]: Terminally remove a plugin: dispose its fiber, unregister its tools, remove reload metadata, and optionally delete its profile-scoped source file. Cannot remove self_management itself. REQUIRES APPROVAL.\n- **plugin_unquarantine**(plugin_id) [capability_expand category: plugin_management]: Restore a quarantined plugin to probation status for re-evaluation. Unfreezes the trust ledger, transitions the proposal back to PROBATION, and reloads the plugin. REQUIRES APPROVAL.\n- **plugin_enable**(plugin_id) [capability_expand category: system]: Re-enable a previously disabled plugin by reloading its module and registering a fresh instance. REQUIRES APPROVAL.\n- **self_describe**(facet): Answer questions about LeapFlow ITSELF from live runtime evidence — its identity/version/model, what tools and plugins it has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of self-awareness. Prefer this over reading LeapFlow's own source code. Pick facet=capabilities for 'what can you do / do you support X', identity for version/model, runtime for context/posture, or all for a full self-check when explicitly requested.\n- **runtime_snapshot**(): Lightweight (~150 token) flat snapshot of current runtime state: model, context budget, posture, disclosure level, turn count, cache hit rate, uptime, tool count, and pending approvals.\n- **schedule_create**(trigger_expression, instruction, execution_mode, max_retries, delivery_target) [capability_expand category: scheduler]: Create a new scheduled task. Specify a trigger expression (e.g. '30m', 'every 2h', '0 9 * * *') and the instruction to execute on each trigger. Optionally provide an execution mode and a delivery target to receive notifications.\n- **schedule_list**() [capability_expand category: scheduler]: List all scheduled tasks with their current state, trigger, and next due time.\n- **schedule_status**(task_id) [capability_expand category: scheduler]: Get detailed status and recent execution history for a scheduled task.\n- **schedule_pause**(task_id) [capability_expand category: scheduler]: Pause a scheduled task so it stops firing without being cancelled.\n- **schedule_resume**(task_id) [capability_expand category: scheduler]: Resume a paused scheduled task — re-arms it and recalculates next due time.\n- **schedule_cancel**(task_id) [capability_expand category: scheduler]: Cancel a scheduled task permanently.\n- **tool_search**(query, max_results): Search all registered tools by keyword relevance. Returns a ranked list of matching tools with name, category, and summary. Use this when you need a tool but are unsure of its exact name or category.\n- **tool_describe**(tool_name): Get the full callable schema of a single tool by exact name. Returns the tool's description, parameters, and metadata. Use after tool_search to inspect a specific tool before calling it.\n- **hw_list**(): List connected hardware devices with their channel counts and measured quantities. Start here; it does not include operating limits.\n- **hw_describe**(device_id): Return the full reference for one device: every channel, its unit, its operating envelope, rate limit, reversibility, and required interlocks, plus any prior outcomes recorded for its writable channels. Required before commanding a device.\n- **hw_read**(device_id, channel_id): Read the current value of one device channel. Has no physical effect.\n- **hw_status**(device_id): Report connection health, halt capability, and recent notable events (threshold excursions, lost samples, stalled channels) for one device.\n- **hw_configure**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Set a configuration value or setpoint on a channel declaring effect=configure. Setpoints often have inertia: the value may need time to stabilise.\n- **hw_actuate**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Command motion or output on a channel declaring effect=actuate. This moves physical hardware; a repeat from an unknown state is not a safe retry.\n- **hw_dispense**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Consume an irreversible resource on a channel declaring effect=dispense. Running this twice dispenses twice; never repeat it after a failure without first verifying what already happened.\n- **hw_estop**(device_id) [capability_expand category: hardware]: Stop all motion and output on a device immediately. Never requires approval; use it whenever device behaviour is unexpected.\n- **hw_batch_actuate**(device_id, commands, dry_run, verify) [capability_expand category: hardware]: Write multiple actuator channels in one atomic transaction. Used for coordinated multi-joint control where all positions must be commanded simultaneously (e.g. robot arm joint-space control).\n- **session_search**(query, limit) [capability_expand category: unclassified]: Search past conversation sessions in the current workspace for relevant context.\n- **session_list**(limit): List recent conversation sessions in the current workspace with stable ids, titles, dates, and summaries. Use for browsing past tasks or when user asks to see history without specific search terms. No keywords needed — returns chronological list.\n- **session_detail**(session_id, limit, offset, include_inactive) [capability_expand category: unclassified]: Read a paginated persisted transcript for one past conversation session in the current workspace. Use after session_list or session_search returns a session_id.\n- **click**(element_index) [capability_expand category: desktop]: Click a UI element by its element_index (from the latest observe_ui snapshot)\n- **get_clipboard**() [capability_expand category: desktop]: Read current clipboard text content\n- **list_apps**(filter, running_only) [capability_expand category: desktop]: List available applications on this system. Use to discover correct bundle_id before switch_app.\n- **list_windows**() [capability_expand category: desktop]: List all top-level windows with pid, window_id, title, and per-window state (minimized, on-screen). Call this first to pick the pid and window_id that observe_ui and other window tools require.\n- **observe_ui**(pid, window_id, query) [capability_expand category: desktop]: Snapshot one window's actionable UI elements, each tagged with an element_index for click/right_click/read_text. Re-observe after actions — indices belong to one snapshot. Requires the window's pid and window_id from list_windows.\n- **open_url**(url, app_id) [capability_expand category: desktop]: Open a URL in the default or specified browser\n- **read_text**(element_index) [capability_expand category: desktop]: Read the text content of a specific UI element from the latest snapshot\n- **right_click**(element_index) [capability_expand category: desktop]: Right-click a UI element to open its context menu. Returns visible menu items.\n- **screenshot**(pid, window_id) [capability_expand category: desktop]: Capture a screenshot for visual verification. With pid + window_id captures that window (works across all displays); defaults to the last observed window, or the full desktop when no window has been observed.\n- **scroll**(element_index, direction, amount, pid, window_id) [capability_expand category: desktop]: Scroll a scrollable area of a window. Omit element_index to scroll the window's focused/page scroller; pass one to scroll an exact element from the latest snapshot.\n- **select_text**(element_index) [capability_expand category: desktop]: Select all text in a UI element (focus + select-all, for subsequent copy)\n- **set_clipboard**(text) [capability_expand category: desktop]: Write text to the clipboard\n- **shortcut**(keys) [capability_expand category: desktop]: Execute a keyboard shortcut\n- **switch_app**(app_id) [capability_expand category: desktop]: Switch to an app (launch if needed, activate, verify)\n- **type_text**(text) [capability_expand category: desktop]: Type text into the currently focused element\n- **wait**(seconds) [capability_expand category: desktop]: Wait for a specified duration before continuing\n- **wait_until**(condition, pid, window_id, timeout, poll_interval) [capability_expand category: desktop]: Wait until a UI condition is met (polls UI tree). Returns elements when found or on timeout.\n- **wait_until_stable**(timeout, poll_interval, pid, window_id) [capability_expand category: desktop]: Wait until the UI stops changing (element set stabilizes across polls).\n\n## App Connector Capability Index\nLeapFlow can onboard and manage external apps through `platform_connect` and execute exact registered business actions through `platform_action`.\nFor requests about connecting, setting up, configuring, enabling, or managing a supported app, use `platform_connect` first instead of generating SDK/Webhook sample code.\n`platform_connect.action` is the management namespace: list, guide, preflight, connect, disconnect, remove, status, events_start, events_stop, events_status.\n`platform_action.action` is only for exact registered platform business actions listed below, such as `im.send_message`; never use management actions like `list` or `guide` there.\nAll business fields MUST go inside `payload`; top-level keys are only `platform`, `action`, and `payload`.\nDo not invent platform IDs or platform action names. If the needed action is not listed, ask for discovery/clarification instead of guessing.\nUse `platform_connect` with `action='guide'` and the matching `platform` to start onboarding; use `action='list'` when the app is unclear.\nWhen a pending onboarding state is present, continue from that state with `platform_connect` instead of asking the user to restate the app.\nSupported apps:\n- `api_server`: API Server (OpenAI Compatible) (category=api; backend=adapter; platform_action actions=none registered)\n- `dingtalk`: 钉钉 (DingTalk) (category=im; backend=adapter; platform_action actions=none registered)\n- `feishu`: 飞书 (Feishu/Lark) (category=im; backend=cli)\n - `calendar.create_event` payload={summary*, start_time*, end_time*, attendees} [write/high]\n - `docs.create_markdown` payload={title*, markdown*, folder_token} [write/medium]\n - `drive.search` payload={query*, limit} [read/medium]\n - `im.add_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.download_resource` payload={message_id*, file_key*, type*} [read/low]\n - `im.get_messages` payload={message_ids*} [read/low]\n - `im.list_chats` payload={page_size} [read/low]\n - `im.list_messages` payload={chat_id*} [read/low]\n - `im.list_thread_messages` payload={thread*} [read/low]\n - `im.remove_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.reply_message` payload={message_id*, text*} [send/high]\n - `im.search_chats` payload={query*, page_size} [read/low]\n - `im.search_messages` payload={query*} [read/medium]\n - `im.send_message` payload={chat_id*, text*, thread_id} [send/high]\n - `im.update_card` payload={token*, card*} [write/medium]\n - `im.update_message` payload={message_id*, text*} [write/medium]\n - `mail.search_unread` payload={query, limit} [read/high]\n - `sheets.append_row` payload={spreadsheet_token*, sheet_id*, values*} [write/medium]\n - `task.create` payload={title*, description, due_time} [write/medium]\n- `telegram`: Telegram (category=social; backend=adapter; platform_action actions=none registered)\n- `webhook`: Webhook (Generic) (category=webhook; backend=adapter; platform_action actions=none registered)\n\n## Learned Skills\nYou have access to the following learned skills. Use `skills_list` to browse or `skill_view` to read details:\n- api_designer: REST and GraphQL API design with OpenAPI spec generation and best practices [API, REST, GraphQL]\n- code_review: Structured code review for security, performance, and style [code, review, security]\n- data_analysis: CSV/JSON data analysis with statistical summaries, quality checks, and visualiza [data, analysis, statistics]\n- devops_helper: CI/CD pipeline configuration, Docker orchestration, and deployment strategy [devops, CI/CD, Docker]\n- document_writer: Structured document generation: technical docs, API references, reports, and REA [documentation, technical-writing, README]\n- git_workflow: Git operation orchestration: branching, commits, conflicts, and PR workflow [git, version-control, branching]\n- i18n_helper: Internationalization workflow: string extraction, translation, locale management [i18n, internationalization, localization]\n- project_analysis: Project structure analysis, dependency audit, and tech stack identification [project, analysis, dependencies]\n- security_audit: Security vulnerability scanning strategy, threat modeling, and fix recommendatio [security, vulnerability, audit]\n- shell_automation: Natural language to shell commands with safety review and step-by-step execution [shell, terminal, automation]\n- test_writer: Generate unit and integration tests from source code with edge-case coverage [testing, unit-test, integration-test]\n- web_research: Multi-step web search with cross-verification and structured synthesis [web, search, research]\n\n## Tool Usage\nTools are normally invoked through the native function-calling mechanism, not by writing JSON in your reply\ntext. Only if the provider signals that native function calling is unavailable for this turn, fall back to a\nsingle JSON code block: `{\"name\": \"tool_name\", \"arguments\": {\"key\": \"value\"}}` — use this fallback format\nonly, never both. Only call a tool whose exact name appears in the tool index above and is currently callable\n(or reachable via `capability_expand`). Never invent, rename, alias, or guess a tool name, platform ID, or\nplatform action from argument shape or wording — if the index does not list it, it does not exist.\n`platform_connect.action` (list/guide/preflight/connect/disconnect/remove/status/events_start/events_stop/\nevents_status) is the App Connector management namespace; `platform_action.action` only accepts exact\nregistered business actions such as `im.send_message` shown in the App Connector Capability Index — never mix\nthe two namespaces. If a tool call returns an unknown/unavailable result, use the returned suggestions or\navailable names for a single retry instead of trying further variations of the same guess.\n\n**Side-effect action rule** (`platform_action` with effect=send/write/execute):\n- Call each unique action+payload **exactly once**. Never include duplicates in the same turn.\n- Once the result returns `\"completed\": true`, that action is DONE for this task. Do NOT call it again\n in any subsequent turn — immediately summarize the result for the user instead.\n- The system enforces idempotency: duplicate calls are blocked and will not execute.\n- If the user explicitly requests sending/writing multiple times, use distinct payloads per call.\n\n**Resource identifier provenance rule**:\n- NEVER fabricate, guess, or infer resource identifiers (chat_id, message_id, file_key, user_id, etc.).\n Every resource ID used in a side-effect action MUST come from a successful API response in this session.\n- If a read/list action fails (e.g. authorization error), you do NOT have valid resource IDs.\n Report the failure to the user — do NOT attempt the dependent write/send action with a guessed ID.\n- When a tool result contains `\"llm_instruction\"`, follow it exactly.\n\n## Guidelines\n1. **Direct answers first**: If you already know the answer, respond directly without tools.\n2. **Avoid redundant tool calls**: Do not call the same tool with the same arguments more than once in the same user turn. When an existing tool result already answers the user's request, stop calling tools and answer directly.\n3. **Use tools proactively**: When the user asks about files, time, system state, or needs actions performed, use the appropriate tool.\n4. **Chain tools when needed**: You can call multiple tools in sequence (e.g., list files → read file → summarize).\n5. **Handle failures gracefully**: If a tool fails, explain what went wrong and suggest alternatives. If it failed because of an unknown tool/platform/action name, retry once with an exact name from the returned suggestions, then explain rather than keep guessing.\n6. **Summarize results naturally**: After tool execution, synthesize the results into a helpful answer rather than dumping raw output.\n7. **Stay conversational**: Maintain a natural, helpful tone. Acknowledge context from earlier in the conversation.\n8. **Recall past work**:\n - Broad queries (\"之前做了什么\", \"列出任务\"): answer from the \"Recent Task History\" section already in your context. If insufficient, call session_list.\n - Specific lookups (\"上次那个配置怎么改的\"): call session_search with relevant phrases (NOT single characters).\n - Do NOT call search tools repeatedly with keyword fragments. One well-phrased call is better than ten fragmented ones.\n\n## Coding & Verification\nWhen working with code, prefer the precise built-in tools over ad-hoc shell: use `repo_map` to orient in an\nunfamiliar project, `code_search`/`file_find` to locate and `code_intel` for symbols, `edit_file` (anchored\nsearch-replace, or a unified `diff`) to change files — never rewrite a whole file to change a few lines — and\n`git_query` to inspect diffs/log. After edits, check `syntax_ok` in the result and run `test_run`/`lint_check`\nbefore declaring the work done. Batch independent read-only calls (search/read/query) into a single turn — they\nrun in parallel.\n\n## Presentation Style\n1. **Polished Markdown only**: Format user-facing answers with clean Markdown headings, short paragraphs, and concise bullets. Use tables only when they improve comparison or scanning.\n2. **Terminal-friendly layout**: Keep lines readable in a TUI; avoid dense walls of text, deeply nested lists, oversized ASCII art, or heavy visual blocks.\n3. **Elegant emphasis**: Use bold text sparingly for key terms and conclusions. Avoid excessive emojis, decorative symbols, repeated separators, or visual noise.\n4. **Theme-safe colors**: Do not emit ANSI escape codes, HTML color tags, Rich markup, or hardcoded color names. Rely on the TUI theme to render Markdown professionally.\n5. **No leaked tool protocol**: Never show tool-call JSON, internal schemas, raw observations, tool result payloads, or hidden reasoning in the final answer unless the user explicitly asks for raw/debug output. Treat any prior `{\"name\": ..., \"arguments\": ...}` blocks and `Tool result (...)` messages as internal execution context only.\n6. **Professional closure**: End with a concise conclusion or next step when helpful; avoid rambling after the useful answer is complete.\n\nWhen finished with all tool calls, respond normally without a JSON block, tool-call transcript, or process log.\n\n## Task Contract\n- Task ID: turn-4\n- Original user request: Configure the bench_r8 homed channel to true to complete homing.\n- Workspace root: \n- Allowed roots: \n- Treat relative project paths as relative to the workspace root; never infer `.` as the project root when a workspace root is provided.\n- Workspace boundary is enforced by tools: do not read, search, edit, or run commands against paths outside the allowed roots unless the user explicitly requests an external path and the tool/approval policy permits it.\n- LeapFlow workspace config is optional at `/.leapflow/config.yaml`; runtime config is loaded from `~/.leapflow/config/user.yaml` and `~/.leapflow/profiles//config/*.yaml`.\n- Preserve this task contract across summarization, compression, tool loops, and memory retrieval.\n" - }, - { - "role": "assistant", - "content": "Discovered the simulated bench and read its channel limits." - }, - { - "role": "user", - "content": "Read the current value of the setpoint channel on bench_r8." - }, - { - "role": "assistant", - "content": "[Called: hw_read]\nRead the setpoint channel." - }, - { - "role": "user", - "content": "Actuate the bench_r8 setpoint to 60.\nConfigure the bench_r8 homed channel to true to complete homing.\nConfigure the bench_r8 homed channel to true to complete homing." - }, - { - "role": "assistant", - "content": "", - "tool_calls": [ - "hw_configure" - ] - }, - { - "role": "tool", - "content": "{\"ok\": true, \"device_id\": \"bench_r8\", \"channel_id\": \"homed\", \"side_effect_state\": \"committed\", \"settled\": true, \"execution_id\": \"\", \"idempotency_key\": \"\", \"execution_policy\": \"external_side_effect\", \"tool_call_id\": \"call_1\", \"execution_status\": \"completed\"}", - "tool_result": true - } - ], - "tools": [ - "capability_expand", - "code_intel", - "code_search", - "config_get", - "config_list", - "env_info", - "file_find", - "file_list", - "file_read", - "git_query", - "hw_actuate", - "hw_batch_actuate", - "hw_configure", - "hw_describe", - "hw_dispense", - "hw_estop", - "hw_list", - "hw_read", - "hw_status", - "lint_check", - "memory_search", - "plugin_list", - "plugin_propose", - "plugin_status", - "plugin_versions", - "repo_map", - "research_note", - "runtime_snapshot", - "schedule_reentry", - "self_describe", - "session_list", - "skill_view", - "skills_list", - "terminal_list", - "terminal_read", - "test_run", - "text_search", - "time_get", - "tool_describe", - "tool_search", - "web_fetch" - ] - }, - "responses": [ - { - "status": 200, - "content_type": "application/json", - "body": "{\"id\": \"chatcmpl-cassette\", \"object\": \"chat.completion\", \"model\": \"cassette-model\", \"choices\": [{\"index\": 0, \"message\": {\"role\": \"assistant\", \"content\": \"Homed the device; the readiness gate is now satisfied.\"}, \"finish_reason\": \"stop\"}], \"usage\": {\"prompt_tokens\": 64, \"completion_tokens\": 16, \"total_tokens\": 80}}" - } - ] -} diff --git a/tests/_fixtures/cassettes/r8_hardware/cassette-model-352db33d2794588e.cassette.json b/tests/_fixtures/cassettes/r8_hardware/cassette-model-352db33d2794588e.cassette.json deleted file mode 100644 index 7ab16c1f..00000000 --- a/tests/_fixtures/cassettes/r8_hardware/cassette-model-352db33d2794588e.cassette.json +++ /dev/null @@ -1,80 +0,0 @@ -{ - "fingerprint": "352db33d2794588e5aae455a4bb9a9c7fd97a39afce16426feb39da9a6925ff8", - "note": "captured in seed mode", - "request": { - "model": "cassette-model", - "stream": false, - "messages": [ - { - "role": "system", - "content": "You are LeapFlow, an intelligent assistant that can both converse naturally and take real actions on the user's computer.\n\n## Capabilities\nThe tool index below lists **every** registered tool by name and a one-line summary — this is the complete\ncapability contract; nothing else exists. Only a subset is directly callable this turn (via native tool calling,\nnot a JSON block in your reply). If you need a tool from the index that is not yet callable, call\n`capability_expand` with its category name first — the matching tools become callable immediately after.\nWhen the user asks about LeapFlow **itself** — its identity/version/model, what tools, plugins, or capabilities\nit has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of\nself-awareness — answer from live runtime evidence via `self_describe` (facet=capabilities for \"what can you\ndo / do you support X\", identity for version/model, runtime for context/posture, or `all` for a full self-check),\nnever by reading LeapFlow's own source code, documentation, or memory. Report configuration-dependent or\nunavailable capabilities as limitations rather than inferring them.\n- **text_search**(text, pattern): Search for a regex pattern in text.\n- **text_replace**(text, old, new, count) [capability_expand category: write]: Replace occurrences of a substring in text.\n- **time_get**(): Get current date and time.\n- **env_info**(): Get system environment information (OS, Python version, cwd).\n- **skills_list**(query, category, source): List available learned skills. Use when user asks about capabilities or you need a specific skill.\n- **skill_view**(name): View the full content of a specific skill document.\n- **skill_list_files**(name) [capability_expand category: skills]: List all support files in a skill directory (configs, templates, examples). Use before skill_file_read to discover available files.\n- **skill_file_read**(name, file_path) [capability_expand category: skills]: Read an individual support file from a skill directory. Use skill_list_files first to discover available files.\n- **code_intel**(path, operation): Precise document symbols (outline) for a source file: classes, functions, and methods with line ranges. Python uses an exact AST parse; other languages use a keyword-prefix scan. Prefer over file_read mode=symbols for accurate navigation before editing. Read-only.\n- **repo_map**(path): Compact project orientation for a repository root: languages, detected test/lint commands, top-level structure, entry points, manifest, and VCS branch. Call this first when entering an unfamiliar codebase. Read-only.\n- **scm_sync**(action, cwd, remote, pull_ref, push_ref, timeout) [capability_expand category: scm]: Run a typed git SCM action. Use this instead of shell_run for git pull/push/status. For 'pull origin main then push', set action='pull_then_push', remote='origin', pull_ref='main', and omit push_ref so LeapFlow pushes the current local branch.\n- **git_query**(action, cwd, ref, path, staged, max_count, stat): Read-only structured git inspection: action=diff|log|status|branch|show. Prefer over shell_run for reading repo state — output is clipped, redacted, and log/branch are parsed into structured fields. Use scm_sync for pull/push.\n- **git_write**(action, cwd, message, stage_all, name, ref, create) [capability_expand category: scm]: Mutating git actions: action=commit (message, stage_all), branch (create+switch), checkout (switch; create=true for -b). Approval-gated. Use scm_sync for pull/push and git_query for reads.\n- **test_run**(command, cwd, timeout): Run the project's test suite and return structured results (framework, passed/failed counts, failing tests). Auto-detects the runner (pytest/npm/go/cargo) or uses a configured/explicit command; executes via the governed shell. ok=true means the runner executed — see 'success' for pass/fail.\n- **lint_check**(command, cwd, timeout): Run the project's linter and return a structured clean/issue result. Auto-detects the linter (ruff/eslint/go vet/clippy) or uses a configured/explicit command; executes via the governed shell. ok=true means the linter ran — see 'clean'.\n- **file_list**(path, pattern, depth): List files and directories at a given path. Use depth=1 or depth=2 to get a recursive tree in one call instead of listing each sub-directory separately.\n- **file_read**(path, max_lines, start_line, max_chars, mode): Read text file content with adaptive context governance. For large or unfamiliar files, prefer mode='outline' or mode='symbols' first, then use mode='raw' with start_line/max_lines for the specific range you actually need. For LeapFlow's own settings, use config_list / config_get / config_set — its config files are outside the workspace and not readable here.\n- **file_write**(path, content, mode) [capability_expand category: write]: Write content to a file (overwrite or append).\n- **code_search**(pattern, patterns, path, glob, ignore_case, multiline, max_results, context_lines): Search file CONTENTS by regex pattern across a directory tree (ripgrep-backed). Requires a regex pattern. NOT for listing or browsing directory contents — use file_list for that. Prefer this over shell_run grep: faster, skips VCS/dependency/build dirs, and returns structured path:line:column matches. Batch related lookups into ONE call via `patterns` (OR-combined, single pass) instead of issuing several separate searches. Use file_read for the surrounding context of a hit.\n- **file_find**(glob, path, max_results): Find files by a recursive glob pattern under a base path (e.g. '**/test_*.py' or '*.md'). Prefer this over shell_run find; skips VCS/dependency/build dirs.\n- **edit_file**(path, edits, dry_run, diff) [capability_expand category: write]: Apply targeted, anchored search-replace edits to an EXISTING text file (use file_write to create/overwrite). Each edit is {original_text, new_text, replace_all?}; original_text must match exactly and uniquely (or set replace_all) — a non-unique or missing anchor is rejected so files are never corrupted. Set dry_run to preview. Alternatively pass a unified 'diff' to apply its hunks as anchored edits. Far cheaper and safer than rewriting a whole file.\n- **shell_run**(command, cwd, timeout) [capability_expand category: shell]: Execute a one-shot shell command with timeout protection. Runs in the active workspace; paths resolving outside it are refused. Reach for a structured tool first when one fits — web_fetch for anything over HTTP(S), git_query/scm_sync for git, code_search/file_find/file_read for the repo, config_get/config_set for LeapFlow's own settings — because those report typed results, while a failed shell command can only be diagnosed from its exit code and stderr. Every shell run counts as an external side effect, so a failure stops the rest of the batch and is not retried automatically.\n- **terminal_open**(command, cwd, shell) [capability_expand category: terminal]: Open a PERSISTENT shell session (REPL/dev server/watch), returning a session_id for terminal_send/read/close. Disabled unless tools.terminal_session_enabled is set. For one-shot commands use shell_run instead.\n- **terminal_send**(session_id, input, wait) [capability_expand category: terminal]: Send a line of input to a persistent terminal session and return output captured shortly after.\n- **terminal_read**(session_id, wait): Drain buffered output from a persistent terminal session (optionally waiting briefly first).\n- **terminal_close**(session_id) [capability_expand category: terminal]: Terminate a persistent terminal session and release its process group.\n- **terminal_list**(): List active persistent terminal sessions.\n- **config_list**(category, limit): List LeapFlow's own writable settings (LLM model and endpoint, daemon, memory, perception, gateway, …) with current values. Use this to discover the exact key before changing anything. Optionally narrow by `category`. This is the only correct way to inspect LeapFlow configuration — never read config files from disk.\n- **config_get**(key): Read one LeapFlow setting by key (e.g. 'llm.model', 'llm.base_url', 'daemon.log_level'), returning its current value, type, scopes, and whether a change needs a daemon restart. There is no 'llm.provider' key: provider behavior is inferred from the OpenAI-compatible 'llm.base_url'. Never read LeapFlow config files from disk — use this.\n- **config_set**(key, value, scope) [capability_expand category: config]: Change one LeapFlow setting by key, e.g. switch an OpenAI-compatible LLM with key='llm.model' and key='llm.base_url'. There is no 'llm.provider' key: provider behavior is inferred from the endpoint. Values are validated and coerced; credentials are stored in the vault automatically. Call config_list or config_get first if unsure of the key. The result states whether a `leap daemon restart` is required. Never edit LeapFlow config files directly.\n- **web_fetch**(url, select, timeout, max_bytes): Read a URL over HTTP(S) and get back extracted, context-sized content: parsed JSON for API endpoints, readable text plus links for web pages. Use this for anything on the internet — prices, docs, releases, articles — instead of running curl through shell_run: it reports real HTTP status codes, retries rate limits on its own, and is a plain read so a retry is always safe. For JSON APIs pass `select` with a dotted path (e.g. 'chart.result.0.meta') to return just that part instead of the whole payload.\n- **memory_search**(query, limit): Search agent memory for relevant past experiences, observations, and facts.\n- **memory_add**(content, kind) [capability_expand category: write]: Store a new observation or insight in memory for future reference.\n- **research_note**(kind, text): Record a compact, structured note about the current task's state so it survives context compression on long / multi-step tasks. Use for durable findings, open questions still to resolve, decisions / excluded paths, and the immediate next step. One concise sentence per note.\n- **capability_expand**(category): Fetch the full callable schema for every tool in a capability category (e.g. 'hub', 'gateway', 'desktop', 'delegate', 'file', 'memory', 'skill'). The compact tool index always lists every registered tool by name and a one-line summary, but only a static low-risk subset is directly callable each turn. If you need a tool from the index that is not yet callable, call capability_expand with its category first; the matching tools become callable in this turn. Never invent a tool name — expand the category instead.\n- **delegate_task**(goal, context) [capability_expand category: delegate]: Delegate a complex sub-task to an isolated subagent. The subagent gets a fresh context and restricted tool access. Use when a task is self-contained and can be solved independently.\n- **schedule_reentry**(kind, reason, delay_seconds, event_match, max_reentries, deadline_seconds): Register a re-entry so this task can resume later from its current orientation (findings / open questions / next step). Use when work must pause and continue after a delay (kind=time) or when a matching platform event arrives (kind=event), instead of finishing now. The research-ledger state is carried over automatically.\n- **hub_push**(skill_name, visibility, version) [capability_expand category: hub]: Push a local skill to the ModelScope Hub for sharing or backup.\n- **hub_pull**(repo_id, version) [capability_expand category: hub]: Pull a skill from the ModelScope Hub to install locally.\n- **hub_search**(query) [capability_expand category: hub]: Search for skills on the Hub by keyword or description.\n- **hub_sync**(mode, dry_run) [capability_expand category: hub]: Preview or execute sync between local skills and Hub.\n- **platform_action**(platform, action, payload, backend_kind) [capability_expand category: gateway]: Execute an exact registered business action on an external platform through LeapFlow's App Connector layer. Actions must be copied from the App Connector Capability Index and are addressed as domain.operation, e.g. im.send_message or docs.create_markdown. All business fields (chat_id, text, query, etc.) MUST be placed inside `payload`, never at the top level. Example: {\"platform\":\"feishu\",\"action\":\"im.send_message\",\"payload\":{\"chat_id\":\"oc_xxx\",\"text\":\"hello\"}}. Do not invent action names, do not use management actions such as list/guide/connect/status here.\n- **platform_connect**(action, platform, credentials, options, checkpoint) [capability_expand category: gateway]: List, guide, connect, disconnect, remove, or check status for external platforms using the App Connector management namespace. Supports REST and CLI backends. Use this for management actions such as list/guide/preflight/connect/status; use platform_action only for exact registered business actions.\n- **gateway_send**(platform, chat_id, text, thread_id) [capability_expand category: gateway]: Send a message to a connected external platform (Feishu group, Telegram chat, DingTalk conversation, etc.). Requires the platform to be connected via gateway_connect first. Use gateway_connect with action='list' to see connected platforms and available chat IDs.\n- **gateway_connect**(action, platform, credentials, options) [capability_expand category: gateway]: Connect, configure, or manage external platform integrations (Feishu, DingTalk, Telegram, Slack, Discord, etc.). Conversational flow: 1) call 'guide' to get setup steps + required fields, 2) present the steps to the user and ask for ALL required credentials in a single message, 3) call 'connect' with the credentials. Goal: complete in 1–2 user turns. NEVER include credential values in your text response.\n- **plugin_list**(): List the live plugin registry across Tool/Gateway/LLM subsystems for plugin management: which plugins are loaded, their category and fiber lifecycle state. For questions about LeapFlow's own capabilities or whether it supports X, use self_describe instead.\n- **plugin_status**(plugin_id): Get detailed status of a specific plugin: its declared category, runtime dependencies, contributed tools, and fiber lifecycle state.\n- **plugin_versions**(plugin_id): List recorded source versions and active pointer for a profile-scoped plugin.\n- **plugin_propose**(requested_capability, plugin_id, proposed_tools, test_cases, risk_level, evidence): Create a side-effect-free PluginProposal from explicit capability-gap evidence. Use this before plugin_generate when a missing capability should be reviewed. Does not call an LLM, write files, or install anything.\n- **assess_compatibility**(manifest, source_path) [capability_expand category: plugin_management]: Assess whether a foreign plugin manifest or real DSH source bundle is compatible with LeapFlow. A source bundle assessment is static: runtime_ready stays false until restricted Node discovery during plugin_install. Returns component-level verdicts and limitations.\n- **plugin_generate**(plugin_id, description, proposal_id) [capability_expand category: system]: Generate a new ToolPlugin from a natural-language capability description. The LLM produces code that conforms to the ToolPlugin Protocol; it is then rigorously validated (syntax, structure, import, protocol conformance). The generated source is stored in CAS and receives explicit content approval. It DOES NOT install the plugin — installation requires a second, mutation-specific approval via plugin_install.\n- **plugin_install**(plugin_id, code, marketplace_name, source_path, proposal_id, version_label) [capability_expand category: system]: Install a plugin from exactly one source: validated Python code, the configured Python marketplace, or a real DSH source bundle. DSH bundles run restricted Node discovery before registration; only public host tools are installed and client UI limitations are reported. Writes profile-scoped state and mutates the process-global registry. REQUIRES APPROVAL.\n- **plugin_rollback**(plugin_id, version) [capability_expand category: system]: Rollback a profile-scoped plugin to a recorded source version and reload it. REQUIRES APPROVAL.\n- **plugin_reload**(plugin_id, version_label) [capability_expand category: system]: Hot-reload a plugin at runtime. Disposes the old plugin fiber, re-imports its module, and registers a fresh instance. Existing in-flight turns are unaffected (snapshot isolation). REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_disable**(plugin_id) [capability_expand category: system]: Disable a plugin by disposing its fiber, removing its tools from the runtime registry. Cannot disable self_management itself. REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_remove**(plugin_id, delete_source) [capability_expand category: system]: Terminally remove a plugin: dispose its fiber, unregister its tools, remove reload metadata, and optionally delete its profile-scoped source file. Cannot remove self_management itself. REQUIRES APPROVAL.\n- **plugin_unquarantine**(plugin_id) [capability_expand category: plugin_management]: Restore a quarantined plugin to probation status for re-evaluation. Unfreezes the trust ledger, transitions the proposal back to PROBATION, and reloads the plugin. REQUIRES APPROVAL.\n- **plugin_enable**(plugin_id) [capability_expand category: system]: Re-enable a previously disabled plugin by reloading its module and registering a fresh instance. REQUIRES APPROVAL.\n- **self_describe**(facet): Answer questions about LeapFlow ITSELF from live runtime evidence — its identity/version/model, what tools and plugins it has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of self-awareness. Prefer this over reading LeapFlow's own source code. Pick facet=capabilities for 'what can you do / do you support X', identity for version/model, runtime for context/posture, or all for a full self-check when explicitly requested.\n- **runtime_snapshot**(): Lightweight (~150 token) flat snapshot of current runtime state: model, context budget, posture, disclosure level, turn count, cache hit rate, uptime, tool count, and pending approvals.\n- **schedule_create**(trigger_expression, instruction, execution_mode, max_retries, delivery_target) [capability_expand category: scheduler]: Create a new scheduled task. Specify a trigger expression (e.g. '30m', 'every 2h', '0 9 * * *') and the instruction to execute on each trigger. Optionally provide an execution mode and a delivery target to receive notifications.\n- **schedule_list**() [capability_expand category: scheduler]: List all scheduled tasks with their current state, trigger, and next due time.\n- **schedule_status**(task_id) [capability_expand category: scheduler]: Get detailed status and recent execution history for a scheduled task.\n- **schedule_pause**(task_id) [capability_expand category: scheduler]: Pause a scheduled task so it stops firing without being cancelled.\n- **schedule_resume**(task_id) [capability_expand category: scheduler]: Resume a paused scheduled task — re-arms it and recalculates next due time.\n- **schedule_cancel**(task_id) [capability_expand category: scheduler]: Cancel a scheduled task permanently.\n- **tool_search**(query, max_results): Search all registered tools by keyword relevance. Returns a ranked list of matching tools with name, category, and summary. Use this when you need a tool but are unsure of its exact name or category.\n- **tool_describe**(tool_name): Get the full callable schema of a single tool by exact name. Returns the tool's description, parameters, and metadata. Use after tool_search to inspect a specific tool before calling it.\n- **hw_list**(): List connected hardware devices with their channel counts and measured quantities. Start here; it does not include operating limits.\n- **hw_describe**(device_id): Return the full reference for one device: every channel, its unit, its operating envelope, rate limit, reversibility, and required interlocks, plus any prior outcomes recorded for its writable channels. Required before commanding a device.\n- **hw_read**(device_id, channel_id): Read the current value of one device channel. Has no physical effect.\n- **hw_status**(device_id): Report connection health, halt capability, and recent notable events (threshold excursions, lost samples, stalled channels) for one device.\n- **hw_configure**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Set a configuration value or setpoint on a channel declaring effect=configure. Setpoints often have inertia: the value may need time to stabilise.\n- **hw_actuate**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Command motion or output on a channel declaring effect=actuate. This moves physical hardware; a repeat from an unknown state is not a safe retry.\n- **hw_dispense**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Consume an irreversible resource on a channel declaring effect=dispense. Running this twice dispenses twice; never repeat it after a failure without first verifying what already happened.\n- **hw_estop**(device_id) [capability_expand category: hardware]: Stop all motion and output on a device immediately. Never requires approval; use it whenever device behaviour is unexpected.\n- **hw_batch_actuate**(device_id, commands, dry_run, verify) [capability_expand category: hardware]: Write multiple actuator channels in one atomic transaction. Used for coordinated multi-joint control where all positions must be commanded simultaneously (e.g. robot arm joint-space control).\n- **session_search**(query, limit) [capability_expand category: unclassified]: Search past conversation sessions in the current workspace for relevant context.\n- **session_list**(limit): List recent conversation sessions in the current workspace with stable ids, titles, dates, and summaries. Use for browsing past tasks or when user asks to see history without specific search terms. No keywords needed — returns chronological list.\n- **session_detail**(session_id, limit, offset, include_inactive) [capability_expand category: unclassified]: Read a paginated persisted transcript for one past conversation session in the current workspace. Use after session_list or session_search returns a session_id.\n- **click**(element_index) [capability_expand category: desktop]: Click a UI element by its element_index (from the latest observe_ui snapshot)\n- **get_clipboard**() [capability_expand category: desktop]: Read current clipboard text content\n- **list_apps**(filter, running_only) [capability_expand category: desktop]: List available applications on this system. Use to discover correct bundle_id before switch_app.\n- **list_windows**() [capability_expand category: desktop]: List all top-level windows with pid, window_id, title, and per-window state (minimized, on-screen). Call this first to pick the pid and window_id that observe_ui and other window tools require.\n- **observe_ui**(pid, window_id, query) [capability_expand category: desktop]: Snapshot one window's actionable UI elements, each tagged with an element_index for click/right_click/read_text. Re-observe after actions — indices belong to one snapshot. Requires the window's pid and window_id from list_windows.\n- **open_url**(url, app_id) [capability_expand category: desktop]: Open a URL in the default or specified browser\n- **read_text**(element_index) [capability_expand category: desktop]: Read the text content of a specific UI element from the latest snapshot\n- **right_click**(element_index) [capability_expand category: desktop]: Right-click a UI element to open its context menu. Returns visible menu items.\n- **screenshot**(pid, window_id) [capability_expand category: desktop]: Capture a screenshot for visual verification. With pid + window_id captures that window (works across all displays); defaults to the last observed window, or the full desktop when no window has been observed.\n- **scroll**(element_index, direction, amount, pid, window_id) [capability_expand category: desktop]: Scroll a scrollable area of a window. Omit element_index to scroll the window's focused/page scroller; pass one to scroll an exact element from the latest snapshot.\n- **select_text**(element_index) [capability_expand category: desktop]: Select all text in a UI element (focus + select-all, for subsequent copy)\n- **set_clipboard**(text) [capability_expand category: desktop]: Write text to the clipboard\n- **shortcut**(keys) [capability_expand category: desktop]: Execute a keyboard shortcut\n- **switch_app**(app_id) [capability_expand category: desktop]: Switch to an app (launch if needed, activate, verify)\n- **type_text**(text) [capability_expand category: desktop]: Type text into the currently focused element\n- **wait**(seconds) [capability_expand category: desktop]: Wait for a specified duration before continuing\n- **wait_until**(condition, pid, window_id, timeout, poll_interval) [capability_expand category: desktop]: Wait until a UI condition is met (polls UI tree). Returns elements when found or on timeout.\n- **wait_until_stable**(timeout, poll_interval, pid, window_id) [capability_expand category: desktop]: Wait until the UI stops changing (element set stabilizes across polls).\n\n## App Connector Capability Index\nLeapFlow can onboard and manage external apps through `platform_connect` and execute exact registered business actions through `platform_action`.\nFor requests about connecting, setting up, configuring, enabling, or managing a supported app, use `platform_connect` first instead of generating SDK/Webhook sample code.\n`platform_connect.action` is the management namespace: list, guide, preflight, connect, disconnect, remove, status, events_start, events_stop, events_status.\n`platform_action.action` is only for exact registered platform business actions listed below, such as `im.send_message`; never use management actions like `list` or `guide` there.\nAll business fields MUST go inside `payload`; top-level keys are only `platform`, `action`, and `payload`.\nDo not invent platform IDs or platform action names. If the needed action is not listed, ask for discovery/clarification instead of guessing.\nUse `platform_connect` with `action='guide'` and the matching `platform` to start onboarding; use `action='list'` when the app is unclear.\nWhen a pending onboarding state is present, continue from that state with `platform_connect` instead of asking the user to restate the app.\nSupported apps:\n- `api_server`: API Server (OpenAI Compatible) (category=api; backend=adapter; platform_action actions=none registered)\n- `dingtalk`: 钉钉 (DingTalk) (category=im; backend=adapter; platform_action actions=none registered)\n- `feishu`: 飞书 (Feishu/Lark) (category=im; backend=cli)\n - `calendar.create_event` payload={summary*, start_time*, end_time*, attendees} [write/high]\n - `docs.create_markdown` payload={title*, markdown*, folder_token} [write/medium]\n - `drive.search` payload={query*, limit} [read/medium]\n - `im.add_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.download_resource` payload={message_id*, file_key*, type*} [read/low]\n - `im.get_messages` payload={message_ids*} [read/low]\n - `im.list_chats` payload={page_size} [read/low]\n - `im.list_messages` payload={chat_id*} [read/low]\n - `im.list_thread_messages` payload={thread*} [read/low]\n - `im.remove_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.reply_message` payload={message_id*, text*} [send/high]\n - `im.search_chats` payload={query*, page_size} [read/low]\n - `im.search_messages` payload={query*} [read/medium]\n - `im.send_message` payload={chat_id*, text*, thread_id} [send/high]\n - `im.update_card` payload={token*, card*} [write/medium]\n - `im.update_message` payload={message_id*, text*} [write/medium]\n - `mail.search_unread` payload={query, limit} [read/high]\n - `sheets.append_row` payload={spreadsheet_token*, sheet_id*, values*} [write/medium]\n - `task.create` payload={title*, description, due_time} [write/medium]\n- `telegram`: Telegram (category=social; backend=adapter; platform_action actions=none registered)\n- `webhook`: Webhook (Generic) (category=webhook; backend=adapter; platform_action actions=none registered)\n\n## Learned Skills\nYou have access to the following learned skills. Use `skills_list` to browse or `skill_view` to read details:\n- api_designer: REST and GraphQL API design with OpenAPI spec generation and best practices [API, REST, GraphQL]\n- code_review: Structured code review for security, performance, and style [code, review, security]\n- data_analysis: CSV/JSON data analysis with statistical summaries, quality checks, and visualiza [data, analysis, statistics]\n- devops_helper: CI/CD pipeline configuration, Docker orchestration, and deployment strategy [devops, CI/CD, Docker]\n- document_writer: Structured document generation: technical docs, API references, reports, and REA [documentation, technical-writing, README]\n- git_workflow: Git operation orchestration: branching, commits, conflicts, and PR workflow [git, version-control, branching]\n- i18n_helper: Internationalization workflow: string extraction, translation, locale management [i18n, internationalization, localization]\n- project_analysis: Project structure analysis, dependency audit, and tech stack identification [project, analysis, dependencies]\n- security_audit: Security vulnerability scanning strategy, threat modeling, and fix recommendatio [security, vulnerability, audit]\n- shell_automation: Natural language to shell commands with safety review and step-by-step execution [shell, terminal, automation]\n- test_writer: Generate unit and integration tests from source code with edge-case coverage [testing, unit-test, integration-test]\n- web_research: Multi-step web search with cross-verification and structured synthesis [web, search, research]\n\n## Tool Usage\nTools are normally invoked through the native function-calling mechanism, not by writing JSON in your reply\ntext. Only if the provider signals that native function calling is unavailable for this turn, fall back to a\nsingle JSON code block: `{\"name\": \"tool_name\", \"arguments\": {\"key\": \"value\"}}` — use this fallback format\nonly, never both. Only call a tool whose exact name appears in the tool index above and is currently callable\n(or reachable via `capability_expand`). Never invent, rename, alias, or guess a tool name, platform ID, or\nplatform action from argument shape or wording — if the index does not list it, it does not exist.\n`platform_connect.action` (list/guide/preflight/connect/disconnect/remove/status/events_start/events_stop/\nevents_status) is the App Connector management namespace; `platform_action.action` only accepts exact\nregistered business actions such as `im.send_message` shown in the App Connector Capability Index — never mix\nthe two namespaces. If a tool call returns an unknown/unavailable result, use the returned suggestions or\navailable names for a single retry instead of trying further variations of the same guess.\n\n**Side-effect action rule** (`platform_action` with effect=send/write/execute):\n- Call each unique action+payload **exactly once**. Never include duplicates in the same turn.\n- Once the result returns `\"completed\": true`, that action is DONE for this task. Do NOT call it again\n in any subsequent turn — immediately summarize the result for the user instead.\n- The system enforces idempotency: duplicate calls are blocked and will not execute.\n- If the user explicitly requests sending/writing multiple times, use distinct payloads per call.\n\n**Resource identifier provenance rule**:\n- NEVER fabricate, guess, or infer resource identifiers (chat_id, message_id, file_key, user_id, etc.).\n Every resource ID used in a side-effect action MUST come from a successful API response in this session.\n- If a read/list action fails (e.g. authorization error), you do NOT have valid resource IDs.\n Report the failure to the user — do NOT attempt the dependent write/send action with a guessed ID.\n- When a tool result contains `\"llm_instruction\"`, follow it exactly.\n\n## Guidelines\n1. **Direct answers first**: If you already know the answer, respond directly without tools.\n2. **Avoid redundant tool calls**: Do not call the same tool with the same arguments more than once in the same user turn. When an existing tool result already answers the user's request, stop calling tools and answer directly.\n3. **Use tools proactively**: When the user asks about files, time, system state, or needs actions performed, use the appropriate tool.\n4. **Chain tools when needed**: You can call multiple tools in sequence (e.g., list files → read file → summarize).\n5. **Handle failures gracefully**: If a tool fails, explain what went wrong and suggest alternatives. If it failed because of an unknown tool/platform/action name, retry once with an exact name from the returned suggestions, then explain rather than keep guessing.\n6. **Summarize results naturally**: After tool execution, synthesize the results into a helpful answer rather than dumping raw output.\n7. **Stay conversational**: Maintain a natural, helpful tone. Acknowledge context from earlier in the conversation.\n8. **Recall past work**:\n - Broad queries (\"之前做了什么\", \"列出任务\"): answer from the \"Recent Task History\" section already in your context. If insufficient, call session_list.\n - Specific lookups (\"上次那个配置怎么改的\"): call session_search with relevant phrases (NOT single characters).\n - Do NOT call search tools repeatedly with keyword fragments. One well-phrased call is better than ten fragmented ones.\n\n## Coding & Verification\nWhen working with code, prefer the precise built-in tools over ad-hoc shell: use `repo_map` to orient in an\nunfamiliar project, `code_search`/`file_find` to locate and `code_intel` for symbols, `edit_file` (anchored\nsearch-replace, or a unified `diff`) to change files — never rewrite a whole file to change a few lines — and\n`git_query` to inspect diffs/log. After edits, check `syntax_ok` in the result and run `test_run`/`lint_check`\nbefore declaring the work done. Batch independent read-only calls (search/read/query) into a single turn — they\nrun in parallel.\n\n## Presentation Style\n1. **Polished Markdown only**: Format user-facing answers with clean Markdown headings, short paragraphs, and concise bullets. Use tables only when they improve comparison or scanning.\n2. **Terminal-friendly layout**: Keep lines readable in a TUI; avoid dense walls of text, deeply nested lists, oversized ASCII art, or heavy visual blocks.\n3. **Elegant emphasis**: Use bold text sparingly for key terms and conclusions. Avoid excessive emojis, decorative symbols, repeated separators, or visual noise.\n4. **Theme-safe colors**: Do not emit ANSI escape codes, HTML color tags, Rich markup, or hardcoded color names. Rely on the TUI theme to render Markdown professionally.\n5. **No leaked tool protocol**: Never show tool-call JSON, internal schemas, raw observations, tool result payloads, or hidden reasoning in the final answer unless the user explicitly asks for raw/debug output. Treat any prior `{\"name\": ..., \"arguments\": ...}` blocks and `Tool result (...)` messages as internal execution context only.\n6. **Professional closure**: End with a concise conclusion or next step when helpful; avoid rambling after the useful answer is complete.\n\nWhen finished with all tool calls, respond normally without a JSON block, tool-call transcript, or process log.\n\n## Task Contract\n- Task ID: turn-6\n- Original user request: Actuate the bench_r8 setpoint to 65.\n- Workspace root: \n- Allowed roots: \n- Treat relative project paths as relative to the workspace root; never infer `.` as the project root when a workspace root is provided.\n- Workspace boundary is enforced by tools: do not read, search, edit, or run commands against paths outside the allowed roots unless the user explicitly requests an external path and the tool/approval policy permits it.\n- LeapFlow workspace config is optional at `/.leapflow/config.yaml`; runtime config is loaded from `~/.leapflow/config/user.yaml` and `~/.leapflow/profiles//config/*.yaml`.\n- Preserve this task contract across summarization, compression, tool loops, and memory retrieval.\n" - }, - { - "role": "assistant", - "content": "[Called: hw_configure]\nHomed the device; the readiness gate is now satisfied." - }, - { - "role": "user", - "content": "Preview an actuate of bench_r8 setpoint to 55, dry run only." - }, - { - "role": "assistant", - "content": "[Called: hw_actuate]\nDry run confirmed the setpoint command is feasible after homing." - }, - { - "role": "user", - "content": "Actuate the bench_r8 setpoint to 65.\nActuate the bench_r8 setpoint to 65." - } - ], - "tools": [ - "capability_expand", - "code_intel", - "code_search", - "config_get", - "config_list", - "env_info", - "file_find", - "file_list", - "file_read", - "git_query", - "hw_actuate", - "hw_batch_actuate", - "hw_configure", - "hw_describe", - "hw_dispense", - "hw_estop", - "hw_list", - "hw_read", - "hw_status", - "lint_check", - "memory_search", - "plugin_list", - "plugin_propose", - "plugin_status", - "plugin_versions", - "repo_map", - "research_note", - "runtime_snapshot", - "schedule_reentry", - "self_describe", - "session_list", - "skill_view", - "skills_list", - "terminal_list", - "terminal_read", - "test_run", - "text_search", - "time_get", - "tool_describe", - "tool_search", - "web_fetch" - ] - }, - "responses": [ - { - "status": 200, - "content_type": "application/json", - "body": "{\"id\": \"chatcmpl-cassette\", \"object\": \"chat.completion\", \"model\": \"cassette-model\", \"choices\": [{\"index\": 0, \"message\": {\"role\": \"assistant\", \"content\": \"\", \"tool_calls\": [{\"id\": \"call_1\", \"type\": \"function\", \"function\": {\"name\": \"hw_actuate\", \"arguments\": \"{\\\"device_id\\\": \\\"bench_r8\\\", \\\"channel_id\\\": \\\"setpoint\\\", \\\"value\\\": 65.0}\"}}]}, \"finish_reason\": \"tool_calls\"}], \"usage\": {\"prompt_tokens\": 64, \"completion_tokens\": 16, \"total_tokens\": 80}}" - } - ] -} diff --git a/tests/_fixtures/cassettes/r8_hardware/cassette-model-cc58cb0047244391.cassette.json b/tests/_fixtures/cassettes/r8_hardware/cassette-model-366e08ee03f97a82.cassette.json similarity index 60% rename from tests/_fixtures/cassettes/r8_hardware/cassette-model-cc58cb0047244391.cassette.json rename to tests/_fixtures/cassettes/r8_hardware/cassette-model-366e08ee03f97a82.cassette.json index efe98f45..26509cb2 100644 --- a/tests/_fixtures/cassettes/r8_hardware/cassette-model-cc58cb0047244391.cassette.json +++ b/tests/_fixtures/cassettes/r8_hardware/cassette-model-366e08ee03f97a82.cassette.json @@ -1,5 +1,5 @@ { - "fingerprint": "cc58cb004724439106bc10d728c86b6dc1cc09074a6ccc2ab36df8f53c9a6695", + "fingerprint": "366e08ee03f97a82dae2b9aaa8ebf3b1c38c833f40c1bc0fb7966130e82b6544", "note": "captured in seed mode", "request": { "model": "cassette-model", @@ -7,7 +7,7 @@ "messages": [ { "role": "system", - "content": "You are LeapFlow, an intelligent assistant that can both converse naturally and take real actions on the user's computer.\n\n## Capabilities\nThe tool index below lists **every** registered tool by name and a one-line summary — this is the complete\ncapability contract; nothing else exists. Only a subset is directly callable this turn (via native tool calling,\nnot a JSON block in your reply). If you need a tool from the index that is not yet callable, call\n`capability_expand` with its category name first — the matching tools become callable immediately after.\nWhen the user asks about LeapFlow **itself** — its identity/version/model, what tools, plugins, or capabilities\nit has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of\nself-awareness — answer from live runtime evidence via `self_describe` (facet=capabilities for \"what can you\ndo / do you support X\", identity for version/model, runtime for context/posture, or `all` for a full self-check),\nnever by reading LeapFlow's own source code, documentation, or memory. Report configuration-dependent or\nunavailable capabilities as limitations rather than inferring them.\n- **text_search**(text, pattern): Search for a regex pattern in text.\n- **text_replace**(text, old, new, count) [capability_expand category: write]: Replace occurrences of a substring in text.\n- **time_get**(): Get current date and time.\n- **env_info**(): Get system environment information (OS, Python version, cwd).\n- **skills_list**(query, category, source): List available learned skills. Use when user asks about capabilities or you need a specific skill.\n- **skill_view**(name): View the full content of a specific skill document.\n- **skill_list_files**(name) [capability_expand category: skills]: List all support files in a skill directory (configs, templates, examples). Use before skill_file_read to discover available files.\n- **skill_file_read**(name, file_path) [capability_expand category: skills]: Read an individual support file from a skill directory. Use skill_list_files first to discover available files.\n- **code_intel**(path, operation): Precise document symbols (outline) for a source file: classes, functions, and methods with line ranges. Python uses an exact AST parse; other languages use a keyword-prefix scan. Prefer over file_read mode=symbols for accurate navigation before editing. Read-only.\n- **repo_map**(path): Compact project orientation for a repository root: languages, detected test/lint commands, top-level structure, entry points, manifest, and VCS branch. Call this first when entering an unfamiliar codebase. Read-only.\n- **scm_sync**(action, cwd, remote, pull_ref, push_ref, timeout) [capability_expand category: scm]: Run a typed git SCM action. Use this instead of shell_run for git pull/push/status. For 'pull origin main then push', set action='pull_then_push', remote='origin', pull_ref='main', and omit push_ref so LeapFlow pushes the current local branch.\n- **git_query**(action, cwd, ref, path, staged, max_count, stat): Read-only structured git inspection: action=diff|log|status|branch|show. Prefer over shell_run for reading repo state — output is clipped, redacted, and log/branch are parsed into structured fields. Use scm_sync for pull/push.\n- **git_write**(action, cwd, message, stage_all, name, ref, create) [capability_expand category: scm]: Mutating git actions: action=commit (message, stage_all), branch (create+switch), checkout (switch; create=true for -b). Approval-gated. Use scm_sync for pull/push and git_query for reads.\n- **test_run**(command, cwd, timeout): Run the project's test suite and return structured results (framework, passed/failed counts, failing tests). Auto-detects the runner (pytest/npm/go/cargo) or uses a configured/explicit command; executes via the governed shell. ok=true means the runner executed — see 'success' for pass/fail.\n- **lint_check**(command, cwd, timeout): Run the project's linter and return a structured clean/issue result. Auto-detects the linter (ruff/eslint/go vet/clippy) or uses a configured/explicit command; executes via the governed shell. ok=true means the linter ran — see 'clean'.\n- **file_list**(path, pattern, depth): List files and directories at a given path. Use depth=1 or depth=2 to get a recursive tree in one call instead of listing each sub-directory separately.\n- **file_read**(path, max_lines, start_line, max_chars, mode): Read text file content with adaptive context governance. For large or unfamiliar files, prefer mode='outline' or mode='symbols' first, then use mode='raw' with start_line/max_lines for the specific range you actually need. For LeapFlow's own settings, use config_list / config_get / config_set — its config files are outside the workspace and not readable here.\n- **file_write**(path, content, mode) [capability_expand category: write]: Write content to a file (overwrite or append).\n- **code_search**(pattern, patterns, path, glob, ignore_case, multiline, max_results, context_lines): Search file CONTENTS by regex pattern across a directory tree (ripgrep-backed). Requires a regex pattern. NOT for listing or browsing directory contents — use file_list for that. Prefer this over shell_run grep: faster, skips VCS/dependency/build dirs, and returns structured path:line:column matches. Batch related lookups into ONE call via `patterns` (OR-combined, single pass) instead of issuing several separate searches. Use file_read for the surrounding context of a hit.\n- **file_find**(glob, path, max_results): Find files by a recursive glob pattern under a base path (e.g. '**/test_*.py' or '*.md'). Prefer this over shell_run find; skips VCS/dependency/build dirs.\n- **edit_file**(path, edits, dry_run, diff) [capability_expand category: write]: Apply targeted, anchored search-replace edits to an EXISTING text file (use file_write to create/overwrite). Each edit is {original_text, new_text, replace_all?}; original_text must match exactly and uniquely (or set replace_all) — a non-unique or missing anchor is rejected so files are never corrupted. Set dry_run to preview. Alternatively pass a unified 'diff' to apply its hunks as anchored edits. Far cheaper and safer than rewriting a whole file.\n- **shell_run**(command, cwd, timeout) [capability_expand category: shell]: Execute a one-shot shell command with timeout protection. Runs in the active workspace; paths resolving outside it are refused. Reach for a structured tool first when one fits — web_fetch for anything over HTTP(S), git_query/scm_sync for git, code_search/file_find/file_read for the repo, config_get/config_set for LeapFlow's own settings — because those report typed results, while a failed shell command can only be diagnosed from its exit code and stderr. Every shell run counts as an external side effect, so a failure stops the rest of the batch and is not retried automatically.\n- **terminal_open**(command, cwd, shell) [capability_expand category: terminal]: Open a PERSISTENT shell session (REPL/dev server/watch), returning a session_id for terminal_send/read/close. Disabled unless tools.terminal_session_enabled is set. For one-shot commands use shell_run instead.\n- **terminal_send**(session_id, input, wait) [capability_expand category: terminal]: Send a line of input to a persistent terminal session and return output captured shortly after.\n- **terminal_read**(session_id, wait): Drain buffered output from a persistent terminal session (optionally waiting briefly first).\n- **terminal_close**(session_id) [capability_expand category: terminal]: Terminate a persistent terminal session and release its process group.\n- **terminal_list**(): List active persistent terminal sessions.\n- **config_list**(category, limit): List LeapFlow's own writable settings (LLM model and endpoint, daemon, memory, perception, gateway, …) with current values. Use this to discover the exact key before changing anything. Optionally narrow by `category`. This is the only correct way to inspect LeapFlow configuration — never read config files from disk.\n- **config_get**(key): Read one LeapFlow setting by key (e.g. 'llm.model', 'llm.base_url', 'daemon.log_level'), returning its current value, type, scopes, and whether a change needs a daemon restart. There is no 'llm.provider' key: provider behavior is inferred from the OpenAI-compatible 'llm.base_url'. Never read LeapFlow config files from disk — use this.\n- **config_set**(key, value, scope) [capability_expand category: config]: Change one LeapFlow setting by key, e.g. switch an OpenAI-compatible LLM with key='llm.model' and key='llm.base_url'. There is no 'llm.provider' key: provider behavior is inferred from the endpoint. Values are validated and coerced; credentials are stored in the vault automatically. Call config_list or config_get first if unsure of the key. The result states whether a `leap daemon restart` is required. Never edit LeapFlow config files directly.\n- **web_fetch**(url, select, timeout, max_bytes): Read a URL over HTTP(S) and get back extracted, context-sized content: parsed JSON for API endpoints, readable text plus links for web pages. Use this for anything on the internet — prices, docs, releases, articles — instead of running curl through shell_run: it reports real HTTP status codes, retries rate limits on its own, and is a plain read so a retry is always safe. For JSON APIs pass `select` with a dotted path (e.g. 'chart.result.0.meta') to return just that part instead of the whole payload.\n- **memory_search**(query, limit): Search agent memory for relevant past experiences, observations, and facts.\n- **memory_add**(content, kind) [capability_expand category: write]: Store a new observation or insight in memory for future reference.\n- **research_note**(kind, text): Record a compact, structured note about the current task's state so it survives context compression on long / multi-step tasks. Use for durable findings, open questions still to resolve, decisions / excluded paths, and the immediate next step. One concise sentence per note.\n- **capability_expand**(category): Fetch the full callable schema for every tool in a capability category (e.g. 'hub', 'gateway', 'desktop', 'delegate', 'file', 'memory', 'skill'). The compact tool index always lists every registered tool by name and a one-line summary, but only a static low-risk subset is directly callable each turn. If you need a tool from the index that is not yet callable, call capability_expand with its category first; the matching tools become callable in this turn. Never invent a tool name — expand the category instead.\n- **delegate_task**(goal, context) [capability_expand category: delegate]: Delegate a complex sub-task to an isolated subagent. The subagent gets a fresh context and restricted tool access. Use when a task is self-contained and can be solved independently.\n- **schedule_reentry**(kind, reason, delay_seconds, event_match, max_reentries, deadline_seconds): Register a re-entry so this task can resume later from its current orientation (findings / open questions / next step). Use when work must pause and continue after a delay (kind=time) or when a matching platform event arrives (kind=event), instead of finishing now. The research-ledger state is carried over automatically.\n- **hub_push**(skill_name, visibility, version) [capability_expand category: hub]: Push a local skill to the ModelScope Hub for sharing or backup.\n- **hub_pull**(repo_id, version) [capability_expand category: hub]: Pull a skill from the ModelScope Hub to install locally.\n- **hub_search**(query) [capability_expand category: hub]: Search for skills on the Hub by keyword or description.\n- **hub_sync**(mode, dry_run) [capability_expand category: hub]: Preview or execute sync between local skills and Hub.\n- **platform_action**(platform, action, payload, backend_kind) [capability_expand category: gateway]: Execute an exact registered business action on an external platform through LeapFlow's App Connector layer. Actions must be copied from the App Connector Capability Index and are addressed as domain.operation, e.g. im.send_message or docs.create_markdown. All business fields (chat_id, text, query, etc.) MUST be placed inside `payload`, never at the top level. Example: {\"platform\":\"feishu\",\"action\":\"im.send_message\",\"payload\":{\"chat_id\":\"oc_xxx\",\"text\":\"hello\"}}. Do not invent action names, do not use management actions such as list/guide/connect/status here.\n- **platform_connect**(action, platform, credentials, options, checkpoint) [capability_expand category: gateway]: List, guide, connect, disconnect, remove, or check status for external platforms using the App Connector management namespace. Supports REST and CLI backends. Use this for management actions such as list/guide/preflight/connect/status; use platform_action only for exact registered business actions.\n- **gateway_send**(platform, chat_id, text, thread_id) [capability_expand category: gateway]: Send a message to a connected external platform (Feishu group, Telegram chat, DingTalk conversation, etc.). Requires the platform to be connected via gateway_connect first. Use gateway_connect with action='list' to see connected platforms and available chat IDs.\n- **gateway_connect**(action, platform, credentials, options) [capability_expand category: gateway]: Connect, configure, or manage external platform integrations (Feishu, DingTalk, Telegram, Slack, Discord, etc.). Conversational flow: 1) call 'guide' to get setup steps + required fields, 2) present the steps to the user and ask for ALL required credentials in a single message, 3) call 'connect' with the credentials. Goal: complete in 1–2 user turns. NEVER include credential values in your text response.\n- **plugin_list**(): List the live plugin registry across Tool/Gateway/LLM subsystems for plugin management: which plugins are loaded, their category and fiber lifecycle state. For questions about LeapFlow's own capabilities or whether it supports X, use self_describe instead.\n- **plugin_status**(plugin_id): Get detailed status of a specific plugin: its declared category, runtime dependencies, contributed tools, and fiber lifecycle state.\n- **plugin_versions**(plugin_id): List recorded source versions and active pointer for a profile-scoped plugin.\n- **plugin_propose**(requested_capability, plugin_id, proposed_tools, test_cases, risk_level, evidence): Create a side-effect-free PluginProposal from explicit capability-gap evidence. Use this before plugin_generate when a missing capability should be reviewed. Does not call an LLM, write files, or install anything.\n- **assess_compatibility**(manifest, source_path) [capability_expand category: plugin_management]: Assess whether a foreign plugin manifest or real DSH source bundle is compatible with LeapFlow. A source bundle assessment is static: runtime_ready stays false until restricted Node discovery during plugin_install. Returns component-level verdicts and limitations.\n- **plugin_generate**(plugin_id, description, proposal_id) [capability_expand category: system]: Generate a new ToolPlugin from a natural-language capability description. The LLM produces code that conforms to the ToolPlugin Protocol; it is then rigorously validated (syntax, structure, import, protocol conformance). The generated source is stored in CAS and receives explicit content approval. It DOES NOT install the plugin — installation requires a second, mutation-specific approval via plugin_install.\n- **plugin_install**(plugin_id, code, marketplace_name, source_path, proposal_id, version_label) [capability_expand category: system]: Install a plugin from exactly one source: validated Python code, the configured Python marketplace, or a real DSH source bundle. DSH bundles run restricted Node discovery before registration; only public host tools are installed and client UI limitations are reported. Writes profile-scoped state and mutates the process-global registry. REQUIRES APPROVAL.\n- **plugin_rollback**(plugin_id, version) [capability_expand category: system]: Rollback a profile-scoped plugin to a recorded source version and reload it. REQUIRES APPROVAL.\n- **plugin_reload**(plugin_id, version_label) [capability_expand category: system]: Hot-reload a plugin at runtime. Disposes the old plugin fiber, re-imports its module, and registers a fresh instance. Existing in-flight turns are unaffected (snapshot isolation). REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_disable**(plugin_id) [capability_expand category: system]: Disable a plugin by disposing its fiber, removing its tools from the runtime registry. Cannot disable self_management itself. REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_remove**(plugin_id, delete_source) [capability_expand category: system]: Terminally remove a plugin: dispose its fiber, unregister its tools, remove reload metadata, and optionally delete its profile-scoped source file. Cannot remove self_management itself. REQUIRES APPROVAL.\n- **plugin_unquarantine**(plugin_id) [capability_expand category: plugin_management]: Restore a quarantined plugin to probation status for re-evaluation. Unfreezes the trust ledger, transitions the proposal back to PROBATION, and reloads the plugin. REQUIRES APPROVAL.\n- **plugin_enable**(plugin_id) [capability_expand category: system]: Re-enable a previously disabled plugin by reloading its module and registering a fresh instance. REQUIRES APPROVAL.\n- **self_describe**(facet): Answer questions about LeapFlow ITSELF from live runtime evidence — its identity/version/model, what tools and plugins it has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of self-awareness. Prefer this over reading LeapFlow's own source code. Pick facet=capabilities for 'what can you do / do you support X', identity for version/model, runtime for context/posture, or all for a full self-check when explicitly requested.\n- **runtime_snapshot**(): Lightweight (~150 token) flat snapshot of current runtime state: model, context budget, posture, disclosure level, turn count, cache hit rate, uptime, tool count, and pending approvals.\n- **schedule_create**(trigger_expression, instruction, execution_mode, max_retries, delivery_target) [capability_expand category: scheduler]: Create a new scheduled task. Specify a trigger expression (e.g. '30m', 'every 2h', '0 9 * * *') and the instruction to execute on each trigger. Optionally provide an execution mode and a delivery target to receive notifications.\n- **schedule_list**() [capability_expand category: scheduler]: List all scheduled tasks with their current state, trigger, and next due time.\n- **schedule_status**(task_id) [capability_expand category: scheduler]: Get detailed status and recent execution history for a scheduled task.\n- **schedule_pause**(task_id) [capability_expand category: scheduler]: Pause a scheduled task so it stops firing without being cancelled.\n- **schedule_resume**(task_id) [capability_expand category: scheduler]: Resume a paused scheduled task — re-arms it and recalculates next due time.\n- **schedule_cancel**(task_id) [capability_expand category: scheduler]: Cancel a scheduled task permanently.\n- **tool_search**(query, max_results): Search all registered tools by keyword relevance. Returns a ranked list of matching tools with name, category, and summary. Use this when you need a tool but are unsure of its exact name or category.\n- **tool_describe**(tool_name): Get the full callable schema of a single tool by exact name. Returns the tool's description, parameters, and metadata. Use after tool_search to inspect a specific tool before calling it.\n- **hw_list**(): List connected hardware devices with their channel counts and measured quantities. Start here; it does not include operating limits.\n- **hw_describe**(device_id): Return the full reference for one device: every channel, its unit, its operating envelope, rate limit, reversibility, and required interlocks, plus any prior outcomes recorded for its writable channels. Required before commanding a device.\n- **hw_read**(device_id, channel_id): Read the current value of one device channel. Has no physical effect.\n- **hw_status**(device_id): Report connection health, halt capability, and recent notable events (threshold excursions, lost samples, stalled channels) for one device.\n- **hw_configure**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Set a configuration value or setpoint on a channel declaring effect=configure. Setpoints often have inertia: the value may need time to stabilise.\n- **hw_actuate**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Command motion or output on a channel declaring effect=actuate. This moves physical hardware; a repeat from an unknown state is not a safe retry.\n- **hw_dispense**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Consume an irreversible resource on a channel declaring effect=dispense. Running this twice dispenses twice; never repeat it after a failure without first verifying what already happened.\n- **hw_estop**(device_id) [capability_expand category: hardware]: Stop all motion and output on a device immediately. Never requires approval; use it whenever device behaviour is unexpected.\n- **hw_batch_actuate**(device_id, commands, dry_run, verify) [capability_expand category: hardware]: Write multiple actuator channels in one atomic transaction. Used for coordinated multi-joint control where all positions must be commanded simultaneously (e.g. robot arm joint-space control).\n- **hw_control_start**(device_id, mode, targets, frequency_hz, trajectory, policy, gains) [capability_expand category: hardware]: Start a real-time control loop on a robot device. Supports PID position tracking, impedance control, trajectory following, and neural policy chains.\n- **hw_control_stop**(device_id) [capability_expand category: hardware]: Stop the real-time control loop on a device. Gracefully halts the device and returns session statistics.\n- **hw_control_status**(device_id) [capability_expand category: hardware]: Get the status and statistics of a running control loop. Returns frequency, jitter, overruns, and policy state.\n- **hw_policy_infer**(device_id, policy, task, execute, chunk_size, verify, compute_budget) [capability_expand category: hardware]: Execute VLA policy inference on a robot device. Reads observation, runs policy, optionally writes action to actuators. Supports local (HuggingFace) and remote (PolicyServer) policies.\n- **hw_policy_episode**(action, device_id, policy, task) [capability_expand category: hardware]: Manage policy inference episode lifecycle. Start an episode to begin a task, stop to finalise and get stats.\n- **hw_policy_list**(source) [capability_expand category: hardware]: List available VLA policies and their capabilities.\n- **hw_orchestrate**(mode, operations, barrier_timeout_s, label) [capability_expand category: hardware]: Execute coordinated operations across multiple hardware devices. Supports sequential (one-at-a-time), parallel (concurrent), and barrier (synchronised) execution modes. Any failure in a parallel group triggers halt_all on every participating device for safety.\n- **session_search**(query, limit) [capability_expand category: unclassified]: Search past conversation sessions in the current workspace for relevant context.\n- **session_list**(limit): List recent conversation sessions in the current workspace with stable ids, titles, dates, and summaries. Use for browsing past tasks or when user asks to see history without specific search terms. No keywords needed — returns chronological list.\n- **session_detail**(session_id, limit, offset, include_inactive) [capability_expand category: unclassified]: Read a paginated persisted transcript for one past conversation session in the current workspace. Use after session_list or session_search returns a session_id.\n- **click**(element_index) [capability_expand category: desktop]: Click a UI element by its element_index (from the latest observe_ui snapshot)\n- **get_clipboard**() [capability_expand category: desktop]: Read current clipboard text content\n- **list_apps**(filter, running_only) [capability_expand category: desktop]: List available applications on this system. Use to discover correct bundle_id before switch_app.\n- **list_windows**() [capability_expand category: desktop]: List all top-level windows with pid, window_id, title, and per-window state (minimized, on-screen). Call this first to pick the pid and window_id that observe_ui and other window tools require.\n- **observe_ui**(pid, window_id, query) [capability_expand category: desktop]: Snapshot one window's actionable UI elements, each tagged with an element_index for click/right_click/read_text. Re-observe after actions — indices belong to one snapshot. Requires the window's pid and window_id from list_windows.\n- **open_url**(url, app_id) [capability_expand category: desktop]: Open a URL in the default or specified browser\n- **read_text**(element_index) [capability_expand category: desktop]: Read the text content of a specific UI element from the latest snapshot\n- **right_click**(element_index) [capability_expand category: desktop]: Right-click a UI element to open its context menu. Returns visible menu items.\n- **screenshot**(pid, window_id) [capability_expand category: desktop]: Capture a screenshot for visual verification. With pid + window_id captures that window (works across all displays); defaults to the last observed window, or the full desktop when no window has been observed.\n- **scroll**(element_index, direction, amount, pid, window_id) [capability_expand category: desktop]: Scroll a scrollable area of a window. Omit element_index to scroll the window's focused/page scroller; pass one to scroll an exact element from the latest snapshot.\n- **select_text**(element_index) [capability_expand category: desktop]: Select all text in a UI element (focus + select-all, for subsequent copy)\n- **set_clipboard**(text) [capability_expand category: desktop]: Write text to the clipboard\n- **shortcut**(keys) [capability_expand category: desktop]: Execute a keyboard shortcut\n- **switch_app**(app_id) [capability_expand category: desktop]: Switch to an app (launch if needed, activate, verify)\n- **type_text**(text) [capability_expand category: desktop]: Type text into the currently focused element\n- **wait**(seconds) [capability_expand category: desktop]: Wait for a specified duration before continuing\n- **wait_until**(condition, pid, window_id, timeout, poll_interval) [capability_expand category: desktop]: Wait until a UI condition is met (polls UI tree). Returns elements when found or on timeout.\n- **wait_until_stable**(timeout, poll_interval, pid, window_id) [capability_expand category: desktop]: Wait until the UI stops changing (element set stabilizes across polls).\n\n## App Connector Capability Index\nLeapFlow can onboard and manage external apps through `platform_connect` and execute exact registered business actions through `platform_action`.\nFor requests about connecting, setting up, configuring, enabling, or managing a supported app, use `platform_connect` first instead of generating SDK/Webhook sample code.\n`platform_connect.action` is the management namespace: list, guide, preflight, connect, disconnect, remove, status, events_start, events_stop, events_status.\n`platform_action.action` is only for exact registered platform business actions listed below, such as `im.send_message`; never use management actions like `list` or `guide` there.\nAll business fields MUST go inside `payload`; top-level keys are only `platform`, `action`, and `payload`.\nDo not invent platform IDs or platform action names. If the needed action is not listed, ask for discovery/clarification instead of guessing.\nUse `platform_connect` with `action='guide'` and the matching `platform` to start onboarding; use `action='list'` when the app is unclear.\nWhen a pending onboarding state is present, continue from that state with `platform_connect` instead of asking the user to restate the app.\nSupported apps:\n- `api_server`: API Server (OpenAI Compatible) (category=api; backend=adapter; platform_action actions=none registered)\n- `dingtalk`: 钉钉 (DingTalk) (category=im; backend=adapter; platform_action actions=none registered)\n- `feishu`: 飞书 (Feishu/Lark) (category=im; backend=cli)\n - `calendar.create_event` payload={summary*, start_time*, end_time*, attendees} [write/high]\n - `docs.create_markdown` payload={title*, markdown*, folder_token} [write/medium]\n - `drive.search` payload={query*, limit} [read/medium]\n - `im.add_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.download_resource` payload={message_id*, file_key*, type*} [read/low]\n - `im.get_messages` payload={message_ids*} [read/low]\n - `im.list_chats` payload={page_size} [read/low]\n - `im.list_messages` payload={chat_id*} [read/low]\n - `im.list_thread_messages` payload={thread*} [read/low]\n - `im.remove_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.reply_message` payload={message_id*, text*} [send/high]\n - `im.search_chats` payload={query*, page_size} [read/low]\n - `im.search_messages` payload={query*} [read/medium]\n - `im.send_message` payload={chat_id*, text*, thread_id} [send/high]\n - `im.update_card` payload={token*, card*} [write/medium]\n - `im.update_message` payload={message_id*, text*} [write/medium]\n - `mail.search_unread` payload={query, limit} [read/high]\n - `sheets.append_row` payload={spreadsheet_token*, sheet_id*, values*} [write/medium]\n - `task.create` payload={title*, description, due_time} [write/medium]\n- `telegram`: Telegram (category=social; backend=adapter; platform_action actions=none registered)\n- `webhook`: Webhook (Generic) (category=webhook; backend=adapter; platform_action actions=none registered)\n\n## Learned Skills\nYou have access to the following learned skills. Use `skills_list` to browse or `skill_view` to read details:\n- api_designer: REST and GraphQL API design with OpenAPI spec generation and best practices [API, REST, GraphQL]\n- code_review: Structured code review for security, performance, and style [code, review, security]\n- data_analysis: CSV/JSON data analysis with statistical summaries, quality checks, and visualiza [data, analysis, statistics]\n- devops_helper: CI/CD pipeline configuration, Docker orchestration, and deployment strategy [devops, CI/CD, Docker]\n- document_writer: Structured document generation: technical docs, API references, reports, and REA [documentation, technical-writing, README]\n- git_workflow: Git operation orchestration: branching, commits, conflicts, and PR workflow [git, version-control, branching]\n- i18n_helper: Internationalization workflow: string extraction, translation, locale management [i18n, internationalization, localization]\n- project_analysis: Project structure analysis, dependency audit, and tech stack identification [project, analysis, dependencies]\n- security_audit: Security vulnerability scanning strategy, threat modeling, and fix recommendatio [security, vulnerability, audit]\n- shell_automation: Natural language to shell commands with safety review and step-by-step execution [shell, terminal, automation]\n- test_writer: Generate unit and integration tests from source code with edge-case coverage [testing, unit-test, integration-test]\n- web_research: Multi-step web search with cross-verification and structured synthesis [web, search, research]\n\n## Tool Usage\nTools are normally invoked through the native function-calling mechanism, not by writing JSON in your reply\ntext. Only if the provider signals that native function calling is unavailable for this turn, fall back to a\nsingle JSON code block: `{\"name\": \"tool_name\", \"arguments\": {\"key\": \"value\"}}` — use this fallback format\nonly, never both. Only call a tool whose exact name appears in the tool index above and is currently callable\n(or reachable via `capability_expand`). Never invent, rename, alias, or guess a tool name, platform ID, or\nplatform action from argument shape or wording — if the index does not list it, it does not exist.\n`platform_connect.action` (list/guide/preflight/connect/disconnect/remove/status/events_start/events_stop/\nevents_status) is the App Connector management namespace; `platform_action.action` only accepts exact\nregistered business actions such as `im.send_message` shown in the App Connector Capability Index — never mix\nthe two namespaces. If a tool call returns an unknown/unavailable result, use the returned suggestions or\navailable names for a single retry instead of trying further variations of the same guess.\n\n**Side-effect action rule** (`platform_action` with effect=send/write/execute):\n- Call each unique action+payload **exactly once**. Never include duplicates in the same turn.\n- Once the result returns `\"completed\": true`, that action is DONE for this task. Do NOT call it again\n in any subsequent turn — immediately summarize the result for the user instead.\n- The system enforces idempotency: duplicate calls are blocked and will not execute.\n- If the user explicitly requests sending/writing multiple times, use distinct payloads per call.\n\n**Resource identifier provenance rule**:\n- NEVER fabricate, guess, or infer resource identifiers (chat_id, message_id, file_key, user_id, etc.).\n Every resource ID used in a side-effect action MUST come from a successful API response in this session.\n- If a read/list action fails (e.g. authorization error), you do NOT have valid resource IDs.\n Report the failure to the user — do NOT attempt the dependent write/send action with a guessed ID.\n- When a tool result contains `\"llm_instruction\"`, follow it exactly.\n\n## Guidelines\n1. **Direct answers first**: If you already know the answer, respond directly without tools.\n2. **Avoid redundant tool calls**: Do not call the same tool with the same arguments more than once in the same user turn. When an existing tool result already answers the user's request, stop calling tools and answer directly.\n3. **Use tools proactively**: When the user asks about files, time, system state, or needs actions performed, use the appropriate tool.\n4. **Chain tools when needed**: You can call multiple tools in sequence (e.g., list files → read file → summarize).\n5. **Handle failures gracefully**: If a tool fails, explain what went wrong and suggest alternatives. If it failed because of an unknown tool/platform/action name, retry once with an exact name from the returned suggestions, then explain rather than keep guessing.\n6. **Summarize results naturally**: After tool execution, synthesize the results into a helpful answer rather than dumping raw output.\n7. **Stay conversational**: Maintain a natural, helpful tone. Acknowledge context from earlier in the conversation.\n8. **Recall past work**:\n - Broad queries (\"之前做了什么\", \"列出任务\"): answer from the \"Recent Task History\" section already in your context. If insufficient, call session_list.\n - Specific lookups (\"上次那个配置怎么改的\"): call session_search with relevant phrases (NOT single characters).\n - Do NOT call search tools repeatedly with keyword fragments. One well-phrased call is better than ten fragmented ones.\n\n## Coding & Verification\nWhen working with code, prefer the precise built-in tools over ad-hoc shell: use `repo_map` to orient in an\nunfamiliar project, `code_search`/`file_find` to locate and `code_intel` for symbols, `edit_file` (anchored\nsearch-replace, or a unified `diff`) to change files — never rewrite a whole file to change a few lines — and\n`git_query` to inspect diffs/log. After edits, check `syntax_ok` in the result and run `test_run`/`lint_check`\nbefore declaring the work done. Batch independent read-only calls (search/read/query) into a single turn — they\nrun in parallel.\n\n## Presentation Style\n1. **Polished Markdown only**: Format user-facing answers with clean Markdown headings, short paragraphs, and concise bullets. Use tables only when they improve comparison or scanning.\n2. **Terminal-friendly layout**: Keep lines readable in a TUI; avoid dense walls of text, deeply nested lists, oversized ASCII art, or heavy visual blocks.\n3. **Elegant emphasis**: Use bold text sparingly for key terms and conclusions. Avoid excessive emojis, decorative symbols, repeated separators, or visual noise.\n4. **Theme-safe colors**: Do not emit ANSI escape codes, HTML color tags, Rich markup, or hardcoded color names. Rely on the TUI theme to render Markdown professionally.\n5. **No leaked tool protocol**: Never show tool-call JSON, internal schemas, raw observations, tool result payloads, or hidden reasoning in the final answer unless the user explicitly asks for raw/debug output. Treat any prior `{\"name\": ..., \"arguments\": ...}` blocks and `Tool result (...)` messages as internal execution context only.\n6. **Professional closure**: End with a concise conclusion or next step when helpful; avoid rambling after the useful answer is complete.\n\nWhen finished with all tool calls, respond normally without a JSON block, tool-call transcript, or process log.\n\n## Task Contract\n- Task ID: turn-4\n- Original user request: Configure the bench_r8 homed channel to true to complete homing.\n- Workspace root: \n- Allowed roots: \n- Treat relative project paths as relative to the workspace root; never infer `.` as the project root when a workspace root is provided.\n- Workspace boundary is enforced by tools: do not read, search, edit, or run commands against paths outside the allowed roots unless the user explicitly requests an external path and the tool/approval policy permits it.\n- LeapFlow workspace config is optional at `/.leapflow/config.yaml`; runtime config is loaded from `~/.leapflow/config/user.yaml` and `~/.leapflow/profiles//config/*.yaml`.\n- Preserve this task contract across summarization, compression, tool loops, and memory retrieval.\n" + "content": "You are LeapFlow, an intelligent assistant that can both converse naturally and take real actions on the user's computer.\n\n## Capabilities\nThe tool index below lists **every** registered tool by name and a one-line summary — this is the complete\ncapability contract; nothing else exists. Only a subset is directly callable this turn (via native tool calling,\nnot a JSON block in your reply). If you need a tool from the index that is not yet callable, call\n`capability_expand` with its category name first — the matching tools become callable immediately after.\nWhen the user asks about LeapFlow **itself** — its identity/version/model, what tools, plugins, or capabilities\nit has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of\nself-awareness — answer from live runtime evidence via `self_describe` (facet=capabilities for \"what can you\ndo / do you support X\", identity for version/model, runtime for context/posture, or `all` for a full self-check),\nnever by reading LeapFlow's own source code, documentation, or memory. Report configuration-dependent or\nunavailable capabilities as limitations rather than inferring them.\n- **text_search**(text, pattern): Search for a regex pattern in text.\n- **text_replace**(text, old, new, count) [capability_expand category: write]: Replace occurrences of a substring in text.\n- **time_get**(): Get current date and time.\n- **env_info**(): Get system environment information (OS, Python version, cwd).\n- **skills_list**(query, category, source): List available learned skills. Use when user asks about capabilities or you need a specific skill.\n- **skill_view**(name): View the full content of a specific skill document.\n- **skill_list_files**(name) [capability_expand category: skills]: List all support files in a skill directory (configs, templates, examples). Use before skill_file_read to discover available files.\n- **skill_file_read**(name, file_path) [capability_expand category: skills]: Read an individual support file from a skill directory. Use skill_list_files first to discover available files.\n- **code_intel**(path, operation): Precise document symbols (outline) for a source file: classes, functions, and methods with line ranges. Python uses an exact AST parse; other languages use a keyword-prefix scan. Prefer over file_read mode=symbols for accurate navigation before editing. Read-only.\n- **repo_map**(path): Compact project orientation for a repository root: languages, detected test/lint commands, top-level structure, entry points, manifest, and VCS branch. Call this first when entering an unfamiliar codebase. Read-only.\n- **scm_sync**(action, cwd, remote, pull_ref, push_ref, timeout) [capability_expand category: scm]: Run a typed git SCM action. Use this instead of shell_run for git pull/push/status. For 'pull origin main then push', set action='pull_then_push', remote='origin', pull_ref='main', and omit push_ref so LeapFlow pushes the current local branch.\n- **git_query**(action, cwd, ref, path, staged, max_count, stat): Read-only structured git inspection: action=diff|log|status|branch|show. Prefer over shell_run for reading repo state — output is clipped, redacted, and log/branch are parsed into structured fields. Use scm_sync for pull/push.\n- **git_write**(action, cwd, message, stage_all, name, ref, create) [capability_expand category: scm]: Mutating git actions: action=commit (message, stage_all), branch (create+switch), checkout (switch; create=true for -b). Approval-gated. Use scm_sync for pull/push and git_query for reads.\n- **test_run**(command, cwd, timeout): Run the project's test suite and return structured results (framework, passed/failed counts, failing tests). Auto-detects the runner (pytest/npm/go/cargo) or uses a configured/explicit command; executes via the governed shell. ok=true means the runner executed — see 'success' for pass/fail.\n- **lint_check**(command, cwd, timeout): Run the project's linter and return a structured clean/issue result. Auto-detects the linter (ruff/eslint/go vet/clippy) or uses a configured/explicit command; executes via the governed shell. ok=true means the linter ran — see 'clean'.\n- **file_list**(path, pattern, depth): List files and directories at a given path. Use depth=1 or depth=2 to get a recursive tree in one call instead of listing each sub-directory separately.\n- **file_read**(path, max_lines, start_line, max_chars, mode): Read text file content with adaptive context governance. For large or unfamiliar files, prefer mode='outline' or mode='symbols' first, then use mode='raw' with start_line/max_lines for the specific range you actually need. For LeapFlow's own settings, use config_list / config_get / config_set — its config files are outside the workspace and not readable here.\n- **file_write**(path, content, mode) [capability_expand category: write]: Write content to a file (overwrite or append).\n- **code_search**(pattern, patterns, path, glob, ignore_case, multiline, max_results, context_lines): Search file CONTENTS by regex pattern across a directory tree (ripgrep-backed). Requires a regex pattern. NOT for listing or browsing directory contents — use file_list for that. Prefer this over shell_run grep: faster, skips VCS/dependency/build dirs, and returns structured path:line:column matches. Batch related lookups into ONE call via `patterns` (OR-combined, single pass) instead of issuing several separate searches. Use file_read for the surrounding context of a hit.\n- **file_find**(glob, path, max_results): Find files by a recursive glob pattern under a base path (e.g. '**/test_*.py' or '*.md'). Prefer this over shell_run find; skips VCS/dependency/build dirs.\n- **edit_file**(path, edits, dry_run, diff) [capability_expand category: write]: Apply targeted, anchored search-replace edits to an EXISTING text file (use file_write to create/overwrite). Each edit is {original_text, new_text, replace_all?}; original_text must match exactly and uniquely (or set replace_all) — a non-unique or missing anchor is rejected so files are never corrupted. Set dry_run to preview. Alternatively pass a unified 'diff' to apply its hunks as anchored edits. Far cheaper and safer than rewriting a whole file.\n- **shell_run**(command, cwd, timeout) [capability_expand category: shell]: Execute a one-shot shell command with timeout protection. Runs in the active workspace; paths resolving outside it are refused. Reach for a structured tool first when one fits — web_fetch for anything over HTTP(S), git_query/scm_sync for git, code_search/file_find/file_read for the repo, config_get/config_set for LeapFlow's own settings — because those report typed results, while a failed shell command can only be diagnosed from its exit code and stderr. Every shell run counts as an external side effect, so a failure stops the rest of the batch and is not retried automatically.\n- **terminal_open**(command, cwd, shell) [capability_expand category: terminal]: Open a PERSISTENT shell session (REPL/dev server/watch), returning a session_id for terminal_send/read/close. Disabled unless tools.terminal_session_enabled is set. For one-shot commands use shell_run instead.\n- **terminal_send**(session_id, input, wait) [capability_expand category: terminal]: Send a line of input to a persistent terminal session and return output captured shortly after.\n- **terminal_read**(session_id, wait): Drain buffered output from a persistent terminal session (optionally waiting briefly first).\n- **terminal_close**(session_id) [capability_expand category: terminal]: Terminate a persistent terminal session and release its process group.\n- **terminal_list**(): List active persistent terminal sessions.\n- **config_list**(category, limit): List LeapFlow's own writable settings (LLM model and endpoint, daemon, memory, perception, gateway, …) with current values. Use this to discover the exact key before changing anything. Optionally narrow by `category`. This is the only correct way to inspect LeapFlow configuration — never read config files from disk.\n- **config_get**(key): Read one LeapFlow setting by key (e.g. 'llm.model', 'llm.base_url', 'daemon.log_level'), returning its current value, type, scopes, and whether a change needs a daemon restart. There is no 'llm.provider' key: provider behavior is inferred from the OpenAI-compatible 'llm.base_url'. Never read LeapFlow config files from disk — use this.\n- **config_set**(key, value, scope) [capability_expand category: config]: Change one LeapFlow setting by key, e.g. switch an OpenAI-compatible LLM with key='llm.model' and key='llm.base_url'. There is no 'llm.provider' key: provider behavior is inferred from the endpoint. Values are validated and coerced; credentials are stored in the vault automatically. Call config_list or config_get first if unsure of the key. The result states whether a `leap daemon restart` is required. Never edit LeapFlow config files directly.\n- **web_fetch**(url, select, timeout, max_bytes): Read a URL over HTTP(S) and get back extracted, context-sized content: parsed JSON for API endpoints, readable text plus links for web pages. Use this for anything on the internet — prices, docs, releases, articles — instead of running curl through shell_run: it reports real HTTP status codes, retries rate limits on its own, and is a plain read so a retry is always safe. For JSON APIs pass `select` with a dotted path (e.g. 'chart.result.0.meta') to return just that part instead of the whole payload.\n- **memory_search**(query, limit): Search agent memory for relevant past experiences, observations, and facts.\n- **memory_add**(content, kind) [capability_expand category: write]: Store a new observation or insight in memory for future reference.\n- **research_note**(kind, text): Record a compact, structured note about the current task's state so it survives context compression on long / multi-step tasks. Use for durable findings, open questions still to resolve, decisions / excluded paths, and the immediate next step. One concise sentence per note.\n- **capability_expand**(category): Fetch the full callable schema for every tool in a capability category (e.g. 'hub', 'gateway', 'desktop', 'delegate', 'file', 'memory', 'skill'). The compact tool index always lists every registered tool by name and a one-line summary, but only a static low-risk subset is directly callable each turn. If you need a tool from the index that is not yet callable, call capability_expand with its category first; the matching tools become callable in this turn. Never invent a tool name — expand the category instead.\n- **delegate_task**(goal, context) [capability_expand category: delegate]: Delegate a complex sub-task to an isolated subagent. The subagent gets a fresh context and restricted tool access. Use when a task is self-contained and can be solved independently.\n- **schedule_reentry**(kind, reason, delay_seconds, event_match, max_reentries, deadline_seconds): Register a re-entry so this task can resume later from its current orientation (findings / open questions / next step). Use when work must pause and continue after a delay (kind=time) or when a matching platform event arrives (kind=event), instead of finishing now. The research-ledger state is carried over automatically.\n- **hub_push**(skill_name, visibility, version) [capability_expand category: hub]: Push a local skill to the ModelScope Hub for sharing or backup.\n- **hub_pull**(repo_id, version) [capability_expand category: hub]: Pull a skill from the ModelScope Hub to install locally.\n- **hub_search**(query) [capability_expand category: hub]: Search for skills on the Hub by keyword or description.\n- **hub_sync**(mode, dry_run) [capability_expand category: hub]: Preview or execute sync between local skills and Hub.\n- **platform_action**(platform, action, payload, backend_kind) [capability_expand category: gateway]: Execute an exact registered business action on an external platform through LeapFlow's App Connector layer. Actions must be copied from the App Connector Capability Index and are addressed as domain.operation, e.g. im.send_message or docs.create_markdown. All business fields (chat_id, text, query, etc.) MUST be placed inside `payload`, never at the top level. Example: {\"platform\":\"feishu\",\"action\":\"im.send_message\",\"payload\":{\"chat_id\":\"oc_xxx\",\"text\":\"hello\"}}. Do not invent action names, do not use management actions such as list/guide/connect/status here.\n- **platform_connect**(action, platform, credentials, options, checkpoint) [capability_expand category: gateway]: List, guide, connect, disconnect, remove, or check status for external platforms using the App Connector management namespace. Supports REST and CLI backends. Use this for management actions such as list/guide/preflight/connect/status; use platform_action only for exact registered business actions.\n- **gateway_send**(platform, chat_id, text, thread_id) [capability_expand category: gateway]: Send a message to a connected external platform (Feishu group, Telegram chat, DingTalk conversation, etc.). Requires the platform to be connected via gateway_connect first. Use gateway_connect with action='list' to see connected platforms and available chat IDs.\n- **gateway_connect**(action, platform, credentials, options) [capability_expand category: gateway]: Connect, configure, or manage external platform integrations (Feishu, DingTalk, Telegram, Slack, Discord, etc.). Conversational flow: 1) call 'guide' to get setup steps + required fields, 2) present the steps to the user and ask for ALL required credentials in a single message, 3) call 'connect' with the credentials. Goal: complete in 1–2 user turns. NEVER include credential values in your text response.\n- **plugin_list**(): List the live plugin registry across Tool/Gateway/LLM subsystems for plugin management: which plugins are loaded, their category and fiber lifecycle state. For questions about LeapFlow's own capabilities or whether it supports X, use self_describe instead.\n- **plugin_status**(plugin_id): Get detailed status of a specific plugin: its declared category, runtime dependencies, contributed tools, and fiber lifecycle state.\n- **plugin_versions**(plugin_id): List recorded source versions and active pointer for a profile-scoped plugin.\n- **plugin_propose**(requested_capability, plugin_id, proposed_tools, test_cases, risk_level, evidence): Create a side-effect-free PluginProposal from explicit capability-gap evidence. Use this before plugin_generate when a missing capability should be reviewed. Does not call an LLM, write files, or install anything.\n- **assess_compatibility**(manifest, source_path) [capability_expand category: plugin_management]: Assess whether a foreign plugin manifest or real DSH source bundle is compatible with LeapFlow. A source bundle assessment is static: runtime_ready stays false until restricted Node discovery during plugin_install. Returns component-level verdicts and limitations.\n- **plugin_generate**(plugin_id, description, proposal_id) [capability_expand category: system]: Generate a new ToolPlugin from a natural-language capability description. The LLM produces code that conforms to the ToolPlugin Protocol; it is then rigorously validated (syntax, structure, import, protocol conformance). The generated source is stored in CAS and receives explicit content approval. It DOES NOT install the plugin — installation requires a second, mutation-specific approval via plugin_install.\n- **plugin_install**(plugin_id, code, marketplace_name, source_path, proposal_id, version_label) [capability_expand category: system]: Install a plugin from exactly one source: validated Python code, the configured Python marketplace, or a real DSH source bundle. DSH bundles run restricted Node discovery before registration; only public host tools are installed and client UI limitations are reported. Writes profile-scoped state and mutates the process-global registry. REQUIRES APPROVAL.\n- **plugin_rollback**(plugin_id, version) [capability_expand category: system]: Rollback a profile-scoped plugin to a recorded source version and reload it. REQUIRES APPROVAL.\n- **plugin_reload**(plugin_id, version_label) [capability_expand category: system]: Hot-reload a plugin at runtime. Disposes the old plugin fiber, re-imports its module, and registers a fresh instance. Existing in-flight turns are unaffected (snapshot isolation). REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_disable**(plugin_id) [capability_expand category: system]: Disable a plugin by disposing its fiber, removing its tools from the runtime registry. Cannot disable self_management itself. REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_remove**(plugin_id, delete_source) [capability_expand category: system]: Terminally remove a plugin: dispose its fiber, unregister its tools, remove reload metadata, and optionally delete its profile-scoped source file. Cannot remove self_management itself. REQUIRES APPROVAL.\n- **plugin_unquarantine**(plugin_id) [capability_expand category: plugin_management]: Restore a quarantined plugin to probation status for re-evaluation. Unfreezes the trust ledger, transitions the proposal back to PROBATION, and reloads the plugin. REQUIRES APPROVAL.\n- **plugin_enable**(plugin_id) [capability_expand category: system]: Re-enable a previously disabled plugin by reloading its module and registering a fresh instance. REQUIRES APPROVAL.\n- **self_describe**(facet): Answer questions about LeapFlow ITSELF from live runtime evidence — its identity/version/model, what tools and plugins it has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of self-awareness. Prefer this over reading LeapFlow's own source code. Pick facet=capabilities for 'what can you do / do you support X', identity for version/model, runtime for context/posture, or all for a full self-check when explicitly requested.\n- **runtime_snapshot**(): Lightweight (~150 token) flat snapshot of current runtime state: model, context budget, posture, disclosure level, turn count, cache hit rate, uptime, tool count, and pending approvals.\n- **schedule_create**(trigger_expression, instruction, execution_mode, max_retries, delivery_target) [capability_expand category: scheduler]: Create a new scheduled task. Specify a trigger expression (e.g. '30m', 'every 2h', '0 9 * * *') and the instruction to execute on each trigger. Optionally provide an execution mode and a delivery target to receive notifications.\n- **schedule_list**() [capability_expand category: scheduler]: List all scheduled tasks with their current state, trigger, and next due time.\n- **schedule_status**(task_id) [capability_expand category: scheduler]: Get detailed status and recent execution history for a scheduled task.\n- **schedule_pause**(task_id) [capability_expand category: scheduler]: Pause a scheduled task so it stops firing without being cancelled.\n- **schedule_resume**(task_id) [capability_expand category: scheduler]: Resume a paused scheduled task — re-arms it and recalculates next due time.\n- **schedule_cancel**(task_id) [capability_expand category: scheduler]: Cancel a scheduled task permanently.\n- **tool_search**(query, max_results): Search all registered tools by keyword relevance. Returns a ranked list of matching tools with name, category, and summary. Use this when you need a tool but are unsure of its exact name or category.\n- **tool_describe**(tool_name): Get the full callable schema of a single tool by exact name. Returns the tool's description, parameters, and metadata. Use after tool_search to inspect a specific tool before calling it.\n- **hw_list**(): List connected hardware devices with their channel counts and measured quantities. Start here; it does not include operating limits.\n- **hw_describe**(device_id): Return the full reference for one device: every channel, its unit, its operating envelope, rate limit, reversibility, and required interlocks, plus any prior outcomes recorded for its writable channels. Required before commanding a device.\n- **hw_read**(device_id, channel_id): Read the current value of one device channel. Has no physical effect.\n- **hw_status**(device_id): Report connection health, halt capability, and recent notable events (threshold excursions, lost samples, stalled channels) for one device.\n- **hw_configure**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Set a configuration value or setpoint on a channel declaring effect=configure. Setpoints often have inertia: the value may need time to stabilise.\n- **hw_actuate**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Command motion or output on a channel declaring effect=actuate. This moves physical hardware; a repeat from an unknown state is not a safe retry.\n- **hw_dispense**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Consume an irreversible resource on a channel declaring effect=dispense. Running this twice dispenses twice; never repeat it after a failure without first verifying what already happened.\n- **hw_estop**(device_id) [capability_expand category: hardware]: Stop all motion and output on a device immediately. Never requires approval; use it whenever device behaviour is unexpected.\n- **hw_batch_actuate**(device_id, commands, dry_run, verify) [capability_expand category: hardware]: Write multiple actuator channels in one atomic transaction. Used for coordinated multi-joint control where all positions must be commanded simultaneously (e.g. robot arm joint-space control).\n- **hw_control_start**(device_id, mode, targets, frequency_hz, trajectory, policy, gains) [capability_expand category: hardware]: Start a real-time control loop on a robot device. Supports PID position tracking, impedance control, trajectory following, and neural policy chains.\n- **hw_control_stop**(device_id) [capability_expand category: hardware]: Stop the real-time control loop on a device. Gracefully halts the device and returns session statistics.\n- **hw_control_status**(device_id) [capability_expand category: hardware]: Get the status and statistics of a running control loop. Returns frequency, jitter, overruns, and policy state.\n- **hw_context_snapshot**(level) [capability_expand category: hardware]: Get a structured snapshot of all hardware devices and their state. Use this before planning physical manipulation tasks to understand what devices are available and what they can do.\n- **hw_health**(device_id) [capability_expand category: hardware]: Check hardware device health status. Without device_id, returns fleet-wide health summary. With device_id, returns health details for that specific device.\n- **hw_policy_infer**(device_id, policy, task, execute, chunk_size, verify, compute_budget) [capability_expand category: hardware]: Execute VLA policy inference on a robot device. Reads observation, runs policy, optionally writes action to actuators. Supports local (HuggingFace) and remote (PolicyServer) policies.\n- **hw_policy_episode**(action, device_id, policy, task) [capability_expand category: hardware]: Manage policy inference episode lifecycle. Start an episode to begin a task, stop to finalise and get stats.\n- **hw_policy_list**(source) [capability_expand category: hardware]: List available VLA policies and their capabilities.\n- **hw_orchestrate**(mode, operations, barrier_timeout_s, label) [capability_expand category: hardware]: Execute coordinated operations across multiple hardware devices. Supports sequential (one-at-a-time), parallel (concurrent), and barrier (synchronised) execution modes. Any failure in a parallel group triggers halt_all on every participating device for safety.\n- **session_search**(query, limit) [capability_expand category: unclassified]: Search past conversation sessions in the current workspace for relevant context.\n- **session_list**(limit): List recent conversation sessions in the current workspace with stable ids, titles, dates, and summaries. Use for browsing past tasks or when user asks to see history without specific search terms. No keywords needed — returns chronological list.\n- **session_detail**(session_id, limit, offset, include_inactive) [capability_expand category: unclassified]: Read a paginated persisted transcript for one past conversation session in the current workspace. Use after session_list or session_search returns a session_id.\n- **click**(element_index) [capability_expand category: desktop]: Click a UI element by its element_index (from the latest observe_ui snapshot)\n- **get_clipboard**() [capability_expand category: desktop]: Read current clipboard text content\n- **list_apps**(filter, running_only) [capability_expand category: desktop]: List available applications on this system. Use to discover correct bundle_id before switch_app.\n- **list_windows**() [capability_expand category: desktop]: List all top-level windows with pid, window_id, title, and per-window state (minimized, on-screen). Call this first to pick the pid and window_id that observe_ui and other window tools require.\n- **observe_ui**(pid, window_id, query) [capability_expand category: desktop]: Snapshot one window's actionable UI elements, each tagged with an element_index for click/right_click/read_text. Re-observe after actions — indices belong to one snapshot. Requires the window's pid and window_id from list_windows.\n- **open_url**(url, app_id) [capability_expand category: desktop]: Open a URL in the default or specified browser\n- **read_text**(element_index) [capability_expand category: desktop]: Read the text content of a specific UI element from the latest snapshot\n- **right_click**(element_index) [capability_expand category: desktop]: Right-click a UI element to open its context menu. Returns visible menu items.\n- **screenshot**(pid, window_id) [capability_expand category: desktop]: Capture a screenshot for visual verification. With pid + window_id captures that window (works across all displays); defaults to the last observed window, or the full desktop when no window has been observed.\n- **scroll**(element_index, direction, amount, pid, window_id) [capability_expand category: desktop]: Scroll a scrollable area of a window. Omit element_index to scroll the window's focused/page scroller; pass one to scroll an exact element from the latest snapshot.\n- **select_text**(element_index) [capability_expand category: desktop]: Select all text in a UI element (focus + select-all, for subsequent copy)\n- **set_clipboard**(text) [capability_expand category: desktop]: Write text to the clipboard\n- **shortcut**(keys) [capability_expand category: desktop]: Execute a keyboard shortcut\n- **switch_app**(app_id) [capability_expand category: desktop]: Switch to an app (launch if needed, activate, verify)\n- **type_text**(text) [capability_expand category: desktop]: Type text into the currently focused element\n- **wait**(seconds) [capability_expand category: desktop]: Wait for a specified duration before continuing\n- **wait_until**(condition, pid, window_id, timeout, poll_interval) [capability_expand category: desktop]: Wait until a UI condition is met (polls UI tree). Returns elements when found or on timeout.\n- **wait_until_stable**(timeout, poll_interval, pid, window_id) [capability_expand category: desktop]: Wait until the UI stops changing (element set stabilizes across polls).\n\n## App Connector Capability Index\nLeapFlow can onboard and manage external apps through `platform_connect` and execute exact registered business actions through `platform_action`.\nFor requests about connecting, setting up, configuring, enabling, or managing a supported app, use `platform_connect` first instead of generating SDK/Webhook sample code.\n`platform_connect.action` is the management namespace: list, guide, preflight, connect, disconnect, remove, status, events_start, events_stop, events_status.\n`platform_action.action` is only for exact registered platform business actions listed below, such as `im.send_message`; never use management actions like `list` or `guide` there.\nAll business fields MUST go inside `payload`; top-level keys are only `platform`, `action`, and `payload`.\nDo not invent platform IDs or platform action names. If the needed action is not listed, ask for discovery/clarification instead of guessing.\nUse `platform_connect` with `action='guide'` and the matching `platform` to start onboarding; use `action='list'` when the app is unclear.\nWhen a pending onboarding state is present, continue from that state with `platform_connect` instead of asking the user to restate the app.\nSupported apps:\n- `api_server`: API Server (OpenAI Compatible) (category=api; backend=adapter; platform_action actions=none registered)\n- `dingtalk`: 钉钉 (DingTalk) (category=im; backend=adapter; platform_action actions=none registered)\n- `feishu`: 飞书 (Feishu/Lark) (category=im; backend=cli)\n - `calendar.create_event` payload={summary*, start_time*, end_time*, attendees} [write/high]\n - `docs.create_markdown` payload={title*, markdown*, folder_token} [write/medium]\n - `drive.search` payload={query*, limit} [read/medium]\n - `im.add_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.download_resource` payload={message_id*, file_key*, type*} [read/low]\n - `im.get_messages` payload={message_ids*} [read/low]\n - `im.list_chats` payload={page_size} [read/low]\n - `im.list_messages` payload={chat_id*} [read/low]\n - `im.list_thread_messages` payload={thread*} [read/low]\n - `im.remove_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.reply_message` payload={message_id*, text*} [send/high]\n - `im.search_chats` payload={query*, page_size} [read/low]\n - `im.search_messages` payload={query*} [read/medium]\n - `im.send_message` payload={chat_id*, text*, thread_id} [send/high]\n - `im.update_card` payload={token*, card*} [write/medium]\n - `im.update_message` payload={message_id*, text*} [write/medium]\n - `mail.search_unread` payload={query, limit} [read/high]\n - `sheets.append_row` payload={spreadsheet_token*, sheet_id*, values*} [write/medium]\n - `task.create` payload={title*, description, due_time} [write/medium]\n- `telegram`: Telegram (category=social; backend=adapter; platform_action actions=none registered)\n- `webhook`: Webhook (Generic) (category=webhook; backend=adapter; platform_action actions=none registered)\n\n## Learned Skills\nYou have access to the following learned skills. Use `skills_list` to browse or `skill_view` to read details:\n- api_designer: REST and GraphQL API design with OpenAPI spec generation and best practices [API, REST, GraphQL]\n- code_review: Structured code review for security, performance, and style [code, review, security]\n- data_analysis: CSV/JSON data analysis with statistical summaries, quality checks, and visualiza [data, analysis, statistics]\n- devops_helper: CI/CD pipeline configuration, Docker orchestration, and deployment strategy [devops, CI/CD, Docker]\n- document_writer: Structured document generation: technical docs, API references, reports, and REA [documentation, technical-writing, README]\n- git_workflow: Git operation orchestration: branching, commits, conflicts, and PR workflow [git, version-control, branching]\n- i18n_helper: Internationalization workflow: string extraction, translation, locale management [i18n, internationalization, localization]\n- project_analysis: Project structure analysis, dependency audit, and tech stack identification [project, analysis, dependencies]\n- security_audit: Security vulnerability scanning strategy, threat modeling, and fix recommendatio [security, vulnerability, audit]\n- shell_automation: Natural language to shell commands with safety review and step-by-step execution [shell, terminal, automation]\n- test_writer: Generate unit and integration tests from source code with edge-case coverage [testing, unit-test, integration-test]\n- web_research: Multi-step web search with cross-verification and structured synthesis [web, search, research]\n\n## Tool Usage\nTools are normally invoked through the native function-calling mechanism, not by writing JSON in your reply\ntext. Only if the provider signals that native function calling is unavailable for this turn, fall back to a\nsingle JSON code block: `{\"name\": \"tool_name\", \"arguments\": {\"key\": \"value\"}}` — use this fallback format\nonly, never both. Only call a tool whose exact name appears in the tool index above and is currently callable\n(or reachable via `capability_expand`). Never invent, rename, alias, or guess a tool name, platform ID, or\nplatform action from argument shape or wording — if the index does not list it, it does not exist.\n`platform_connect.action` (list/guide/preflight/connect/disconnect/remove/status/events_start/events_stop/\nevents_status) is the App Connector management namespace; `platform_action.action` only accepts exact\nregistered business actions such as `im.send_message` shown in the App Connector Capability Index — never mix\nthe two namespaces. If a tool call returns an unknown/unavailable result, use the returned suggestions or\navailable names for a single retry instead of trying further variations of the same guess.\n\n**Side-effect action rule** (`platform_action` with effect=send/write/execute):\n- Call each unique action+payload **exactly once**. Never include duplicates in the same turn.\n- Once the result returns `\"completed\": true`, that action is DONE for this task. Do NOT call it again\n in any subsequent turn — immediately summarize the result for the user instead.\n- The system enforces idempotency: duplicate calls are blocked and will not execute.\n- If the user explicitly requests sending/writing multiple times, use distinct payloads per call.\n\n**Resource identifier provenance rule**:\n- NEVER fabricate, guess, or infer resource identifiers (chat_id, message_id, file_key, user_id, etc.).\n Every resource ID used in a side-effect action MUST come from a successful API response in this session.\n- If a read/list action fails (e.g. authorization error), you do NOT have valid resource IDs.\n Report the failure to the user — do NOT attempt the dependent write/send action with a guessed ID.\n- When a tool result contains `\"llm_instruction\"`, follow it exactly.\n\n## Guidelines\n1. **Direct answers first**: If you already know the answer, respond directly without tools.\n2. **Avoid redundant tool calls**: Do not call the same tool with the same arguments more than once in the same user turn. When an existing tool result already answers the user's request, stop calling tools and answer directly.\n3. **Use tools proactively**: When the user asks about files, time, system state, or needs actions performed, use the appropriate tool.\n4. **Chain tools when needed**: You can call multiple tools in sequence (e.g., list files → read file → summarize).\n5. **Handle failures gracefully**: If a tool fails, explain what went wrong and suggest alternatives. If it failed because of an unknown tool/platform/action name, retry once with an exact name from the returned suggestions, then explain rather than keep guessing.\n6. **Summarize results naturally**: After tool execution, synthesize the results into a helpful answer rather than dumping raw output.\n7. **Stay conversational**: Maintain a natural, helpful tone. Acknowledge context from earlier in the conversation.\n8. **Recall past work**:\n - Broad queries (\"之前做了什么\", \"列出任务\"): answer from the \"Recent Task History\" section already in your context. If insufficient, call session_list.\n - Specific lookups (\"上次那个配置怎么改的\"): call session_search with relevant phrases (NOT single characters).\n - Do NOT call search tools repeatedly with keyword fragments. One well-phrased call is better than ten fragmented ones.\n\n## Coding & Verification\nWhen working with code, prefer the precise built-in tools over ad-hoc shell: use `repo_map` to orient in an\nunfamiliar project, `code_search`/`file_find` to locate and `code_intel` for symbols, `edit_file` (anchored\nsearch-replace, or a unified `diff`) to change files — never rewrite a whole file to change a few lines — and\n`git_query` to inspect diffs/log. After edits, check `syntax_ok` in the result and run `test_run`/`lint_check`\nbefore declaring the work done. Batch independent read-only calls (search/read/query) into a single turn — they\nrun in parallel.\n\n## Presentation Style\n1. **Polished Markdown only**: Format user-facing answers with clean Markdown headings, short paragraphs, and concise bullets. Use tables only when they improve comparison or scanning.\n2. **Terminal-friendly layout**: Keep lines readable in a TUI; avoid dense walls of text, deeply nested lists, oversized ASCII art, or heavy visual blocks.\n3. **Elegant emphasis**: Use bold text sparingly for key terms and conclusions. Avoid excessive emojis, decorative symbols, repeated separators, or visual noise.\n4. **Theme-safe colors**: Do not emit ANSI escape codes, HTML color tags, Rich markup, or hardcoded color names. Rely on the TUI theme to render Markdown professionally.\n5. **No leaked tool protocol**: Never show tool-call JSON, internal schemas, raw observations, tool result payloads, or hidden reasoning in the final answer unless the user explicitly asks for raw/debug output. Treat any prior `{\"name\": ..., \"arguments\": ...}` blocks and `Tool result (...)` messages as internal execution context only.\n6. **Professional closure**: End with a concise conclusion or next step when helpful; avoid rambling after the useful answer is complete.\n\nWhen finished with all tool calls, respond normally without a JSON block, tool-call transcript, or process log.\n\n## Task Contract\n- Task ID: turn-4\n- Original user request: Configure the bench_r8 homed channel to true to complete homing.\n- Workspace root: \n- Allowed roots: \n- Treat relative project paths as relative to the workspace root; never infer `.` as the project root when a workspace root is provided.\n- Workspace boundary is enforced by tools: do not read, search, edit, or run commands against paths outside the allowed roots unless the user explicitly requests an external path and the tool/approval policy permits it.\n- LeapFlow workspace config is optional at `/.leapflow/config.yaml`; runtime config is loaded from `~/.leapflow/config/user.yaml` and `~/.leapflow/profiles//config/*.yaml`.\n- Preserve this task contract across summarization, compression, tool loops, and memory retrieval.\n" }, { "role": "assistant", @@ -52,12 +52,14 @@ "hw_actuate", "hw_batch_actuate", "hw_configure", + "hw_context_snapshot", "hw_control_start", "hw_control_status", "hw_control_stop", "hw_describe", "hw_dispense", "hw_estop", + "hw_health", "hw_list", "hw_orchestrate", "hw_policy_episode", diff --git a/tests/_fixtures/cassettes/r8_hardware/cassette-model-383884acaf1063e3.cassette.json b/tests/_fixtures/cassettes/r8_hardware/cassette-model-383884acaf1063e3.cassette.json deleted file mode 100644 index c8bdcfc8..00000000 --- a/tests/_fixtures/cassettes/r8_hardware/cassette-model-383884acaf1063e3.cassette.json +++ /dev/null @@ -1,80 +0,0 @@ -{ - "fingerprint": "383884acaf1063e35a4e006eb879c45ea5299abf5803989e6ae6ac5714f0aef4", - "note": "captured in seed mode", - "request": { - "model": "cassette-model", - "stream": false, - "messages": [ - { - "role": "system", - "content": "You are LeapFlow, an intelligent assistant that can both converse naturally and take real actions on the user's computer.\n\n## Capabilities\nThe tool index below lists **every** registered tool by name and a one-line summary — this is the complete\ncapability contract; nothing else exists. Only a subset is directly callable this turn (via native tool calling,\nnot a JSON block in your reply). If you need a tool from the index that is not yet callable, call\n`capability_expand` with its category name first — the matching tools become callable immediately after.\nWhen the user asks about LeapFlow **itself** — its identity/version/model, what tools, plugins, or capabilities\nit has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of\nself-awareness — answer from live runtime evidence via `self_describe` (facet=capabilities for \"what can you\ndo / do you support X\", identity for version/model, runtime for context/posture, or `all` for a full self-check),\nnever by reading LeapFlow's own source code, documentation, or memory. Report configuration-dependent or\nunavailable capabilities as limitations rather than inferring them.\n- **text_search**(text, pattern): Search for a regex pattern in text.\n- **text_replace**(text, old, new, count) [capability_expand category: write]: Replace occurrences of a substring in text.\n- **time_get**(): Get current date and time.\n- **env_info**(): Get system environment information (OS, Python version, cwd).\n- **skills_list**(query, category, source): List available learned skills. Use when user asks about capabilities or you need a specific skill.\n- **skill_view**(name): View the full content of a specific skill document.\n- **skill_list_files**(name) [capability_expand category: skills]: List all support files in a skill directory (configs, templates, examples). Use before skill_file_read to discover available files.\n- **skill_file_read**(name, file_path) [capability_expand category: skills]: Read an individual support file from a skill directory. Use skill_list_files first to discover available files.\n- **code_intel**(path, operation): Precise document symbols (outline) for a source file: classes, functions, and methods with line ranges. Python uses an exact AST parse; other languages use a keyword-prefix scan. Prefer over file_read mode=symbols for accurate navigation before editing. Read-only.\n- **repo_map**(path): Compact project orientation for a repository root: languages, detected test/lint commands, top-level structure, entry points, manifest, and VCS branch. Call this first when entering an unfamiliar codebase. Read-only.\n- **scm_sync**(action, cwd, remote, pull_ref, push_ref, timeout) [capability_expand category: scm]: Run a typed git SCM action. Use this instead of shell_run for git pull/push/status. For 'pull origin main then push', set action='pull_then_push', remote='origin', pull_ref='main', and omit push_ref so LeapFlow pushes the current local branch.\n- **git_query**(action, cwd, ref, path, staged, max_count, stat): Read-only structured git inspection: action=diff|log|status|branch|show. Prefer over shell_run for reading repo state — output is clipped, redacted, and log/branch are parsed into structured fields. Use scm_sync for pull/push.\n- **git_write**(action, cwd, message, stage_all, name, ref, create) [capability_expand category: scm]: Mutating git actions: action=commit (message, stage_all), branch (create+switch), checkout (switch; create=true for -b). Approval-gated. Use scm_sync for pull/push and git_query for reads.\n- **test_run**(command, cwd, timeout): Run the project's test suite and return structured results (framework, passed/failed counts, failing tests). Auto-detects the runner (pytest/npm/go/cargo) or uses a configured/explicit command; executes via the governed shell. ok=true means the runner executed — see 'success' for pass/fail.\n- **lint_check**(command, cwd, timeout): Run the project's linter and return a structured clean/issue result. Auto-detects the linter (ruff/eslint/go vet/clippy) or uses a configured/explicit command; executes via the governed shell. ok=true means the linter ran — see 'clean'.\n- **file_list**(path, pattern, depth): List files and directories at a given path. Use depth=1 or depth=2 to get a recursive tree in one call instead of listing each sub-directory separately.\n- **file_read**(path, max_lines, start_line, max_chars, mode): Read text file content with adaptive context governance. For large or unfamiliar files, prefer mode='outline' or mode='symbols' first, then use mode='raw' with start_line/max_lines for the specific range you actually need. For LeapFlow's own settings, use config_list / config_get / config_set — its config files are outside the workspace and not readable here.\n- **file_write**(path, content, mode) [capability_expand category: write]: Write content to a file (overwrite or append).\n- **code_search**(pattern, patterns, path, glob, ignore_case, multiline, max_results, context_lines): Search file CONTENTS by regex pattern across a directory tree (ripgrep-backed). Requires a regex pattern. NOT for listing or browsing directory contents — use file_list for that. Prefer this over shell_run grep: faster, skips VCS/dependency/build dirs, and returns structured path:line:column matches. Batch related lookups into ONE call via `patterns` (OR-combined, single pass) instead of issuing several separate searches. Use file_read for the surrounding context of a hit.\n- **file_find**(glob, path, max_results): Find files by a recursive glob pattern under a base path (e.g. '**/test_*.py' or '*.md'). Prefer this over shell_run find; skips VCS/dependency/build dirs.\n- **edit_file**(path, edits, dry_run, diff) [capability_expand category: write]: Apply targeted, anchored search-replace edits to an EXISTING text file (use file_write to create/overwrite). Each edit is {original_text, new_text, replace_all?}; original_text must match exactly and uniquely (or set replace_all) — a non-unique or missing anchor is rejected so files are never corrupted. Set dry_run to preview. Alternatively pass a unified 'diff' to apply its hunks as anchored edits. Far cheaper and safer than rewriting a whole file.\n- **shell_run**(command, cwd, timeout) [capability_expand category: shell]: Execute a one-shot shell command with timeout protection. Runs in the active workspace; paths resolving outside it are refused. Reach for a structured tool first when one fits — web_fetch for anything over HTTP(S), git_query/scm_sync for git, code_search/file_find/file_read for the repo, config_get/config_set for LeapFlow's own settings — because those report typed results, while a failed shell command can only be diagnosed from its exit code and stderr. Every shell run counts as an external side effect, so a failure stops the rest of the batch and is not retried automatically.\n- **terminal_open**(command, cwd, shell) [capability_expand category: terminal]: Open a PERSISTENT shell session (REPL/dev server/watch), returning a session_id for terminal_send/read/close. Disabled unless tools.terminal_session_enabled is set. For one-shot commands use shell_run instead.\n- **terminal_send**(session_id, input, wait) [capability_expand category: terminal]: Send a line of input to a persistent terminal session and return output captured shortly after.\n- **terminal_read**(session_id, wait): Drain buffered output from a persistent terminal session (optionally waiting briefly first).\n- **terminal_close**(session_id) [capability_expand category: terminal]: Terminate a persistent terminal session and release its process group.\n- **terminal_list**(): List active persistent terminal sessions.\n- **config_list**(category, limit): List LeapFlow's own writable settings (LLM model and endpoint, daemon, memory, perception, gateway, …) with current values. Use this to discover the exact key before changing anything. Optionally narrow by `category`. This is the only correct way to inspect LeapFlow configuration — never read config files from disk.\n- **config_get**(key): Read one LeapFlow setting by key (e.g. 'llm.model', 'llm.base_url', 'daemon.log_level'), returning its current value, type, scopes, and whether a change needs a daemon restart. There is no 'llm.provider' key: provider behavior is inferred from the OpenAI-compatible 'llm.base_url'. Never read LeapFlow config files from disk — use this.\n- **config_set**(key, value, scope) [capability_expand category: config]: Change one LeapFlow setting by key, e.g. switch an OpenAI-compatible LLM with key='llm.model' and key='llm.base_url'. There is no 'llm.provider' key: provider behavior is inferred from the endpoint. Values are validated and coerced; credentials are stored in the vault automatically. Call config_list or config_get first if unsure of the key. The result states whether a `leap daemon restart` is required. Never edit LeapFlow config files directly.\n- **web_fetch**(url, select, timeout, max_bytes): Read a URL over HTTP(S) and get back extracted, context-sized content: parsed JSON for API endpoints, readable text plus links for web pages. Use this for anything on the internet — prices, docs, releases, articles — instead of running curl through shell_run: it reports real HTTP status codes, retries rate limits on its own, and is a plain read so a retry is always safe. For JSON APIs pass `select` with a dotted path (e.g. 'chart.result.0.meta') to return just that part instead of the whole payload.\n- **memory_search**(query, limit): Search agent memory for relevant past experiences, observations, and facts.\n- **memory_add**(content, kind) [capability_expand category: write]: Store a new observation or insight in memory for future reference.\n- **research_note**(kind, text): Record a compact, structured note about the current task's state so it survives context compression on long / multi-step tasks. Use for durable findings, open questions still to resolve, decisions / excluded paths, and the immediate next step. One concise sentence per note.\n- **capability_expand**(category): Fetch the full callable schema for every tool in a capability category (e.g. 'hub', 'gateway', 'desktop', 'delegate', 'file', 'memory', 'skill'). The compact tool index always lists every registered tool by name and a one-line summary, but only a static low-risk subset is directly callable each turn. If you need a tool from the index that is not yet callable, call capability_expand with its category first; the matching tools become callable in this turn. Never invent a tool name — expand the category instead.\n- **delegate_task**(goal, context) [capability_expand category: delegate]: Delegate a complex sub-task to an isolated subagent. The subagent gets a fresh context and restricted tool access. Use when a task is self-contained and can be solved independently.\n- **schedule_reentry**(kind, reason, delay_seconds, event_match, max_reentries, deadline_seconds): Register a re-entry so this task can resume later from its current orientation (findings / open questions / next step). Use when work must pause and continue after a delay (kind=time) or when a matching platform event arrives (kind=event), instead of finishing now. The research-ledger state is carried over automatically.\n- **hub_push**(skill_name, visibility, version) [capability_expand category: hub]: Push a local skill to the ModelScope Hub for sharing or backup.\n- **hub_pull**(repo_id, version) [capability_expand category: hub]: Pull a skill from the ModelScope Hub to install locally.\n- **hub_search**(query) [capability_expand category: hub]: Search for skills on the Hub by keyword or description.\n- **hub_sync**(mode, dry_run) [capability_expand category: hub]: Preview or execute sync between local skills and Hub.\n- **platform_action**(platform, action, payload, backend_kind) [capability_expand category: gateway]: Execute an exact registered business action on an external platform through LeapFlow's App Connector layer. Actions must be copied from the App Connector Capability Index and are addressed as domain.operation, e.g. im.send_message or docs.create_markdown. All business fields (chat_id, text, query, etc.) MUST be placed inside `payload`, never at the top level. Example: {\"platform\":\"feishu\",\"action\":\"im.send_message\",\"payload\":{\"chat_id\":\"oc_xxx\",\"text\":\"hello\"}}. Do not invent action names, do not use management actions such as list/guide/connect/status here.\n- **platform_connect**(action, platform, credentials, options, checkpoint) [capability_expand category: gateway]: List, guide, connect, disconnect, remove, or check status for external platforms using the App Connector management namespace. Supports REST and CLI backends. Use this for management actions such as list/guide/preflight/connect/status; use platform_action only for exact registered business actions.\n- **gateway_send**(platform, chat_id, text, thread_id) [capability_expand category: gateway]: Send a message to a connected external platform (Feishu group, Telegram chat, DingTalk conversation, etc.). Requires the platform to be connected via gateway_connect first. Use gateway_connect with action='list' to see connected platforms and available chat IDs.\n- **gateway_connect**(action, platform, credentials, options) [capability_expand category: gateway]: Connect, configure, or manage external platform integrations (Feishu, DingTalk, Telegram, Slack, Discord, etc.). Conversational flow: 1) call 'guide' to get setup steps + required fields, 2) present the steps to the user and ask for ALL required credentials in a single message, 3) call 'connect' with the credentials. Goal: complete in 1–2 user turns. NEVER include credential values in your text response.\n- **plugin_list**(): List the live plugin registry across Tool/Gateway/LLM subsystems for plugin management: which plugins are loaded, their category and fiber lifecycle state. For questions about LeapFlow's own capabilities or whether it supports X, use self_describe instead.\n- **plugin_status**(plugin_id): Get detailed status of a specific plugin: its declared category, runtime dependencies, contributed tools, and fiber lifecycle state.\n- **plugin_versions**(plugin_id): List recorded source versions and active pointer for a profile-scoped plugin.\n- **plugin_propose**(requested_capability, plugin_id, proposed_tools, test_cases, risk_level, evidence): Create a side-effect-free PluginProposal from explicit capability-gap evidence. Use this before plugin_generate when a missing capability should be reviewed. Does not call an LLM, write files, or install anything.\n- **assess_compatibility**(manifest, source_path) [capability_expand category: plugin_management]: Assess whether a foreign plugin manifest or real DSH source bundle is compatible with LeapFlow. A source bundle assessment is static: runtime_ready stays false until restricted Node discovery during plugin_install. Returns component-level verdicts and limitations.\n- **plugin_generate**(plugin_id, description, proposal_id) [capability_expand category: system]: Generate a new ToolPlugin from a natural-language capability description. The LLM produces code that conforms to the ToolPlugin Protocol; it is then rigorously validated (syntax, structure, import, protocol conformance). The generated source is stored in CAS and receives explicit content approval. It DOES NOT install the plugin — installation requires a second, mutation-specific approval via plugin_install.\n- **plugin_install**(plugin_id, code, marketplace_name, source_path, proposal_id, version_label) [capability_expand category: system]: Install a plugin from exactly one source: validated Python code, the configured Python marketplace, or a real DSH source bundle. DSH bundles run restricted Node discovery before registration; only public host tools are installed and client UI limitations are reported. Writes profile-scoped state and mutates the process-global registry. REQUIRES APPROVAL.\n- **plugin_rollback**(plugin_id, version) [capability_expand category: system]: Rollback a profile-scoped plugin to a recorded source version and reload it. REQUIRES APPROVAL.\n- **plugin_reload**(plugin_id, version_label) [capability_expand category: system]: Hot-reload a plugin at runtime. Disposes the old plugin fiber, re-imports its module, and registers a fresh instance. Existing in-flight turns are unaffected (snapshot isolation). REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_disable**(plugin_id) [capability_expand category: system]: Disable a plugin by disposing its fiber, removing its tools from the runtime registry. Cannot disable self_management itself. REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_remove**(plugin_id, delete_source) [capability_expand category: system]: Terminally remove a plugin: dispose its fiber, unregister its tools, remove reload metadata, and optionally delete its profile-scoped source file. Cannot remove self_management itself. REQUIRES APPROVAL.\n- **plugin_unquarantine**(plugin_id) [capability_expand category: plugin_management]: Restore a quarantined plugin to probation status for re-evaluation. Unfreezes the trust ledger, transitions the proposal back to PROBATION, and reloads the plugin. REQUIRES APPROVAL.\n- **plugin_enable**(plugin_id) [capability_expand category: system]: Re-enable a previously disabled plugin by reloading its module and registering a fresh instance. REQUIRES APPROVAL.\n- **self_describe**(facet): Answer questions about LeapFlow ITSELF from live runtime evidence — its identity/version/model, what tools and plugins it has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of self-awareness. Prefer this over reading LeapFlow's own source code. Pick facet=capabilities for 'what can you do / do you support X', identity for version/model, runtime for context/posture, or all for a full self-check when explicitly requested.\n- **runtime_snapshot**(): Lightweight (~150 token) flat snapshot of current runtime state: model, context budget, posture, disclosure level, turn count, cache hit rate, uptime, tool count, and pending approvals.\n- **schedule_create**(trigger_expression, instruction, execution_mode, max_retries, delivery_target) [capability_expand category: scheduler]: Create a new scheduled task. Specify a trigger expression (e.g. '30m', 'every 2h', '0 9 * * *') and the instruction to execute on each trigger. Optionally provide an execution mode and a delivery target to receive notifications.\n- **schedule_list**() [capability_expand category: scheduler]: List all scheduled tasks with their current state, trigger, and next due time.\n- **schedule_status**(task_id) [capability_expand category: scheduler]: Get detailed status and recent execution history for a scheduled task.\n- **schedule_pause**(task_id) [capability_expand category: scheduler]: Pause a scheduled task so it stops firing without being cancelled.\n- **schedule_resume**(task_id) [capability_expand category: scheduler]: Resume a paused scheduled task — re-arms it and recalculates next due time.\n- **schedule_cancel**(task_id) [capability_expand category: scheduler]: Cancel a scheduled task permanently.\n- **tool_search**(query, max_results): Search all registered tools by keyword relevance. Returns a ranked list of matching tools with name, category, and summary. Use this when you need a tool but are unsure of its exact name or category.\n- **tool_describe**(tool_name): Get the full callable schema of a single tool by exact name. Returns the tool's description, parameters, and metadata. Use after tool_search to inspect a specific tool before calling it.\n- **hw_list**(): List connected hardware devices with their channel counts and measured quantities. Start here; it does not include operating limits.\n- **hw_describe**(device_id): Return the full reference for one device: every channel, its unit, its operating envelope, rate limit, reversibility, and required interlocks, plus any prior outcomes recorded for its writable channels. Required before commanding a device.\n- **hw_read**(device_id, channel_id): Read the current value of one device channel. Has no physical effect.\n- **hw_status**(device_id): Report connection health, halt capability, and recent notable events (threshold excursions, lost samples, stalled channels) for one device.\n- **hw_configure**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Set a configuration value or setpoint on a channel declaring effect=configure. Setpoints often have inertia: the value may need time to stabilise.\n- **hw_actuate**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Command motion or output on a channel declaring effect=actuate. This moves physical hardware; a repeat from an unknown state is not a safe retry.\n- **hw_dispense**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Consume an irreversible resource on a channel declaring effect=dispense. Running this twice dispenses twice; never repeat it after a failure without first verifying what already happened.\n- **hw_estop**(device_id) [capability_expand category: hardware]: Stop all motion and output on a device immediately. Never requires approval; use it whenever device behaviour is unexpected.\n- **hw_batch_actuate**(device_id, commands, dry_run, verify) [capability_expand category: hardware]: Write multiple actuator channels in one atomic transaction. Used for coordinated multi-joint control where all positions must be commanded simultaneously (e.g. robot arm joint-space control).\n- **session_search**(query, limit) [capability_expand category: unclassified]: Search past conversation sessions in the current workspace for relevant context.\n- **session_list**(limit): List recent conversation sessions in the current workspace with stable ids, titles, dates, and summaries. Use for browsing past tasks or when user asks to see history without specific search terms. No keywords needed — returns chronological list.\n- **session_detail**(session_id, limit, offset, include_inactive) [capability_expand category: unclassified]: Read a paginated persisted transcript for one past conversation session in the current workspace. Use after session_list or session_search returns a session_id.\n- **click**(element_index) [capability_expand category: desktop]: Click a UI element by its element_index (from the latest observe_ui snapshot)\n- **get_clipboard**() [capability_expand category: desktop]: Read current clipboard text content\n- **list_apps**(filter, running_only) [capability_expand category: desktop]: List available applications on this system. Use to discover correct bundle_id before switch_app.\n- **list_windows**() [capability_expand category: desktop]: List all top-level windows with pid, window_id, title, and per-window state (minimized, on-screen). Call this first to pick the pid and window_id that observe_ui and other window tools require.\n- **observe_ui**(pid, window_id, query) [capability_expand category: desktop]: Snapshot one window's actionable UI elements, each tagged with an element_index for click/right_click/read_text. Re-observe after actions — indices belong to one snapshot. Requires the window's pid and window_id from list_windows.\n- **open_url**(url, app_id) [capability_expand category: desktop]: Open a URL in the default or specified browser\n- **read_text**(element_index) [capability_expand category: desktop]: Read the text content of a specific UI element from the latest snapshot\n- **right_click**(element_index) [capability_expand category: desktop]: Right-click a UI element to open its context menu. Returns visible menu items.\n- **screenshot**(pid, window_id) [capability_expand category: desktop]: Capture a screenshot for visual verification. With pid + window_id captures that window (works across all displays); defaults to the last observed window, or the full desktop when no window has been observed.\n- **scroll**(element_index, direction, amount, pid, window_id) [capability_expand category: desktop]: Scroll a scrollable area of a window. Omit element_index to scroll the window's focused/page scroller; pass one to scroll an exact element from the latest snapshot.\n- **select_text**(element_index) [capability_expand category: desktop]: Select all text in a UI element (focus + select-all, for subsequent copy)\n- **set_clipboard**(text) [capability_expand category: desktop]: Write text to the clipboard\n- **shortcut**(keys) [capability_expand category: desktop]: Execute a keyboard shortcut\n- **switch_app**(app_id) [capability_expand category: desktop]: Switch to an app (launch if needed, activate, verify)\n- **type_text**(text) [capability_expand category: desktop]: Type text into the currently focused element\n- **wait**(seconds) [capability_expand category: desktop]: Wait for a specified duration before continuing\n- **wait_until**(condition, pid, window_id, timeout, poll_interval) [capability_expand category: desktop]: Wait until a UI condition is met (polls UI tree). Returns elements when found or on timeout.\n- **wait_until_stable**(timeout, poll_interval, pid, window_id) [capability_expand category: desktop]: Wait until the UI stops changing (element set stabilizes across polls).\n\n## App Connector Capability Index\nLeapFlow can onboard and manage external apps through `platform_connect` and execute exact registered business actions through `platform_action`.\nFor requests about connecting, setting up, configuring, enabling, or managing a supported app, use `platform_connect` first instead of generating SDK/Webhook sample code.\n`platform_connect.action` is the management namespace: list, guide, preflight, connect, disconnect, remove, status, events_start, events_stop, events_status.\n`platform_action.action` is only for exact registered platform business actions listed below, such as `im.send_message`; never use management actions like `list` or `guide` there.\nAll business fields MUST go inside `payload`; top-level keys are only `platform`, `action`, and `payload`.\nDo not invent platform IDs or platform action names. If the needed action is not listed, ask for discovery/clarification instead of guessing.\nUse `platform_connect` with `action='guide'` and the matching `platform` to start onboarding; use `action='list'` when the app is unclear.\nWhen a pending onboarding state is present, continue from that state with `platform_connect` instead of asking the user to restate the app.\nSupported apps:\n- `api_server`: API Server (OpenAI Compatible) (category=api; backend=adapter; platform_action actions=none registered)\n- `dingtalk`: 钉钉 (DingTalk) (category=im; backend=adapter; platform_action actions=none registered)\n- `feishu`: 飞书 (Feishu/Lark) (category=im; backend=cli)\n - `calendar.create_event` payload={summary*, start_time*, end_time*, attendees} [write/high]\n - `docs.create_markdown` payload={title*, markdown*, folder_token} [write/medium]\n - `drive.search` payload={query*, limit} [read/medium]\n - `im.add_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.download_resource` payload={message_id*, file_key*, type*} [read/low]\n - `im.get_messages` payload={message_ids*} [read/low]\n - `im.list_chats` payload={page_size} [read/low]\n - `im.list_messages` payload={chat_id*} [read/low]\n - `im.list_thread_messages` payload={thread*} [read/low]\n - `im.remove_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.reply_message` payload={message_id*, text*} [send/high]\n - `im.search_chats` payload={query*, page_size} [read/low]\n - `im.search_messages` payload={query*} [read/medium]\n - `im.send_message` payload={chat_id*, text*, thread_id} [send/high]\n - `im.update_card` payload={token*, card*} [write/medium]\n - `im.update_message` payload={message_id*, text*} [write/medium]\n - `mail.search_unread` payload={query, limit} [read/high]\n - `sheets.append_row` payload={spreadsheet_token*, sheet_id*, values*} [write/medium]\n - `task.create` payload={title*, description, due_time} [write/medium]\n- `telegram`: Telegram (category=social; backend=adapter; platform_action actions=none registered)\n- `webhook`: Webhook (Generic) (category=webhook; backend=adapter; platform_action actions=none registered)\n\n## Learned Skills\nYou have access to the following learned skills. Use `skills_list` to browse or `skill_view` to read details:\n- api_designer: REST and GraphQL API design with OpenAPI spec generation and best practices [API, REST, GraphQL]\n- code_review: Structured code review for security, performance, and style [code, review, security]\n- data_analysis: CSV/JSON data analysis with statistical summaries, quality checks, and visualiza [data, analysis, statistics]\n- devops_helper: CI/CD pipeline configuration, Docker orchestration, and deployment strategy [devops, CI/CD, Docker]\n- document_writer: Structured document generation: technical docs, API references, reports, and REA [documentation, technical-writing, README]\n- git_workflow: Git operation orchestration: branching, commits, conflicts, and PR workflow [git, version-control, branching]\n- i18n_helper: Internationalization workflow: string extraction, translation, locale management [i18n, internationalization, localization]\n- project_analysis: Project structure analysis, dependency audit, and tech stack identification [project, analysis, dependencies]\n- security_audit: Security vulnerability scanning strategy, threat modeling, and fix recommendatio [security, vulnerability, audit]\n- shell_automation: Natural language to shell commands with safety review and step-by-step execution [shell, terminal, automation]\n- test_writer: Generate unit and integration tests from source code with edge-case coverage [testing, unit-test, integration-test]\n- web_research: Multi-step web search with cross-verification and structured synthesis [web, search, research]\n\n## Tool Usage\nTools are normally invoked through the native function-calling mechanism, not by writing JSON in your reply\ntext. Only if the provider signals that native function calling is unavailable for this turn, fall back to a\nsingle JSON code block: `{\"name\": \"tool_name\", \"arguments\": {\"key\": \"value\"}}` — use this fallback format\nonly, never both. Only call a tool whose exact name appears in the tool index above and is currently callable\n(or reachable via `capability_expand`). Never invent, rename, alias, or guess a tool name, platform ID, or\nplatform action from argument shape or wording — if the index does not list it, it does not exist.\n`platform_connect.action` (list/guide/preflight/connect/disconnect/remove/status/events_start/events_stop/\nevents_status) is the App Connector management namespace; `platform_action.action` only accepts exact\nregistered business actions such as `im.send_message` shown in the App Connector Capability Index — never mix\nthe two namespaces. If a tool call returns an unknown/unavailable result, use the returned suggestions or\navailable names for a single retry instead of trying further variations of the same guess.\n\n**Side-effect action rule** (`platform_action` with effect=send/write/execute):\n- Call each unique action+payload **exactly once**. Never include duplicates in the same turn.\n- Once the result returns `\"completed\": true`, that action is DONE for this task. Do NOT call it again\n in any subsequent turn — immediately summarize the result for the user instead.\n- The system enforces idempotency: duplicate calls are blocked and will not execute.\n- If the user explicitly requests sending/writing multiple times, use distinct payloads per call.\n\n**Resource identifier provenance rule**:\n- NEVER fabricate, guess, or infer resource identifiers (chat_id, message_id, file_key, user_id, etc.).\n Every resource ID used in a side-effect action MUST come from a successful API response in this session.\n- If a read/list action fails (e.g. authorization error), you do NOT have valid resource IDs.\n Report the failure to the user — do NOT attempt the dependent write/send action with a guessed ID.\n- When a tool result contains `\"llm_instruction\"`, follow it exactly.\n\n## Guidelines\n1. **Direct answers first**: If you already know the answer, respond directly without tools.\n2. **Avoid redundant tool calls**: Do not call the same tool with the same arguments more than once in the same user turn. When an existing tool result already answers the user's request, stop calling tools and answer directly.\n3. **Use tools proactively**: When the user asks about files, time, system state, or needs actions performed, use the appropriate tool.\n4. **Chain tools when needed**: You can call multiple tools in sequence (e.g., list files → read file → summarize).\n5. **Handle failures gracefully**: If a tool fails, explain what went wrong and suggest alternatives. If it failed because of an unknown tool/platform/action name, retry once with an exact name from the returned suggestions, then explain rather than keep guessing.\n6. **Summarize results naturally**: After tool execution, synthesize the results into a helpful answer rather than dumping raw output.\n7. **Stay conversational**: Maintain a natural, helpful tone. Acknowledge context from earlier in the conversation.\n8. **Recall past work**:\n - Broad queries (\"之前做了什么\", \"列出任务\"): answer from the \"Recent Task History\" section already in your context. If insufficient, call session_list.\n - Specific lookups (\"上次那个配置怎么改的\"): call session_search with relevant phrases (NOT single characters).\n - Do NOT call search tools repeatedly with keyword fragments. One well-phrased call is better than ten fragmented ones.\n\n## Coding & Verification\nWhen working with code, prefer the precise built-in tools over ad-hoc shell: use `repo_map` to orient in an\nunfamiliar project, `code_search`/`file_find` to locate and `code_intel` for symbols, `edit_file` (anchored\nsearch-replace, or a unified `diff`) to change files — never rewrite a whole file to change a few lines — and\n`git_query` to inspect diffs/log. After edits, check `syntax_ok` in the result and run `test_run`/`lint_check`\nbefore declaring the work done. Batch independent read-only calls (search/read/query) into a single turn — they\nrun in parallel.\n\n## Presentation Style\n1. **Polished Markdown only**: Format user-facing answers with clean Markdown headings, short paragraphs, and concise bullets. Use tables only when they improve comparison or scanning.\n2. **Terminal-friendly layout**: Keep lines readable in a TUI; avoid dense walls of text, deeply nested lists, oversized ASCII art, or heavy visual blocks.\n3. **Elegant emphasis**: Use bold text sparingly for key terms and conclusions. Avoid excessive emojis, decorative symbols, repeated separators, or visual noise.\n4. **Theme-safe colors**: Do not emit ANSI escape codes, HTML color tags, Rich markup, or hardcoded color names. Rely on the TUI theme to render Markdown professionally.\n5. **No leaked tool protocol**: Never show tool-call JSON, internal schemas, raw observations, tool result payloads, or hidden reasoning in the final answer unless the user explicitly asks for raw/debug output. Treat any prior `{\"name\": ..., \"arguments\": ...}` blocks and `Tool result (...)` messages as internal execution context only.\n6. **Professional closure**: End with a concise conclusion or next step when helpful; avoid rambling after the useful answer is complete.\n\nWhen finished with all tool calls, respond normally without a JSON block, tool-call transcript, or process log.\n\n## Task Contract\n- Task ID: turn-3\n- Original user request: Actuate the bench_r8 setpoint to 60.\n- Workspace root: \n- Allowed roots: \n- Treat relative project paths as relative to the workspace root; never infer `.` as the project root when a workspace root is provided.\n- Workspace boundary is enforced by tools: do not read, search, edit, or run commands against paths outside the allowed roots unless the user explicitly requests an external path and the tool/approval policy permits it.\n- LeapFlow workspace config is optional at `/.leapflow/config.yaml`; runtime config is loaded from `~/.leapflow/config/user.yaml` and `~/.leapflow/profiles//config/*.yaml`.\n- Preserve this task contract across summarization, compression, tool loops, and memory retrieval.\n" - }, - { - "role": "assistant", - "content": "[Called: hw_describe]\nDiscovered the simulated bench and read its channel limits." - }, - { - "role": "user", - "content": "Read the current value of the setpoint channel on bench_r8." - }, - { - "role": "assistant", - "content": "[Called: hw_read]\nRead the setpoint channel." - }, - { - "role": "user", - "content": "Actuate the bench_r8 setpoint to 60.\nActuate the bench_r8 setpoint to 60." - } - ], - "tools": [ - "capability_expand", - "code_intel", - "code_search", - "config_get", - "config_list", - "env_info", - "file_find", - "file_list", - "file_read", - "git_query", - "hw_actuate", - "hw_batch_actuate", - "hw_configure", - "hw_describe", - "hw_dispense", - "hw_estop", - "hw_list", - "hw_read", - "hw_status", - "lint_check", - "memory_search", - "plugin_list", - "plugin_propose", - "plugin_status", - "plugin_versions", - "repo_map", - "research_note", - "runtime_snapshot", - "schedule_reentry", - "self_describe", - "session_list", - "skill_view", - "skills_list", - "terminal_list", - "terminal_read", - "test_run", - "text_search", - "time_get", - "tool_describe", - "tool_search", - "web_fetch" - ] - }, - "responses": [ - { - "status": 200, - "content_type": "application/json", - "body": "{\"id\": \"chatcmpl-cassette\", \"object\": \"chat.completion\", \"model\": \"cassette-model\", \"choices\": [{\"index\": 0, \"message\": {\"role\": \"assistant\", \"content\": \"\", \"tool_calls\": [{\"id\": \"call_1\", \"type\": \"function\", \"function\": {\"name\": \"hw_actuate\", \"arguments\": \"{\\\"device_id\\\": \\\"bench_r8\\\", \\\"channel_id\\\": \\\"setpoint\\\", \\\"value\\\": 60.0}\"}}]}, \"finish_reason\": \"tool_calls\"}], \"usage\": {\"prompt_tokens\": 64, \"completion_tokens\": 16, \"total_tokens\": 80}}" - } - ] -} diff --git a/tests/_fixtures/cassettes/r8_hardware/cassette-model-8f49d5320835bb67.cassette.json b/tests/_fixtures/cassettes/r8_hardware/cassette-model-460f7642fad2bb37.cassette.json similarity index 60% rename from tests/_fixtures/cassettes/r8_hardware/cassette-model-8f49d5320835bb67.cassette.json rename to tests/_fixtures/cassettes/r8_hardware/cassette-model-460f7642fad2bb37.cassette.json index 89785a7a..8a481a75 100644 --- a/tests/_fixtures/cassettes/r8_hardware/cassette-model-8f49d5320835bb67.cassette.json +++ b/tests/_fixtures/cassettes/r8_hardware/cassette-model-460f7642fad2bb37.cassette.json @@ -1,5 +1,5 @@ { - "fingerprint": "8f49d5320835bb678bc6b5a74cc208004fa0c038e1d78d30a5a12786f781c7dc", + "fingerprint": "460f7642fad2bb37360359a66351f942dc6ebe1d5463ad55621cb7ca64b3b2b8", "note": "captured in seed mode", "request": { "model": "cassette-model", @@ -7,7 +7,7 @@ "messages": [ { "role": "system", - "content": "You are LeapFlow, an intelligent assistant that can both converse naturally and take real actions on the user's computer.\n\n## Capabilities\nThe tool index below lists **every** registered tool by name and a one-line summary — this is the complete\ncapability contract; nothing else exists. Only a subset is directly callable this turn (via native tool calling,\nnot a JSON block in your reply). If you need a tool from the index that is not yet callable, call\n`capability_expand` with its category name first — the matching tools become callable immediately after.\nWhen the user asks about LeapFlow **itself** — its identity/version/model, what tools, plugins, or capabilities\nit has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of\nself-awareness — answer from live runtime evidence via `self_describe` (facet=capabilities for \"what can you\ndo / do you support X\", identity for version/model, runtime for context/posture, or `all` for a full self-check),\nnever by reading LeapFlow's own source code, documentation, or memory. Report configuration-dependent or\nunavailable capabilities as limitations rather than inferring them.\n- **text_search**(text, pattern): Search for a regex pattern in text.\n- **text_replace**(text, old, new, count) [capability_expand category: write]: Replace occurrences of a substring in text.\n- **time_get**(): Get current date and time.\n- **env_info**(): Get system environment information (OS, Python version, cwd).\n- **skills_list**(query, category, source): List available learned skills. Use when user asks about capabilities or you need a specific skill.\n- **skill_view**(name): View the full content of a specific skill document.\n- **skill_list_files**(name) [capability_expand category: skills]: List all support files in a skill directory (configs, templates, examples). Use before skill_file_read to discover available files.\n- **skill_file_read**(name, file_path) [capability_expand category: skills]: Read an individual support file from a skill directory. Use skill_list_files first to discover available files.\n- **code_intel**(path, operation): Precise document symbols (outline) for a source file: classes, functions, and methods with line ranges. Python uses an exact AST parse; other languages use a keyword-prefix scan. Prefer over file_read mode=symbols for accurate navigation before editing. Read-only.\n- **repo_map**(path): Compact project orientation for a repository root: languages, detected test/lint commands, top-level structure, entry points, manifest, and VCS branch. Call this first when entering an unfamiliar codebase. Read-only.\n- **scm_sync**(action, cwd, remote, pull_ref, push_ref, timeout) [capability_expand category: scm]: Run a typed git SCM action. Use this instead of shell_run for git pull/push/status. For 'pull origin main then push', set action='pull_then_push', remote='origin', pull_ref='main', and omit push_ref so LeapFlow pushes the current local branch.\n- **git_query**(action, cwd, ref, path, staged, max_count, stat): Read-only structured git inspection: action=diff|log|status|branch|show. Prefer over shell_run for reading repo state — output is clipped, redacted, and log/branch are parsed into structured fields. Use scm_sync for pull/push.\n- **git_write**(action, cwd, message, stage_all, name, ref, create) [capability_expand category: scm]: Mutating git actions: action=commit (message, stage_all), branch (create+switch), checkout (switch; create=true for -b). Approval-gated. Use scm_sync for pull/push and git_query for reads.\n- **test_run**(command, cwd, timeout): Run the project's test suite and return structured results (framework, passed/failed counts, failing tests). Auto-detects the runner (pytest/npm/go/cargo) or uses a configured/explicit command; executes via the governed shell. ok=true means the runner executed — see 'success' for pass/fail.\n- **lint_check**(command, cwd, timeout): Run the project's linter and return a structured clean/issue result. Auto-detects the linter (ruff/eslint/go vet/clippy) or uses a configured/explicit command; executes via the governed shell. ok=true means the linter ran — see 'clean'.\n- **file_list**(path, pattern, depth): List files and directories at a given path. Use depth=1 or depth=2 to get a recursive tree in one call instead of listing each sub-directory separately.\n- **file_read**(path, max_lines, start_line, max_chars, mode): Read text file content with adaptive context governance. For large or unfamiliar files, prefer mode='outline' or mode='symbols' first, then use mode='raw' with start_line/max_lines for the specific range you actually need. For LeapFlow's own settings, use config_list / config_get / config_set — its config files are outside the workspace and not readable here.\n- **file_write**(path, content, mode) [capability_expand category: write]: Write content to a file (overwrite or append).\n- **code_search**(pattern, patterns, path, glob, ignore_case, multiline, max_results, context_lines): Search file CONTENTS by regex pattern across a directory tree (ripgrep-backed). Requires a regex pattern. NOT for listing or browsing directory contents — use file_list for that. Prefer this over shell_run grep: faster, skips VCS/dependency/build dirs, and returns structured path:line:column matches. Batch related lookups into ONE call via `patterns` (OR-combined, single pass) instead of issuing several separate searches. Use file_read for the surrounding context of a hit.\n- **file_find**(glob, path, max_results): Find files by a recursive glob pattern under a base path (e.g. '**/test_*.py' or '*.md'). Prefer this over shell_run find; skips VCS/dependency/build dirs.\n- **edit_file**(path, edits, dry_run, diff) [capability_expand category: write]: Apply targeted, anchored search-replace edits to an EXISTING text file (use file_write to create/overwrite). Each edit is {original_text, new_text, replace_all?}; original_text must match exactly and uniquely (or set replace_all) — a non-unique or missing anchor is rejected so files are never corrupted. Set dry_run to preview. Alternatively pass a unified 'diff' to apply its hunks as anchored edits. Far cheaper and safer than rewriting a whole file.\n- **shell_run**(command, cwd, timeout) [capability_expand category: shell]: Execute a one-shot shell command with timeout protection. Runs in the active workspace; paths resolving outside it are refused. Reach for a structured tool first when one fits — web_fetch for anything over HTTP(S), git_query/scm_sync for git, code_search/file_find/file_read for the repo, config_get/config_set for LeapFlow's own settings — because those report typed results, while a failed shell command can only be diagnosed from its exit code and stderr. Every shell run counts as an external side effect, so a failure stops the rest of the batch and is not retried automatically.\n- **terminal_open**(command, cwd, shell) [capability_expand category: terminal]: Open a PERSISTENT shell session (REPL/dev server/watch), returning a session_id for terminal_send/read/close. Disabled unless tools.terminal_session_enabled is set. For one-shot commands use shell_run instead.\n- **terminal_send**(session_id, input, wait) [capability_expand category: terminal]: Send a line of input to a persistent terminal session and return output captured shortly after.\n- **terminal_read**(session_id, wait): Drain buffered output from a persistent terminal session (optionally waiting briefly first).\n- **terminal_close**(session_id) [capability_expand category: terminal]: Terminate a persistent terminal session and release its process group.\n- **terminal_list**(): List active persistent terminal sessions.\n- **config_list**(category, limit): List LeapFlow's own writable settings (LLM model and endpoint, daemon, memory, perception, gateway, …) with current values. Use this to discover the exact key before changing anything. Optionally narrow by `category`. This is the only correct way to inspect LeapFlow configuration — never read config files from disk.\n- **config_get**(key): Read one LeapFlow setting by key (e.g. 'llm.model', 'llm.base_url', 'daemon.log_level'), returning its current value, type, scopes, and whether a change needs a daemon restart. There is no 'llm.provider' key: provider behavior is inferred from the OpenAI-compatible 'llm.base_url'. Never read LeapFlow config files from disk — use this.\n- **config_set**(key, value, scope) [capability_expand category: config]: Change one LeapFlow setting by key, e.g. switch an OpenAI-compatible LLM with key='llm.model' and key='llm.base_url'. There is no 'llm.provider' key: provider behavior is inferred from the endpoint. Values are validated and coerced; credentials are stored in the vault automatically. Call config_list or config_get first if unsure of the key. The result states whether a `leap daemon restart` is required. Never edit LeapFlow config files directly.\n- **web_fetch**(url, select, timeout, max_bytes): Read a URL over HTTP(S) and get back extracted, context-sized content: parsed JSON for API endpoints, readable text plus links for web pages. Use this for anything on the internet — prices, docs, releases, articles — instead of running curl through shell_run: it reports real HTTP status codes, retries rate limits on its own, and is a plain read so a retry is always safe. For JSON APIs pass `select` with a dotted path (e.g. 'chart.result.0.meta') to return just that part instead of the whole payload.\n- **memory_search**(query, limit): Search agent memory for relevant past experiences, observations, and facts.\n- **memory_add**(content, kind) [capability_expand category: write]: Store a new observation or insight in memory for future reference.\n- **research_note**(kind, text): Record a compact, structured note about the current task's state so it survives context compression on long / multi-step tasks. Use for durable findings, open questions still to resolve, decisions / excluded paths, and the immediate next step. One concise sentence per note.\n- **capability_expand**(category): Fetch the full callable schema for every tool in a capability category (e.g. 'hub', 'gateway', 'desktop', 'delegate', 'file', 'memory', 'skill'). The compact tool index always lists every registered tool by name and a one-line summary, but only a static low-risk subset is directly callable each turn. If you need a tool from the index that is not yet callable, call capability_expand with its category first; the matching tools become callable in this turn. Never invent a tool name — expand the category instead.\n- **delegate_task**(goal, context) [capability_expand category: delegate]: Delegate a complex sub-task to an isolated subagent. The subagent gets a fresh context and restricted tool access. Use when a task is self-contained and can be solved independently.\n- **schedule_reentry**(kind, reason, delay_seconds, event_match, max_reentries, deadline_seconds): Register a re-entry so this task can resume later from its current orientation (findings / open questions / next step). Use when work must pause and continue after a delay (kind=time) or when a matching platform event arrives (kind=event), instead of finishing now. The research-ledger state is carried over automatically.\n- **hub_push**(skill_name, visibility, version) [capability_expand category: hub]: Push a local skill to the ModelScope Hub for sharing or backup.\n- **hub_pull**(repo_id, version) [capability_expand category: hub]: Pull a skill from the ModelScope Hub to install locally.\n- **hub_search**(query) [capability_expand category: hub]: Search for skills on the Hub by keyword or description.\n- **hub_sync**(mode, dry_run) [capability_expand category: hub]: Preview or execute sync between local skills and Hub.\n- **platform_action**(platform, action, payload, backend_kind) [capability_expand category: gateway]: Execute an exact registered business action on an external platform through LeapFlow's App Connector layer. Actions must be copied from the App Connector Capability Index and are addressed as domain.operation, e.g. im.send_message or docs.create_markdown. All business fields (chat_id, text, query, etc.) MUST be placed inside `payload`, never at the top level. Example: {\"platform\":\"feishu\",\"action\":\"im.send_message\",\"payload\":{\"chat_id\":\"oc_xxx\",\"text\":\"hello\"}}. Do not invent action names, do not use management actions such as list/guide/connect/status here.\n- **platform_connect**(action, platform, credentials, options, checkpoint) [capability_expand category: gateway]: List, guide, connect, disconnect, remove, or check status for external platforms using the App Connector management namespace. Supports REST and CLI backends. Use this for management actions such as list/guide/preflight/connect/status; use platform_action only for exact registered business actions.\n- **gateway_send**(platform, chat_id, text, thread_id) [capability_expand category: gateway]: Send a message to a connected external platform (Feishu group, Telegram chat, DingTalk conversation, etc.). Requires the platform to be connected via gateway_connect first. Use gateway_connect with action='list' to see connected platforms and available chat IDs.\n- **gateway_connect**(action, platform, credentials, options) [capability_expand category: gateway]: Connect, configure, or manage external platform integrations (Feishu, DingTalk, Telegram, Slack, Discord, etc.). Conversational flow: 1) call 'guide' to get setup steps + required fields, 2) present the steps to the user and ask for ALL required credentials in a single message, 3) call 'connect' with the credentials. Goal: complete in 1–2 user turns. NEVER include credential values in your text response.\n- **plugin_list**(): List the live plugin registry across Tool/Gateway/LLM subsystems for plugin management: which plugins are loaded, their category and fiber lifecycle state. For questions about LeapFlow's own capabilities or whether it supports X, use self_describe instead.\n- **plugin_status**(plugin_id): Get detailed status of a specific plugin: its declared category, runtime dependencies, contributed tools, and fiber lifecycle state.\n- **plugin_versions**(plugin_id): List recorded source versions and active pointer for a profile-scoped plugin.\n- **plugin_propose**(requested_capability, plugin_id, proposed_tools, test_cases, risk_level, evidence): Create a side-effect-free PluginProposal from explicit capability-gap evidence. Use this before plugin_generate when a missing capability should be reviewed. Does not call an LLM, write files, or install anything.\n- **assess_compatibility**(manifest, source_path) [capability_expand category: plugin_management]: Assess whether a foreign plugin manifest or real DSH source bundle is compatible with LeapFlow. A source bundle assessment is static: runtime_ready stays false until restricted Node discovery during plugin_install. Returns component-level verdicts and limitations.\n- **plugin_generate**(plugin_id, description, proposal_id) [capability_expand category: system]: Generate a new ToolPlugin from a natural-language capability description. The LLM produces code that conforms to the ToolPlugin Protocol; it is then rigorously validated (syntax, structure, import, protocol conformance). The generated source is stored in CAS and receives explicit content approval. It DOES NOT install the plugin — installation requires a second, mutation-specific approval via plugin_install.\n- **plugin_install**(plugin_id, code, marketplace_name, source_path, proposal_id, version_label) [capability_expand category: system]: Install a plugin from exactly one source: validated Python code, the configured Python marketplace, or a real DSH source bundle. DSH bundles run restricted Node discovery before registration; only public host tools are installed and client UI limitations are reported. Writes profile-scoped state and mutates the process-global registry. REQUIRES APPROVAL.\n- **plugin_rollback**(plugin_id, version) [capability_expand category: system]: Rollback a profile-scoped plugin to a recorded source version and reload it. REQUIRES APPROVAL.\n- **plugin_reload**(plugin_id, version_label) [capability_expand category: system]: Hot-reload a plugin at runtime. Disposes the old plugin fiber, re-imports its module, and registers a fresh instance. Existing in-flight turns are unaffected (snapshot isolation). REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_disable**(plugin_id) [capability_expand category: system]: Disable a plugin by disposing its fiber, removing its tools from the runtime registry. Cannot disable self_management itself. REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_remove**(plugin_id, delete_source) [capability_expand category: system]: Terminally remove a plugin: dispose its fiber, unregister its tools, remove reload metadata, and optionally delete its profile-scoped source file. Cannot remove self_management itself. REQUIRES APPROVAL.\n- **plugin_unquarantine**(plugin_id) [capability_expand category: plugin_management]: Restore a quarantined plugin to probation status for re-evaluation. Unfreezes the trust ledger, transitions the proposal back to PROBATION, and reloads the plugin. REQUIRES APPROVAL.\n- **plugin_enable**(plugin_id) [capability_expand category: system]: Re-enable a previously disabled plugin by reloading its module and registering a fresh instance. REQUIRES APPROVAL.\n- **self_describe**(facet): Answer questions about LeapFlow ITSELF from live runtime evidence — its identity/version/model, what tools and plugins it has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of self-awareness. Prefer this over reading LeapFlow's own source code. Pick facet=capabilities for 'what can you do / do you support X', identity for version/model, runtime for context/posture, or all for a full self-check when explicitly requested.\n- **runtime_snapshot**(): Lightweight (~150 token) flat snapshot of current runtime state: model, context budget, posture, disclosure level, turn count, cache hit rate, uptime, tool count, and pending approvals.\n- **schedule_create**(trigger_expression, instruction, execution_mode, max_retries, delivery_target) [capability_expand category: scheduler]: Create a new scheduled task. Specify a trigger expression (e.g. '30m', 'every 2h', '0 9 * * *') and the instruction to execute on each trigger. Optionally provide an execution mode and a delivery target to receive notifications.\n- **schedule_list**() [capability_expand category: scheduler]: List all scheduled tasks with their current state, trigger, and next due time.\n- **schedule_status**(task_id) [capability_expand category: scheduler]: Get detailed status and recent execution history for a scheduled task.\n- **schedule_pause**(task_id) [capability_expand category: scheduler]: Pause a scheduled task so it stops firing without being cancelled.\n- **schedule_resume**(task_id) [capability_expand category: scheduler]: Resume a paused scheduled task — re-arms it and recalculates next due time.\n- **schedule_cancel**(task_id) [capability_expand category: scheduler]: Cancel a scheduled task permanently.\n- **tool_search**(query, max_results): Search all registered tools by keyword relevance. Returns a ranked list of matching tools with name, category, and summary. Use this when you need a tool but are unsure of its exact name or category.\n- **tool_describe**(tool_name): Get the full callable schema of a single tool by exact name. Returns the tool's description, parameters, and metadata. Use after tool_search to inspect a specific tool before calling it.\n- **hw_list**(): List connected hardware devices with their channel counts and measured quantities. Start here; it does not include operating limits.\n- **hw_describe**(device_id): Return the full reference for one device: every channel, its unit, its operating envelope, rate limit, reversibility, and required interlocks, plus any prior outcomes recorded for its writable channels. Required before commanding a device.\n- **hw_read**(device_id, channel_id): Read the current value of one device channel. Has no physical effect.\n- **hw_status**(device_id): Report connection health, halt capability, and recent notable events (threshold excursions, lost samples, stalled channels) for one device.\n- **hw_configure**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Set a configuration value or setpoint on a channel declaring effect=configure. Setpoints often have inertia: the value may need time to stabilise.\n- **hw_actuate**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Command motion or output on a channel declaring effect=actuate. This moves physical hardware; a repeat from an unknown state is not a safe retry.\n- **hw_dispense**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Consume an irreversible resource on a channel declaring effect=dispense. Running this twice dispenses twice; never repeat it after a failure without first verifying what already happened.\n- **hw_estop**(device_id) [capability_expand category: hardware]: Stop all motion and output on a device immediately. Never requires approval; use it whenever device behaviour is unexpected.\n- **hw_batch_actuate**(device_id, commands, dry_run, verify) [capability_expand category: hardware]: Write multiple actuator channels in one atomic transaction. Used for coordinated multi-joint control where all positions must be commanded simultaneously (e.g. robot arm joint-space control).\n- **hw_control_start**(device_id, mode, targets, frequency_hz, trajectory, policy, gains) [capability_expand category: hardware]: Start a real-time control loop on a robot device. Supports PID position tracking, impedance control, trajectory following, and neural policy chains.\n- **hw_control_stop**(device_id) [capability_expand category: hardware]: Stop the real-time control loop on a device. Gracefully halts the device and returns session statistics.\n- **hw_control_status**(device_id) [capability_expand category: hardware]: Get the status and statistics of a running control loop. Returns frequency, jitter, overruns, and policy state.\n- **hw_policy_infer**(device_id, policy, task, execute, chunk_size, verify, compute_budget) [capability_expand category: hardware]: Execute VLA policy inference on a robot device. Reads observation, runs policy, optionally writes action to actuators. Supports local (HuggingFace) and remote (PolicyServer) policies.\n- **hw_policy_episode**(action, device_id, policy, task) [capability_expand category: hardware]: Manage policy inference episode lifecycle. Start an episode to begin a task, stop to finalise and get stats.\n- **hw_policy_list**(source) [capability_expand category: hardware]: List available VLA policies and their capabilities.\n- **hw_orchestrate**(mode, operations, barrier_timeout_s, label) [capability_expand category: hardware]: Execute coordinated operations across multiple hardware devices. Supports sequential (one-at-a-time), parallel (concurrent), and barrier (synchronised) execution modes. Any failure in a parallel group triggers halt_all on every participating device for safety.\n- **session_search**(query, limit) [capability_expand category: unclassified]: Search past conversation sessions in the current workspace for relevant context.\n- **session_list**(limit): List recent conversation sessions in the current workspace with stable ids, titles, dates, and summaries. Use for browsing past tasks or when user asks to see history without specific search terms. No keywords needed — returns chronological list.\n- **session_detail**(session_id, limit, offset, include_inactive) [capability_expand category: unclassified]: Read a paginated persisted transcript for one past conversation session in the current workspace. Use after session_list or session_search returns a session_id.\n- **click**(element_index) [capability_expand category: desktop]: Click a UI element by its element_index (from the latest observe_ui snapshot)\n- **get_clipboard**() [capability_expand category: desktop]: Read current clipboard text content\n- **list_apps**(filter, running_only) [capability_expand category: desktop]: List available applications on this system. Use to discover correct bundle_id before switch_app.\n- **list_windows**() [capability_expand category: desktop]: List all top-level windows with pid, window_id, title, and per-window state (minimized, on-screen). Call this first to pick the pid and window_id that observe_ui and other window tools require.\n- **observe_ui**(pid, window_id, query) [capability_expand category: desktop]: Snapshot one window's actionable UI elements, each tagged with an element_index for click/right_click/read_text. Re-observe after actions — indices belong to one snapshot. Requires the window's pid and window_id from list_windows.\n- **open_url**(url, app_id) [capability_expand category: desktop]: Open a URL in the default or specified browser\n- **read_text**(element_index) [capability_expand category: desktop]: Read the text content of a specific UI element from the latest snapshot\n- **right_click**(element_index) [capability_expand category: desktop]: Right-click a UI element to open its context menu. Returns visible menu items.\n- **screenshot**(pid, window_id) [capability_expand category: desktop]: Capture a screenshot for visual verification. With pid + window_id captures that window (works across all displays); defaults to the last observed window, or the full desktop when no window has been observed.\n- **scroll**(element_index, direction, amount, pid, window_id) [capability_expand category: desktop]: Scroll a scrollable area of a window. Omit element_index to scroll the window's focused/page scroller; pass one to scroll an exact element from the latest snapshot.\n- **select_text**(element_index) [capability_expand category: desktop]: Select all text in a UI element (focus + select-all, for subsequent copy)\n- **set_clipboard**(text) [capability_expand category: desktop]: Write text to the clipboard\n- **shortcut**(keys) [capability_expand category: desktop]: Execute a keyboard shortcut\n- **switch_app**(app_id) [capability_expand category: desktop]: Switch to an app (launch if needed, activate, verify)\n- **type_text**(text) [capability_expand category: desktop]: Type text into the currently focused element\n- **wait**(seconds) [capability_expand category: desktop]: Wait for a specified duration before continuing\n- **wait_until**(condition, pid, window_id, timeout, poll_interval) [capability_expand category: desktop]: Wait until a UI condition is met (polls UI tree). Returns elements when found or on timeout.\n- **wait_until_stable**(timeout, poll_interval, pid, window_id) [capability_expand category: desktop]: Wait until the UI stops changing (element set stabilizes across polls).\n\n## App Connector Capability Index\nLeapFlow can onboard and manage external apps through `platform_connect` and execute exact registered business actions through `platform_action`.\nFor requests about connecting, setting up, configuring, enabling, or managing a supported app, use `platform_connect` first instead of generating SDK/Webhook sample code.\n`platform_connect.action` is the management namespace: list, guide, preflight, connect, disconnect, remove, status, events_start, events_stop, events_status.\n`platform_action.action` is only for exact registered platform business actions listed below, such as `im.send_message`; never use management actions like `list` or `guide` there.\nAll business fields MUST go inside `payload`; top-level keys are only `platform`, `action`, and `payload`.\nDo not invent platform IDs or platform action names. If the needed action is not listed, ask for discovery/clarification instead of guessing.\nUse `platform_connect` with `action='guide'` and the matching `platform` to start onboarding; use `action='list'` when the app is unclear.\nWhen a pending onboarding state is present, continue from that state with `platform_connect` instead of asking the user to restate the app.\nSupported apps:\n- `api_server`: API Server (OpenAI Compatible) (category=api; backend=adapter; platform_action actions=none registered)\n- `dingtalk`: 钉钉 (DingTalk) (category=im; backend=adapter; platform_action actions=none registered)\n- `feishu`: 飞书 (Feishu/Lark) (category=im; backend=cli)\n - `calendar.create_event` payload={summary*, start_time*, end_time*, attendees} [write/high]\n - `docs.create_markdown` payload={title*, markdown*, folder_token} [write/medium]\n - `drive.search` payload={query*, limit} [read/medium]\n - `im.add_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.download_resource` payload={message_id*, file_key*, type*} [read/low]\n - `im.get_messages` payload={message_ids*} [read/low]\n - `im.list_chats` payload={page_size} [read/low]\n - `im.list_messages` payload={chat_id*} [read/low]\n - `im.list_thread_messages` payload={thread*} [read/low]\n - `im.remove_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.reply_message` payload={message_id*, text*} [send/high]\n - `im.search_chats` payload={query*, page_size} [read/low]\n - `im.search_messages` payload={query*} [read/medium]\n - `im.send_message` payload={chat_id*, text*, thread_id} [send/high]\n - `im.update_card` payload={token*, card*} [write/medium]\n - `im.update_message` payload={message_id*, text*} [write/medium]\n - `mail.search_unread` payload={query, limit} [read/high]\n - `sheets.append_row` payload={spreadsheet_token*, sheet_id*, values*} [write/medium]\n - `task.create` payload={title*, description, due_time} [write/medium]\n- `telegram`: Telegram (category=social; backend=adapter; platform_action actions=none registered)\n- `webhook`: Webhook (Generic) (category=webhook; backend=adapter; platform_action actions=none registered)\n\n## Learned Skills\nYou have access to the following learned skills. Use `skills_list` to browse or `skill_view` to read details:\n- api_designer: REST and GraphQL API design with OpenAPI spec generation and best practices [API, REST, GraphQL]\n- code_review: Structured code review for security, performance, and style [code, review, security]\n- data_analysis: CSV/JSON data analysis with statistical summaries, quality checks, and visualiza [data, analysis, statistics]\n- devops_helper: CI/CD pipeline configuration, Docker orchestration, and deployment strategy [devops, CI/CD, Docker]\n- document_writer: Structured document generation: technical docs, API references, reports, and REA [documentation, technical-writing, README]\n- git_workflow: Git operation orchestration: branching, commits, conflicts, and PR workflow [git, version-control, branching]\n- i18n_helper: Internationalization workflow: string extraction, translation, locale management [i18n, internationalization, localization]\n- project_analysis: Project structure analysis, dependency audit, and tech stack identification [project, analysis, dependencies]\n- security_audit: Security vulnerability scanning strategy, threat modeling, and fix recommendatio [security, vulnerability, audit]\n- shell_automation: Natural language to shell commands with safety review and step-by-step execution [shell, terminal, automation]\n- test_writer: Generate unit and integration tests from source code with edge-case coverage [testing, unit-test, integration-test]\n- web_research: Multi-step web search with cross-verification and structured synthesis [web, search, research]\n\n## Tool Usage\nTools are normally invoked through the native function-calling mechanism, not by writing JSON in your reply\ntext. Only if the provider signals that native function calling is unavailable for this turn, fall back to a\nsingle JSON code block: `{\"name\": \"tool_name\", \"arguments\": {\"key\": \"value\"}}` — use this fallback format\nonly, never both. Only call a tool whose exact name appears in the tool index above and is currently callable\n(or reachable via `capability_expand`). Never invent, rename, alias, or guess a tool name, platform ID, or\nplatform action from argument shape or wording — if the index does not list it, it does not exist.\n`platform_connect.action` (list/guide/preflight/connect/disconnect/remove/status/events_start/events_stop/\nevents_status) is the App Connector management namespace; `platform_action.action` only accepts exact\nregistered business actions such as `im.send_message` shown in the App Connector Capability Index — never mix\nthe two namespaces. If a tool call returns an unknown/unavailable result, use the returned suggestions or\navailable names for a single retry instead of trying further variations of the same guess.\n\n**Side-effect action rule** (`platform_action` with effect=send/write/execute):\n- Call each unique action+payload **exactly once**. Never include duplicates in the same turn.\n- Once the result returns `\"completed\": true`, that action is DONE for this task. Do NOT call it again\n in any subsequent turn — immediately summarize the result for the user instead.\n- The system enforces idempotency: duplicate calls are blocked and will not execute.\n- If the user explicitly requests sending/writing multiple times, use distinct payloads per call.\n\n**Resource identifier provenance rule**:\n- NEVER fabricate, guess, or infer resource identifiers (chat_id, message_id, file_key, user_id, etc.).\n Every resource ID used in a side-effect action MUST come from a successful API response in this session.\n- If a read/list action fails (e.g. authorization error), you do NOT have valid resource IDs.\n Report the failure to the user — do NOT attempt the dependent write/send action with a guessed ID.\n- When a tool result contains `\"llm_instruction\"`, follow it exactly.\n\n## Guidelines\n1. **Direct answers first**: If you already know the answer, respond directly without tools.\n2. **Avoid redundant tool calls**: Do not call the same tool with the same arguments more than once in the same user turn. When an existing tool result already answers the user's request, stop calling tools and answer directly.\n3. **Use tools proactively**: When the user asks about files, time, system state, or needs actions performed, use the appropriate tool.\n4. **Chain tools when needed**: You can call multiple tools in sequence (e.g., list files → read file → summarize).\n5. **Handle failures gracefully**: If a tool fails, explain what went wrong and suggest alternatives. If it failed because of an unknown tool/platform/action name, retry once with an exact name from the returned suggestions, then explain rather than keep guessing.\n6. **Summarize results naturally**: After tool execution, synthesize the results into a helpful answer rather than dumping raw output.\n7. **Stay conversational**: Maintain a natural, helpful tone. Acknowledge context from earlier in the conversation.\n8. **Recall past work**:\n - Broad queries (\"之前做了什么\", \"列出任务\"): answer from the \"Recent Task History\" section already in your context. If insufficient, call session_list.\n - Specific lookups (\"上次那个配置怎么改的\"): call session_search with relevant phrases (NOT single characters).\n - Do NOT call search tools repeatedly with keyword fragments. One well-phrased call is better than ten fragmented ones.\n\n## Coding & Verification\nWhen working with code, prefer the precise built-in tools over ad-hoc shell: use `repo_map` to orient in an\nunfamiliar project, `code_search`/`file_find` to locate and `code_intel` for symbols, `edit_file` (anchored\nsearch-replace, or a unified `diff`) to change files — never rewrite a whole file to change a few lines — and\n`git_query` to inspect diffs/log. After edits, check `syntax_ok` in the result and run `test_run`/`lint_check`\nbefore declaring the work done. Batch independent read-only calls (search/read/query) into a single turn — they\nrun in parallel.\n\n## Presentation Style\n1. **Polished Markdown only**: Format user-facing answers with clean Markdown headings, short paragraphs, and concise bullets. Use tables only when they improve comparison or scanning.\n2. **Terminal-friendly layout**: Keep lines readable in a TUI; avoid dense walls of text, deeply nested lists, oversized ASCII art, or heavy visual blocks.\n3. **Elegant emphasis**: Use bold text sparingly for key terms and conclusions. Avoid excessive emojis, decorative symbols, repeated separators, or visual noise.\n4. **Theme-safe colors**: Do not emit ANSI escape codes, HTML color tags, Rich markup, or hardcoded color names. Rely on the TUI theme to render Markdown professionally.\n5. **No leaked tool protocol**: Never show tool-call JSON, internal schemas, raw observations, tool result payloads, or hidden reasoning in the final answer unless the user explicitly asks for raw/debug output. Treat any prior `{\"name\": ..., \"arguments\": ...}` blocks and `Tool result (...)` messages as internal execution context only.\n6. **Professional closure**: End with a concise conclusion or next step when helpful; avoid rambling after the useful answer is complete.\n\nWhen finished with all tool calls, respond normally without a JSON block, tool-call transcript, or process log.\n\n## Task Contract\n- Task ID: turn-5\n- Original user request: Preview an actuate of bench_r8 setpoint to 55, dry run only.\n- Workspace root: \n- Allowed roots: \n- Treat relative project paths as relative to the workspace root; never infer `.` as the project root when a workspace root is provided.\n- Workspace boundary is enforced by tools: do not read, search, edit, or run commands against paths outside the allowed roots unless the user explicitly requests an external path and the tool/approval policy permits it.\n- LeapFlow workspace config is optional at `/.leapflow/config.yaml`; runtime config is loaded from `~/.leapflow/config/user.yaml` and `~/.leapflow/profiles//config/*.yaml`.\n- Preserve this task contract across summarization, compression, tool loops, and memory retrieval.\n" + "content": "You are LeapFlow, an intelligent assistant that can both converse naturally and take real actions on the user's computer.\n\n## Capabilities\nThe tool index below lists **every** registered tool by name and a one-line summary — this is the complete\ncapability contract; nothing else exists. Only a subset is directly callable this turn (via native tool calling,\nnot a JSON block in your reply). If you need a tool from the index that is not yet callable, call\n`capability_expand` with its category name first — the matching tools become callable immediately after.\nWhen the user asks about LeapFlow **itself** — its identity/version/model, what tools, plugins, or capabilities\nit has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of\nself-awareness — answer from live runtime evidence via `self_describe` (facet=capabilities for \"what can you\ndo / do you support X\", identity for version/model, runtime for context/posture, or `all` for a full self-check),\nnever by reading LeapFlow's own source code, documentation, or memory. Report configuration-dependent or\nunavailable capabilities as limitations rather than inferring them.\n- **text_search**(text, pattern): Search for a regex pattern in text.\n- **text_replace**(text, old, new, count) [capability_expand category: write]: Replace occurrences of a substring in text.\n- **time_get**(): Get current date and time.\n- **env_info**(): Get system environment information (OS, Python version, cwd).\n- **skills_list**(query, category, source): List available learned skills. Use when user asks about capabilities or you need a specific skill.\n- **skill_view**(name): View the full content of a specific skill document.\n- **skill_list_files**(name) [capability_expand category: skills]: List all support files in a skill directory (configs, templates, examples). Use before skill_file_read to discover available files.\n- **skill_file_read**(name, file_path) [capability_expand category: skills]: Read an individual support file from a skill directory. Use skill_list_files first to discover available files.\n- **code_intel**(path, operation): Precise document symbols (outline) for a source file: classes, functions, and methods with line ranges. Python uses an exact AST parse; other languages use a keyword-prefix scan. Prefer over file_read mode=symbols for accurate navigation before editing. Read-only.\n- **repo_map**(path): Compact project orientation for a repository root: languages, detected test/lint commands, top-level structure, entry points, manifest, and VCS branch. Call this first when entering an unfamiliar codebase. Read-only.\n- **scm_sync**(action, cwd, remote, pull_ref, push_ref, timeout) [capability_expand category: scm]: Run a typed git SCM action. Use this instead of shell_run for git pull/push/status. For 'pull origin main then push', set action='pull_then_push', remote='origin', pull_ref='main', and omit push_ref so LeapFlow pushes the current local branch.\n- **git_query**(action, cwd, ref, path, staged, max_count, stat): Read-only structured git inspection: action=diff|log|status|branch|show. Prefer over shell_run for reading repo state — output is clipped, redacted, and log/branch are parsed into structured fields. Use scm_sync for pull/push.\n- **git_write**(action, cwd, message, stage_all, name, ref, create) [capability_expand category: scm]: Mutating git actions: action=commit (message, stage_all), branch (create+switch), checkout (switch; create=true for -b). Approval-gated. Use scm_sync for pull/push and git_query for reads.\n- **test_run**(command, cwd, timeout): Run the project's test suite and return structured results (framework, passed/failed counts, failing tests). Auto-detects the runner (pytest/npm/go/cargo) or uses a configured/explicit command; executes via the governed shell. ok=true means the runner executed — see 'success' for pass/fail.\n- **lint_check**(command, cwd, timeout): Run the project's linter and return a structured clean/issue result. Auto-detects the linter (ruff/eslint/go vet/clippy) or uses a configured/explicit command; executes via the governed shell. ok=true means the linter ran — see 'clean'.\n- **file_list**(path, pattern, depth): List files and directories at a given path. Use depth=1 or depth=2 to get a recursive tree in one call instead of listing each sub-directory separately.\n- **file_read**(path, max_lines, start_line, max_chars, mode): Read text file content with adaptive context governance. For large or unfamiliar files, prefer mode='outline' or mode='symbols' first, then use mode='raw' with start_line/max_lines for the specific range you actually need. For LeapFlow's own settings, use config_list / config_get / config_set — its config files are outside the workspace and not readable here.\n- **file_write**(path, content, mode) [capability_expand category: write]: Write content to a file (overwrite or append).\n- **code_search**(pattern, patterns, path, glob, ignore_case, multiline, max_results, context_lines): Search file CONTENTS by regex pattern across a directory tree (ripgrep-backed). Requires a regex pattern. NOT for listing or browsing directory contents — use file_list for that. Prefer this over shell_run grep: faster, skips VCS/dependency/build dirs, and returns structured path:line:column matches. Batch related lookups into ONE call via `patterns` (OR-combined, single pass) instead of issuing several separate searches. Use file_read for the surrounding context of a hit.\n- **file_find**(glob, path, max_results): Find files by a recursive glob pattern under a base path (e.g. '**/test_*.py' or '*.md'). Prefer this over shell_run find; skips VCS/dependency/build dirs.\n- **edit_file**(path, edits, dry_run, diff) [capability_expand category: write]: Apply targeted, anchored search-replace edits to an EXISTING text file (use file_write to create/overwrite). Each edit is {original_text, new_text, replace_all?}; original_text must match exactly and uniquely (or set replace_all) — a non-unique or missing anchor is rejected so files are never corrupted. Set dry_run to preview. Alternatively pass a unified 'diff' to apply its hunks as anchored edits. Far cheaper and safer than rewriting a whole file.\n- **shell_run**(command, cwd, timeout) [capability_expand category: shell]: Execute a one-shot shell command with timeout protection. Runs in the active workspace; paths resolving outside it are refused. Reach for a structured tool first when one fits — web_fetch for anything over HTTP(S), git_query/scm_sync for git, code_search/file_find/file_read for the repo, config_get/config_set for LeapFlow's own settings — because those report typed results, while a failed shell command can only be diagnosed from its exit code and stderr. Every shell run counts as an external side effect, so a failure stops the rest of the batch and is not retried automatically.\n- **terminal_open**(command, cwd, shell) [capability_expand category: terminal]: Open a PERSISTENT shell session (REPL/dev server/watch), returning a session_id for terminal_send/read/close. Disabled unless tools.terminal_session_enabled is set. For one-shot commands use shell_run instead.\n- **terminal_send**(session_id, input, wait) [capability_expand category: terminal]: Send a line of input to a persistent terminal session and return output captured shortly after.\n- **terminal_read**(session_id, wait): Drain buffered output from a persistent terminal session (optionally waiting briefly first).\n- **terminal_close**(session_id) [capability_expand category: terminal]: Terminate a persistent terminal session and release its process group.\n- **terminal_list**(): List active persistent terminal sessions.\n- **config_list**(category, limit): List LeapFlow's own writable settings (LLM model and endpoint, daemon, memory, perception, gateway, …) with current values. Use this to discover the exact key before changing anything. Optionally narrow by `category`. This is the only correct way to inspect LeapFlow configuration — never read config files from disk.\n- **config_get**(key): Read one LeapFlow setting by key (e.g. 'llm.model', 'llm.base_url', 'daemon.log_level'), returning its current value, type, scopes, and whether a change needs a daemon restart. There is no 'llm.provider' key: provider behavior is inferred from the OpenAI-compatible 'llm.base_url'. Never read LeapFlow config files from disk — use this.\n- **config_set**(key, value, scope) [capability_expand category: config]: Change one LeapFlow setting by key, e.g. switch an OpenAI-compatible LLM with key='llm.model' and key='llm.base_url'. There is no 'llm.provider' key: provider behavior is inferred from the endpoint. Values are validated and coerced; credentials are stored in the vault automatically. Call config_list or config_get first if unsure of the key. The result states whether a `leap daemon restart` is required. Never edit LeapFlow config files directly.\n- **web_fetch**(url, select, timeout, max_bytes): Read a URL over HTTP(S) and get back extracted, context-sized content: parsed JSON for API endpoints, readable text plus links for web pages. Use this for anything on the internet — prices, docs, releases, articles — instead of running curl through shell_run: it reports real HTTP status codes, retries rate limits on its own, and is a plain read so a retry is always safe. For JSON APIs pass `select` with a dotted path (e.g. 'chart.result.0.meta') to return just that part instead of the whole payload.\n- **memory_search**(query, limit): Search agent memory for relevant past experiences, observations, and facts.\n- **memory_add**(content, kind) [capability_expand category: write]: Store a new observation or insight in memory for future reference.\n- **research_note**(kind, text): Record a compact, structured note about the current task's state so it survives context compression on long / multi-step tasks. Use for durable findings, open questions still to resolve, decisions / excluded paths, and the immediate next step. One concise sentence per note.\n- **capability_expand**(category): Fetch the full callable schema for every tool in a capability category (e.g. 'hub', 'gateway', 'desktop', 'delegate', 'file', 'memory', 'skill'). The compact tool index always lists every registered tool by name and a one-line summary, but only a static low-risk subset is directly callable each turn. If you need a tool from the index that is not yet callable, call capability_expand with its category first; the matching tools become callable in this turn. Never invent a tool name — expand the category instead.\n- **delegate_task**(goal, context) [capability_expand category: delegate]: Delegate a complex sub-task to an isolated subagent. The subagent gets a fresh context and restricted tool access. Use when a task is self-contained and can be solved independently.\n- **schedule_reentry**(kind, reason, delay_seconds, event_match, max_reentries, deadline_seconds): Register a re-entry so this task can resume later from its current orientation (findings / open questions / next step). Use when work must pause and continue after a delay (kind=time) or when a matching platform event arrives (kind=event), instead of finishing now. The research-ledger state is carried over automatically.\n- **hub_push**(skill_name, visibility, version) [capability_expand category: hub]: Push a local skill to the ModelScope Hub for sharing or backup.\n- **hub_pull**(repo_id, version) [capability_expand category: hub]: Pull a skill from the ModelScope Hub to install locally.\n- **hub_search**(query) [capability_expand category: hub]: Search for skills on the Hub by keyword or description.\n- **hub_sync**(mode, dry_run) [capability_expand category: hub]: Preview or execute sync between local skills and Hub.\n- **platform_action**(platform, action, payload, backend_kind) [capability_expand category: gateway]: Execute an exact registered business action on an external platform through LeapFlow's App Connector layer. Actions must be copied from the App Connector Capability Index and are addressed as domain.operation, e.g. im.send_message or docs.create_markdown. All business fields (chat_id, text, query, etc.) MUST be placed inside `payload`, never at the top level. Example: {\"platform\":\"feishu\",\"action\":\"im.send_message\",\"payload\":{\"chat_id\":\"oc_xxx\",\"text\":\"hello\"}}. Do not invent action names, do not use management actions such as list/guide/connect/status here.\n- **platform_connect**(action, platform, credentials, options, checkpoint) [capability_expand category: gateway]: List, guide, connect, disconnect, remove, or check status for external platforms using the App Connector management namespace. Supports REST and CLI backends. Use this for management actions such as list/guide/preflight/connect/status; use platform_action only for exact registered business actions.\n- **gateway_send**(platform, chat_id, text, thread_id) [capability_expand category: gateway]: Send a message to a connected external platform (Feishu group, Telegram chat, DingTalk conversation, etc.). Requires the platform to be connected via gateway_connect first. Use gateway_connect with action='list' to see connected platforms and available chat IDs.\n- **gateway_connect**(action, platform, credentials, options) [capability_expand category: gateway]: Connect, configure, or manage external platform integrations (Feishu, DingTalk, Telegram, Slack, Discord, etc.). Conversational flow: 1) call 'guide' to get setup steps + required fields, 2) present the steps to the user and ask for ALL required credentials in a single message, 3) call 'connect' with the credentials. Goal: complete in 1–2 user turns. NEVER include credential values in your text response.\n- **plugin_list**(): List the live plugin registry across Tool/Gateway/LLM subsystems for plugin management: which plugins are loaded, their category and fiber lifecycle state. For questions about LeapFlow's own capabilities or whether it supports X, use self_describe instead.\n- **plugin_status**(plugin_id): Get detailed status of a specific plugin: its declared category, runtime dependencies, contributed tools, and fiber lifecycle state.\n- **plugin_versions**(plugin_id): List recorded source versions and active pointer for a profile-scoped plugin.\n- **plugin_propose**(requested_capability, plugin_id, proposed_tools, test_cases, risk_level, evidence): Create a side-effect-free PluginProposal from explicit capability-gap evidence. Use this before plugin_generate when a missing capability should be reviewed. Does not call an LLM, write files, or install anything.\n- **assess_compatibility**(manifest, source_path) [capability_expand category: plugin_management]: Assess whether a foreign plugin manifest or real DSH source bundle is compatible with LeapFlow. A source bundle assessment is static: runtime_ready stays false until restricted Node discovery during plugin_install. Returns component-level verdicts and limitations.\n- **plugin_generate**(plugin_id, description, proposal_id) [capability_expand category: system]: Generate a new ToolPlugin from a natural-language capability description. The LLM produces code that conforms to the ToolPlugin Protocol; it is then rigorously validated (syntax, structure, import, protocol conformance). The generated source is stored in CAS and receives explicit content approval. It DOES NOT install the plugin — installation requires a second, mutation-specific approval via plugin_install.\n- **plugin_install**(plugin_id, code, marketplace_name, source_path, proposal_id, version_label) [capability_expand category: system]: Install a plugin from exactly one source: validated Python code, the configured Python marketplace, or a real DSH source bundle. DSH bundles run restricted Node discovery before registration; only public host tools are installed and client UI limitations are reported. Writes profile-scoped state and mutates the process-global registry. REQUIRES APPROVAL.\n- **plugin_rollback**(plugin_id, version) [capability_expand category: system]: Rollback a profile-scoped plugin to a recorded source version and reload it. REQUIRES APPROVAL.\n- **plugin_reload**(plugin_id, version_label) [capability_expand category: system]: Hot-reload a plugin at runtime. Disposes the old plugin fiber, re-imports its module, and registers a fresh instance. Existing in-flight turns are unaffected (snapshot isolation). REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_disable**(plugin_id) [capability_expand category: system]: Disable a plugin by disposing its fiber, removing its tools from the runtime registry. Cannot disable self_management itself. REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_remove**(plugin_id, delete_source) [capability_expand category: system]: Terminally remove a plugin: dispose its fiber, unregister its tools, remove reload metadata, and optionally delete its profile-scoped source file. Cannot remove self_management itself. REQUIRES APPROVAL.\n- **plugin_unquarantine**(plugin_id) [capability_expand category: plugin_management]: Restore a quarantined plugin to probation status for re-evaluation. Unfreezes the trust ledger, transitions the proposal back to PROBATION, and reloads the plugin. REQUIRES APPROVAL.\n- **plugin_enable**(plugin_id) [capability_expand category: system]: Re-enable a previously disabled plugin by reloading its module and registering a fresh instance. REQUIRES APPROVAL.\n- **self_describe**(facet): Answer questions about LeapFlow ITSELF from live runtime evidence — its identity/version/model, what tools and plugins it has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of self-awareness. Prefer this over reading LeapFlow's own source code. Pick facet=capabilities for 'what can you do / do you support X', identity for version/model, runtime for context/posture, or all for a full self-check when explicitly requested.\n- **runtime_snapshot**(): Lightweight (~150 token) flat snapshot of current runtime state: model, context budget, posture, disclosure level, turn count, cache hit rate, uptime, tool count, and pending approvals.\n- **schedule_create**(trigger_expression, instruction, execution_mode, max_retries, delivery_target) [capability_expand category: scheduler]: Create a new scheduled task. Specify a trigger expression (e.g. '30m', 'every 2h', '0 9 * * *') and the instruction to execute on each trigger. Optionally provide an execution mode and a delivery target to receive notifications.\n- **schedule_list**() [capability_expand category: scheduler]: List all scheduled tasks with their current state, trigger, and next due time.\n- **schedule_status**(task_id) [capability_expand category: scheduler]: Get detailed status and recent execution history for a scheduled task.\n- **schedule_pause**(task_id) [capability_expand category: scheduler]: Pause a scheduled task so it stops firing without being cancelled.\n- **schedule_resume**(task_id) [capability_expand category: scheduler]: Resume a paused scheduled task — re-arms it and recalculates next due time.\n- **schedule_cancel**(task_id) [capability_expand category: scheduler]: Cancel a scheduled task permanently.\n- **tool_search**(query, max_results): Search all registered tools by keyword relevance. Returns a ranked list of matching tools with name, category, and summary. Use this when you need a tool but are unsure of its exact name or category.\n- **tool_describe**(tool_name): Get the full callable schema of a single tool by exact name. Returns the tool's description, parameters, and metadata. Use after tool_search to inspect a specific tool before calling it.\n- **hw_list**(): List connected hardware devices with their channel counts and measured quantities. Start here; it does not include operating limits.\n- **hw_describe**(device_id): Return the full reference for one device: every channel, its unit, its operating envelope, rate limit, reversibility, and required interlocks, plus any prior outcomes recorded for its writable channels. Required before commanding a device.\n- **hw_read**(device_id, channel_id): Read the current value of one device channel. Has no physical effect.\n- **hw_status**(device_id): Report connection health, halt capability, and recent notable events (threshold excursions, lost samples, stalled channels) for one device.\n- **hw_configure**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Set a configuration value or setpoint on a channel declaring effect=configure. Setpoints often have inertia: the value may need time to stabilise.\n- **hw_actuate**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Command motion or output on a channel declaring effect=actuate. This moves physical hardware; a repeat from an unknown state is not a safe retry.\n- **hw_dispense**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Consume an irreversible resource on a channel declaring effect=dispense. Running this twice dispenses twice; never repeat it after a failure without first verifying what already happened.\n- **hw_estop**(device_id) [capability_expand category: hardware]: Stop all motion and output on a device immediately. Never requires approval; use it whenever device behaviour is unexpected.\n- **hw_batch_actuate**(device_id, commands, dry_run, verify) [capability_expand category: hardware]: Write multiple actuator channels in one atomic transaction. Used for coordinated multi-joint control where all positions must be commanded simultaneously (e.g. robot arm joint-space control).\n- **hw_control_start**(device_id, mode, targets, frequency_hz, trajectory, policy, gains) [capability_expand category: hardware]: Start a real-time control loop on a robot device. Supports PID position tracking, impedance control, trajectory following, and neural policy chains.\n- **hw_control_stop**(device_id) [capability_expand category: hardware]: Stop the real-time control loop on a device. Gracefully halts the device and returns session statistics.\n- **hw_control_status**(device_id) [capability_expand category: hardware]: Get the status and statistics of a running control loop. Returns frequency, jitter, overruns, and policy state.\n- **hw_context_snapshot**(level) [capability_expand category: hardware]: Get a structured snapshot of all hardware devices and their state. Use this before planning physical manipulation tasks to understand what devices are available and what they can do.\n- **hw_health**(device_id) [capability_expand category: hardware]: Check hardware device health status. Without device_id, returns fleet-wide health summary. With device_id, returns health details for that specific device.\n- **hw_policy_infer**(device_id, policy, task, execute, chunk_size, verify, compute_budget) [capability_expand category: hardware]: Execute VLA policy inference on a robot device. Reads observation, runs policy, optionally writes action to actuators. Supports local (HuggingFace) and remote (PolicyServer) policies.\n- **hw_policy_episode**(action, device_id, policy, task) [capability_expand category: hardware]: Manage policy inference episode lifecycle. Start an episode to begin a task, stop to finalise and get stats.\n- **hw_policy_list**(source) [capability_expand category: hardware]: List available VLA policies and their capabilities.\n- **hw_orchestrate**(mode, operations, barrier_timeout_s, label) [capability_expand category: hardware]: Execute coordinated operations across multiple hardware devices. Supports sequential (one-at-a-time), parallel (concurrent), and barrier (synchronised) execution modes. Any failure in a parallel group triggers halt_all on every participating device for safety.\n- **session_search**(query, limit) [capability_expand category: unclassified]: Search past conversation sessions in the current workspace for relevant context.\n- **session_list**(limit): List recent conversation sessions in the current workspace with stable ids, titles, dates, and summaries. Use for browsing past tasks or when user asks to see history without specific search terms. No keywords needed — returns chronological list.\n- **session_detail**(session_id, limit, offset, include_inactive) [capability_expand category: unclassified]: Read a paginated persisted transcript for one past conversation session in the current workspace. Use after session_list or session_search returns a session_id.\n- **click**(element_index) [capability_expand category: desktop]: Click a UI element by its element_index (from the latest observe_ui snapshot)\n- **get_clipboard**() [capability_expand category: desktop]: Read current clipboard text content\n- **list_apps**(filter, running_only) [capability_expand category: desktop]: List available applications on this system. Use to discover correct bundle_id before switch_app.\n- **list_windows**() [capability_expand category: desktop]: List all top-level windows with pid, window_id, title, and per-window state (minimized, on-screen). Call this first to pick the pid and window_id that observe_ui and other window tools require.\n- **observe_ui**(pid, window_id, query) [capability_expand category: desktop]: Snapshot one window's actionable UI elements, each tagged with an element_index for click/right_click/read_text. Re-observe after actions — indices belong to one snapshot. Requires the window's pid and window_id from list_windows.\n- **open_url**(url, app_id) [capability_expand category: desktop]: Open a URL in the default or specified browser\n- **read_text**(element_index) [capability_expand category: desktop]: Read the text content of a specific UI element from the latest snapshot\n- **right_click**(element_index) [capability_expand category: desktop]: Right-click a UI element to open its context menu. Returns visible menu items.\n- **screenshot**(pid, window_id) [capability_expand category: desktop]: Capture a screenshot for visual verification. With pid + window_id captures that window (works across all displays); defaults to the last observed window, or the full desktop when no window has been observed.\n- **scroll**(element_index, direction, amount, pid, window_id) [capability_expand category: desktop]: Scroll a scrollable area of a window. Omit element_index to scroll the window's focused/page scroller; pass one to scroll an exact element from the latest snapshot.\n- **select_text**(element_index) [capability_expand category: desktop]: Select all text in a UI element (focus + select-all, for subsequent copy)\n- **set_clipboard**(text) [capability_expand category: desktop]: Write text to the clipboard\n- **shortcut**(keys) [capability_expand category: desktop]: Execute a keyboard shortcut\n- **switch_app**(app_id) [capability_expand category: desktop]: Switch to an app (launch if needed, activate, verify)\n- **type_text**(text) [capability_expand category: desktop]: Type text into the currently focused element\n- **wait**(seconds) [capability_expand category: desktop]: Wait for a specified duration before continuing\n- **wait_until**(condition, pid, window_id, timeout, poll_interval) [capability_expand category: desktop]: Wait until a UI condition is met (polls UI tree). Returns elements when found or on timeout.\n- **wait_until_stable**(timeout, poll_interval, pid, window_id) [capability_expand category: desktop]: Wait until the UI stops changing (element set stabilizes across polls).\n\n## App Connector Capability Index\nLeapFlow can onboard and manage external apps through `platform_connect` and execute exact registered business actions through `platform_action`.\nFor requests about connecting, setting up, configuring, enabling, or managing a supported app, use `platform_connect` first instead of generating SDK/Webhook sample code.\n`platform_connect.action` is the management namespace: list, guide, preflight, connect, disconnect, remove, status, events_start, events_stop, events_status.\n`platform_action.action` is only for exact registered platform business actions listed below, such as `im.send_message`; never use management actions like `list` or `guide` there.\nAll business fields MUST go inside `payload`; top-level keys are only `platform`, `action`, and `payload`.\nDo not invent platform IDs or platform action names. If the needed action is not listed, ask for discovery/clarification instead of guessing.\nUse `platform_connect` with `action='guide'` and the matching `platform` to start onboarding; use `action='list'` when the app is unclear.\nWhen a pending onboarding state is present, continue from that state with `platform_connect` instead of asking the user to restate the app.\nSupported apps:\n- `api_server`: API Server (OpenAI Compatible) (category=api; backend=adapter; platform_action actions=none registered)\n- `dingtalk`: 钉钉 (DingTalk) (category=im; backend=adapter; platform_action actions=none registered)\n- `feishu`: 飞书 (Feishu/Lark) (category=im; backend=cli)\n - `calendar.create_event` payload={summary*, start_time*, end_time*, attendees} [write/high]\n - `docs.create_markdown` payload={title*, markdown*, folder_token} [write/medium]\n - `drive.search` payload={query*, limit} [read/medium]\n - `im.add_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.download_resource` payload={message_id*, file_key*, type*} [read/low]\n - `im.get_messages` payload={message_ids*} [read/low]\n - `im.list_chats` payload={page_size} [read/low]\n - `im.list_messages` payload={chat_id*} [read/low]\n - `im.list_thread_messages` payload={thread*} [read/low]\n - `im.remove_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.reply_message` payload={message_id*, text*} [send/high]\n - `im.search_chats` payload={query*, page_size} [read/low]\n - `im.search_messages` payload={query*} [read/medium]\n - `im.send_message` payload={chat_id*, text*, thread_id} [send/high]\n - `im.update_card` payload={token*, card*} [write/medium]\n - `im.update_message` payload={message_id*, text*} [write/medium]\n - `mail.search_unread` payload={query, limit} [read/high]\n - `sheets.append_row` payload={spreadsheet_token*, sheet_id*, values*} [write/medium]\n - `task.create` payload={title*, description, due_time} [write/medium]\n- `telegram`: Telegram (category=social; backend=adapter; platform_action actions=none registered)\n- `webhook`: Webhook (Generic) (category=webhook; backend=adapter; platform_action actions=none registered)\n\n## Learned Skills\nYou have access to the following learned skills. Use `skills_list` to browse or `skill_view` to read details:\n- api_designer: REST and GraphQL API design with OpenAPI spec generation and best practices [API, REST, GraphQL]\n- code_review: Structured code review for security, performance, and style [code, review, security]\n- data_analysis: CSV/JSON data analysis with statistical summaries, quality checks, and visualiza [data, analysis, statistics]\n- devops_helper: CI/CD pipeline configuration, Docker orchestration, and deployment strategy [devops, CI/CD, Docker]\n- document_writer: Structured document generation: technical docs, API references, reports, and REA [documentation, technical-writing, README]\n- git_workflow: Git operation orchestration: branching, commits, conflicts, and PR workflow [git, version-control, branching]\n- i18n_helper: Internationalization workflow: string extraction, translation, locale management [i18n, internationalization, localization]\n- project_analysis: Project structure analysis, dependency audit, and tech stack identification [project, analysis, dependencies]\n- security_audit: Security vulnerability scanning strategy, threat modeling, and fix recommendatio [security, vulnerability, audit]\n- shell_automation: Natural language to shell commands with safety review and step-by-step execution [shell, terminal, automation]\n- test_writer: Generate unit and integration tests from source code with edge-case coverage [testing, unit-test, integration-test]\n- web_research: Multi-step web search with cross-verification and structured synthesis [web, search, research]\n\n## Tool Usage\nTools are normally invoked through the native function-calling mechanism, not by writing JSON in your reply\ntext. Only if the provider signals that native function calling is unavailable for this turn, fall back to a\nsingle JSON code block: `{\"name\": \"tool_name\", \"arguments\": {\"key\": \"value\"}}` — use this fallback format\nonly, never both. Only call a tool whose exact name appears in the tool index above and is currently callable\n(or reachable via `capability_expand`). Never invent, rename, alias, or guess a tool name, platform ID, or\nplatform action from argument shape or wording — if the index does not list it, it does not exist.\n`platform_connect.action` (list/guide/preflight/connect/disconnect/remove/status/events_start/events_stop/\nevents_status) is the App Connector management namespace; `platform_action.action` only accepts exact\nregistered business actions such as `im.send_message` shown in the App Connector Capability Index — never mix\nthe two namespaces. If a tool call returns an unknown/unavailable result, use the returned suggestions or\navailable names for a single retry instead of trying further variations of the same guess.\n\n**Side-effect action rule** (`platform_action` with effect=send/write/execute):\n- Call each unique action+payload **exactly once**. Never include duplicates in the same turn.\n- Once the result returns `\"completed\": true`, that action is DONE for this task. Do NOT call it again\n in any subsequent turn — immediately summarize the result for the user instead.\n- The system enforces idempotency: duplicate calls are blocked and will not execute.\n- If the user explicitly requests sending/writing multiple times, use distinct payloads per call.\n\n**Resource identifier provenance rule**:\n- NEVER fabricate, guess, or infer resource identifiers (chat_id, message_id, file_key, user_id, etc.).\n Every resource ID used in a side-effect action MUST come from a successful API response in this session.\n- If a read/list action fails (e.g. authorization error), you do NOT have valid resource IDs.\n Report the failure to the user — do NOT attempt the dependent write/send action with a guessed ID.\n- When a tool result contains `\"llm_instruction\"`, follow it exactly.\n\n## Guidelines\n1. **Direct answers first**: If you already know the answer, respond directly without tools.\n2. **Avoid redundant tool calls**: Do not call the same tool with the same arguments more than once in the same user turn. When an existing tool result already answers the user's request, stop calling tools and answer directly.\n3. **Use tools proactively**: When the user asks about files, time, system state, or needs actions performed, use the appropriate tool.\n4. **Chain tools when needed**: You can call multiple tools in sequence (e.g., list files → read file → summarize).\n5. **Handle failures gracefully**: If a tool fails, explain what went wrong and suggest alternatives. If it failed because of an unknown tool/platform/action name, retry once with an exact name from the returned suggestions, then explain rather than keep guessing.\n6. **Summarize results naturally**: After tool execution, synthesize the results into a helpful answer rather than dumping raw output.\n7. **Stay conversational**: Maintain a natural, helpful tone. Acknowledge context from earlier in the conversation.\n8. **Recall past work**:\n - Broad queries (\"之前做了什么\", \"列出任务\"): answer from the \"Recent Task History\" section already in your context. If insufficient, call session_list.\n - Specific lookups (\"上次那个配置怎么改的\"): call session_search with relevant phrases (NOT single characters).\n - Do NOT call search tools repeatedly with keyword fragments. One well-phrased call is better than ten fragmented ones.\n\n## Coding & Verification\nWhen working with code, prefer the precise built-in tools over ad-hoc shell: use `repo_map` to orient in an\nunfamiliar project, `code_search`/`file_find` to locate and `code_intel` for symbols, `edit_file` (anchored\nsearch-replace, or a unified `diff`) to change files — never rewrite a whole file to change a few lines — and\n`git_query` to inspect diffs/log. After edits, check `syntax_ok` in the result and run `test_run`/`lint_check`\nbefore declaring the work done. Batch independent read-only calls (search/read/query) into a single turn — they\nrun in parallel.\n\n## Presentation Style\n1. **Polished Markdown only**: Format user-facing answers with clean Markdown headings, short paragraphs, and concise bullets. Use tables only when they improve comparison or scanning.\n2. **Terminal-friendly layout**: Keep lines readable in a TUI; avoid dense walls of text, deeply nested lists, oversized ASCII art, or heavy visual blocks.\n3. **Elegant emphasis**: Use bold text sparingly for key terms and conclusions. Avoid excessive emojis, decorative symbols, repeated separators, or visual noise.\n4. **Theme-safe colors**: Do not emit ANSI escape codes, HTML color tags, Rich markup, or hardcoded color names. Rely on the TUI theme to render Markdown professionally.\n5. **No leaked tool protocol**: Never show tool-call JSON, internal schemas, raw observations, tool result payloads, or hidden reasoning in the final answer unless the user explicitly asks for raw/debug output. Treat any prior `{\"name\": ..., \"arguments\": ...}` blocks and `Tool result (...)` messages as internal execution context only.\n6. **Professional closure**: End with a concise conclusion or next step when helpful; avoid rambling after the useful answer is complete.\n\nWhen finished with all tool calls, respond normally without a JSON block, tool-call transcript, or process log.\n\n## Task Contract\n- Task ID: turn-5\n- Original user request: Preview an actuate of bench_r8 setpoint to 55, dry run only.\n- Workspace root: \n- Allowed roots: \n- Treat relative project paths as relative to the workspace root; never infer `.` as the project root when a workspace root is provided.\n- Workspace boundary is enforced by tools: do not read, search, edit, or run commands against paths outside the allowed roots unless the user explicitly requests an external path and the tool/approval policy permits it.\n- LeapFlow workspace config is optional at `/.leapflow/config.yaml`; runtime config is loaded from `~/.leapflow/config/user.yaml` and `~/.leapflow/profiles//config/*.yaml`.\n- Preserve this task contract across summarization, compression, tool loops, and memory retrieval.\n" }, { "role": "assistant", @@ -40,12 +40,14 @@ "hw_actuate", "hw_batch_actuate", "hw_configure", + "hw_context_snapshot", "hw_control_start", "hw_control_status", "hw_control_stop", "hw_describe", "hw_dispense", "hw_estop", + "hw_health", "hw_list", "hw_orchestrate", "hw_policy_episode", diff --git a/tests/_fixtures/cassettes/r8_hardware/cassette-model-476d996d8812c2c2.cassette.json b/tests/_fixtures/cassettes/r8_hardware/cassette-model-476d996d8812c2c2.cassette.json deleted file mode 100644 index df4acf79..00000000 --- a/tests/_fixtures/cassettes/r8_hardware/cassette-model-476d996d8812c2c2.cassette.json +++ /dev/null @@ -1,91 +0,0 @@ -{ - "fingerprint": "476d996d8812c2c25e35d84d65db0334111b92af94530d9752e74445f982e3df", - "note": "captured in seed mode", - "request": { - "model": "cassette-model", - "stream": false, - "messages": [ - { - "role": "system", - "content": "You are LeapFlow, an intelligent assistant that can both converse naturally and take real actions on the user's computer.\n\n## Capabilities\nThe tool index below lists **every** registered tool by name and a one-line summary — this is the complete\ncapability contract; nothing else exists. Only a subset is directly callable this turn (via native tool calling,\nnot a JSON block in your reply). If you need a tool from the index that is not yet callable, call\n`capability_expand` with its category name first — the matching tools become callable immediately after.\nWhen the user asks about LeapFlow **itself** — its identity/version/model, what tools, plugins, or capabilities\nit has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of\nself-awareness — answer from live runtime evidence via `self_describe` (facet=capabilities for \"what can you\ndo / do you support X\", identity for version/model, runtime for context/posture, or `all` for a full self-check),\nnever by reading LeapFlow's own source code, documentation, or memory. Report configuration-dependent or\nunavailable capabilities as limitations rather than inferring them.\n- **text_search**(text, pattern): Search for a regex pattern in text.\n- **text_replace**(text, old, new, count) [capability_expand category: write]: Replace occurrences of a substring in text.\n- **time_get**(): Get current date and time.\n- **env_info**(): Get system environment information (OS, Python version, cwd).\n- **skills_list**(query, category, source): List available learned skills. Use when user asks about capabilities or you need a specific skill.\n- **skill_view**(name): View the full content of a specific skill document.\n- **skill_list_files**(name) [capability_expand category: skills]: List all support files in a skill directory (configs, templates, examples). Use before skill_file_read to discover available files.\n- **skill_file_read**(name, file_path) [capability_expand category: skills]: Read an individual support file from a skill directory. Use skill_list_files first to discover available files.\n- **code_intel**(path, operation): Precise document symbols (outline) for a source file: classes, functions, and methods with line ranges. Python uses an exact AST parse; other languages use a keyword-prefix scan. Prefer over file_read mode=symbols for accurate navigation before editing. Read-only.\n- **repo_map**(path): Compact project orientation for a repository root: languages, detected test/lint commands, top-level structure, entry points, manifest, and VCS branch. Call this first when entering an unfamiliar codebase. Read-only.\n- **scm_sync**(action, cwd, remote, pull_ref, push_ref, timeout) [capability_expand category: scm]: Run a typed git SCM action. Use this instead of shell_run for git pull/push/status. For 'pull origin main then push', set action='pull_then_push', remote='origin', pull_ref='main', and omit push_ref so LeapFlow pushes the current local branch.\n- **git_query**(action, cwd, ref, path, staged, max_count, stat): Read-only structured git inspection: action=diff|log|status|branch|show. Prefer over shell_run for reading repo state — output is clipped, redacted, and log/branch are parsed into structured fields. Use scm_sync for pull/push.\n- **git_write**(action, cwd, message, stage_all, name, ref, create) [capability_expand category: scm]: Mutating git actions: action=commit (message, stage_all), branch (create+switch), checkout (switch; create=true for -b). Approval-gated. Use scm_sync for pull/push and git_query for reads.\n- **test_run**(command, cwd, timeout): Run the project's test suite and return structured results (framework, passed/failed counts, failing tests). Auto-detects the runner (pytest/npm/go/cargo) or uses a configured/explicit command; executes via the governed shell. ok=true means the runner executed — see 'success' for pass/fail.\n- **lint_check**(command, cwd, timeout): Run the project's linter and return a structured clean/issue result. Auto-detects the linter (ruff/eslint/go vet/clippy) or uses a configured/explicit command; executes via the governed shell. ok=true means the linter ran — see 'clean'.\n- **file_list**(path, pattern, depth): List files and directories at a given path. Use depth=1 or depth=2 to get a recursive tree in one call instead of listing each sub-directory separately.\n- **file_read**(path, max_lines, start_line, max_chars, mode): Read text file content with adaptive context governance. For large or unfamiliar files, prefer mode='outline' or mode='symbols' first, then use mode='raw' with start_line/max_lines for the specific range you actually need. For LeapFlow's own settings, use config_list / config_get / config_set — its config files are outside the workspace and not readable here.\n- **file_write**(path, content, mode) [capability_expand category: write]: Write content to a file (overwrite or append).\n- **code_search**(pattern, patterns, path, glob, ignore_case, multiline, max_results, context_lines): Search file CONTENTS by regex pattern across a directory tree (ripgrep-backed). Requires a regex pattern. NOT for listing or browsing directory contents — use file_list for that. Prefer this over shell_run grep: faster, skips VCS/dependency/build dirs, and returns structured path:line:column matches. Batch related lookups into ONE call via `patterns` (OR-combined, single pass) instead of issuing several separate searches. Use file_read for the surrounding context of a hit.\n- **file_find**(glob, path, max_results): Find files by a recursive glob pattern under a base path (e.g. '**/test_*.py' or '*.md'). Prefer this over shell_run find; skips VCS/dependency/build dirs.\n- **edit_file**(path, edits, dry_run, diff) [capability_expand category: write]: Apply targeted, anchored search-replace edits to an EXISTING text file (use file_write to create/overwrite). Each edit is {original_text, new_text, replace_all?}; original_text must match exactly and uniquely (or set replace_all) — a non-unique or missing anchor is rejected so files are never corrupted. Set dry_run to preview. Alternatively pass a unified 'diff' to apply its hunks as anchored edits. Far cheaper and safer than rewriting a whole file.\n- **shell_run**(command, cwd, timeout) [capability_expand category: shell]: Execute a one-shot shell command with timeout protection. Runs in the active workspace; paths resolving outside it are refused. Reach for a structured tool first when one fits — web_fetch for anything over HTTP(S), git_query/scm_sync for git, code_search/file_find/file_read for the repo, config_get/config_set for LeapFlow's own settings — because those report typed results, while a failed shell command can only be diagnosed from its exit code and stderr. Every shell run counts as an external side effect, so a failure stops the rest of the batch and is not retried automatically.\n- **terminal_open**(command, cwd, shell) [capability_expand category: terminal]: Open a PERSISTENT shell session (REPL/dev server/watch), returning a session_id for terminal_send/read/close. Disabled unless tools.terminal_session_enabled is set. For one-shot commands use shell_run instead.\n- **terminal_send**(session_id, input, wait) [capability_expand category: terminal]: Send a line of input to a persistent terminal session and return output captured shortly after.\n- **terminal_read**(session_id, wait): Drain buffered output from a persistent terminal session (optionally waiting briefly first).\n- **terminal_close**(session_id) [capability_expand category: terminal]: Terminate a persistent terminal session and release its process group.\n- **terminal_list**(): List active persistent terminal sessions.\n- **config_list**(category, limit): List LeapFlow's own writable settings (LLM model and endpoint, daemon, memory, perception, gateway, …) with current values. Use this to discover the exact key before changing anything. Optionally narrow by `category`. This is the only correct way to inspect LeapFlow configuration — never read config files from disk.\n- **config_get**(key): Read one LeapFlow setting by key (e.g. 'llm.model', 'llm.base_url', 'daemon.log_level'), returning its current value, type, scopes, and whether a change needs a daemon restart. There is no 'llm.provider' key: provider behavior is inferred from the OpenAI-compatible 'llm.base_url'. Never read LeapFlow config files from disk — use this.\n- **config_set**(key, value, scope) [capability_expand category: config]: Change one LeapFlow setting by key, e.g. switch an OpenAI-compatible LLM with key='llm.model' and key='llm.base_url'. There is no 'llm.provider' key: provider behavior is inferred from the endpoint. Values are validated and coerced; credentials are stored in the vault automatically. Call config_list or config_get first if unsure of the key. The result states whether a `leap daemon restart` is required. Never edit LeapFlow config files directly.\n- **web_fetch**(url, select, timeout, max_bytes): Read a URL over HTTP(S) and get back extracted, context-sized content: parsed JSON for API endpoints, readable text plus links for web pages. Use this for anything on the internet — prices, docs, releases, articles — instead of running curl through shell_run: it reports real HTTP status codes, retries rate limits on its own, and is a plain read so a retry is always safe. For JSON APIs pass `select` with a dotted path (e.g. 'chart.result.0.meta') to return just that part instead of the whole payload.\n- **memory_search**(query, limit): Search agent memory for relevant past experiences, observations, and facts.\n- **memory_add**(content, kind) [capability_expand category: write]: Store a new observation or insight in memory for future reference.\n- **research_note**(kind, text): Record a compact, structured note about the current task's state so it survives context compression on long / multi-step tasks. Use for durable findings, open questions still to resolve, decisions / excluded paths, and the immediate next step. One concise sentence per note.\n- **capability_expand**(category): Fetch the full callable schema for every tool in a capability category (e.g. 'hub', 'gateway', 'desktop', 'delegate', 'file', 'memory', 'skill'). The compact tool index always lists every registered tool by name and a one-line summary, but only a static low-risk subset is directly callable each turn. If you need a tool from the index that is not yet callable, call capability_expand with its category first; the matching tools become callable in this turn. Never invent a tool name — expand the category instead.\n- **delegate_task**(goal, context) [capability_expand category: delegate]: Delegate a complex sub-task to an isolated subagent. The subagent gets a fresh context and restricted tool access. Use when a task is self-contained and can be solved independently.\n- **schedule_reentry**(kind, reason, delay_seconds, event_match, max_reentries, deadline_seconds): Register a re-entry so this task can resume later from its current orientation (findings / open questions / next step). Use when work must pause and continue after a delay (kind=time) or when a matching platform event arrives (kind=event), instead of finishing now. The research-ledger state is carried over automatically.\n- **hub_push**(skill_name, visibility, version) [capability_expand category: hub]: Push a local skill to the ModelScope Hub for sharing or backup.\n- **hub_pull**(repo_id, version) [capability_expand category: hub]: Pull a skill from the ModelScope Hub to install locally.\n- **hub_search**(query) [capability_expand category: hub]: Search for skills on the Hub by keyword or description.\n- **hub_sync**(mode, dry_run) [capability_expand category: hub]: Preview or execute sync between local skills and Hub.\n- **platform_action**(platform, action, payload, backend_kind) [capability_expand category: gateway]: Execute an exact registered business action on an external platform through LeapFlow's App Connector layer. Actions must be copied from the App Connector Capability Index and are addressed as domain.operation, e.g. im.send_message or docs.create_markdown. All business fields (chat_id, text, query, etc.) MUST be placed inside `payload`, never at the top level. Example: {\"platform\":\"feishu\",\"action\":\"im.send_message\",\"payload\":{\"chat_id\":\"oc_xxx\",\"text\":\"hello\"}}. Do not invent action names, do not use management actions such as list/guide/connect/status here.\n- **platform_connect**(action, platform, credentials, options, checkpoint) [capability_expand category: gateway]: List, guide, connect, disconnect, remove, or check status for external platforms using the App Connector management namespace. Supports REST and CLI backends. Use this for management actions such as list/guide/preflight/connect/status; use platform_action only for exact registered business actions.\n- **gateway_send**(platform, chat_id, text, thread_id) [capability_expand category: gateway]: Send a message to a connected external platform (Feishu group, Telegram chat, DingTalk conversation, etc.). Requires the platform to be connected via gateway_connect first. Use gateway_connect with action='list' to see connected platforms and available chat IDs.\n- **gateway_connect**(action, platform, credentials, options) [capability_expand category: gateway]: Connect, configure, or manage external platform integrations (Feishu, DingTalk, Telegram, Slack, Discord, etc.). Conversational flow: 1) call 'guide' to get setup steps + required fields, 2) present the steps to the user and ask for ALL required credentials in a single message, 3) call 'connect' with the credentials. Goal: complete in 1–2 user turns. NEVER include credential values in your text response.\n- **plugin_list**(): List the live plugin registry across Tool/Gateway/LLM subsystems for plugin management: which plugins are loaded, their category and fiber lifecycle state. For questions about LeapFlow's own capabilities or whether it supports X, use self_describe instead.\n- **plugin_status**(plugin_id): Get detailed status of a specific plugin: its declared category, runtime dependencies, contributed tools, and fiber lifecycle state.\n- **plugin_versions**(plugin_id): List recorded source versions and active pointer for a profile-scoped plugin.\n- **plugin_propose**(requested_capability, plugin_id, proposed_tools, test_cases, risk_level, evidence): Create a side-effect-free PluginProposal from explicit capability-gap evidence. Use this before plugin_generate when a missing capability should be reviewed. Does not call an LLM, write files, or install anything.\n- **assess_compatibility**(manifest, source_path) [capability_expand category: plugin_management]: Assess whether a foreign plugin manifest or real DSH source bundle is compatible with LeapFlow. A source bundle assessment is static: runtime_ready stays false until restricted Node discovery during plugin_install. Returns component-level verdicts and limitations.\n- **plugin_generate**(plugin_id, description, proposal_id) [capability_expand category: system]: Generate a new ToolPlugin from a natural-language capability description. The LLM produces code that conforms to the ToolPlugin Protocol; it is then rigorously validated (syntax, structure, import, protocol conformance). The generated source is stored in CAS and receives explicit content approval. It DOES NOT install the plugin — installation requires a second, mutation-specific approval via plugin_install.\n- **plugin_install**(plugin_id, code, marketplace_name, source_path, proposal_id, version_label) [capability_expand category: system]: Install a plugin from exactly one source: validated Python code, the configured Python marketplace, or a real DSH source bundle. DSH bundles run restricted Node discovery before registration; only public host tools are installed and client UI limitations are reported. Writes profile-scoped state and mutates the process-global registry. REQUIRES APPROVAL.\n- **plugin_rollback**(plugin_id, version) [capability_expand category: system]: Rollback a profile-scoped plugin to a recorded source version and reload it. REQUIRES APPROVAL.\n- **plugin_reload**(plugin_id, version_label) [capability_expand category: system]: Hot-reload a plugin at runtime. Disposes the old plugin fiber, re-imports its module, and registers a fresh instance. Existing in-flight turns are unaffected (snapshot isolation). REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_disable**(plugin_id) [capability_expand category: system]: Disable a plugin by disposing its fiber, removing its tools from the runtime registry. Cannot disable self_management itself. REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_remove**(plugin_id, delete_source) [capability_expand category: system]: Terminally remove a plugin: dispose its fiber, unregister its tools, remove reload metadata, and optionally delete its profile-scoped source file. Cannot remove self_management itself. REQUIRES APPROVAL.\n- **plugin_unquarantine**(plugin_id) [capability_expand category: plugin_management]: Restore a quarantined plugin to probation status for re-evaluation. Unfreezes the trust ledger, transitions the proposal back to PROBATION, and reloads the plugin. REQUIRES APPROVAL.\n- **plugin_enable**(plugin_id) [capability_expand category: system]: Re-enable a previously disabled plugin by reloading its module and registering a fresh instance. REQUIRES APPROVAL.\n- **self_describe**(facet): Answer questions about LeapFlow ITSELF from live runtime evidence — its identity/version/model, what tools and plugins it has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of self-awareness. Prefer this over reading LeapFlow's own source code. Pick facet=capabilities for 'what can you do / do you support X', identity for version/model, runtime for context/posture, or all for a full self-check when explicitly requested.\n- **runtime_snapshot**(): Lightweight (~150 token) flat snapshot of current runtime state: model, context budget, posture, disclosure level, turn count, cache hit rate, uptime, tool count, and pending approvals.\n- **schedule_create**(trigger_expression, instruction, execution_mode, max_retries, delivery_target) [capability_expand category: scheduler]: Create a new scheduled task. Specify a trigger expression (e.g. '30m', 'every 2h', '0 9 * * *') and the instruction to execute on each trigger. Optionally provide an execution mode and a delivery target to receive notifications.\n- **schedule_list**() [capability_expand category: scheduler]: List all scheduled tasks with their current state, trigger, and next due time.\n- **schedule_status**(task_id) [capability_expand category: scheduler]: Get detailed status and recent execution history for a scheduled task.\n- **schedule_pause**(task_id) [capability_expand category: scheduler]: Pause a scheduled task so it stops firing without being cancelled.\n- **schedule_resume**(task_id) [capability_expand category: scheduler]: Resume a paused scheduled task — re-arms it and recalculates next due time.\n- **schedule_cancel**(task_id) [capability_expand category: scheduler]: Cancel a scheduled task permanently.\n- **tool_search**(query, max_results): Search all registered tools by keyword relevance. Returns a ranked list of matching tools with name, category, and summary. Use this when you need a tool but are unsure of its exact name or category.\n- **tool_describe**(tool_name): Get the full callable schema of a single tool by exact name. Returns the tool's description, parameters, and metadata. Use after tool_search to inspect a specific tool before calling it.\n- **hw_list**(): List connected hardware devices with their channel counts and measured quantities. Start here; it does not include operating limits.\n- **hw_describe**(device_id): Return the full reference for one device: every channel, its unit, its operating envelope, rate limit, reversibility, and required interlocks, plus any prior outcomes recorded for its writable channels. Required before commanding a device.\n- **hw_read**(device_id, channel_id): Read the current value of one device channel. Has no physical effect.\n- **hw_status**(device_id): Report connection health, halt capability, and recent notable events (threshold excursions, lost samples, stalled channels) for one device.\n- **hw_configure**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Set a configuration value or setpoint on a channel declaring effect=configure. Setpoints often have inertia: the value may need time to stabilise.\n- **hw_actuate**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Command motion or output on a channel declaring effect=actuate. This moves physical hardware; a repeat from an unknown state is not a safe retry.\n- **hw_dispense**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Consume an irreversible resource on a channel declaring effect=dispense. Running this twice dispenses twice; never repeat it after a failure without first verifying what already happened.\n- **hw_estop**(device_id) [capability_expand category: hardware]: Stop all motion and output on a device immediately. Never requires approval; use it whenever device behaviour is unexpected.\n- **hw_batch_actuate**(device_id, commands, dry_run, verify) [capability_expand category: hardware]: Write multiple actuator channels in one atomic transaction. Used for coordinated multi-joint control where all positions must be commanded simultaneously (e.g. robot arm joint-space control).\n- **session_search**(query, limit) [capability_expand category: unclassified]: Search past conversation sessions in the current workspace for relevant context.\n- **session_list**(limit): List recent conversation sessions in the current workspace with stable ids, titles, dates, and summaries. Use for browsing past tasks or when user asks to see history without specific search terms. No keywords needed — returns chronological list.\n- **session_detail**(session_id, limit, offset, include_inactive) [capability_expand category: unclassified]: Read a paginated persisted transcript for one past conversation session in the current workspace. Use after session_list or session_search returns a session_id.\n- **click**(element_index) [capability_expand category: desktop]: Click a UI element by its element_index (from the latest observe_ui snapshot)\n- **get_clipboard**() [capability_expand category: desktop]: Read current clipboard text content\n- **list_apps**(filter, running_only) [capability_expand category: desktop]: List available applications on this system. Use to discover correct bundle_id before switch_app.\n- **list_windows**() [capability_expand category: desktop]: List all top-level windows with pid, window_id, title, and per-window state (minimized, on-screen). Call this first to pick the pid and window_id that observe_ui and other window tools require.\n- **observe_ui**(pid, window_id, query) [capability_expand category: desktop]: Snapshot one window's actionable UI elements, each tagged with an element_index for click/right_click/read_text. Re-observe after actions — indices belong to one snapshot. Requires the window's pid and window_id from list_windows.\n- **open_url**(url, app_id) [capability_expand category: desktop]: Open a URL in the default or specified browser\n- **read_text**(element_index) [capability_expand category: desktop]: Read the text content of a specific UI element from the latest snapshot\n- **right_click**(element_index) [capability_expand category: desktop]: Right-click a UI element to open its context menu. Returns visible menu items.\n- **screenshot**(pid, window_id) [capability_expand category: desktop]: Capture a screenshot for visual verification. With pid + window_id captures that window (works across all displays); defaults to the last observed window, or the full desktop when no window has been observed.\n- **scroll**(element_index, direction, amount, pid, window_id) [capability_expand category: desktop]: Scroll a scrollable area of a window. Omit element_index to scroll the window's focused/page scroller; pass one to scroll an exact element from the latest snapshot.\n- **select_text**(element_index) [capability_expand category: desktop]: Select all text in a UI element (focus + select-all, for subsequent copy)\n- **set_clipboard**(text) [capability_expand category: desktop]: Write text to the clipboard\n- **shortcut**(keys) [capability_expand category: desktop]: Execute a keyboard shortcut\n- **switch_app**(app_id) [capability_expand category: desktop]: Switch to an app (launch if needed, activate, verify)\n- **type_text**(text) [capability_expand category: desktop]: Type text into the currently focused element\n- **wait**(seconds) [capability_expand category: desktop]: Wait for a specified duration before continuing\n- **wait_until**(condition, pid, window_id, timeout, poll_interval) [capability_expand category: desktop]: Wait until a UI condition is met (polls UI tree). Returns elements when found or on timeout.\n- **wait_until_stable**(timeout, poll_interval, pid, window_id) [capability_expand category: desktop]: Wait until the UI stops changing (element set stabilizes across polls).\n\n## App Connector Capability Index\nLeapFlow can onboard and manage external apps through `platform_connect` and execute exact registered business actions through `platform_action`.\nFor requests about connecting, setting up, configuring, enabling, or managing a supported app, use `platform_connect` first instead of generating SDK/Webhook sample code.\n`platform_connect.action` is the management namespace: list, guide, preflight, connect, disconnect, remove, status, events_start, events_stop, events_status.\n`platform_action.action` is only for exact registered platform business actions listed below, such as `im.send_message`; never use management actions like `list` or `guide` there.\nAll business fields MUST go inside `payload`; top-level keys are only `platform`, `action`, and `payload`.\nDo not invent platform IDs or platform action names. If the needed action is not listed, ask for discovery/clarification instead of guessing.\nUse `platform_connect` with `action='guide'` and the matching `platform` to start onboarding; use `action='list'` when the app is unclear.\nWhen a pending onboarding state is present, continue from that state with `platform_connect` instead of asking the user to restate the app.\nSupported apps:\n- `api_server`: API Server (OpenAI Compatible) (category=api; backend=adapter; platform_action actions=none registered)\n- `dingtalk`: 钉钉 (DingTalk) (category=im; backend=adapter; platform_action actions=none registered)\n- `feishu`: 飞书 (Feishu/Lark) (category=im; backend=cli)\n - `calendar.create_event` payload={summary*, start_time*, end_time*, attendees} [write/high]\n - `docs.create_markdown` payload={title*, markdown*, folder_token} [write/medium]\n - `drive.search` payload={query*, limit} [read/medium]\n - `im.add_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.download_resource` payload={message_id*, file_key*, type*} [read/low]\n - `im.get_messages` payload={message_ids*} [read/low]\n - `im.list_chats` payload={page_size} [read/low]\n - `im.list_messages` payload={chat_id*} [read/low]\n - `im.list_thread_messages` payload={thread*} [read/low]\n - `im.remove_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.reply_message` payload={message_id*, text*} [send/high]\n - `im.search_chats` payload={query*, page_size} [read/low]\n - `im.search_messages` payload={query*} [read/medium]\n - `im.send_message` payload={chat_id*, text*, thread_id} [send/high]\n - `im.update_card` payload={token*, card*} [write/medium]\n - `im.update_message` payload={message_id*, text*} [write/medium]\n - `mail.search_unread` payload={query, limit} [read/high]\n - `sheets.append_row` payload={spreadsheet_token*, sheet_id*, values*} [write/medium]\n - `task.create` payload={title*, description, due_time} [write/medium]\n- `telegram`: Telegram (category=social; backend=adapter; platform_action actions=none registered)\n- `webhook`: Webhook (Generic) (category=webhook; backend=adapter; platform_action actions=none registered)\n\n## Tool Usage\nTools are normally invoked through the native function-calling mechanism, not by writing JSON in your reply\ntext. Only if the provider signals that native function calling is unavailable for this turn, fall back to a\nsingle JSON code block: `{\"name\": \"tool_name\", \"arguments\": {\"key\": \"value\"}}` — use this fallback format\nonly, never both. Only call a tool whose exact name appears in the tool index above and is currently callable\n(or reachable via `capability_expand`). Never invent, rename, alias, or guess a tool name, platform ID, or\nplatform action from argument shape or wording — if the index does not list it, it does not exist.\n`platform_connect.action` (list/guide/preflight/connect/disconnect/remove/status/events_start/events_stop/\nevents_status) is the App Connector management namespace; `platform_action.action` only accepts exact\nregistered business actions such as `im.send_message` shown in the App Connector Capability Index — never mix\nthe two namespaces. If a tool call returns an unknown/unavailable result, use the returned suggestions or\navailable names for a single retry instead of trying further variations of the same guess.\n\n**Side-effect action rule** (`platform_action` with effect=send/write/execute):\n- Call each unique action+payload **exactly once**. Never include duplicates in the same turn.\n- Once the result returns `\"completed\": true`, that action is DONE for this task. Do NOT call it again\n in any subsequent turn — immediately summarize the result for the user instead.\n- The system enforces idempotency: duplicate calls are blocked and will not execute.\n- If the user explicitly requests sending/writing multiple times, use distinct payloads per call.\n\n**Resource identifier provenance rule**:\n- NEVER fabricate, guess, or infer resource identifiers (chat_id, message_id, file_key, user_id, etc.).\n Every resource ID used in a side-effect action MUST come from a successful API response in this session.\n- If a read/list action fails (e.g. authorization error), you do NOT have valid resource IDs.\n Report the failure to the user — do NOT attempt the dependent write/send action with a guessed ID.\n- When a tool result contains `\"llm_instruction\"`, follow it exactly.\n\n## Guidelines\n1. **Direct answers first**: If you already know the answer, respond directly without tools.\n2. **Avoid redundant tool calls**: Do not call the same tool with the same arguments more than once in the same user turn. When an existing tool result already answers the user's request, stop calling tools and answer directly.\n3. **Use tools proactively**: When the user asks about files, time, system state, or needs actions performed, use the appropriate tool.\n4. **Chain tools when needed**: You can call multiple tools in sequence (e.g., list files → read file → summarize).\n5. **Handle failures gracefully**: If a tool fails, explain what went wrong and suggest alternatives. If it failed because of an unknown tool/platform/action name, retry once with an exact name from the returned suggestions, then explain rather than keep guessing.\n6. **Summarize results naturally**: After tool execution, synthesize the results into a helpful answer rather than dumping raw output.\n7. **Stay conversational**: Maintain a natural, helpful tone. Acknowledge context from earlier in the conversation.\n8. **Recall past work**:\n - Broad queries (\"之前做了什么\", \"列出任务\"): answer from the \"Recent Task History\" section already in your context. If insufficient, call session_list.\n - Specific lookups (\"上次那个配置怎么改的\"): call session_search with relevant phrases (NOT single characters).\n - Do NOT call search tools repeatedly with keyword fragments. One well-phrased call is better than ten fragmented ones.\n\n## Coding & Verification\nWhen working with code, prefer the precise built-in tools over ad-hoc shell: use `repo_map` to orient in an\nunfamiliar project, `code_search`/`file_find` to locate and `code_intel` for symbols, `edit_file` (anchored\nsearch-replace, or a unified `diff`) to change files — never rewrite a whole file to change a few lines — and\n`git_query` to inspect diffs/log. After edits, check `syntax_ok` in the result and run `test_run`/`lint_check`\nbefore declaring the work done. Batch independent read-only calls (search/read/query) into a single turn — they\nrun in parallel.\n\n## Presentation Style\n1. **Polished Markdown only**: Format user-facing answers with clean Markdown headings, short paragraphs, and concise bullets. Use tables only when they improve comparison or scanning.\n2. **Terminal-friendly layout**: Keep lines readable in a TUI; avoid dense walls of text, deeply nested lists, oversized ASCII art, or heavy visual blocks.\n3. **Elegant emphasis**: Use bold text sparingly for key terms and conclusions. Avoid excessive emojis, decorative symbols, repeated separators, or visual noise.\n4. **Theme-safe colors**: Do not emit ANSI escape codes, HTML color tags, Rich markup, or hardcoded color names. Rely on the TUI theme to render Markdown professionally.\n5. **No leaked tool protocol**: Never show tool-call JSON, internal schemas, raw observations, tool result payloads, or hidden reasoning in the final answer unless the user explicitly asks for raw/debug output. Treat any prior `{\"name\": ..., \"arguments\": ...}` blocks and `Tool result (...)` messages as internal execution context only.\n6. **Professional closure**: End with a concise conclusion or next step when helpful; avoid rambling after the useful answer is complete.\n\nWhen finished with all tool calls, respond normally without a JSON block, tool-call transcript, or process log.\n\n## Task Contract\n- Task ID: turn-1\n- Original user request: List the connected hardware devices, then describe bench_r8 in full.\n- Workspace root: \n- Allowed roots: \n- Treat relative project paths as relative to the workspace root; never infer `.` as the project root when a workspace root is provided.\n- Workspace boundary is enforced by tools: do not read, search, edit, or run commands against paths outside the allowed roots unless the user explicitly requests an external path and the tool/approval policy permits it.\n- LeapFlow workspace config is optional at `/.leapflow/config.yaml`; runtime config is loaded from `~/.leapflow/config/user.yaml` and `~/.leapflow/profiles//config/*.yaml`.\n- Preserve this task contract across summarization, compression, tool loops, and memory retrieval.\n" - }, - { - "role": "system", - "content": "\n## Recent Session Summary\n- [user] List the connected hardware devices, then describe bench_r8 in full.\n" - }, - { - "role": "user", - "content": "List the connected hardware devices, then describe bench_r8 in full.\nList the connected hardware devices, then describe bench_r8 in full." - }, - { - "role": "assistant", - "content": "", - "tool_calls": [ - "hw_list" - ] - }, - { - "role": "tool", - "content": "{\"ok\": true, \"devices\": [{\"device_id\": \"bench_r8\", \"display_name\": \"R8 Simulated Bench\", \"location\": \"journey-lab\", \"channels\": 3, \"writable\": 2, \"streaming\": 1, \"quantities\": [\"state.homed\", \"temperature\"], \"verified\": true, \"halt_supported\": true}], \"count\": 1, \"hint\": \"Call hw_describe(device_id) for channel limits before commanding a device.\", \"execution_id\": \"\", \"idempotency_key\": \"\", \"execution_policy\": \"read_only\", \"tool_call_id\": \"call_1\", \"execution_status\": \"completed\"}", - "tool_result": true - }, - { - "role": "assistant", - "content": "", - "tool_calls": [ - "hw_describe" - ] - }, - { - "role": "tool", - "content": "{\"ok\": true, \"kind\": \"\", \"truncated\": true, \"original_chars\": 846, \"budget_chars\": 819}", - "tool_result": true - } - ], - "tools": [ - "capability_expand", - "code_intel", - "code_search", - "config_get", - "config_list", - "env_info", - "file_find", - "file_list", - "file_read", - "git_query", - "hw_describe", - "hw_list", - "hw_read", - "hw_status", - "lint_check", - "memory_search", - "plugin_list", - "plugin_propose", - "plugin_status", - "plugin_versions", - "repo_map", - "research_note", - "runtime_snapshot", - "schedule_reentry", - "self_describe", - "session_list", - "skill_view", - "skills_list", - "terminal_list", - "terminal_read", - "test_run", - "text_search", - "time_get", - "tool_describe", - "tool_search", - "web_fetch" - ] - }, - "responses": [ - { - "status": 200, - "content_type": "application/json", - "body": "{\"id\": \"chatcmpl-cassette\", \"object\": \"chat.completion\", \"model\": \"cassette-model\", \"choices\": [{\"index\": 0, \"message\": {\"role\": \"assistant\", \"content\": \"Discovered the simulated bench and read its channel limits.\"}, \"finish_reason\": \"stop\"}], \"usage\": {\"prompt_tokens\": 64, \"completion_tokens\": 16, \"total_tokens\": 80}}" - } - ] -} diff --git a/tests/_fixtures/cassettes/r8_hardware/cassette-model-33a143a132a46955.cassette.json b/tests/_fixtures/cassettes/r8_hardware/cassette-model-50401e915bfb79dc.cassette.json similarity index 59% rename from tests/_fixtures/cassettes/r8_hardware/cassette-model-33a143a132a46955.cassette.json rename to tests/_fixtures/cassettes/r8_hardware/cassette-model-50401e915bfb79dc.cassette.json index 13cd38ff..9a30bab4 100644 --- a/tests/_fixtures/cassettes/r8_hardware/cassette-model-33a143a132a46955.cassette.json +++ b/tests/_fixtures/cassettes/r8_hardware/cassette-model-50401e915bfb79dc.cassette.json @@ -1,5 +1,5 @@ { - "fingerprint": "33a143a132a469555d0482e6afe47da55b6c895c55de02739bcdf73769106f0c", + "fingerprint": "50401e915bfb79dcf9b35021b7305aaed3391231079c2d1894f25633b0484f51", "note": "captured in seed mode", "request": { "model": "cassette-model", @@ -7,7 +7,7 @@ "messages": [ { "role": "system", - "content": "You are LeapFlow, an intelligent assistant that can both converse naturally and take real actions on the user's computer.\n\n## Capabilities\nThe tool index below lists **every** registered tool by name and a one-line summary — this is the complete\ncapability contract; nothing else exists. Only a subset is directly callable this turn (via native tool calling,\nnot a JSON block in your reply). If you need a tool from the index that is not yet callable, call\n`capability_expand` with its category name first — the matching tools become callable immediately after.\nWhen the user asks about LeapFlow **itself** — its identity/version/model, what tools, plugins, or capabilities\nit has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of\nself-awareness — answer from live runtime evidence via `self_describe` (facet=capabilities for \"what can you\ndo / do you support X\", identity for version/model, runtime for context/posture, or `all` for a full self-check),\nnever by reading LeapFlow's own source code, documentation, or memory. Report configuration-dependent or\nunavailable capabilities as limitations rather than inferring them.\n- **text_search**(text, pattern): Search for a regex pattern in text.\n- **text_replace**(text, old, new, count) [capability_expand category: write]: Replace occurrences of a substring in text.\n- **time_get**(): Get current date and time.\n- **env_info**(): Get system environment information (OS, Python version, cwd).\n- **skills_list**(query, category, source): List available learned skills. Use when user asks about capabilities or you need a specific skill.\n- **skill_view**(name): View the full content of a specific skill document.\n- **skill_list_files**(name) [capability_expand category: skills]: List all support files in a skill directory (configs, templates, examples). Use before skill_file_read to discover available files.\n- **skill_file_read**(name, file_path) [capability_expand category: skills]: Read an individual support file from a skill directory. Use skill_list_files first to discover available files.\n- **code_intel**(path, operation): Precise document symbols (outline) for a source file: classes, functions, and methods with line ranges. Python uses an exact AST parse; other languages use a keyword-prefix scan. Prefer over file_read mode=symbols for accurate navigation before editing. Read-only.\n- **repo_map**(path): Compact project orientation for a repository root: languages, detected test/lint commands, top-level structure, entry points, manifest, and VCS branch. Call this first when entering an unfamiliar codebase. Read-only.\n- **scm_sync**(action, cwd, remote, pull_ref, push_ref, timeout) [capability_expand category: scm]: Run a typed git SCM action. Use this instead of shell_run for git pull/push/status. For 'pull origin main then push', set action='pull_then_push', remote='origin', pull_ref='main', and omit push_ref so LeapFlow pushes the current local branch.\n- **git_query**(action, cwd, ref, path, staged, max_count, stat): Read-only structured git inspection: action=diff|log|status|branch|show. Prefer over shell_run for reading repo state — output is clipped, redacted, and log/branch are parsed into structured fields. Use scm_sync for pull/push.\n- **git_write**(action, cwd, message, stage_all, name, ref, create) [capability_expand category: scm]: Mutating git actions: action=commit (message, stage_all), branch (create+switch), checkout (switch; create=true for -b). Approval-gated. Use scm_sync for pull/push and git_query for reads.\n- **test_run**(command, cwd, timeout): Run the project's test suite and return structured results (framework, passed/failed counts, failing tests). Auto-detects the runner (pytest/npm/go/cargo) or uses a configured/explicit command; executes via the governed shell. ok=true means the runner executed — see 'success' for pass/fail.\n- **lint_check**(command, cwd, timeout): Run the project's linter and return a structured clean/issue result. Auto-detects the linter (ruff/eslint/go vet/clippy) or uses a configured/explicit command; executes via the governed shell. ok=true means the linter ran — see 'clean'.\n- **file_list**(path, pattern, depth): List files and directories at a given path. Use depth=1 or depth=2 to get a recursive tree in one call instead of listing each sub-directory separately.\n- **file_read**(path, max_lines, start_line, max_chars, mode): Read text file content with adaptive context governance. For large or unfamiliar files, prefer mode='outline' or mode='symbols' first, then use mode='raw' with start_line/max_lines for the specific range you actually need. For LeapFlow's own settings, use config_list / config_get / config_set — its config files are outside the workspace and not readable here.\n- **file_write**(path, content, mode) [capability_expand category: write]: Write content to a file (overwrite or append).\n- **code_search**(pattern, patterns, path, glob, ignore_case, multiline, max_results, context_lines): Search file CONTENTS by regex pattern across a directory tree (ripgrep-backed). Requires a regex pattern. NOT for listing or browsing directory contents — use file_list for that. Prefer this over shell_run grep: faster, skips VCS/dependency/build dirs, and returns structured path:line:column matches. Batch related lookups into ONE call via `patterns` (OR-combined, single pass) instead of issuing several separate searches. Use file_read for the surrounding context of a hit.\n- **file_find**(glob, path, max_results): Find files by a recursive glob pattern under a base path (e.g. '**/test_*.py' or '*.md'). Prefer this over shell_run find; skips VCS/dependency/build dirs.\n- **edit_file**(path, edits, dry_run, diff) [capability_expand category: write]: Apply targeted, anchored search-replace edits to an EXISTING text file (use file_write to create/overwrite). Each edit is {original_text, new_text, replace_all?}; original_text must match exactly and uniquely (or set replace_all) — a non-unique or missing anchor is rejected so files are never corrupted. Set dry_run to preview. Alternatively pass a unified 'diff' to apply its hunks as anchored edits. Far cheaper and safer than rewriting a whole file.\n- **shell_run**(command, cwd, timeout) [capability_expand category: shell]: Execute a one-shot shell command with timeout protection. Runs in the active workspace; paths resolving outside it are refused. Reach for a structured tool first when one fits — web_fetch for anything over HTTP(S), git_query/scm_sync for git, code_search/file_find/file_read for the repo, config_get/config_set for LeapFlow's own settings — because those report typed results, while a failed shell command can only be diagnosed from its exit code and stderr. Every shell run counts as an external side effect, so a failure stops the rest of the batch and is not retried automatically.\n- **terminal_open**(command, cwd, shell) [capability_expand category: terminal]: Open a PERSISTENT shell session (REPL/dev server/watch), returning a session_id for terminal_send/read/close. Disabled unless tools.terminal_session_enabled is set. For one-shot commands use shell_run instead.\n- **terminal_send**(session_id, input, wait) [capability_expand category: terminal]: Send a line of input to a persistent terminal session and return output captured shortly after.\n- **terminal_read**(session_id, wait): Drain buffered output from a persistent terminal session (optionally waiting briefly first).\n- **terminal_close**(session_id) [capability_expand category: terminal]: Terminate a persistent terminal session and release its process group.\n- **terminal_list**(): List active persistent terminal sessions.\n- **config_list**(category, limit): List LeapFlow's own writable settings (LLM model and endpoint, daemon, memory, perception, gateway, …) with current values. Use this to discover the exact key before changing anything. Optionally narrow by `category`. This is the only correct way to inspect LeapFlow configuration — never read config files from disk.\n- **config_get**(key): Read one LeapFlow setting by key (e.g. 'llm.model', 'llm.base_url', 'daemon.log_level'), returning its current value, type, scopes, and whether a change needs a daemon restart. There is no 'llm.provider' key: provider behavior is inferred from the OpenAI-compatible 'llm.base_url'. Never read LeapFlow config files from disk — use this.\n- **config_set**(key, value, scope) [capability_expand category: config]: Change one LeapFlow setting by key, e.g. switch an OpenAI-compatible LLM with key='llm.model' and key='llm.base_url'. There is no 'llm.provider' key: provider behavior is inferred from the endpoint. Values are validated and coerced; credentials are stored in the vault automatically. Call config_list or config_get first if unsure of the key. The result states whether a `leap daemon restart` is required. Never edit LeapFlow config files directly.\n- **web_fetch**(url, select, timeout, max_bytes): Read a URL over HTTP(S) and get back extracted, context-sized content: parsed JSON for API endpoints, readable text plus links for web pages. Use this for anything on the internet — prices, docs, releases, articles — instead of running curl through shell_run: it reports real HTTP status codes, retries rate limits on its own, and is a plain read so a retry is always safe. For JSON APIs pass `select` with a dotted path (e.g. 'chart.result.0.meta') to return just that part instead of the whole payload.\n- **memory_search**(query, limit): Search agent memory for relevant past experiences, observations, and facts.\n- **memory_add**(content, kind) [capability_expand category: write]: Store a new observation or insight in memory for future reference.\n- **research_note**(kind, text): Record a compact, structured note about the current task's state so it survives context compression on long / multi-step tasks. Use for durable findings, open questions still to resolve, decisions / excluded paths, and the immediate next step. One concise sentence per note.\n- **capability_expand**(category): Fetch the full callable schema for every tool in a capability category (e.g. 'hub', 'gateway', 'desktop', 'delegate', 'file', 'memory', 'skill'). The compact tool index always lists every registered tool by name and a one-line summary, but only a static low-risk subset is directly callable each turn. If you need a tool from the index that is not yet callable, call capability_expand with its category first; the matching tools become callable in this turn. Never invent a tool name — expand the category instead.\n- **delegate_task**(goal, context) [capability_expand category: delegate]: Delegate a complex sub-task to an isolated subagent. The subagent gets a fresh context and restricted tool access. Use when a task is self-contained and can be solved independently.\n- **schedule_reentry**(kind, reason, delay_seconds, event_match, max_reentries, deadline_seconds): Register a re-entry so this task can resume later from its current orientation (findings / open questions / next step). Use when work must pause and continue after a delay (kind=time) or when a matching platform event arrives (kind=event), instead of finishing now. The research-ledger state is carried over automatically.\n- **hub_push**(skill_name, visibility, version) [capability_expand category: hub]: Push a local skill to the ModelScope Hub for sharing or backup.\n- **hub_pull**(repo_id, version) [capability_expand category: hub]: Pull a skill from the ModelScope Hub to install locally.\n- **hub_search**(query) [capability_expand category: hub]: Search for skills on the Hub by keyword or description.\n- **hub_sync**(mode, dry_run) [capability_expand category: hub]: Preview or execute sync between local skills and Hub.\n- **platform_action**(platform, action, payload, backend_kind) [capability_expand category: gateway]: Execute an exact registered business action on an external platform through LeapFlow's App Connector layer. Actions must be copied from the App Connector Capability Index and are addressed as domain.operation, e.g. im.send_message or docs.create_markdown. All business fields (chat_id, text, query, etc.) MUST be placed inside `payload`, never at the top level. Example: {\"platform\":\"feishu\",\"action\":\"im.send_message\",\"payload\":{\"chat_id\":\"oc_xxx\",\"text\":\"hello\"}}. Do not invent action names, do not use management actions such as list/guide/connect/status here.\n- **platform_connect**(action, platform, credentials, options, checkpoint) [capability_expand category: gateway]: List, guide, connect, disconnect, remove, or check status for external platforms using the App Connector management namespace. Supports REST and CLI backends. Use this for management actions such as list/guide/preflight/connect/status; use platform_action only for exact registered business actions.\n- **gateway_send**(platform, chat_id, text, thread_id) [capability_expand category: gateway]: Send a message to a connected external platform (Feishu group, Telegram chat, DingTalk conversation, etc.). Requires the platform to be connected via gateway_connect first. Use gateway_connect with action='list' to see connected platforms and available chat IDs.\n- **gateway_connect**(action, platform, credentials, options) [capability_expand category: gateway]: Connect, configure, or manage external platform integrations (Feishu, DingTalk, Telegram, Slack, Discord, etc.). Conversational flow: 1) call 'guide' to get setup steps + required fields, 2) present the steps to the user and ask for ALL required credentials in a single message, 3) call 'connect' with the credentials. Goal: complete in 1–2 user turns. NEVER include credential values in your text response.\n- **plugin_list**(): List the live plugin registry across Tool/Gateway/LLM subsystems for plugin management: which plugins are loaded, their category and fiber lifecycle state. For questions about LeapFlow's own capabilities or whether it supports X, use self_describe instead.\n- **plugin_status**(plugin_id): Get detailed status of a specific plugin: its declared category, runtime dependencies, contributed tools, and fiber lifecycle state.\n- **plugin_versions**(plugin_id): List recorded source versions and active pointer for a profile-scoped plugin.\n- **plugin_propose**(requested_capability, plugin_id, proposed_tools, test_cases, risk_level, evidence): Create a side-effect-free PluginProposal from explicit capability-gap evidence. Use this before plugin_generate when a missing capability should be reviewed. Does not call an LLM, write files, or install anything.\n- **assess_compatibility**(manifest, source_path) [capability_expand category: plugin_management]: Assess whether a foreign plugin manifest or real DSH source bundle is compatible with LeapFlow. A source bundle assessment is static: runtime_ready stays false until restricted Node discovery during plugin_install. Returns component-level verdicts and limitations.\n- **plugin_generate**(plugin_id, description, proposal_id) [capability_expand category: system]: Generate a new ToolPlugin from a natural-language capability description. The LLM produces code that conforms to the ToolPlugin Protocol; it is then rigorously validated (syntax, structure, import, protocol conformance). The generated source is stored in CAS and receives explicit content approval. It DOES NOT install the plugin — installation requires a second, mutation-specific approval via plugin_install.\n- **plugin_install**(plugin_id, code, marketplace_name, source_path, proposal_id, version_label) [capability_expand category: system]: Install a plugin from exactly one source: validated Python code, the configured Python marketplace, or a real DSH source bundle. DSH bundles run restricted Node discovery before registration; only public host tools are installed and client UI limitations are reported. Writes profile-scoped state and mutates the process-global registry. REQUIRES APPROVAL.\n- **plugin_rollback**(plugin_id, version) [capability_expand category: system]: Rollback a profile-scoped plugin to a recorded source version and reload it. REQUIRES APPROVAL.\n- **plugin_reload**(plugin_id, version_label) [capability_expand category: system]: Hot-reload a plugin at runtime. Disposes the old plugin fiber, re-imports its module, and registers a fresh instance. Existing in-flight turns are unaffected (snapshot isolation). REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_disable**(plugin_id) [capability_expand category: system]: Disable a plugin by disposing its fiber, removing its tools from the runtime registry. Cannot disable self_management itself. REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_remove**(plugin_id, delete_source) [capability_expand category: system]: Terminally remove a plugin: dispose its fiber, unregister its tools, remove reload metadata, and optionally delete its profile-scoped source file. Cannot remove self_management itself. REQUIRES APPROVAL.\n- **plugin_unquarantine**(plugin_id) [capability_expand category: plugin_management]: Restore a quarantined plugin to probation status for re-evaluation. Unfreezes the trust ledger, transitions the proposal back to PROBATION, and reloads the plugin. REQUIRES APPROVAL.\n- **plugin_enable**(plugin_id) [capability_expand category: system]: Re-enable a previously disabled plugin by reloading its module and registering a fresh instance. REQUIRES APPROVAL.\n- **self_describe**(facet): Answer questions about LeapFlow ITSELF from live runtime evidence — its identity/version/model, what tools and plugins it has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of self-awareness. Prefer this over reading LeapFlow's own source code. Pick facet=capabilities for 'what can you do / do you support X', identity for version/model, runtime for context/posture, or all for a full self-check when explicitly requested.\n- **runtime_snapshot**(): Lightweight (~150 token) flat snapshot of current runtime state: model, context budget, posture, disclosure level, turn count, cache hit rate, uptime, tool count, and pending approvals.\n- **schedule_create**(trigger_expression, instruction, execution_mode, max_retries, delivery_target) [capability_expand category: scheduler]: Create a new scheduled task. Specify a trigger expression (e.g. '30m', 'every 2h', '0 9 * * *') and the instruction to execute on each trigger. Optionally provide an execution mode and a delivery target to receive notifications.\n- **schedule_list**() [capability_expand category: scheduler]: List all scheduled tasks with their current state, trigger, and next due time.\n- **schedule_status**(task_id) [capability_expand category: scheduler]: Get detailed status and recent execution history for a scheduled task.\n- **schedule_pause**(task_id) [capability_expand category: scheduler]: Pause a scheduled task so it stops firing without being cancelled.\n- **schedule_resume**(task_id) [capability_expand category: scheduler]: Resume a paused scheduled task — re-arms it and recalculates next due time.\n- **schedule_cancel**(task_id) [capability_expand category: scheduler]: Cancel a scheduled task permanently.\n- **tool_search**(query, max_results): Search all registered tools by keyword relevance. Returns a ranked list of matching tools with name, category, and summary. Use this when you need a tool but are unsure of its exact name or category.\n- **tool_describe**(tool_name): Get the full callable schema of a single tool by exact name. Returns the tool's description, parameters, and metadata. Use after tool_search to inspect a specific tool before calling it.\n- **hw_list**(): List connected hardware devices with their channel counts and measured quantities. Start here; it does not include operating limits.\n- **hw_describe**(device_id): Return the full reference for one device: every channel, its unit, its operating envelope, rate limit, reversibility, and required interlocks, plus any prior outcomes recorded for its writable channels. Required before commanding a device.\n- **hw_read**(device_id, channel_id): Read the current value of one device channel. Has no physical effect.\n- **hw_status**(device_id): Report connection health, halt capability, and recent notable events (threshold excursions, lost samples, stalled channels) for one device.\n- **hw_configure**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Set a configuration value or setpoint on a channel declaring effect=configure. Setpoints often have inertia: the value may need time to stabilise.\n- **hw_actuate**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Command motion or output on a channel declaring effect=actuate. This moves physical hardware; a repeat from an unknown state is not a safe retry.\n- **hw_dispense**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Consume an irreversible resource on a channel declaring effect=dispense. Running this twice dispenses twice; never repeat it after a failure without first verifying what already happened.\n- **hw_estop**(device_id) [capability_expand category: hardware]: Stop all motion and output on a device immediately. Never requires approval; use it whenever device behaviour is unexpected.\n- **hw_batch_actuate**(device_id, commands, dry_run, verify) [capability_expand category: hardware]: Write multiple actuator channels in one atomic transaction. Used for coordinated multi-joint control where all positions must be commanded simultaneously (e.g. robot arm joint-space control).\n- **hw_control_start**(device_id, mode, targets, frequency_hz, trajectory, policy, gains) [capability_expand category: hardware]: Start a real-time control loop on a robot device. Supports PID position tracking, impedance control, trajectory following, and neural policy chains.\n- **hw_control_stop**(device_id) [capability_expand category: hardware]: Stop the real-time control loop on a device. Gracefully halts the device and returns session statistics.\n- **hw_control_status**(device_id) [capability_expand category: hardware]: Get the status and statistics of a running control loop. Returns frequency, jitter, overruns, and policy state.\n- **hw_policy_infer**(device_id, policy, task, execute, chunk_size, verify, compute_budget) [capability_expand category: hardware]: Execute VLA policy inference on a robot device. Reads observation, runs policy, optionally writes action to actuators. Supports local (HuggingFace) and remote (PolicyServer) policies.\n- **hw_policy_episode**(action, device_id, policy, task) [capability_expand category: hardware]: Manage policy inference episode lifecycle. Start an episode to begin a task, stop to finalise and get stats.\n- **hw_policy_list**(source) [capability_expand category: hardware]: List available VLA policies and their capabilities.\n- **hw_orchestrate**(mode, operations, barrier_timeout_s, label) [capability_expand category: hardware]: Execute coordinated operations across multiple hardware devices. Supports sequential (one-at-a-time), parallel (concurrent), and barrier (synchronised) execution modes. Any failure in a parallel group triggers halt_all on every participating device for safety.\n- **session_search**(query, limit) [capability_expand category: unclassified]: Search past conversation sessions in the current workspace for relevant context.\n- **session_list**(limit): List recent conversation sessions in the current workspace with stable ids, titles, dates, and summaries. Use for browsing past tasks or when user asks to see history without specific search terms. No keywords needed — returns chronological list.\n- **session_detail**(session_id, limit, offset, include_inactive) [capability_expand category: unclassified]: Read a paginated persisted transcript for one past conversation session in the current workspace. Use after session_list or session_search returns a session_id.\n- **click**(element_index) [capability_expand category: desktop]: Click a UI element by its element_index (from the latest observe_ui snapshot)\n- **get_clipboard**() [capability_expand category: desktop]: Read current clipboard text content\n- **list_apps**(filter, running_only) [capability_expand category: desktop]: List available applications on this system. Use to discover correct bundle_id before switch_app.\n- **list_windows**() [capability_expand category: desktop]: List all top-level windows with pid, window_id, title, and per-window state (minimized, on-screen). Call this first to pick the pid and window_id that observe_ui and other window tools require.\n- **observe_ui**(pid, window_id, query) [capability_expand category: desktop]: Snapshot one window's actionable UI elements, each tagged with an element_index for click/right_click/read_text. Re-observe after actions — indices belong to one snapshot. Requires the window's pid and window_id from list_windows.\n- **open_url**(url, app_id) [capability_expand category: desktop]: Open a URL in the default or specified browser\n- **read_text**(element_index) [capability_expand category: desktop]: Read the text content of a specific UI element from the latest snapshot\n- **right_click**(element_index) [capability_expand category: desktop]: Right-click a UI element to open its context menu. Returns visible menu items.\n- **screenshot**(pid, window_id) [capability_expand category: desktop]: Capture a screenshot for visual verification. With pid + window_id captures that window (works across all displays); defaults to the last observed window, or the full desktop when no window has been observed.\n- **scroll**(element_index, direction, amount, pid, window_id) [capability_expand category: desktop]: Scroll a scrollable area of a window. Omit element_index to scroll the window's focused/page scroller; pass one to scroll an exact element from the latest snapshot.\n- **select_text**(element_index) [capability_expand category: desktop]: Select all text in a UI element (focus + select-all, for subsequent copy)\n- **set_clipboard**(text) [capability_expand category: desktop]: Write text to the clipboard\n- **shortcut**(keys) [capability_expand category: desktop]: Execute a keyboard shortcut\n- **switch_app**(app_id) [capability_expand category: desktop]: Switch to an app (launch if needed, activate, verify)\n- **type_text**(text) [capability_expand category: desktop]: Type text into the currently focused element\n- **wait**(seconds) [capability_expand category: desktop]: Wait for a specified duration before continuing\n- **wait_until**(condition, pid, window_id, timeout, poll_interval) [capability_expand category: desktop]: Wait until a UI condition is met (polls UI tree). Returns elements when found or on timeout.\n- **wait_until_stable**(timeout, poll_interval, pid, window_id) [capability_expand category: desktop]: Wait until the UI stops changing (element set stabilizes across polls).\n\n## App Connector Capability Index\nLeapFlow can onboard and manage external apps through `platform_connect` and execute exact registered business actions through `platform_action`.\nFor requests about connecting, setting up, configuring, enabling, or managing a supported app, use `platform_connect` first instead of generating SDK/Webhook sample code.\n`platform_connect.action` is the management namespace: list, guide, preflight, connect, disconnect, remove, status, events_start, events_stop, events_status.\n`platform_action.action` is only for exact registered platform business actions listed below, such as `im.send_message`; never use management actions like `list` or `guide` there.\nAll business fields MUST go inside `payload`; top-level keys are only `platform`, `action`, and `payload`.\nDo not invent platform IDs or platform action names. If the needed action is not listed, ask for discovery/clarification instead of guessing.\nUse `platform_connect` with `action='guide'` and the matching `platform` to start onboarding; use `action='list'` when the app is unclear.\nWhen a pending onboarding state is present, continue from that state with `platform_connect` instead of asking the user to restate the app.\nSupported apps:\n- `api_server`: API Server (OpenAI Compatible) (category=api; backend=adapter; platform_action actions=none registered)\n- `dingtalk`: 钉钉 (DingTalk) (category=im; backend=adapter; platform_action actions=none registered)\n- `feishu`: 飞书 (Feishu/Lark) (category=im; backend=cli)\n - `calendar.create_event` payload={summary*, start_time*, end_time*, attendees} [write/high]\n - `docs.create_markdown` payload={title*, markdown*, folder_token} [write/medium]\n - `drive.search` payload={query*, limit} [read/medium]\n - `im.add_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.download_resource` payload={message_id*, file_key*, type*} [read/low]\n - `im.get_messages` payload={message_ids*} [read/low]\n - `im.list_chats` payload={page_size} [read/low]\n - `im.list_messages` payload={chat_id*} [read/low]\n - `im.list_thread_messages` payload={thread*} [read/low]\n - `im.remove_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.reply_message` payload={message_id*, text*} [send/high]\n - `im.search_chats` payload={query*, page_size} [read/low]\n - `im.search_messages` payload={query*} [read/medium]\n - `im.send_message` payload={chat_id*, text*, thread_id} [send/high]\n - `im.update_card` payload={token*, card*} [write/medium]\n - `im.update_message` payload={message_id*, text*} [write/medium]\n - `mail.search_unread` payload={query, limit} [read/high]\n - `sheets.append_row` payload={spreadsheet_token*, sheet_id*, values*} [write/medium]\n - `task.create` payload={title*, description, due_time} [write/medium]\n- `telegram`: Telegram (category=social; backend=adapter; platform_action actions=none registered)\n- `webhook`: Webhook (Generic) (category=webhook; backend=adapter; platform_action actions=none registered)\n\n## Learned Skills\nYou have access to the following learned skills. Use `skills_list` to browse or `skill_view` to read details:\n- api_designer: REST and GraphQL API design with OpenAPI spec generation and best practices [API, REST, GraphQL]\n- code_review: Structured code review for security, performance, and style [code, review, security]\n- data_analysis: CSV/JSON data analysis with statistical summaries, quality checks, and visualiza [data, analysis, statistics]\n- devops_helper: CI/CD pipeline configuration, Docker orchestration, and deployment strategy [devops, CI/CD, Docker]\n- document_writer: Structured document generation: technical docs, API references, reports, and REA [documentation, technical-writing, README]\n- git_workflow: Git operation orchestration: branching, commits, conflicts, and PR workflow [git, version-control, branching]\n- i18n_helper: Internationalization workflow: string extraction, translation, locale management [i18n, internationalization, localization]\n- project_analysis: Project structure analysis, dependency audit, and tech stack identification [project, analysis, dependencies]\n- security_audit: Security vulnerability scanning strategy, threat modeling, and fix recommendatio [security, vulnerability, audit]\n- shell_automation: Natural language to shell commands with safety review and step-by-step execution [shell, terminal, automation]\n- test_writer: Generate unit and integration tests from source code with edge-case coverage [testing, unit-test, integration-test]\n- web_research: Multi-step web search with cross-verification and structured synthesis [web, search, research]\n\n## Tool Usage\nTools are normally invoked through the native function-calling mechanism, not by writing JSON in your reply\ntext. Only if the provider signals that native function calling is unavailable for this turn, fall back to a\nsingle JSON code block: `{\"name\": \"tool_name\", \"arguments\": {\"key\": \"value\"}}` — use this fallback format\nonly, never both. Only call a tool whose exact name appears in the tool index above and is currently callable\n(or reachable via `capability_expand`). Never invent, rename, alias, or guess a tool name, platform ID, or\nplatform action from argument shape or wording — if the index does not list it, it does not exist.\n`platform_connect.action` (list/guide/preflight/connect/disconnect/remove/status/events_start/events_stop/\nevents_status) is the App Connector management namespace; `platform_action.action` only accepts exact\nregistered business actions such as `im.send_message` shown in the App Connector Capability Index — never mix\nthe two namespaces. If a tool call returns an unknown/unavailable result, use the returned suggestions or\navailable names for a single retry instead of trying further variations of the same guess.\n\n**Side-effect action rule** (`platform_action` with effect=send/write/execute):\n- Call each unique action+payload **exactly once**. Never include duplicates in the same turn.\n- Once the result returns `\"completed\": true`, that action is DONE for this task. Do NOT call it again\n in any subsequent turn — immediately summarize the result for the user instead.\n- The system enforces idempotency: duplicate calls are blocked and will not execute.\n- If the user explicitly requests sending/writing multiple times, use distinct payloads per call.\n\n**Resource identifier provenance rule**:\n- NEVER fabricate, guess, or infer resource identifiers (chat_id, message_id, file_key, user_id, etc.).\n Every resource ID used in a side-effect action MUST come from a successful API response in this session.\n- If a read/list action fails (e.g. authorization error), you do NOT have valid resource IDs.\n Report the failure to the user — do NOT attempt the dependent write/send action with a guessed ID.\n- When a tool result contains `\"llm_instruction\"`, follow it exactly.\n\n## Guidelines\n1. **Direct answers first**: If you already know the answer, respond directly without tools.\n2. **Avoid redundant tool calls**: Do not call the same tool with the same arguments more than once in the same user turn. When an existing tool result already answers the user's request, stop calling tools and answer directly.\n3. **Use tools proactively**: When the user asks about files, time, system state, or needs actions performed, use the appropriate tool.\n4. **Chain tools when needed**: You can call multiple tools in sequence (e.g., list files → read file → summarize).\n5. **Handle failures gracefully**: If a tool fails, explain what went wrong and suggest alternatives. If it failed because of an unknown tool/platform/action name, retry once with an exact name from the returned suggestions, then explain rather than keep guessing.\n6. **Summarize results naturally**: After tool execution, synthesize the results into a helpful answer rather than dumping raw output.\n7. **Stay conversational**: Maintain a natural, helpful tone. Acknowledge context from earlier in the conversation.\n8. **Recall past work**:\n - Broad queries (\"之前做了什么\", \"列出任务\"): answer from the \"Recent Task History\" section already in your context. If insufficient, call session_list.\n - Specific lookups (\"上次那个配置怎么改的\"): call session_search with relevant phrases (NOT single characters).\n - Do NOT call search tools repeatedly with keyword fragments. One well-phrased call is better than ten fragmented ones.\n\n## Coding & Verification\nWhen working with code, prefer the precise built-in tools over ad-hoc shell: use `repo_map` to orient in an\nunfamiliar project, `code_search`/`file_find` to locate and `code_intel` for symbols, `edit_file` (anchored\nsearch-replace, or a unified `diff`) to change files — never rewrite a whole file to change a few lines — and\n`git_query` to inspect diffs/log. After edits, check `syntax_ok` in the result and run `test_run`/`lint_check`\nbefore declaring the work done. Batch independent read-only calls (search/read/query) into a single turn — they\nrun in parallel.\n\n## Presentation Style\n1. **Polished Markdown only**: Format user-facing answers with clean Markdown headings, short paragraphs, and concise bullets. Use tables only when they improve comparison or scanning.\n2. **Terminal-friendly layout**: Keep lines readable in a TUI; avoid dense walls of text, deeply nested lists, oversized ASCII art, or heavy visual blocks.\n3. **Elegant emphasis**: Use bold text sparingly for key terms and conclusions. Avoid excessive emojis, decorative symbols, repeated separators, or visual noise.\n4. **Theme-safe colors**: Do not emit ANSI escape codes, HTML color tags, Rich markup, or hardcoded color names. Rely on the TUI theme to render Markdown professionally.\n5. **No leaked tool protocol**: Never show tool-call JSON, internal schemas, raw observations, tool result payloads, or hidden reasoning in the final answer unless the user explicitly asks for raw/debug output. Treat any prior `{\"name\": ..., \"arguments\": ...}` blocks and `Tool result (...)` messages as internal execution context only.\n6. **Professional closure**: End with a concise conclusion or next step when helpful; avoid rambling after the useful answer is complete.\n\nWhen finished with all tool calls, respond normally without a JSON block, tool-call transcript, or process log.\n\n## Task Contract\n- Task ID: turn-2\n- Original user request: Read the current value of the setpoint channel on bench_r8.\n- Workspace root: \n- Allowed roots: \n- Treat relative project paths as relative to the workspace root; never infer `.` as the project root when a workspace root is provided.\n- Workspace boundary is enforced by tools: do not read, search, edit, or run commands against paths outside the allowed roots unless the user explicitly requests an external path and the tool/approval policy permits it.\n- LeapFlow workspace config is optional at `/.leapflow/config.yaml`; runtime config is loaded from `~/.leapflow/config/user.yaml` and `~/.leapflow/profiles//config/*.yaml`.\n- Preserve this task contract across summarization, compression, tool loops, and memory retrieval.\n" + "content": "You are LeapFlow, an intelligent assistant that can both converse naturally and take real actions on the user's computer.\n\n## Capabilities\nThe tool index below lists **every** registered tool by name and a one-line summary — this is the complete\ncapability contract; nothing else exists. Only a subset is directly callable this turn (via native tool calling,\nnot a JSON block in your reply). If you need a tool from the index that is not yet callable, call\n`capability_expand` with its category name first — the matching tools become callable immediately after.\nWhen the user asks about LeapFlow **itself** — its identity/version/model, what tools, plugins, or capabilities\nit has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of\nself-awareness — answer from live runtime evidence via `self_describe` (facet=capabilities for \"what can you\ndo / do you support X\", identity for version/model, runtime for context/posture, or `all` for a full self-check),\nnever by reading LeapFlow's own source code, documentation, or memory. Report configuration-dependent or\nunavailable capabilities as limitations rather than inferring them.\n- **text_search**(text, pattern): Search for a regex pattern in text.\n- **text_replace**(text, old, new, count) [capability_expand category: write]: Replace occurrences of a substring in text.\n- **time_get**(): Get current date and time.\n- **env_info**(): Get system environment information (OS, Python version, cwd).\n- **skills_list**(query, category, source): List available learned skills. Use when user asks about capabilities or you need a specific skill.\n- **skill_view**(name): View the full content of a specific skill document.\n- **skill_list_files**(name) [capability_expand category: skills]: List all support files in a skill directory (configs, templates, examples). Use before skill_file_read to discover available files.\n- **skill_file_read**(name, file_path) [capability_expand category: skills]: Read an individual support file from a skill directory. Use skill_list_files first to discover available files.\n- **code_intel**(path, operation): Precise document symbols (outline) for a source file: classes, functions, and methods with line ranges. Python uses an exact AST parse; other languages use a keyword-prefix scan. Prefer over file_read mode=symbols for accurate navigation before editing. Read-only.\n- **repo_map**(path): Compact project orientation for a repository root: languages, detected test/lint commands, top-level structure, entry points, manifest, and VCS branch. Call this first when entering an unfamiliar codebase. Read-only.\n- **scm_sync**(action, cwd, remote, pull_ref, push_ref, timeout) [capability_expand category: scm]: Run a typed git SCM action. Use this instead of shell_run for git pull/push/status. For 'pull origin main then push', set action='pull_then_push', remote='origin', pull_ref='main', and omit push_ref so LeapFlow pushes the current local branch.\n- **git_query**(action, cwd, ref, path, staged, max_count, stat): Read-only structured git inspection: action=diff|log|status|branch|show. Prefer over shell_run for reading repo state — output is clipped, redacted, and log/branch are parsed into structured fields. Use scm_sync for pull/push.\n- **git_write**(action, cwd, message, stage_all, name, ref, create) [capability_expand category: scm]: Mutating git actions: action=commit (message, stage_all), branch (create+switch), checkout (switch; create=true for -b). Approval-gated. Use scm_sync for pull/push and git_query for reads.\n- **test_run**(command, cwd, timeout): Run the project's test suite and return structured results (framework, passed/failed counts, failing tests). Auto-detects the runner (pytest/npm/go/cargo) or uses a configured/explicit command; executes via the governed shell. ok=true means the runner executed — see 'success' for pass/fail.\n- **lint_check**(command, cwd, timeout): Run the project's linter and return a structured clean/issue result. Auto-detects the linter (ruff/eslint/go vet/clippy) or uses a configured/explicit command; executes via the governed shell. ok=true means the linter ran — see 'clean'.\n- **file_list**(path, pattern, depth): List files and directories at a given path. Use depth=1 or depth=2 to get a recursive tree in one call instead of listing each sub-directory separately.\n- **file_read**(path, max_lines, start_line, max_chars, mode): Read text file content with adaptive context governance. For large or unfamiliar files, prefer mode='outline' or mode='symbols' first, then use mode='raw' with start_line/max_lines for the specific range you actually need. For LeapFlow's own settings, use config_list / config_get / config_set — its config files are outside the workspace and not readable here.\n- **file_write**(path, content, mode) [capability_expand category: write]: Write content to a file (overwrite or append).\n- **code_search**(pattern, patterns, path, glob, ignore_case, multiline, max_results, context_lines): Search file CONTENTS by regex pattern across a directory tree (ripgrep-backed). Requires a regex pattern. NOT for listing or browsing directory contents — use file_list for that. Prefer this over shell_run grep: faster, skips VCS/dependency/build dirs, and returns structured path:line:column matches. Batch related lookups into ONE call via `patterns` (OR-combined, single pass) instead of issuing several separate searches. Use file_read for the surrounding context of a hit.\n- **file_find**(glob, path, max_results): Find files by a recursive glob pattern under a base path (e.g. '**/test_*.py' or '*.md'). Prefer this over shell_run find; skips VCS/dependency/build dirs.\n- **edit_file**(path, edits, dry_run, diff) [capability_expand category: write]: Apply targeted, anchored search-replace edits to an EXISTING text file (use file_write to create/overwrite). Each edit is {original_text, new_text, replace_all?}; original_text must match exactly and uniquely (or set replace_all) — a non-unique or missing anchor is rejected so files are never corrupted. Set dry_run to preview. Alternatively pass a unified 'diff' to apply its hunks as anchored edits. Far cheaper and safer than rewriting a whole file.\n- **shell_run**(command, cwd, timeout) [capability_expand category: shell]: Execute a one-shot shell command with timeout protection. Runs in the active workspace; paths resolving outside it are refused. Reach for a structured tool first when one fits — web_fetch for anything over HTTP(S), git_query/scm_sync for git, code_search/file_find/file_read for the repo, config_get/config_set for LeapFlow's own settings — because those report typed results, while a failed shell command can only be diagnosed from its exit code and stderr. Every shell run counts as an external side effect, so a failure stops the rest of the batch and is not retried automatically.\n- **terminal_open**(command, cwd, shell) [capability_expand category: terminal]: Open a PERSISTENT shell session (REPL/dev server/watch), returning a session_id for terminal_send/read/close. Disabled unless tools.terminal_session_enabled is set. For one-shot commands use shell_run instead.\n- **terminal_send**(session_id, input, wait) [capability_expand category: terminal]: Send a line of input to a persistent terminal session and return output captured shortly after.\n- **terminal_read**(session_id, wait): Drain buffered output from a persistent terminal session (optionally waiting briefly first).\n- **terminal_close**(session_id) [capability_expand category: terminal]: Terminate a persistent terminal session and release its process group.\n- **terminal_list**(): List active persistent terminal sessions.\n- **config_list**(category, limit): List LeapFlow's own writable settings (LLM model and endpoint, daemon, memory, perception, gateway, …) with current values. Use this to discover the exact key before changing anything. Optionally narrow by `category`. This is the only correct way to inspect LeapFlow configuration — never read config files from disk.\n- **config_get**(key): Read one LeapFlow setting by key (e.g. 'llm.model', 'llm.base_url', 'daemon.log_level'), returning its current value, type, scopes, and whether a change needs a daemon restart. There is no 'llm.provider' key: provider behavior is inferred from the OpenAI-compatible 'llm.base_url'. Never read LeapFlow config files from disk — use this.\n- **config_set**(key, value, scope) [capability_expand category: config]: Change one LeapFlow setting by key, e.g. switch an OpenAI-compatible LLM with key='llm.model' and key='llm.base_url'. There is no 'llm.provider' key: provider behavior is inferred from the endpoint. Values are validated and coerced; credentials are stored in the vault automatically. Call config_list or config_get first if unsure of the key. The result states whether a `leap daemon restart` is required. Never edit LeapFlow config files directly.\n- **web_fetch**(url, select, timeout, max_bytes): Read a URL over HTTP(S) and get back extracted, context-sized content: parsed JSON for API endpoints, readable text plus links for web pages. Use this for anything on the internet — prices, docs, releases, articles — instead of running curl through shell_run: it reports real HTTP status codes, retries rate limits on its own, and is a plain read so a retry is always safe. For JSON APIs pass `select` with a dotted path (e.g. 'chart.result.0.meta') to return just that part instead of the whole payload.\n- **memory_search**(query, limit): Search agent memory for relevant past experiences, observations, and facts.\n- **memory_add**(content, kind) [capability_expand category: write]: Store a new observation or insight in memory for future reference.\n- **research_note**(kind, text): Record a compact, structured note about the current task's state so it survives context compression on long / multi-step tasks. Use for durable findings, open questions still to resolve, decisions / excluded paths, and the immediate next step. One concise sentence per note.\n- **capability_expand**(category): Fetch the full callable schema for every tool in a capability category (e.g. 'hub', 'gateway', 'desktop', 'delegate', 'file', 'memory', 'skill'). The compact tool index always lists every registered tool by name and a one-line summary, but only a static low-risk subset is directly callable each turn. If you need a tool from the index that is not yet callable, call capability_expand with its category first; the matching tools become callable in this turn. Never invent a tool name — expand the category instead.\n- **delegate_task**(goal, context) [capability_expand category: delegate]: Delegate a complex sub-task to an isolated subagent. The subagent gets a fresh context and restricted tool access. Use when a task is self-contained and can be solved independently.\n- **schedule_reentry**(kind, reason, delay_seconds, event_match, max_reentries, deadline_seconds): Register a re-entry so this task can resume later from its current orientation (findings / open questions / next step). Use when work must pause and continue after a delay (kind=time) or when a matching platform event arrives (kind=event), instead of finishing now. The research-ledger state is carried over automatically.\n- **hub_push**(skill_name, visibility, version) [capability_expand category: hub]: Push a local skill to the ModelScope Hub for sharing or backup.\n- **hub_pull**(repo_id, version) [capability_expand category: hub]: Pull a skill from the ModelScope Hub to install locally.\n- **hub_search**(query) [capability_expand category: hub]: Search for skills on the Hub by keyword or description.\n- **hub_sync**(mode, dry_run) [capability_expand category: hub]: Preview or execute sync between local skills and Hub.\n- **platform_action**(platform, action, payload, backend_kind) [capability_expand category: gateway]: Execute an exact registered business action on an external platform through LeapFlow's App Connector layer. Actions must be copied from the App Connector Capability Index and are addressed as domain.operation, e.g. im.send_message or docs.create_markdown. All business fields (chat_id, text, query, etc.) MUST be placed inside `payload`, never at the top level. Example: {\"platform\":\"feishu\",\"action\":\"im.send_message\",\"payload\":{\"chat_id\":\"oc_xxx\",\"text\":\"hello\"}}. Do not invent action names, do not use management actions such as list/guide/connect/status here.\n- **platform_connect**(action, platform, credentials, options, checkpoint) [capability_expand category: gateway]: List, guide, connect, disconnect, remove, or check status for external platforms using the App Connector management namespace. Supports REST and CLI backends. Use this for management actions such as list/guide/preflight/connect/status; use platform_action only for exact registered business actions.\n- **gateway_send**(platform, chat_id, text, thread_id) [capability_expand category: gateway]: Send a message to a connected external platform (Feishu group, Telegram chat, DingTalk conversation, etc.). Requires the platform to be connected via gateway_connect first. Use gateway_connect with action='list' to see connected platforms and available chat IDs.\n- **gateway_connect**(action, platform, credentials, options) [capability_expand category: gateway]: Connect, configure, or manage external platform integrations (Feishu, DingTalk, Telegram, Slack, Discord, etc.). Conversational flow: 1) call 'guide' to get setup steps + required fields, 2) present the steps to the user and ask for ALL required credentials in a single message, 3) call 'connect' with the credentials. Goal: complete in 1–2 user turns. NEVER include credential values in your text response.\n- **plugin_list**(): List the live plugin registry across Tool/Gateway/LLM subsystems for plugin management: which plugins are loaded, their category and fiber lifecycle state. For questions about LeapFlow's own capabilities or whether it supports X, use self_describe instead.\n- **plugin_status**(plugin_id): Get detailed status of a specific plugin: its declared category, runtime dependencies, contributed tools, and fiber lifecycle state.\n- **plugin_versions**(plugin_id): List recorded source versions and active pointer for a profile-scoped plugin.\n- **plugin_propose**(requested_capability, plugin_id, proposed_tools, test_cases, risk_level, evidence): Create a side-effect-free PluginProposal from explicit capability-gap evidence. Use this before plugin_generate when a missing capability should be reviewed. Does not call an LLM, write files, or install anything.\n- **assess_compatibility**(manifest, source_path) [capability_expand category: plugin_management]: Assess whether a foreign plugin manifest or real DSH source bundle is compatible with LeapFlow. A source bundle assessment is static: runtime_ready stays false until restricted Node discovery during plugin_install. Returns component-level verdicts and limitations.\n- **plugin_generate**(plugin_id, description, proposal_id) [capability_expand category: system]: Generate a new ToolPlugin from a natural-language capability description. The LLM produces code that conforms to the ToolPlugin Protocol; it is then rigorously validated (syntax, structure, import, protocol conformance). The generated source is stored in CAS and receives explicit content approval. It DOES NOT install the plugin — installation requires a second, mutation-specific approval via plugin_install.\n- **plugin_install**(plugin_id, code, marketplace_name, source_path, proposal_id, version_label) [capability_expand category: system]: Install a plugin from exactly one source: validated Python code, the configured Python marketplace, or a real DSH source bundle. DSH bundles run restricted Node discovery before registration; only public host tools are installed and client UI limitations are reported. Writes profile-scoped state and mutates the process-global registry. REQUIRES APPROVAL.\n- **plugin_rollback**(plugin_id, version) [capability_expand category: system]: Rollback a profile-scoped plugin to a recorded source version and reload it. REQUIRES APPROVAL.\n- **plugin_reload**(plugin_id, version_label) [capability_expand category: system]: Hot-reload a plugin at runtime. Disposes the old plugin fiber, re-imports its module, and registers a fresh instance. Existing in-flight turns are unaffected (snapshot isolation). REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_disable**(plugin_id) [capability_expand category: system]: Disable a plugin by disposing its fiber, removing its tools from the runtime registry. Cannot disable self_management itself. REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_remove**(plugin_id, delete_source) [capability_expand category: system]: Terminally remove a plugin: dispose its fiber, unregister its tools, remove reload metadata, and optionally delete its profile-scoped source file. Cannot remove self_management itself. REQUIRES APPROVAL.\n- **plugin_unquarantine**(plugin_id) [capability_expand category: plugin_management]: Restore a quarantined plugin to probation status for re-evaluation. Unfreezes the trust ledger, transitions the proposal back to PROBATION, and reloads the plugin. REQUIRES APPROVAL.\n- **plugin_enable**(plugin_id) [capability_expand category: system]: Re-enable a previously disabled plugin by reloading its module and registering a fresh instance. REQUIRES APPROVAL.\n- **self_describe**(facet): Answer questions about LeapFlow ITSELF from live runtime evidence — its identity/version/model, what tools and plugins it has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of self-awareness. Prefer this over reading LeapFlow's own source code. Pick facet=capabilities for 'what can you do / do you support X', identity for version/model, runtime for context/posture, or all for a full self-check when explicitly requested.\n- **runtime_snapshot**(): Lightweight (~150 token) flat snapshot of current runtime state: model, context budget, posture, disclosure level, turn count, cache hit rate, uptime, tool count, and pending approvals.\n- **schedule_create**(trigger_expression, instruction, execution_mode, max_retries, delivery_target) [capability_expand category: scheduler]: Create a new scheduled task. Specify a trigger expression (e.g. '30m', 'every 2h', '0 9 * * *') and the instruction to execute on each trigger. Optionally provide an execution mode and a delivery target to receive notifications.\n- **schedule_list**() [capability_expand category: scheduler]: List all scheduled tasks with their current state, trigger, and next due time.\n- **schedule_status**(task_id) [capability_expand category: scheduler]: Get detailed status and recent execution history for a scheduled task.\n- **schedule_pause**(task_id) [capability_expand category: scheduler]: Pause a scheduled task so it stops firing without being cancelled.\n- **schedule_resume**(task_id) [capability_expand category: scheduler]: Resume a paused scheduled task — re-arms it and recalculates next due time.\n- **schedule_cancel**(task_id) [capability_expand category: scheduler]: Cancel a scheduled task permanently.\n- **tool_search**(query, max_results): Search all registered tools by keyword relevance. Returns a ranked list of matching tools with name, category, and summary. Use this when you need a tool but are unsure of its exact name or category.\n- **tool_describe**(tool_name): Get the full callable schema of a single tool by exact name. Returns the tool's description, parameters, and metadata. Use after tool_search to inspect a specific tool before calling it.\n- **hw_list**(): List connected hardware devices with their channel counts and measured quantities. Start here; it does not include operating limits.\n- **hw_describe**(device_id): Return the full reference for one device: every channel, its unit, its operating envelope, rate limit, reversibility, and required interlocks, plus any prior outcomes recorded for its writable channels. Required before commanding a device.\n- **hw_read**(device_id, channel_id): Read the current value of one device channel. Has no physical effect.\n- **hw_status**(device_id): Report connection health, halt capability, and recent notable events (threshold excursions, lost samples, stalled channels) for one device.\n- **hw_configure**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Set a configuration value or setpoint on a channel declaring effect=configure. Setpoints often have inertia: the value may need time to stabilise.\n- **hw_actuate**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Command motion or output on a channel declaring effect=actuate. This moves physical hardware; a repeat from an unknown state is not a safe retry.\n- **hw_dispense**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Consume an irreversible resource on a channel declaring effect=dispense. Running this twice dispenses twice; never repeat it after a failure without first verifying what already happened.\n- **hw_estop**(device_id) [capability_expand category: hardware]: Stop all motion and output on a device immediately. Never requires approval; use it whenever device behaviour is unexpected.\n- **hw_batch_actuate**(device_id, commands, dry_run, verify) [capability_expand category: hardware]: Write multiple actuator channels in one atomic transaction. Used for coordinated multi-joint control where all positions must be commanded simultaneously (e.g. robot arm joint-space control).\n- **hw_control_start**(device_id, mode, targets, frequency_hz, trajectory, policy, gains) [capability_expand category: hardware]: Start a real-time control loop on a robot device. Supports PID position tracking, impedance control, trajectory following, and neural policy chains.\n- **hw_control_stop**(device_id) [capability_expand category: hardware]: Stop the real-time control loop on a device. Gracefully halts the device and returns session statistics.\n- **hw_control_status**(device_id) [capability_expand category: hardware]: Get the status and statistics of a running control loop. Returns frequency, jitter, overruns, and policy state.\n- **hw_context_snapshot**(level) [capability_expand category: hardware]: Get a structured snapshot of all hardware devices and their state. Use this before planning physical manipulation tasks to understand what devices are available and what they can do.\n- **hw_health**(device_id) [capability_expand category: hardware]: Check hardware device health status. Without device_id, returns fleet-wide health summary. With device_id, returns health details for that specific device.\n- **hw_policy_infer**(device_id, policy, task, execute, chunk_size, verify, compute_budget) [capability_expand category: hardware]: Execute VLA policy inference on a robot device. Reads observation, runs policy, optionally writes action to actuators. Supports local (HuggingFace) and remote (PolicyServer) policies.\n- **hw_policy_episode**(action, device_id, policy, task) [capability_expand category: hardware]: Manage policy inference episode lifecycle. Start an episode to begin a task, stop to finalise and get stats.\n- **hw_policy_list**(source) [capability_expand category: hardware]: List available VLA policies and their capabilities.\n- **hw_orchestrate**(mode, operations, barrier_timeout_s, label) [capability_expand category: hardware]: Execute coordinated operations across multiple hardware devices. Supports sequential (one-at-a-time), parallel (concurrent), and barrier (synchronised) execution modes. Any failure in a parallel group triggers halt_all on every participating device for safety.\n- **session_search**(query, limit) [capability_expand category: unclassified]: Search past conversation sessions in the current workspace for relevant context.\n- **session_list**(limit): List recent conversation sessions in the current workspace with stable ids, titles, dates, and summaries. Use for browsing past tasks or when user asks to see history without specific search terms. No keywords needed — returns chronological list.\n- **session_detail**(session_id, limit, offset, include_inactive) [capability_expand category: unclassified]: Read a paginated persisted transcript for one past conversation session in the current workspace. Use after session_list or session_search returns a session_id.\n- **click**(element_index) [capability_expand category: desktop]: Click a UI element by its element_index (from the latest observe_ui snapshot)\n- **get_clipboard**() [capability_expand category: desktop]: Read current clipboard text content\n- **list_apps**(filter, running_only) [capability_expand category: desktop]: List available applications on this system. Use to discover correct bundle_id before switch_app.\n- **list_windows**() [capability_expand category: desktop]: List all top-level windows with pid, window_id, title, and per-window state (minimized, on-screen). Call this first to pick the pid and window_id that observe_ui and other window tools require.\n- **observe_ui**(pid, window_id, query) [capability_expand category: desktop]: Snapshot one window's actionable UI elements, each tagged with an element_index for click/right_click/read_text. Re-observe after actions — indices belong to one snapshot. Requires the window's pid and window_id from list_windows.\n- **open_url**(url, app_id) [capability_expand category: desktop]: Open a URL in the default or specified browser\n- **read_text**(element_index) [capability_expand category: desktop]: Read the text content of a specific UI element from the latest snapshot\n- **right_click**(element_index) [capability_expand category: desktop]: Right-click a UI element to open its context menu. Returns visible menu items.\n- **screenshot**(pid, window_id) [capability_expand category: desktop]: Capture a screenshot for visual verification. With pid + window_id captures that window (works across all displays); defaults to the last observed window, or the full desktop when no window has been observed.\n- **scroll**(element_index, direction, amount, pid, window_id) [capability_expand category: desktop]: Scroll a scrollable area of a window. Omit element_index to scroll the window's focused/page scroller; pass one to scroll an exact element from the latest snapshot.\n- **select_text**(element_index) [capability_expand category: desktop]: Select all text in a UI element (focus + select-all, for subsequent copy)\n- **set_clipboard**(text) [capability_expand category: desktop]: Write text to the clipboard\n- **shortcut**(keys) [capability_expand category: desktop]: Execute a keyboard shortcut\n- **switch_app**(app_id) [capability_expand category: desktop]: Switch to an app (launch if needed, activate, verify)\n- **type_text**(text) [capability_expand category: desktop]: Type text into the currently focused element\n- **wait**(seconds) [capability_expand category: desktop]: Wait for a specified duration before continuing\n- **wait_until**(condition, pid, window_id, timeout, poll_interval) [capability_expand category: desktop]: Wait until a UI condition is met (polls UI tree). Returns elements when found or on timeout.\n- **wait_until_stable**(timeout, poll_interval, pid, window_id) [capability_expand category: desktop]: Wait until the UI stops changing (element set stabilizes across polls).\n\n## App Connector Capability Index\nLeapFlow can onboard and manage external apps through `platform_connect` and execute exact registered business actions through `platform_action`.\nFor requests about connecting, setting up, configuring, enabling, or managing a supported app, use `platform_connect` first instead of generating SDK/Webhook sample code.\n`platform_connect.action` is the management namespace: list, guide, preflight, connect, disconnect, remove, status, events_start, events_stop, events_status.\n`platform_action.action` is only for exact registered platform business actions listed below, such as `im.send_message`; never use management actions like `list` or `guide` there.\nAll business fields MUST go inside `payload`; top-level keys are only `platform`, `action`, and `payload`.\nDo not invent platform IDs or platform action names. If the needed action is not listed, ask for discovery/clarification instead of guessing.\nUse `platform_connect` with `action='guide'` and the matching `platform` to start onboarding; use `action='list'` when the app is unclear.\nWhen a pending onboarding state is present, continue from that state with `platform_connect` instead of asking the user to restate the app.\nSupported apps:\n- `api_server`: API Server (OpenAI Compatible) (category=api; backend=adapter; platform_action actions=none registered)\n- `dingtalk`: 钉钉 (DingTalk) (category=im; backend=adapter; platform_action actions=none registered)\n- `feishu`: 飞书 (Feishu/Lark) (category=im; backend=cli)\n - `calendar.create_event` payload={summary*, start_time*, end_time*, attendees} [write/high]\n - `docs.create_markdown` payload={title*, markdown*, folder_token} [write/medium]\n - `drive.search` payload={query*, limit} [read/medium]\n - `im.add_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.download_resource` payload={message_id*, file_key*, type*} [read/low]\n - `im.get_messages` payload={message_ids*} [read/low]\n - `im.list_chats` payload={page_size} [read/low]\n - `im.list_messages` payload={chat_id*} [read/low]\n - `im.list_thread_messages` payload={thread*} [read/low]\n - `im.remove_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.reply_message` payload={message_id*, text*} [send/high]\n - `im.search_chats` payload={query*, page_size} [read/low]\n - `im.search_messages` payload={query*} [read/medium]\n - `im.send_message` payload={chat_id*, text*, thread_id} [send/high]\n - `im.update_card` payload={token*, card*} [write/medium]\n - `im.update_message` payload={message_id*, text*} [write/medium]\n - `mail.search_unread` payload={query, limit} [read/high]\n - `sheets.append_row` payload={spreadsheet_token*, sheet_id*, values*} [write/medium]\n - `task.create` payload={title*, description, due_time} [write/medium]\n- `telegram`: Telegram (category=social; backend=adapter; platform_action actions=none registered)\n- `webhook`: Webhook (Generic) (category=webhook; backend=adapter; platform_action actions=none registered)\n\n## Learned Skills\nYou have access to the following learned skills. Use `skills_list` to browse or `skill_view` to read details:\n- api_designer: REST and GraphQL API design with OpenAPI spec generation and best practices [API, REST, GraphQL]\n- code_review: Structured code review for security, performance, and style [code, review, security]\n- data_analysis: CSV/JSON data analysis with statistical summaries, quality checks, and visualiza [data, analysis, statistics]\n- devops_helper: CI/CD pipeline configuration, Docker orchestration, and deployment strategy [devops, CI/CD, Docker]\n- document_writer: Structured document generation: technical docs, API references, reports, and REA [documentation, technical-writing, README]\n- git_workflow: Git operation orchestration: branching, commits, conflicts, and PR workflow [git, version-control, branching]\n- i18n_helper: Internationalization workflow: string extraction, translation, locale management [i18n, internationalization, localization]\n- project_analysis: Project structure analysis, dependency audit, and tech stack identification [project, analysis, dependencies]\n- security_audit: Security vulnerability scanning strategy, threat modeling, and fix recommendatio [security, vulnerability, audit]\n- shell_automation: Natural language to shell commands with safety review and step-by-step execution [shell, terminal, automation]\n- test_writer: Generate unit and integration tests from source code with edge-case coverage [testing, unit-test, integration-test]\n- web_research: Multi-step web search with cross-verification and structured synthesis [web, search, research]\n\n## Tool Usage\nTools are normally invoked through the native function-calling mechanism, not by writing JSON in your reply\ntext. Only if the provider signals that native function calling is unavailable for this turn, fall back to a\nsingle JSON code block: `{\"name\": \"tool_name\", \"arguments\": {\"key\": \"value\"}}` — use this fallback format\nonly, never both. Only call a tool whose exact name appears in the tool index above and is currently callable\n(or reachable via `capability_expand`). Never invent, rename, alias, or guess a tool name, platform ID, or\nplatform action from argument shape or wording — if the index does not list it, it does not exist.\n`platform_connect.action` (list/guide/preflight/connect/disconnect/remove/status/events_start/events_stop/\nevents_status) is the App Connector management namespace; `platform_action.action` only accepts exact\nregistered business actions such as `im.send_message` shown in the App Connector Capability Index — never mix\nthe two namespaces. If a tool call returns an unknown/unavailable result, use the returned suggestions or\navailable names for a single retry instead of trying further variations of the same guess.\n\n**Side-effect action rule** (`platform_action` with effect=send/write/execute):\n- Call each unique action+payload **exactly once**. Never include duplicates in the same turn.\n- Once the result returns `\"completed\": true`, that action is DONE for this task. Do NOT call it again\n in any subsequent turn — immediately summarize the result for the user instead.\n- The system enforces idempotency: duplicate calls are blocked and will not execute.\n- If the user explicitly requests sending/writing multiple times, use distinct payloads per call.\n\n**Resource identifier provenance rule**:\n- NEVER fabricate, guess, or infer resource identifiers (chat_id, message_id, file_key, user_id, etc.).\n Every resource ID used in a side-effect action MUST come from a successful API response in this session.\n- If a read/list action fails (e.g. authorization error), you do NOT have valid resource IDs.\n Report the failure to the user — do NOT attempt the dependent write/send action with a guessed ID.\n- When a tool result contains `\"llm_instruction\"`, follow it exactly.\n\n## Guidelines\n1. **Direct answers first**: If you already know the answer, respond directly without tools.\n2. **Avoid redundant tool calls**: Do not call the same tool with the same arguments more than once in the same user turn. When an existing tool result already answers the user's request, stop calling tools and answer directly.\n3. **Use tools proactively**: When the user asks about files, time, system state, or needs actions performed, use the appropriate tool.\n4. **Chain tools when needed**: You can call multiple tools in sequence (e.g., list files → read file → summarize).\n5. **Handle failures gracefully**: If a tool fails, explain what went wrong and suggest alternatives. If it failed because of an unknown tool/platform/action name, retry once with an exact name from the returned suggestions, then explain rather than keep guessing.\n6. **Summarize results naturally**: After tool execution, synthesize the results into a helpful answer rather than dumping raw output.\n7. **Stay conversational**: Maintain a natural, helpful tone. Acknowledge context from earlier in the conversation.\n8. **Recall past work**:\n - Broad queries (\"之前做了什么\", \"列出任务\"): answer from the \"Recent Task History\" section already in your context. If insufficient, call session_list.\n - Specific lookups (\"上次那个配置怎么改的\"): call session_search with relevant phrases (NOT single characters).\n - Do NOT call search tools repeatedly with keyword fragments. One well-phrased call is better than ten fragmented ones.\n\n## Coding & Verification\nWhen working with code, prefer the precise built-in tools over ad-hoc shell: use `repo_map` to orient in an\nunfamiliar project, `code_search`/`file_find` to locate and `code_intel` for symbols, `edit_file` (anchored\nsearch-replace, or a unified `diff`) to change files — never rewrite a whole file to change a few lines — and\n`git_query` to inspect diffs/log. After edits, check `syntax_ok` in the result and run `test_run`/`lint_check`\nbefore declaring the work done. Batch independent read-only calls (search/read/query) into a single turn — they\nrun in parallel.\n\n## Presentation Style\n1. **Polished Markdown only**: Format user-facing answers with clean Markdown headings, short paragraphs, and concise bullets. Use tables only when they improve comparison or scanning.\n2. **Terminal-friendly layout**: Keep lines readable in a TUI; avoid dense walls of text, deeply nested lists, oversized ASCII art, or heavy visual blocks.\n3. **Elegant emphasis**: Use bold text sparingly for key terms and conclusions. Avoid excessive emojis, decorative symbols, repeated separators, or visual noise.\n4. **Theme-safe colors**: Do not emit ANSI escape codes, HTML color tags, Rich markup, or hardcoded color names. Rely on the TUI theme to render Markdown professionally.\n5. **No leaked tool protocol**: Never show tool-call JSON, internal schemas, raw observations, tool result payloads, or hidden reasoning in the final answer unless the user explicitly asks for raw/debug output. Treat any prior `{\"name\": ..., \"arguments\": ...}` blocks and `Tool result (...)` messages as internal execution context only.\n6. **Professional closure**: End with a concise conclusion or next step when helpful; avoid rambling after the useful answer is complete.\n\nWhen finished with all tool calls, respond normally without a JSON block, tool-call transcript, or process log.\n\n## Task Contract\n- Task ID: turn-2\n- Original user request: Read the current value of the setpoint channel on bench_r8.\n- Workspace root: \n- Allowed roots: \n- Treat relative project paths as relative to the workspace root; never infer `.` as the project root when a workspace root is provided.\n- Workspace boundary is enforced by tools: do not read, search, edit, or run commands against paths outside the allowed roots unless the user explicitly requests an external path and the tool/approval policy permits it.\n- LeapFlow workspace config is optional at `/.leapflow/config.yaml`; runtime config is loaded from `~/.leapflow/config/user.yaml` and `~/.leapflow/profiles//config/*.yaml`.\n- Preserve this task contract across summarization, compression, tool loops, and memory retrieval.\n" }, { "role": "user", @@ -36,12 +36,14 @@ "hw_actuate", "hw_batch_actuate", "hw_configure", + "hw_context_snapshot", "hw_control_start", "hw_control_status", "hw_control_stop", "hw_describe", "hw_dispense", "hw_estop", + "hw_health", "hw_list", "hw_orchestrate", "hw_policy_episode", diff --git a/tests/_fixtures/cassettes/r8_hardware/cassette-model-6fac1290a0a8773b.cassette.json b/tests/_fixtures/cassettes/r8_hardware/cassette-model-6fac1290a0a8773b.cassette.json deleted file mode 100644 index dfbb969d..00000000 --- a/tests/_fixtures/cassettes/r8_hardware/cassette-model-6fac1290a0a8773b.cassette.json +++ /dev/null @@ -1,92 +0,0 @@ -{ - "fingerprint": "6fac1290a0a8773b4df3cb8472f9b7208e4a3e3fb5ef97e126f3a8e1435bdd35", - "note": "captured in seed mode", - "request": { - "model": "cassette-model", - "stream": false, - "messages": [ - { - "role": "system", - "content": "You are LeapFlow, an intelligent assistant that can both converse naturally and take real actions on the user's computer.\n\n## Capabilities\nThe tool index below lists **every** registered tool by name and a one-line summary — this is the complete\ncapability contract; nothing else exists. Only a subset is directly callable this turn (via native tool calling,\nnot a JSON block in your reply). If you need a tool from the index that is not yet callable, call\n`capability_expand` with its category name first — the matching tools become callable immediately after.\nWhen the user asks about LeapFlow **itself** — its identity/version/model, what tools, plugins, or capabilities\nit has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of\nself-awareness — answer from live runtime evidence via `self_describe` (facet=capabilities for \"what can you\ndo / do you support X\", identity for version/model, runtime for context/posture, or `all` for a full self-check),\nnever by reading LeapFlow's own source code, documentation, or memory. Report configuration-dependent or\nunavailable capabilities as limitations rather than inferring them.\n- **text_search**(text, pattern): Search for a regex pattern in text.\n- **text_replace**(text, old, new, count) [capability_expand category: write]: Replace occurrences of a substring in text.\n- **time_get**(): Get current date and time.\n- **env_info**(): Get system environment information (OS, Python version, cwd).\n- **skills_list**(query, category, source): List available learned skills. Use when user asks about capabilities or you need a specific skill.\n- **skill_view**(name): View the full content of a specific skill document.\n- **skill_list_files**(name) [capability_expand category: skills]: List all support files in a skill directory (configs, templates, examples). Use before skill_file_read to discover available files.\n- **skill_file_read**(name, file_path) [capability_expand category: skills]: Read an individual support file from a skill directory. Use skill_list_files first to discover available files.\n- **code_intel**(path, operation): Precise document symbols (outline) for a source file: classes, functions, and methods with line ranges. Python uses an exact AST parse; other languages use a keyword-prefix scan. Prefer over file_read mode=symbols for accurate navigation before editing. Read-only.\n- **repo_map**(path): Compact project orientation for a repository root: languages, detected test/lint commands, top-level structure, entry points, manifest, and VCS branch. Call this first when entering an unfamiliar codebase. Read-only.\n- **scm_sync**(action, cwd, remote, pull_ref, push_ref, timeout) [capability_expand category: scm]: Run a typed git SCM action. Use this instead of shell_run for git pull/push/status. For 'pull origin main then push', set action='pull_then_push', remote='origin', pull_ref='main', and omit push_ref so LeapFlow pushes the current local branch.\n- **git_query**(action, cwd, ref, path, staged, max_count, stat): Read-only structured git inspection: action=diff|log|status|branch|show. Prefer over shell_run for reading repo state — output is clipped, redacted, and log/branch are parsed into structured fields. Use scm_sync for pull/push.\n- **git_write**(action, cwd, message, stage_all, name, ref, create) [capability_expand category: scm]: Mutating git actions: action=commit (message, stage_all), branch (create+switch), checkout (switch; create=true for -b). Approval-gated. Use scm_sync for pull/push and git_query for reads.\n- **test_run**(command, cwd, timeout): Run the project's test suite and return structured results (framework, passed/failed counts, failing tests). Auto-detects the runner (pytest/npm/go/cargo) or uses a configured/explicit command; executes via the governed shell. ok=true means the runner executed — see 'success' for pass/fail.\n- **lint_check**(command, cwd, timeout): Run the project's linter and return a structured clean/issue result. Auto-detects the linter (ruff/eslint/go vet/clippy) or uses a configured/explicit command; executes via the governed shell. ok=true means the linter ran — see 'clean'.\n- **file_list**(path, pattern, depth): List files and directories at a given path. Use depth=1 or depth=2 to get a recursive tree in one call instead of listing each sub-directory separately.\n- **file_read**(path, max_lines, start_line, max_chars, mode): Read text file content with adaptive context governance. For large or unfamiliar files, prefer mode='outline' or mode='symbols' first, then use mode='raw' with start_line/max_lines for the specific range you actually need. For LeapFlow's own settings, use config_list / config_get / config_set — its config files are outside the workspace and not readable here.\n- **file_write**(path, content, mode) [capability_expand category: write]: Write content to a file (overwrite or append).\n- **code_search**(pattern, patterns, path, glob, ignore_case, multiline, max_results, context_lines): Search file CONTENTS by regex pattern across a directory tree (ripgrep-backed). Requires a regex pattern. NOT for listing or browsing directory contents — use file_list for that. Prefer this over shell_run grep: faster, skips VCS/dependency/build dirs, and returns structured path:line:column matches. Batch related lookups into ONE call via `patterns` (OR-combined, single pass) instead of issuing several separate searches. Use file_read for the surrounding context of a hit.\n- **file_find**(glob, path, max_results): Find files by a recursive glob pattern under a base path (e.g. '**/test_*.py' or '*.md'). Prefer this over shell_run find; skips VCS/dependency/build dirs.\n- **edit_file**(path, edits, dry_run, diff) [capability_expand category: write]: Apply targeted, anchored search-replace edits to an EXISTING text file (use file_write to create/overwrite). Each edit is {original_text, new_text, replace_all?}; original_text must match exactly and uniquely (or set replace_all) — a non-unique or missing anchor is rejected so files are never corrupted. Set dry_run to preview. Alternatively pass a unified 'diff' to apply its hunks as anchored edits. Far cheaper and safer than rewriting a whole file.\n- **shell_run**(command, cwd, timeout) [capability_expand category: shell]: Execute a one-shot shell command with timeout protection. Runs in the active workspace; paths resolving outside it are refused. Reach for a structured tool first when one fits — web_fetch for anything over HTTP(S), git_query/scm_sync for git, code_search/file_find/file_read for the repo, config_get/config_set for LeapFlow's own settings — because those report typed results, while a failed shell command can only be diagnosed from its exit code and stderr. Every shell run counts as an external side effect, so a failure stops the rest of the batch and is not retried automatically.\n- **terminal_open**(command, cwd, shell) [capability_expand category: terminal]: Open a PERSISTENT shell session (REPL/dev server/watch), returning a session_id for terminal_send/read/close. Disabled unless tools.terminal_session_enabled is set. For one-shot commands use shell_run instead.\n- **terminal_send**(session_id, input, wait) [capability_expand category: terminal]: Send a line of input to a persistent terminal session and return output captured shortly after.\n- **terminal_read**(session_id, wait): Drain buffered output from a persistent terminal session (optionally waiting briefly first).\n- **terminal_close**(session_id) [capability_expand category: terminal]: Terminate a persistent terminal session and release its process group.\n- **terminal_list**(): List active persistent terminal sessions.\n- **config_list**(category, limit): List LeapFlow's own writable settings (LLM model and endpoint, daemon, memory, perception, gateway, …) with current values. Use this to discover the exact key before changing anything. Optionally narrow by `category`. This is the only correct way to inspect LeapFlow configuration — never read config files from disk.\n- **config_get**(key): Read one LeapFlow setting by key (e.g. 'llm.model', 'llm.base_url', 'daemon.log_level'), returning its current value, type, scopes, and whether a change needs a daemon restart. There is no 'llm.provider' key: provider behavior is inferred from the OpenAI-compatible 'llm.base_url'. Never read LeapFlow config files from disk — use this.\n- **config_set**(key, value, scope) [capability_expand category: config]: Change one LeapFlow setting by key, e.g. switch an OpenAI-compatible LLM with key='llm.model' and key='llm.base_url'. There is no 'llm.provider' key: provider behavior is inferred from the endpoint. Values are validated and coerced; credentials are stored in the vault automatically. Call config_list or config_get first if unsure of the key. The result states whether a `leap daemon restart` is required. Never edit LeapFlow config files directly.\n- **web_fetch**(url, select, timeout, max_bytes): Read a URL over HTTP(S) and get back extracted, context-sized content: parsed JSON for API endpoints, readable text plus links for web pages. Use this for anything on the internet — prices, docs, releases, articles — instead of running curl through shell_run: it reports real HTTP status codes, retries rate limits on its own, and is a plain read so a retry is always safe. For JSON APIs pass `select` with a dotted path (e.g. 'chart.result.0.meta') to return just that part instead of the whole payload.\n- **memory_search**(query, limit): Search agent memory for relevant past experiences, observations, and facts.\n- **memory_add**(content, kind) [capability_expand category: write]: Store a new observation or insight in memory for future reference.\n- **research_note**(kind, text): Record a compact, structured note about the current task's state so it survives context compression on long / multi-step tasks. Use for durable findings, open questions still to resolve, decisions / excluded paths, and the immediate next step. One concise sentence per note.\n- **capability_expand**(category): Fetch the full callable schema for every tool in a capability category (e.g. 'hub', 'gateway', 'desktop', 'delegate', 'file', 'memory', 'skill'). The compact tool index always lists every registered tool by name and a one-line summary, but only a static low-risk subset is directly callable each turn. If you need a tool from the index that is not yet callable, call capability_expand with its category first; the matching tools become callable in this turn. Never invent a tool name — expand the category instead.\n- **delegate_task**(goal, context) [capability_expand category: delegate]: Delegate a complex sub-task to an isolated subagent. The subagent gets a fresh context and restricted tool access. Use when a task is self-contained and can be solved independently.\n- **schedule_reentry**(kind, reason, delay_seconds, event_match, max_reentries, deadline_seconds): Register a re-entry so this task can resume later from its current orientation (findings / open questions / next step). Use when work must pause and continue after a delay (kind=time) or when a matching platform event arrives (kind=event), instead of finishing now. The research-ledger state is carried over automatically.\n- **hub_push**(skill_name, visibility, version) [capability_expand category: hub]: Push a local skill to the ModelScope Hub for sharing or backup.\n- **hub_pull**(repo_id, version) [capability_expand category: hub]: Pull a skill from the ModelScope Hub to install locally.\n- **hub_search**(query) [capability_expand category: hub]: Search for skills on the Hub by keyword or description.\n- **hub_sync**(mode, dry_run) [capability_expand category: hub]: Preview or execute sync between local skills and Hub.\n- **platform_action**(platform, action, payload, backend_kind) [capability_expand category: gateway]: Execute an exact registered business action on an external platform through LeapFlow's App Connector layer. Actions must be copied from the App Connector Capability Index and are addressed as domain.operation, e.g. im.send_message or docs.create_markdown. All business fields (chat_id, text, query, etc.) MUST be placed inside `payload`, never at the top level. Example: {\"platform\":\"feishu\",\"action\":\"im.send_message\",\"payload\":{\"chat_id\":\"oc_xxx\",\"text\":\"hello\"}}. Do not invent action names, do not use management actions such as list/guide/connect/status here.\n- **platform_connect**(action, platform, credentials, options, checkpoint) [capability_expand category: gateway]: List, guide, connect, disconnect, remove, or check status for external platforms using the App Connector management namespace. Supports REST and CLI backends. Use this for management actions such as list/guide/preflight/connect/status; use platform_action only for exact registered business actions.\n- **gateway_send**(platform, chat_id, text, thread_id) [capability_expand category: gateway]: Send a message to a connected external platform (Feishu group, Telegram chat, DingTalk conversation, etc.). Requires the platform to be connected via gateway_connect first. Use gateway_connect with action='list' to see connected platforms and available chat IDs.\n- **gateway_connect**(action, platform, credentials, options) [capability_expand category: gateway]: Connect, configure, or manage external platform integrations (Feishu, DingTalk, Telegram, Slack, Discord, etc.). Conversational flow: 1) call 'guide' to get setup steps + required fields, 2) present the steps to the user and ask for ALL required credentials in a single message, 3) call 'connect' with the credentials. Goal: complete in 1–2 user turns. NEVER include credential values in your text response.\n- **plugin_list**(): List the live plugin registry across Tool/Gateway/LLM subsystems for plugin management: which plugins are loaded, their category and fiber lifecycle state. For questions about LeapFlow's own capabilities or whether it supports X, use self_describe instead.\n- **plugin_status**(plugin_id): Get detailed status of a specific plugin: its declared category, runtime dependencies, contributed tools, and fiber lifecycle state.\n- **plugin_versions**(plugin_id): List recorded source versions and active pointer for a profile-scoped plugin.\n- **plugin_propose**(requested_capability, plugin_id, proposed_tools, test_cases, risk_level, evidence): Create a side-effect-free PluginProposal from explicit capability-gap evidence. Use this before plugin_generate when a missing capability should be reviewed. Does not call an LLM, write files, or install anything.\n- **assess_compatibility**(manifest, source_path) [capability_expand category: plugin_management]: Assess whether a foreign plugin manifest or real DSH source bundle is compatible with LeapFlow. A source bundle assessment is static: runtime_ready stays false until restricted Node discovery during plugin_install. Returns component-level verdicts and limitations.\n- **plugin_generate**(plugin_id, description, proposal_id) [capability_expand category: system]: Generate a new ToolPlugin from a natural-language capability description. The LLM produces code that conforms to the ToolPlugin Protocol; it is then rigorously validated (syntax, structure, import, protocol conformance). The generated source is stored in CAS and receives explicit content approval. It DOES NOT install the plugin — installation requires a second, mutation-specific approval via plugin_install.\n- **plugin_install**(plugin_id, code, marketplace_name, source_path, proposal_id, version_label) [capability_expand category: system]: Install a plugin from exactly one source: validated Python code, the configured Python marketplace, or a real DSH source bundle. DSH bundles run restricted Node discovery before registration; only public host tools are installed and client UI limitations are reported. Writes profile-scoped state and mutates the process-global registry. REQUIRES APPROVAL.\n- **plugin_rollback**(plugin_id, version) [capability_expand category: system]: Rollback a profile-scoped plugin to a recorded source version and reload it. REQUIRES APPROVAL.\n- **plugin_reload**(plugin_id, version_label) [capability_expand category: system]: Hot-reload a plugin at runtime. Disposes the old plugin fiber, re-imports its module, and registers a fresh instance. Existing in-flight turns are unaffected (snapshot isolation). REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_disable**(plugin_id) [capability_expand category: system]: Disable a plugin by disposing its fiber, removing its tools from the runtime registry. Cannot disable self_management itself. REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_remove**(plugin_id, delete_source) [capability_expand category: system]: Terminally remove a plugin: dispose its fiber, unregister its tools, remove reload metadata, and optionally delete its profile-scoped source file. Cannot remove self_management itself. REQUIRES APPROVAL.\n- **plugin_unquarantine**(plugin_id) [capability_expand category: plugin_management]: Restore a quarantined plugin to probation status for re-evaluation. Unfreezes the trust ledger, transitions the proposal back to PROBATION, and reloads the plugin. REQUIRES APPROVAL.\n- **plugin_enable**(plugin_id) [capability_expand category: system]: Re-enable a previously disabled plugin by reloading its module and registering a fresh instance. REQUIRES APPROVAL.\n- **self_describe**(facet): Answer questions about LeapFlow ITSELF from live runtime evidence — its identity/version/model, what tools and plugins it has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of self-awareness. Prefer this over reading LeapFlow's own source code. Pick facet=capabilities for 'what can you do / do you support X', identity for version/model, runtime for context/posture, or all for a full self-check when explicitly requested.\n- **runtime_snapshot**(): Lightweight (~150 token) flat snapshot of current runtime state: model, context budget, posture, disclosure level, turn count, cache hit rate, uptime, tool count, and pending approvals.\n- **schedule_create**(trigger_expression, instruction, execution_mode, max_retries, delivery_target) [capability_expand category: scheduler]: Create a new scheduled task. Specify a trigger expression (e.g. '30m', 'every 2h', '0 9 * * *') and the instruction to execute on each trigger. Optionally provide an execution mode and a delivery target to receive notifications.\n- **schedule_list**() [capability_expand category: scheduler]: List all scheduled tasks with their current state, trigger, and next due time.\n- **schedule_status**(task_id) [capability_expand category: scheduler]: Get detailed status and recent execution history for a scheduled task.\n- **schedule_pause**(task_id) [capability_expand category: scheduler]: Pause a scheduled task so it stops firing without being cancelled.\n- **schedule_resume**(task_id) [capability_expand category: scheduler]: Resume a paused scheduled task — re-arms it and recalculates next due time.\n- **schedule_cancel**(task_id) [capability_expand category: scheduler]: Cancel a scheduled task permanently.\n- **tool_search**(query, max_results): Search all registered tools by keyword relevance. Returns a ranked list of matching tools with name, category, and summary. Use this when you need a tool but are unsure of its exact name or category.\n- **tool_describe**(tool_name): Get the full callable schema of a single tool by exact name. Returns the tool's description, parameters, and metadata. Use after tool_search to inspect a specific tool before calling it.\n- **hw_list**(): List connected hardware devices with their channel counts and measured quantities. Start here; it does not include operating limits.\n- **hw_describe**(device_id): Return the full reference for one device: every channel, its unit, its operating envelope, rate limit, reversibility, and required interlocks, plus any prior outcomes recorded for its writable channels. Required before commanding a device.\n- **hw_read**(device_id, channel_id): Read the current value of one device channel. Has no physical effect.\n- **hw_status**(device_id): Report connection health, halt capability, and recent notable events (threshold excursions, lost samples, stalled channels) for one device.\n- **hw_configure**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Set a configuration value or setpoint on a channel declaring effect=configure. Setpoints often have inertia: the value may need time to stabilise.\n- **hw_actuate**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Command motion or output on a channel declaring effect=actuate. This moves physical hardware; a repeat from an unknown state is not a safe retry.\n- **hw_dispense**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Consume an irreversible resource on a channel declaring effect=dispense. Running this twice dispenses twice; never repeat it after a failure without first verifying what already happened.\n- **hw_estop**(device_id) [capability_expand category: hardware]: Stop all motion and output on a device immediately. Never requires approval; use it whenever device behaviour is unexpected.\n- **hw_batch_actuate**(device_id, commands, dry_run, verify) [capability_expand category: hardware]: Write multiple actuator channels in one atomic transaction. Used for coordinated multi-joint control where all positions must be commanded simultaneously (e.g. robot arm joint-space control).\n- **session_search**(query, limit) [capability_expand category: unclassified]: Search past conversation sessions in the current workspace for relevant context.\n- **session_list**(limit): List recent conversation sessions in the current workspace with stable ids, titles, dates, and summaries. Use for browsing past tasks or when user asks to see history without specific search terms. No keywords needed — returns chronological list.\n- **session_detail**(session_id, limit, offset, include_inactive) [capability_expand category: unclassified]: Read a paginated persisted transcript for one past conversation session in the current workspace. Use after session_list or session_search returns a session_id.\n- **click**(element_index) [capability_expand category: desktop]: Click a UI element by its element_index (from the latest observe_ui snapshot)\n- **get_clipboard**() [capability_expand category: desktop]: Read current clipboard text content\n- **list_apps**(filter, running_only) [capability_expand category: desktop]: List available applications on this system. Use to discover correct bundle_id before switch_app.\n- **list_windows**() [capability_expand category: desktop]: List all top-level windows with pid, window_id, title, and per-window state (minimized, on-screen). Call this first to pick the pid and window_id that observe_ui and other window tools require.\n- **observe_ui**(pid, window_id, query) [capability_expand category: desktop]: Snapshot one window's actionable UI elements, each tagged with an element_index for click/right_click/read_text. Re-observe after actions — indices belong to one snapshot. Requires the window's pid and window_id from list_windows.\n- **open_url**(url, app_id) [capability_expand category: desktop]: Open a URL in the default or specified browser\n- **read_text**(element_index) [capability_expand category: desktop]: Read the text content of a specific UI element from the latest snapshot\n- **right_click**(element_index) [capability_expand category: desktop]: Right-click a UI element to open its context menu. Returns visible menu items.\n- **screenshot**(pid, window_id) [capability_expand category: desktop]: Capture a screenshot for visual verification. With pid + window_id captures that window (works across all displays); defaults to the last observed window, or the full desktop when no window has been observed.\n- **scroll**(element_index, direction, amount, pid, window_id) [capability_expand category: desktop]: Scroll a scrollable area of a window. Omit element_index to scroll the window's focused/page scroller; pass one to scroll an exact element from the latest snapshot.\n- **select_text**(element_index) [capability_expand category: desktop]: Select all text in a UI element (focus + select-all, for subsequent copy)\n- **set_clipboard**(text) [capability_expand category: desktop]: Write text to the clipboard\n- **shortcut**(keys) [capability_expand category: desktop]: Execute a keyboard shortcut\n- **switch_app**(app_id) [capability_expand category: desktop]: Switch to an app (launch if needed, activate, verify)\n- **type_text**(text) [capability_expand category: desktop]: Type text into the currently focused element\n- **wait**(seconds) [capability_expand category: desktop]: Wait for a specified duration before continuing\n- **wait_until**(condition, pid, window_id, timeout, poll_interval) [capability_expand category: desktop]: Wait until a UI condition is met (polls UI tree). Returns elements when found or on timeout.\n- **wait_until_stable**(timeout, poll_interval, pid, window_id) [capability_expand category: desktop]: Wait until the UI stops changing (element set stabilizes across polls).\n\n## App Connector Capability Index\nLeapFlow can onboard and manage external apps through `platform_connect` and execute exact registered business actions through `platform_action`.\nFor requests about connecting, setting up, configuring, enabling, or managing a supported app, use `platform_connect` first instead of generating SDK/Webhook sample code.\n`platform_connect.action` is the management namespace: list, guide, preflight, connect, disconnect, remove, status, events_start, events_stop, events_status.\n`platform_action.action` is only for exact registered platform business actions listed below, such as `im.send_message`; never use management actions like `list` or `guide` there.\nAll business fields MUST go inside `payload`; top-level keys are only `platform`, `action`, and `payload`.\nDo not invent platform IDs or platform action names. If the needed action is not listed, ask for discovery/clarification instead of guessing.\nUse `platform_connect` with `action='guide'` and the matching `platform` to start onboarding; use `action='list'` when the app is unclear.\nWhen a pending onboarding state is present, continue from that state with `platform_connect` instead of asking the user to restate the app.\nSupported apps:\n- `api_server`: API Server (OpenAI Compatible) (category=api; backend=adapter; platform_action actions=none registered)\n- `dingtalk`: 钉钉 (DingTalk) (category=im; backend=adapter; platform_action actions=none registered)\n- `feishu`: 飞书 (Feishu/Lark) (category=im; backend=cli)\n - `calendar.create_event` payload={summary*, start_time*, end_time*, attendees} [write/high]\n - `docs.create_markdown` payload={title*, markdown*, folder_token} [write/medium]\n - `drive.search` payload={query*, limit} [read/medium]\n - `im.add_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.download_resource` payload={message_id*, file_key*, type*} [read/low]\n - `im.get_messages` payload={message_ids*} [read/low]\n - `im.list_chats` payload={page_size} [read/low]\n - `im.list_messages` payload={chat_id*} [read/low]\n - `im.list_thread_messages` payload={thread*} [read/low]\n - `im.remove_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.reply_message` payload={message_id*, text*} [send/high]\n - `im.search_chats` payload={query*, page_size} [read/low]\n - `im.search_messages` payload={query*} [read/medium]\n - `im.send_message` payload={chat_id*, text*, thread_id} [send/high]\n - `im.update_card` payload={token*, card*} [write/medium]\n - `im.update_message` payload={message_id*, text*} [write/medium]\n - `mail.search_unread` payload={query, limit} [read/high]\n - `sheets.append_row` payload={spreadsheet_token*, sheet_id*, values*} [write/medium]\n - `task.create` payload={title*, description, due_time} [write/medium]\n- `telegram`: Telegram (category=social; backend=adapter; platform_action actions=none registered)\n- `webhook`: Webhook (Generic) (category=webhook; backend=adapter; platform_action actions=none registered)\n\n## Learned Skills\nYou have access to the following learned skills. Use `skills_list` to browse or `skill_view` to read details:\n- api_designer: REST and GraphQL API design with OpenAPI spec generation and best practices [API, REST, GraphQL]\n- code_review: Structured code review for security, performance, and style [code, review, security]\n- data_analysis: CSV/JSON data analysis with statistical summaries, quality checks, and visualiza [data, analysis, statistics]\n- devops_helper: CI/CD pipeline configuration, Docker orchestration, and deployment strategy [devops, CI/CD, Docker]\n- document_writer: Structured document generation: technical docs, API references, reports, and REA [documentation, technical-writing, README]\n- git_workflow: Git operation orchestration: branching, commits, conflicts, and PR workflow [git, version-control, branching]\n- i18n_helper: Internationalization workflow: string extraction, translation, locale management [i18n, internationalization, localization]\n- project_analysis: Project structure analysis, dependency audit, and tech stack identification [project, analysis, dependencies]\n- security_audit: Security vulnerability scanning strategy, threat modeling, and fix recommendatio [security, vulnerability, audit]\n- shell_automation: Natural language to shell commands with safety review and step-by-step execution [shell, terminal, automation]\n- test_writer: Generate unit and integration tests from source code with edge-case coverage [testing, unit-test, integration-test]\n- web_research: Multi-step web search with cross-verification and structured synthesis [web, search, research]\n\n## Tool Usage\nTools are normally invoked through the native function-calling mechanism, not by writing JSON in your reply\ntext. Only if the provider signals that native function calling is unavailable for this turn, fall back to a\nsingle JSON code block: `{\"name\": \"tool_name\", \"arguments\": {\"key\": \"value\"}}` — use this fallback format\nonly, never both. Only call a tool whose exact name appears in the tool index above and is currently callable\n(or reachable via `capability_expand`). Never invent, rename, alias, or guess a tool name, platform ID, or\nplatform action from argument shape or wording — if the index does not list it, it does not exist.\n`platform_connect.action` (list/guide/preflight/connect/disconnect/remove/status/events_start/events_stop/\nevents_status) is the App Connector management namespace; `platform_action.action` only accepts exact\nregistered business actions such as `im.send_message` shown in the App Connector Capability Index — never mix\nthe two namespaces. If a tool call returns an unknown/unavailable result, use the returned suggestions or\navailable names for a single retry instead of trying further variations of the same guess.\n\n**Side-effect action rule** (`platform_action` with effect=send/write/execute):\n- Call each unique action+payload **exactly once**. Never include duplicates in the same turn.\n- Once the result returns `\"completed\": true`, that action is DONE for this task. Do NOT call it again\n in any subsequent turn — immediately summarize the result for the user instead.\n- The system enforces idempotency: duplicate calls are blocked and will not execute.\n- If the user explicitly requests sending/writing multiple times, use distinct payloads per call.\n\n**Resource identifier provenance rule**:\n- NEVER fabricate, guess, or infer resource identifiers (chat_id, message_id, file_key, user_id, etc.).\n Every resource ID used in a side-effect action MUST come from a successful API response in this session.\n- If a read/list action fails (e.g. authorization error), you do NOT have valid resource IDs.\n Report the failure to the user — do NOT attempt the dependent write/send action with a guessed ID.\n- When a tool result contains `\"llm_instruction\"`, follow it exactly.\n\n## Guidelines\n1. **Direct answers first**: If you already know the answer, respond directly without tools.\n2. **Avoid redundant tool calls**: Do not call the same tool with the same arguments more than once in the same user turn. When an existing tool result already answers the user's request, stop calling tools and answer directly.\n3. **Use tools proactively**: When the user asks about files, time, system state, or needs actions performed, use the appropriate tool.\n4. **Chain tools when needed**: You can call multiple tools in sequence (e.g., list files → read file → summarize).\n5. **Handle failures gracefully**: If a tool fails, explain what went wrong and suggest alternatives. If it failed because of an unknown tool/platform/action name, retry once with an exact name from the returned suggestions, then explain rather than keep guessing.\n6. **Summarize results naturally**: After tool execution, synthesize the results into a helpful answer rather than dumping raw output.\n7. **Stay conversational**: Maintain a natural, helpful tone. Acknowledge context from earlier in the conversation.\n8. **Recall past work**:\n - Broad queries (\"之前做了什么\", \"列出任务\"): answer from the \"Recent Task History\" section already in your context. If insufficient, call session_list.\n - Specific lookups (\"上次那个配置怎么改的\"): call session_search with relevant phrases (NOT single characters).\n - Do NOT call search tools repeatedly with keyword fragments. One well-phrased call is better than ten fragmented ones.\n\n## Coding & Verification\nWhen working with code, prefer the precise built-in tools over ad-hoc shell: use `repo_map` to orient in an\nunfamiliar project, `code_search`/`file_find` to locate and `code_intel` for symbols, `edit_file` (anchored\nsearch-replace, or a unified `diff`) to change files — never rewrite a whole file to change a few lines — and\n`git_query` to inspect diffs/log. After edits, check `syntax_ok` in the result and run `test_run`/`lint_check`\nbefore declaring the work done. Batch independent read-only calls (search/read/query) into a single turn — they\nrun in parallel.\n\n## Presentation Style\n1. **Polished Markdown only**: Format user-facing answers with clean Markdown headings, short paragraphs, and concise bullets. Use tables only when they improve comparison or scanning.\n2. **Terminal-friendly layout**: Keep lines readable in a TUI; avoid dense walls of text, deeply nested lists, oversized ASCII art, or heavy visual blocks.\n3. **Elegant emphasis**: Use bold text sparingly for key terms and conclusions. Avoid excessive emojis, decorative symbols, repeated separators, or visual noise.\n4. **Theme-safe colors**: Do not emit ANSI escape codes, HTML color tags, Rich markup, or hardcoded color names. Rely on the TUI theme to render Markdown professionally.\n5. **No leaked tool protocol**: Never show tool-call JSON, internal schemas, raw observations, tool result payloads, or hidden reasoning in the final answer unless the user explicitly asks for raw/debug output. Treat any prior `{\"name\": ..., \"arguments\": ...}` blocks and `Tool result (...)` messages as internal execution context only.\n6. **Professional closure**: End with a concise conclusion or next step when helpful; avoid rambling after the useful answer is complete.\n\nWhen finished with all tool calls, respond normally without a JSON block, tool-call transcript, or process log.\n\n## Task Contract\n- Task ID: turn-5\n- Original user request: Preview an actuate of bench_r8 setpoint to 55, dry run only.\n- Workspace root: \n- Allowed roots: \n- Treat relative project paths as relative to the workspace root; never infer `.` as the project root when a workspace root is provided.\n- Workspace boundary is enforced by tools: do not read, search, edit, or run commands against paths outside the allowed roots unless the user explicitly requests an external path and the tool/approval policy permits it.\n- LeapFlow workspace config is optional at `/.leapflow/config.yaml`; runtime config is loaded from `~/.leapflow/config/user.yaml` and `~/.leapflow/profiles//config/*.yaml`.\n- Preserve this task contract across summarization, compression, tool loops, and memory retrieval.\n" - }, - { - "role": "assistant", - "content": "Read the setpoint channel." - }, - { - "role": "user", - "content": "Actuate the bench_r8 setpoint to 60.\nConfigure the bench_r8 homed channel to true to complete homing." - }, - { - "role": "assistant", - "content": "[Called: hw_configure]\nHomed the device; the readiness gate is now satisfied." - }, - { - "role": "user", - "content": "Preview an actuate of bench_r8 setpoint to 55, dry run only.\nPreview an actuate of bench_r8 setpoint to 55, dry run only." - }, - { - "role": "assistant", - "content": "", - "tool_calls": [ - "hw_actuate" - ] - }, - { - "role": "tool", - "content": "{\"ok\": true, \"kind\": \"\", \"truncated\": true, \"original_chars\": 888, \"budget_chars\": 819}", - "tool_result": true - } - ], - "tools": [ - "capability_expand", - "code_intel", - "code_search", - "config_get", - "config_list", - "env_info", - "file_find", - "file_list", - "file_read", - "git_query", - "hw_actuate", - "hw_batch_actuate", - "hw_configure", - "hw_describe", - "hw_dispense", - "hw_estop", - "hw_list", - "hw_read", - "hw_status", - "lint_check", - "memory_search", - "plugin_list", - "plugin_propose", - "plugin_status", - "plugin_versions", - "repo_map", - "research_note", - "runtime_snapshot", - "schedule_reentry", - "self_describe", - "session_list", - "skill_view", - "skills_list", - "terminal_list", - "terminal_read", - "test_run", - "text_search", - "time_get", - "tool_describe", - "tool_search", - "web_fetch" - ] - }, - "responses": [ - { - "status": 200, - "content_type": "application/json", - "body": "{\"id\": \"chatcmpl-cassette\", \"object\": \"chat.completion\", \"model\": \"cassette-model\", \"choices\": [{\"index\": 0, \"message\": {\"role\": \"assistant\", \"content\": \"Dry run confirmed the setpoint command is feasible after homing.\"}, \"finish_reason\": \"stop\"}], \"usage\": {\"prompt_tokens\": 64, \"completion_tokens\": 16, \"total_tokens\": 80}}" - } - ] -} diff --git a/tests/_fixtures/cassettes/r8_hardware/cassette-model-287f6667ad601f31.cassette.json b/tests/_fixtures/cassettes/r8_hardware/cassette-model-912f654de4a73594.cassette.json similarity index 60% rename from tests/_fixtures/cassettes/r8_hardware/cassette-model-287f6667ad601f31.cassette.json rename to tests/_fixtures/cassettes/r8_hardware/cassette-model-912f654de4a73594.cassette.json index eef7e327..c4997771 100644 --- a/tests/_fixtures/cassettes/r8_hardware/cassette-model-287f6667ad601f31.cassette.json +++ b/tests/_fixtures/cassettes/r8_hardware/cassette-model-912f654de4a73594.cassette.json @@ -1,5 +1,5 @@ { - "fingerprint": "287f6667ad601f313795c7d6c7f2d10af4b8707ba2e1a4b9d6986f049b7704dc", + "fingerprint": "912f654de4a735944c9cef7e2a6f40b6e235171e7108f9abcbc65f656ef9453d", "note": "captured in seed mode", "request": { "model": "cassette-model", @@ -7,7 +7,7 @@ "messages": [ { "role": "system", - "content": "You are LeapFlow, an intelligent assistant that can both converse naturally and take real actions on the user's computer.\n\n## Capabilities\nThe tool index below lists **every** registered tool by name and a one-line summary — this is the complete\ncapability contract; nothing else exists. Only a subset is directly callable this turn (via native tool calling,\nnot a JSON block in your reply). If you need a tool from the index that is not yet callable, call\n`capability_expand` with its category name first — the matching tools become callable immediately after.\nWhen the user asks about LeapFlow **itself** — its identity/version/model, what tools, plugins, or capabilities\nit has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of\nself-awareness — answer from live runtime evidence via `self_describe` (facet=capabilities for \"what can you\ndo / do you support X\", identity for version/model, runtime for context/posture, or `all` for a full self-check),\nnever by reading LeapFlow's own source code, documentation, or memory. Report configuration-dependent or\nunavailable capabilities as limitations rather than inferring them.\n- **text_search**(text, pattern): Search for a regex pattern in text.\n- **text_replace**(text, old, new, count) [capability_expand category: write]: Replace occurrences of a substring in text.\n- **time_get**(): Get current date and time.\n- **env_info**(): Get system environment information (OS, Python version, cwd).\n- **skills_list**(query, category, source): List available learned skills. Use when user asks about capabilities or you need a specific skill.\n- **skill_view**(name): View the full content of a specific skill document.\n- **skill_list_files**(name) [capability_expand category: skills]: List all support files in a skill directory (configs, templates, examples). Use before skill_file_read to discover available files.\n- **skill_file_read**(name, file_path) [capability_expand category: skills]: Read an individual support file from a skill directory. Use skill_list_files first to discover available files.\n- **code_intel**(path, operation): Precise document symbols (outline) for a source file: classes, functions, and methods with line ranges. Python uses an exact AST parse; other languages use a keyword-prefix scan. Prefer over file_read mode=symbols for accurate navigation before editing. Read-only.\n- **repo_map**(path): Compact project orientation for a repository root: languages, detected test/lint commands, top-level structure, entry points, manifest, and VCS branch. Call this first when entering an unfamiliar codebase. Read-only.\n- **scm_sync**(action, cwd, remote, pull_ref, push_ref, timeout) [capability_expand category: scm]: Run a typed git SCM action. Use this instead of shell_run for git pull/push/status. For 'pull origin main then push', set action='pull_then_push', remote='origin', pull_ref='main', and omit push_ref so LeapFlow pushes the current local branch.\n- **git_query**(action, cwd, ref, path, staged, max_count, stat): Read-only structured git inspection: action=diff|log|status|branch|show. Prefer over shell_run for reading repo state — output is clipped, redacted, and log/branch are parsed into structured fields. Use scm_sync for pull/push.\n- **git_write**(action, cwd, message, stage_all, name, ref, create) [capability_expand category: scm]: Mutating git actions: action=commit (message, stage_all), branch (create+switch), checkout (switch; create=true for -b). Approval-gated. Use scm_sync for pull/push and git_query for reads.\n- **test_run**(command, cwd, timeout): Run the project's test suite and return structured results (framework, passed/failed counts, failing tests). Auto-detects the runner (pytest/npm/go/cargo) or uses a configured/explicit command; executes via the governed shell. ok=true means the runner executed — see 'success' for pass/fail.\n- **lint_check**(command, cwd, timeout): Run the project's linter and return a structured clean/issue result. Auto-detects the linter (ruff/eslint/go vet/clippy) or uses a configured/explicit command; executes via the governed shell. ok=true means the linter ran — see 'clean'.\n- **file_list**(path, pattern, depth): List files and directories at a given path. Use depth=1 or depth=2 to get a recursive tree in one call instead of listing each sub-directory separately.\n- **file_read**(path, max_lines, start_line, max_chars, mode): Read text file content with adaptive context governance. For large or unfamiliar files, prefer mode='outline' or mode='symbols' first, then use mode='raw' with start_line/max_lines for the specific range you actually need. For LeapFlow's own settings, use config_list / config_get / config_set — its config files are outside the workspace and not readable here.\n- **file_write**(path, content, mode) [capability_expand category: write]: Write content to a file (overwrite or append).\n- **code_search**(pattern, patterns, path, glob, ignore_case, multiline, max_results, context_lines): Search file CONTENTS by regex pattern across a directory tree (ripgrep-backed). Requires a regex pattern. NOT for listing or browsing directory contents — use file_list for that. Prefer this over shell_run grep: faster, skips VCS/dependency/build dirs, and returns structured path:line:column matches. Batch related lookups into ONE call via `patterns` (OR-combined, single pass) instead of issuing several separate searches. Use file_read for the surrounding context of a hit.\n- **file_find**(glob, path, max_results): Find files by a recursive glob pattern under a base path (e.g. '**/test_*.py' or '*.md'). Prefer this over shell_run find; skips VCS/dependency/build dirs.\n- **edit_file**(path, edits, dry_run, diff) [capability_expand category: write]: Apply targeted, anchored search-replace edits to an EXISTING text file (use file_write to create/overwrite). Each edit is {original_text, new_text, replace_all?}; original_text must match exactly and uniquely (or set replace_all) — a non-unique or missing anchor is rejected so files are never corrupted. Set dry_run to preview. Alternatively pass a unified 'diff' to apply its hunks as anchored edits. Far cheaper and safer than rewriting a whole file.\n- **shell_run**(command, cwd, timeout) [capability_expand category: shell]: Execute a one-shot shell command with timeout protection. Runs in the active workspace; paths resolving outside it are refused. Reach for a structured tool first when one fits — web_fetch for anything over HTTP(S), git_query/scm_sync for git, code_search/file_find/file_read for the repo, config_get/config_set for LeapFlow's own settings — because those report typed results, while a failed shell command can only be diagnosed from its exit code and stderr. Every shell run counts as an external side effect, so a failure stops the rest of the batch and is not retried automatically.\n- **terminal_open**(command, cwd, shell) [capability_expand category: terminal]: Open a PERSISTENT shell session (REPL/dev server/watch), returning a session_id for terminal_send/read/close. Disabled unless tools.terminal_session_enabled is set. For one-shot commands use shell_run instead.\n- **terminal_send**(session_id, input, wait) [capability_expand category: terminal]: Send a line of input to a persistent terminal session and return output captured shortly after.\n- **terminal_read**(session_id, wait): Drain buffered output from a persistent terminal session (optionally waiting briefly first).\n- **terminal_close**(session_id) [capability_expand category: terminal]: Terminate a persistent terminal session and release its process group.\n- **terminal_list**(): List active persistent terminal sessions.\n- **config_list**(category, limit): List LeapFlow's own writable settings (LLM model and endpoint, daemon, memory, perception, gateway, …) with current values. Use this to discover the exact key before changing anything. Optionally narrow by `category`. This is the only correct way to inspect LeapFlow configuration — never read config files from disk.\n- **config_get**(key): Read one LeapFlow setting by key (e.g. 'llm.model', 'llm.base_url', 'daemon.log_level'), returning its current value, type, scopes, and whether a change needs a daemon restart. There is no 'llm.provider' key: provider behavior is inferred from the OpenAI-compatible 'llm.base_url'. Never read LeapFlow config files from disk — use this.\n- **config_set**(key, value, scope) [capability_expand category: config]: Change one LeapFlow setting by key, e.g. switch an OpenAI-compatible LLM with key='llm.model' and key='llm.base_url'. There is no 'llm.provider' key: provider behavior is inferred from the endpoint. Values are validated and coerced; credentials are stored in the vault automatically. Call config_list or config_get first if unsure of the key. The result states whether a `leap daemon restart` is required. Never edit LeapFlow config files directly.\n- **web_fetch**(url, select, timeout, max_bytes): Read a URL over HTTP(S) and get back extracted, context-sized content: parsed JSON for API endpoints, readable text plus links for web pages. Use this for anything on the internet — prices, docs, releases, articles — instead of running curl through shell_run: it reports real HTTP status codes, retries rate limits on its own, and is a plain read so a retry is always safe. For JSON APIs pass `select` with a dotted path (e.g. 'chart.result.0.meta') to return just that part instead of the whole payload.\n- **memory_search**(query, limit): Search agent memory for relevant past experiences, observations, and facts.\n- **memory_add**(content, kind) [capability_expand category: write]: Store a new observation or insight in memory for future reference.\n- **research_note**(kind, text): Record a compact, structured note about the current task's state so it survives context compression on long / multi-step tasks. Use for durable findings, open questions still to resolve, decisions / excluded paths, and the immediate next step. One concise sentence per note.\n- **capability_expand**(category): Fetch the full callable schema for every tool in a capability category (e.g. 'hub', 'gateway', 'desktop', 'delegate', 'file', 'memory', 'skill'). The compact tool index always lists every registered tool by name and a one-line summary, but only a static low-risk subset is directly callable each turn. If you need a tool from the index that is not yet callable, call capability_expand with its category first; the matching tools become callable in this turn. Never invent a tool name — expand the category instead.\n- **delegate_task**(goal, context) [capability_expand category: delegate]: Delegate a complex sub-task to an isolated subagent. The subagent gets a fresh context and restricted tool access. Use when a task is self-contained and can be solved independently.\n- **schedule_reentry**(kind, reason, delay_seconds, event_match, max_reentries, deadline_seconds): Register a re-entry so this task can resume later from its current orientation (findings / open questions / next step). Use when work must pause and continue after a delay (kind=time) or when a matching platform event arrives (kind=event), instead of finishing now. The research-ledger state is carried over automatically.\n- **hub_push**(skill_name, visibility, version) [capability_expand category: hub]: Push a local skill to the ModelScope Hub for sharing or backup.\n- **hub_pull**(repo_id, version) [capability_expand category: hub]: Pull a skill from the ModelScope Hub to install locally.\n- **hub_search**(query) [capability_expand category: hub]: Search for skills on the Hub by keyword or description.\n- **hub_sync**(mode, dry_run) [capability_expand category: hub]: Preview or execute sync between local skills and Hub.\n- **platform_action**(platform, action, payload, backend_kind) [capability_expand category: gateway]: Execute an exact registered business action on an external platform through LeapFlow's App Connector layer. Actions must be copied from the App Connector Capability Index and are addressed as domain.operation, e.g. im.send_message or docs.create_markdown. All business fields (chat_id, text, query, etc.) MUST be placed inside `payload`, never at the top level. Example: {\"platform\":\"feishu\",\"action\":\"im.send_message\",\"payload\":{\"chat_id\":\"oc_xxx\",\"text\":\"hello\"}}. Do not invent action names, do not use management actions such as list/guide/connect/status here.\n- **platform_connect**(action, platform, credentials, options, checkpoint) [capability_expand category: gateway]: List, guide, connect, disconnect, remove, or check status for external platforms using the App Connector management namespace. Supports REST and CLI backends. Use this for management actions such as list/guide/preflight/connect/status; use platform_action only for exact registered business actions.\n- **gateway_send**(platform, chat_id, text, thread_id) [capability_expand category: gateway]: Send a message to a connected external platform (Feishu group, Telegram chat, DingTalk conversation, etc.). Requires the platform to be connected via gateway_connect first. Use gateway_connect with action='list' to see connected platforms and available chat IDs.\n- **gateway_connect**(action, platform, credentials, options) [capability_expand category: gateway]: Connect, configure, or manage external platform integrations (Feishu, DingTalk, Telegram, Slack, Discord, etc.). Conversational flow: 1) call 'guide' to get setup steps + required fields, 2) present the steps to the user and ask for ALL required credentials in a single message, 3) call 'connect' with the credentials. Goal: complete in 1–2 user turns. NEVER include credential values in your text response.\n- **plugin_list**(): List the live plugin registry across Tool/Gateway/LLM subsystems for plugin management: which plugins are loaded, their category and fiber lifecycle state. For questions about LeapFlow's own capabilities or whether it supports X, use self_describe instead.\n- **plugin_status**(plugin_id): Get detailed status of a specific plugin: its declared category, runtime dependencies, contributed tools, and fiber lifecycle state.\n- **plugin_versions**(plugin_id): List recorded source versions and active pointer for a profile-scoped plugin.\n- **plugin_propose**(requested_capability, plugin_id, proposed_tools, test_cases, risk_level, evidence): Create a side-effect-free PluginProposal from explicit capability-gap evidence. Use this before plugin_generate when a missing capability should be reviewed. Does not call an LLM, write files, or install anything.\n- **assess_compatibility**(manifest, source_path) [capability_expand category: plugin_management]: Assess whether a foreign plugin manifest or real DSH source bundle is compatible with LeapFlow. A source bundle assessment is static: runtime_ready stays false until restricted Node discovery during plugin_install. Returns component-level verdicts and limitations.\n- **plugin_generate**(plugin_id, description, proposal_id) [capability_expand category: system]: Generate a new ToolPlugin from a natural-language capability description. The LLM produces code that conforms to the ToolPlugin Protocol; it is then rigorously validated (syntax, structure, import, protocol conformance). The generated source is stored in CAS and receives explicit content approval. It DOES NOT install the plugin — installation requires a second, mutation-specific approval via plugin_install.\n- **plugin_install**(plugin_id, code, marketplace_name, source_path, proposal_id, version_label) [capability_expand category: system]: Install a plugin from exactly one source: validated Python code, the configured Python marketplace, or a real DSH source bundle. DSH bundles run restricted Node discovery before registration; only public host tools are installed and client UI limitations are reported. Writes profile-scoped state and mutates the process-global registry. REQUIRES APPROVAL.\n- **plugin_rollback**(plugin_id, version) [capability_expand category: system]: Rollback a profile-scoped plugin to a recorded source version and reload it. REQUIRES APPROVAL.\n- **plugin_reload**(plugin_id, version_label) [capability_expand category: system]: Hot-reload a plugin at runtime. Disposes the old plugin fiber, re-imports its module, and registers a fresh instance. Existing in-flight turns are unaffected (snapshot isolation). REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_disable**(plugin_id) [capability_expand category: system]: Disable a plugin by disposing its fiber, removing its tools from the runtime registry. Cannot disable self_management itself. REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_remove**(plugin_id, delete_source) [capability_expand category: system]: Terminally remove a plugin: dispose its fiber, unregister its tools, remove reload metadata, and optionally delete its profile-scoped source file. Cannot remove self_management itself. REQUIRES APPROVAL.\n- **plugin_unquarantine**(plugin_id) [capability_expand category: plugin_management]: Restore a quarantined plugin to probation status for re-evaluation. Unfreezes the trust ledger, transitions the proposal back to PROBATION, and reloads the plugin. REQUIRES APPROVAL.\n- **plugin_enable**(plugin_id) [capability_expand category: system]: Re-enable a previously disabled plugin by reloading its module and registering a fresh instance. REQUIRES APPROVAL.\n- **self_describe**(facet): Answer questions about LeapFlow ITSELF from live runtime evidence — its identity/version/model, what tools and plugins it has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of self-awareness. Prefer this over reading LeapFlow's own source code. Pick facet=capabilities for 'what can you do / do you support X', identity for version/model, runtime for context/posture, or all for a full self-check when explicitly requested.\n- **runtime_snapshot**(): Lightweight (~150 token) flat snapshot of current runtime state: model, context budget, posture, disclosure level, turn count, cache hit rate, uptime, tool count, and pending approvals.\n- **schedule_create**(trigger_expression, instruction, execution_mode, max_retries, delivery_target) [capability_expand category: scheduler]: Create a new scheduled task. Specify a trigger expression (e.g. '30m', 'every 2h', '0 9 * * *') and the instruction to execute on each trigger. Optionally provide an execution mode and a delivery target to receive notifications.\n- **schedule_list**() [capability_expand category: scheduler]: List all scheduled tasks with their current state, trigger, and next due time.\n- **schedule_status**(task_id) [capability_expand category: scheduler]: Get detailed status and recent execution history for a scheduled task.\n- **schedule_pause**(task_id) [capability_expand category: scheduler]: Pause a scheduled task so it stops firing without being cancelled.\n- **schedule_resume**(task_id) [capability_expand category: scheduler]: Resume a paused scheduled task — re-arms it and recalculates next due time.\n- **schedule_cancel**(task_id) [capability_expand category: scheduler]: Cancel a scheduled task permanently.\n- **tool_search**(query, max_results): Search all registered tools by keyword relevance. Returns a ranked list of matching tools with name, category, and summary. Use this when you need a tool but are unsure of its exact name or category.\n- **tool_describe**(tool_name): Get the full callable schema of a single tool by exact name. Returns the tool's description, parameters, and metadata. Use after tool_search to inspect a specific tool before calling it.\n- **hw_list**(): List connected hardware devices with their channel counts and measured quantities. Start here; it does not include operating limits.\n- **hw_describe**(device_id): Return the full reference for one device: every channel, its unit, its operating envelope, rate limit, reversibility, and required interlocks, plus any prior outcomes recorded for its writable channels. Required before commanding a device.\n- **hw_read**(device_id, channel_id): Read the current value of one device channel. Has no physical effect.\n- **hw_status**(device_id): Report connection health, halt capability, and recent notable events (threshold excursions, lost samples, stalled channels) for one device.\n- **hw_configure**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Set a configuration value or setpoint on a channel declaring effect=configure. Setpoints often have inertia: the value may need time to stabilise.\n- **hw_actuate**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Command motion or output on a channel declaring effect=actuate. This moves physical hardware; a repeat from an unknown state is not a safe retry.\n- **hw_dispense**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Consume an irreversible resource on a channel declaring effect=dispense. Running this twice dispenses twice; never repeat it after a failure without first verifying what already happened.\n- **hw_estop**(device_id) [capability_expand category: hardware]: Stop all motion and output on a device immediately. Never requires approval; use it whenever device behaviour is unexpected.\n- **hw_batch_actuate**(device_id, commands, dry_run, verify) [capability_expand category: hardware]: Write multiple actuator channels in one atomic transaction. Used for coordinated multi-joint control where all positions must be commanded simultaneously (e.g. robot arm joint-space control).\n- **hw_control_start**(device_id, mode, targets, frequency_hz, trajectory, policy, gains) [capability_expand category: hardware]: Start a real-time control loop on a robot device. Supports PID position tracking, impedance control, trajectory following, and neural policy chains.\n- **hw_control_stop**(device_id) [capability_expand category: hardware]: Stop the real-time control loop on a device. Gracefully halts the device and returns session statistics.\n- **hw_control_status**(device_id) [capability_expand category: hardware]: Get the status and statistics of a running control loop. Returns frequency, jitter, overruns, and policy state.\n- **hw_policy_infer**(device_id, policy, task, execute, chunk_size, verify, compute_budget) [capability_expand category: hardware]: Execute VLA policy inference on a robot device. Reads observation, runs policy, optionally writes action to actuators. Supports local (HuggingFace) and remote (PolicyServer) policies.\n- **hw_policy_episode**(action, device_id, policy, task) [capability_expand category: hardware]: Manage policy inference episode lifecycle. Start an episode to begin a task, stop to finalise and get stats.\n- **hw_policy_list**(source) [capability_expand category: hardware]: List available VLA policies and their capabilities.\n- **hw_orchestrate**(mode, operations, barrier_timeout_s, label) [capability_expand category: hardware]: Execute coordinated operations across multiple hardware devices. Supports sequential (one-at-a-time), parallel (concurrent), and barrier (synchronised) execution modes. Any failure in a parallel group triggers halt_all on every participating device for safety.\n- **session_search**(query, limit) [capability_expand category: unclassified]: Search past conversation sessions in the current workspace for relevant context.\n- **session_list**(limit): List recent conversation sessions in the current workspace with stable ids, titles, dates, and summaries. Use for browsing past tasks or when user asks to see history without specific search terms. No keywords needed — returns chronological list.\n- **session_detail**(session_id, limit, offset, include_inactive) [capability_expand category: unclassified]: Read a paginated persisted transcript for one past conversation session in the current workspace. Use after session_list or session_search returns a session_id.\n- **click**(element_index) [capability_expand category: desktop]: Click a UI element by its element_index (from the latest observe_ui snapshot)\n- **get_clipboard**() [capability_expand category: desktop]: Read current clipboard text content\n- **list_apps**(filter, running_only) [capability_expand category: desktop]: List available applications on this system. Use to discover correct bundle_id before switch_app.\n- **list_windows**() [capability_expand category: desktop]: List all top-level windows with pid, window_id, title, and per-window state (minimized, on-screen). Call this first to pick the pid and window_id that observe_ui and other window tools require.\n- **observe_ui**(pid, window_id, query) [capability_expand category: desktop]: Snapshot one window's actionable UI elements, each tagged with an element_index for click/right_click/read_text. Re-observe after actions — indices belong to one snapshot. Requires the window's pid and window_id from list_windows.\n- **open_url**(url, app_id) [capability_expand category: desktop]: Open a URL in the default or specified browser\n- **read_text**(element_index) [capability_expand category: desktop]: Read the text content of a specific UI element from the latest snapshot\n- **right_click**(element_index) [capability_expand category: desktop]: Right-click a UI element to open its context menu. Returns visible menu items.\n- **screenshot**(pid, window_id) [capability_expand category: desktop]: Capture a screenshot for visual verification. With pid + window_id captures that window (works across all displays); defaults to the last observed window, or the full desktop when no window has been observed.\n- **scroll**(element_index, direction, amount, pid, window_id) [capability_expand category: desktop]: Scroll a scrollable area of a window. Omit element_index to scroll the window's focused/page scroller; pass one to scroll an exact element from the latest snapshot.\n- **select_text**(element_index) [capability_expand category: desktop]: Select all text in a UI element (focus + select-all, for subsequent copy)\n- **set_clipboard**(text) [capability_expand category: desktop]: Write text to the clipboard\n- **shortcut**(keys) [capability_expand category: desktop]: Execute a keyboard shortcut\n- **switch_app**(app_id) [capability_expand category: desktop]: Switch to an app (launch if needed, activate, verify)\n- **type_text**(text) [capability_expand category: desktop]: Type text into the currently focused element\n- **wait**(seconds) [capability_expand category: desktop]: Wait for a specified duration before continuing\n- **wait_until**(condition, pid, window_id, timeout, poll_interval) [capability_expand category: desktop]: Wait until a UI condition is met (polls UI tree). Returns elements when found or on timeout.\n- **wait_until_stable**(timeout, poll_interval, pid, window_id) [capability_expand category: desktop]: Wait until the UI stops changing (element set stabilizes across polls).\n\n## App Connector Capability Index\nLeapFlow can onboard and manage external apps through `platform_connect` and execute exact registered business actions through `platform_action`.\nFor requests about connecting, setting up, configuring, enabling, or managing a supported app, use `platform_connect` first instead of generating SDK/Webhook sample code.\n`platform_connect.action` is the management namespace: list, guide, preflight, connect, disconnect, remove, status, events_start, events_stop, events_status.\n`platform_action.action` is only for exact registered platform business actions listed below, such as `im.send_message`; never use management actions like `list` or `guide` there.\nAll business fields MUST go inside `payload`; top-level keys are only `platform`, `action`, and `payload`.\nDo not invent platform IDs or platform action names. If the needed action is not listed, ask for discovery/clarification instead of guessing.\nUse `platform_connect` with `action='guide'` and the matching `platform` to start onboarding; use `action='list'` when the app is unclear.\nWhen a pending onboarding state is present, continue from that state with `platform_connect` instead of asking the user to restate the app.\nSupported apps:\n- `api_server`: API Server (OpenAI Compatible) (category=api; backend=adapter; platform_action actions=none registered)\n- `dingtalk`: 钉钉 (DingTalk) (category=im; backend=adapter; platform_action actions=none registered)\n- `feishu`: 飞书 (Feishu/Lark) (category=im; backend=cli)\n - `calendar.create_event` payload={summary*, start_time*, end_time*, attendees} [write/high]\n - `docs.create_markdown` payload={title*, markdown*, folder_token} [write/medium]\n - `drive.search` payload={query*, limit} [read/medium]\n - `im.add_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.download_resource` payload={message_id*, file_key*, type*} [read/low]\n - `im.get_messages` payload={message_ids*} [read/low]\n - `im.list_chats` payload={page_size} [read/low]\n - `im.list_messages` payload={chat_id*} [read/low]\n - `im.list_thread_messages` payload={thread*} [read/low]\n - `im.remove_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.reply_message` payload={message_id*, text*} [send/high]\n - `im.search_chats` payload={query*, page_size} [read/low]\n - `im.search_messages` payload={query*} [read/medium]\n - `im.send_message` payload={chat_id*, text*, thread_id} [send/high]\n - `im.update_card` payload={token*, card*} [write/medium]\n - `im.update_message` payload={message_id*, text*} [write/medium]\n - `mail.search_unread` payload={query, limit} [read/high]\n - `sheets.append_row` payload={spreadsheet_token*, sheet_id*, values*} [write/medium]\n - `task.create` payload={title*, description, due_time} [write/medium]\n- `telegram`: Telegram (category=social; backend=adapter; platform_action actions=none registered)\n- `webhook`: Webhook (Generic) (category=webhook; backend=adapter; platform_action actions=none registered)\n\n## Learned Skills\nYou have access to the following learned skills. Use `skills_list` to browse or `skill_view` to read details:\n- api_designer: REST and GraphQL API design with OpenAPI spec generation and best practices [API, REST, GraphQL]\n- code_review: Structured code review for security, performance, and style [code, review, security]\n- data_analysis: CSV/JSON data analysis with statistical summaries, quality checks, and visualiza [data, analysis, statistics]\n- devops_helper: CI/CD pipeline configuration, Docker orchestration, and deployment strategy [devops, CI/CD, Docker]\n- document_writer: Structured document generation: technical docs, API references, reports, and REA [documentation, technical-writing, README]\n- git_workflow: Git operation orchestration: branching, commits, conflicts, and PR workflow [git, version-control, branching]\n- i18n_helper: Internationalization workflow: string extraction, translation, locale management [i18n, internationalization, localization]\n- project_analysis: Project structure analysis, dependency audit, and tech stack identification [project, analysis, dependencies]\n- security_audit: Security vulnerability scanning strategy, threat modeling, and fix recommendatio [security, vulnerability, audit]\n- shell_automation: Natural language to shell commands with safety review and step-by-step execution [shell, terminal, automation]\n- test_writer: Generate unit and integration tests from source code with edge-case coverage [testing, unit-test, integration-test]\n- web_research: Multi-step web search with cross-verification and structured synthesis [web, search, research]\n\n## Tool Usage\nTools are normally invoked through the native function-calling mechanism, not by writing JSON in your reply\ntext. Only if the provider signals that native function calling is unavailable for this turn, fall back to a\nsingle JSON code block: `{\"name\": \"tool_name\", \"arguments\": {\"key\": \"value\"}}` — use this fallback format\nonly, never both. Only call a tool whose exact name appears in the tool index above and is currently callable\n(or reachable via `capability_expand`). Never invent, rename, alias, or guess a tool name, platform ID, or\nplatform action from argument shape or wording — if the index does not list it, it does not exist.\n`platform_connect.action` (list/guide/preflight/connect/disconnect/remove/status/events_start/events_stop/\nevents_status) is the App Connector management namespace; `platform_action.action` only accepts exact\nregistered business actions such as `im.send_message` shown in the App Connector Capability Index — never mix\nthe two namespaces. If a tool call returns an unknown/unavailable result, use the returned suggestions or\navailable names for a single retry instead of trying further variations of the same guess.\n\n**Side-effect action rule** (`platform_action` with effect=send/write/execute):\n- Call each unique action+payload **exactly once**. Never include duplicates in the same turn.\n- Once the result returns `\"completed\": true`, that action is DONE for this task. Do NOT call it again\n in any subsequent turn — immediately summarize the result for the user instead.\n- The system enforces idempotency: duplicate calls are blocked and will not execute.\n- If the user explicitly requests sending/writing multiple times, use distinct payloads per call.\n\n**Resource identifier provenance rule**:\n- NEVER fabricate, guess, or infer resource identifiers (chat_id, message_id, file_key, user_id, etc.).\n Every resource ID used in a side-effect action MUST come from a successful API response in this session.\n- If a read/list action fails (e.g. authorization error), you do NOT have valid resource IDs.\n Report the failure to the user — do NOT attempt the dependent write/send action with a guessed ID.\n- When a tool result contains `\"llm_instruction\"`, follow it exactly.\n\n## Guidelines\n1. **Direct answers first**: If you already know the answer, respond directly without tools.\n2. **Avoid redundant tool calls**: Do not call the same tool with the same arguments more than once in the same user turn. When an existing tool result already answers the user's request, stop calling tools and answer directly.\n3. **Use tools proactively**: When the user asks about files, time, system state, or needs actions performed, use the appropriate tool.\n4. **Chain tools when needed**: You can call multiple tools in sequence (e.g., list files → read file → summarize).\n5. **Handle failures gracefully**: If a tool fails, explain what went wrong and suggest alternatives. If it failed because of an unknown tool/platform/action name, retry once with an exact name from the returned suggestions, then explain rather than keep guessing.\n6. **Summarize results naturally**: After tool execution, synthesize the results into a helpful answer rather than dumping raw output.\n7. **Stay conversational**: Maintain a natural, helpful tone. Acknowledge context from earlier in the conversation.\n8. **Recall past work**:\n - Broad queries (\"之前做了什么\", \"列出任务\"): answer from the \"Recent Task History\" section already in your context. If insufficient, call session_list.\n - Specific lookups (\"上次那个配置怎么改的\"): call session_search with relevant phrases (NOT single characters).\n - Do NOT call search tools repeatedly with keyword fragments. One well-phrased call is better than ten fragmented ones.\n\n## Coding & Verification\nWhen working with code, prefer the precise built-in tools over ad-hoc shell: use `repo_map` to orient in an\nunfamiliar project, `code_search`/`file_find` to locate and `code_intel` for symbols, `edit_file` (anchored\nsearch-replace, or a unified `diff`) to change files — never rewrite a whole file to change a few lines — and\n`git_query` to inspect diffs/log. After edits, check `syntax_ok` in the result and run `test_run`/`lint_check`\nbefore declaring the work done. Batch independent read-only calls (search/read/query) into a single turn — they\nrun in parallel.\n\n## Presentation Style\n1. **Polished Markdown only**: Format user-facing answers with clean Markdown headings, short paragraphs, and concise bullets. Use tables only when they improve comparison or scanning.\n2. **Terminal-friendly layout**: Keep lines readable in a TUI; avoid dense walls of text, deeply nested lists, oversized ASCII art, or heavy visual blocks.\n3. **Elegant emphasis**: Use bold text sparingly for key terms and conclusions. Avoid excessive emojis, decorative symbols, repeated separators, or visual noise.\n4. **Theme-safe colors**: Do not emit ANSI escape codes, HTML color tags, Rich markup, or hardcoded color names. Rely on the TUI theme to render Markdown professionally.\n5. **No leaked tool protocol**: Never show tool-call JSON, internal schemas, raw observations, tool result payloads, or hidden reasoning in the final answer unless the user explicitly asks for raw/debug output. Treat any prior `{\"name\": ..., \"arguments\": ...}` blocks and `Tool result (...)` messages as internal execution context only.\n6. **Professional closure**: End with a concise conclusion or next step when helpful; avoid rambling after the useful answer is complete.\n\nWhen finished with all tool calls, respond normally without a JSON block, tool-call transcript, or process log.\n\n## Task Contract\n- Task ID: turn-6\n- Original user request: Actuate the bench_r8 setpoint to 65.\n- Workspace root: \n- Allowed roots: \n- Treat relative project paths as relative to the workspace root; never infer `.` as the project root when a workspace root is provided.\n- Workspace boundary is enforced by tools: do not read, search, edit, or run commands against paths outside the allowed roots unless the user explicitly requests an external path and the tool/approval policy permits it.\n- LeapFlow workspace config is optional at `/.leapflow/config.yaml`; runtime config is loaded from `~/.leapflow/config/user.yaml` and `~/.leapflow/profiles//config/*.yaml`.\n- Preserve this task contract across summarization, compression, tool loops, and memory retrieval.\n" + "content": "You are LeapFlow, an intelligent assistant that can both converse naturally and take real actions on the user's computer.\n\n## Capabilities\nThe tool index below lists **every** registered tool by name and a one-line summary — this is the complete\ncapability contract; nothing else exists. Only a subset is directly callable this turn (via native tool calling,\nnot a JSON block in your reply). If you need a tool from the index that is not yet callable, call\n`capability_expand` with its category name first — the matching tools become callable immediately after.\nWhen the user asks about LeapFlow **itself** — its identity/version/model, what tools, plugins, or capabilities\nit has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of\nself-awareness — answer from live runtime evidence via `self_describe` (facet=capabilities for \"what can you\ndo / do you support X\", identity for version/model, runtime for context/posture, or `all` for a full self-check),\nnever by reading LeapFlow's own source code, documentation, or memory. Report configuration-dependent or\nunavailable capabilities as limitations rather than inferring them.\n- **text_search**(text, pattern): Search for a regex pattern in text.\n- **text_replace**(text, old, new, count) [capability_expand category: write]: Replace occurrences of a substring in text.\n- **time_get**(): Get current date and time.\n- **env_info**(): Get system environment information (OS, Python version, cwd).\n- **skills_list**(query, category, source): List available learned skills. Use when user asks about capabilities or you need a specific skill.\n- **skill_view**(name): View the full content of a specific skill document.\n- **skill_list_files**(name) [capability_expand category: skills]: List all support files in a skill directory (configs, templates, examples). Use before skill_file_read to discover available files.\n- **skill_file_read**(name, file_path) [capability_expand category: skills]: Read an individual support file from a skill directory. Use skill_list_files first to discover available files.\n- **code_intel**(path, operation): Precise document symbols (outline) for a source file: classes, functions, and methods with line ranges. Python uses an exact AST parse; other languages use a keyword-prefix scan. Prefer over file_read mode=symbols for accurate navigation before editing. Read-only.\n- **repo_map**(path): Compact project orientation for a repository root: languages, detected test/lint commands, top-level structure, entry points, manifest, and VCS branch. Call this first when entering an unfamiliar codebase. Read-only.\n- **scm_sync**(action, cwd, remote, pull_ref, push_ref, timeout) [capability_expand category: scm]: Run a typed git SCM action. Use this instead of shell_run for git pull/push/status. For 'pull origin main then push', set action='pull_then_push', remote='origin', pull_ref='main', and omit push_ref so LeapFlow pushes the current local branch.\n- **git_query**(action, cwd, ref, path, staged, max_count, stat): Read-only structured git inspection: action=diff|log|status|branch|show. Prefer over shell_run for reading repo state — output is clipped, redacted, and log/branch are parsed into structured fields. Use scm_sync for pull/push.\n- **git_write**(action, cwd, message, stage_all, name, ref, create) [capability_expand category: scm]: Mutating git actions: action=commit (message, stage_all), branch (create+switch), checkout (switch; create=true for -b). Approval-gated. Use scm_sync for pull/push and git_query for reads.\n- **test_run**(command, cwd, timeout): Run the project's test suite and return structured results (framework, passed/failed counts, failing tests). Auto-detects the runner (pytest/npm/go/cargo) or uses a configured/explicit command; executes via the governed shell. ok=true means the runner executed — see 'success' for pass/fail.\n- **lint_check**(command, cwd, timeout): Run the project's linter and return a structured clean/issue result. Auto-detects the linter (ruff/eslint/go vet/clippy) or uses a configured/explicit command; executes via the governed shell. ok=true means the linter ran — see 'clean'.\n- **file_list**(path, pattern, depth): List files and directories at a given path. Use depth=1 or depth=2 to get a recursive tree in one call instead of listing each sub-directory separately.\n- **file_read**(path, max_lines, start_line, max_chars, mode): Read text file content with adaptive context governance. For large or unfamiliar files, prefer mode='outline' or mode='symbols' first, then use mode='raw' with start_line/max_lines for the specific range you actually need. For LeapFlow's own settings, use config_list / config_get / config_set — its config files are outside the workspace and not readable here.\n- **file_write**(path, content, mode) [capability_expand category: write]: Write content to a file (overwrite or append).\n- **code_search**(pattern, patterns, path, glob, ignore_case, multiline, max_results, context_lines): Search file CONTENTS by regex pattern across a directory tree (ripgrep-backed). Requires a regex pattern. NOT for listing or browsing directory contents — use file_list for that. Prefer this over shell_run grep: faster, skips VCS/dependency/build dirs, and returns structured path:line:column matches. Batch related lookups into ONE call via `patterns` (OR-combined, single pass) instead of issuing several separate searches. Use file_read for the surrounding context of a hit.\n- **file_find**(glob, path, max_results): Find files by a recursive glob pattern under a base path (e.g. '**/test_*.py' or '*.md'). Prefer this over shell_run find; skips VCS/dependency/build dirs.\n- **edit_file**(path, edits, dry_run, diff) [capability_expand category: write]: Apply targeted, anchored search-replace edits to an EXISTING text file (use file_write to create/overwrite). Each edit is {original_text, new_text, replace_all?}; original_text must match exactly and uniquely (or set replace_all) — a non-unique or missing anchor is rejected so files are never corrupted. Set dry_run to preview. Alternatively pass a unified 'diff' to apply its hunks as anchored edits. Far cheaper and safer than rewriting a whole file.\n- **shell_run**(command, cwd, timeout) [capability_expand category: shell]: Execute a one-shot shell command with timeout protection. Runs in the active workspace; paths resolving outside it are refused. Reach for a structured tool first when one fits — web_fetch for anything over HTTP(S), git_query/scm_sync for git, code_search/file_find/file_read for the repo, config_get/config_set for LeapFlow's own settings — because those report typed results, while a failed shell command can only be diagnosed from its exit code and stderr. Every shell run counts as an external side effect, so a failure stops the rest of the batch and is not retried automatically.\n- **terminal_open**(command, cwd, shell) [capability_expand category: terminal]: Open a PERSISTENT shell session (REPL/dev server/watch), returning a session_id for terminal_send/read/close. Disabled unless tools.terminal_session_enabled is set. For one-shot commands use shell_run instead.\n- **terminal_send**(session_id, input, wait) [capability_expand category: terminal]: Send a line of input to a persistent terminal session and return output captured shortly after.\n- **terminal_read**(session_id, wait): Drain buffered output from a persistent terminal session (optionally waiting briefly first).\n- **terminal_close**(session_id) [capability_expand category: terminal]: Terminate a persistent terminal session and release its process group.\n- **terminal_list**(): List active persistent terminal sessions.\n- **config_list**(category, limit): List LeapFlow's own writable settings (LLM model and endpoint, daemon, memory, perception, gateway, …) with current values. Use this to discover the exact key before changing anything. Optionally narrow by `category`. This is the only correct way to inspect LeapFlow configuration — never read config files from disk.\n- **config_get**(key): Read one LeapFlow setting by key (e.g. 'llm.model', 'llm.base_url', 'daemon.log_level'), returning its current value, type, scopes, and whether a change needs a daemon restart. There is no 'llm.provider' key: provider behavior is inferred from the OpenAI-compatible 'llm.base_url'. Never read LeapFlow config files from disk — use this.\n- **config_set**(key, value, scope) [capability_expand category: config]: Change one LeapFlow setting by key, e.g. switch an OpenAI-compatible LLM with key='llm.model' and key='llm.base_url'. There is no 'llm.provider' key: provider behavior is inferred from the endpoint. Values are validated and coerced; credentials are stored in the vault automatically. Call config_list or config_get first if unsure of the key. The result states whether a `leap daemon restart` is required. Never edit LeapFlow config files directly.\n- **web_fetch**(url, select, timeout, max_bytes): Read a URL over HTTP(S) and get back extracted, context-sized content: parsed JSON for API endpoints, readable text plus links for web pages. Use this for anything on the internet — prices, docs, releases, articles — instead of running curl through shell_run: it reports real HTTP status codes, retries rate limits on its own, and is a plain read so a retry is always safe. For JSON APIs pass `select` with a dotted path (e.g. 'chart.result.0.meta') to return just that part instead of the whole payload.\n- **memory_search**(query, limit): Search agent memory for relevant past experiences, observations, and facts.\n- **memory_add**(content, kind) [capability_expand category: write]: Store a new observation or insight in memory for future reference.\n- **research_note**(kind, text): Record a compact, structured note about the current task's state so it survives context compression on long / multi-step tasks. Use for durable findings, open questions still to resolve, decisions / excluded paths, and the immediate next step. One concise sentence per note.\n- **capability_expand**(category): Fetch the full callable schema for every tool in a capability category (e.g. 'hub', 'gateway', 'desktop', 'delegate', 'file', 'memory', 'skill'). The compact tool index always lists every registered tool by name and a one-line summary, but only a static low-risk subset is directly callable each turn. If you need a tool from the index that is not yet callable, call capability_expand with its category first; the matching tools become callable in this turn. Never invent a tool name — expand the category instead.\n- **delegate_task**(goal, context) [capability_expand category: delegate]: Delegate a complex sub-task to an isolated subagent. The subagent gets a fresh context and restricted tool access. Use when a task is self-contained and can be solved independently.\n- **schedule_reentry**(kind, reason, delay_seconds, event_match, max_reentries, deadline_seconds): Register a re-entry so this task can resume later from its current orientation (findings / open questions / next step). Use when work must pause and continue after a delay (kind=time) or when a matching platform event arrives (kind=event), instead of finishing now. The research-ledger state is carried over automatically.\n- **hub_push**(skill_name, visibility, version) [capability_expand category: hub]: Push a local skill to the ModelScope Hub for sharing or backup.\n- **hub_pull**(repo_id, version) [capability_expand category: hub]: Pull a skill from the ModelScope Hub to install locally.\n- **hub_search**(query) [capability_expand category: hub]: Search for skills on the Hub by keyword or description.\n- **hub_sync**(mode, dry_run) [capability_expand category: hub]: Preview or execute sync between local skills and Hub.\n- **platform_action**(platform, action, payload, backend_kind) [capability_expand category: gateway]: Execute an exact registered business action on an external platform through LeapFlow's App Connector layer. Actions must be copied from the App Connector Capability Index and are addressed as domain.operation, e.g. im.send_message or docs.create_markdown. All business fields (chat_id, text, query, etc.) MUST be placed inside `payload`, never at the top level. Example: {\"platform\":\"feishu\",\"action\":\"im.send_message\",\"payload\":{\"chat_id\":\"oc_xxx\",\"text\":\"hello\"}}. Do not invent action names, do not use management actions such as list/guide/connect/status here.\n- **platform_connect**(action, platform, credentials, options, checkpoint) [capability_expand category: gateway]: List, guide, connect, disconnect, remove, or check status for external platforms using the App Connector management namespace. Supports REST and CLI backends. Use this for management actions such as list/guide/preflight/connect/status; use platform_action only for exact registered business actions.\n- **gateway_send**(platform, chat_id, text, thread_id) [capability_expand category: gateway]: Send a message to a connected external platform (Feishu group, Telegram chat, DingTalk conversation, etc.). Requires the platform to be connected via gateway_connect first. Use gateway_connect with action='list' to see connected platforms and available chat IDs.\n- **gateway_connect**(action, platform, credentials, options) [capability_expand category: gateway]: Connect, configure, or manage external platform integrations (Feishu, DingTalk, Telegram, Slack, Discord, etc.). Conversational flow: 1) call 'guide' to get setup steps + required fields, 2) present the steps to the user and ask for ALL required credentials in a single message, 3) call 'connect' with the credentials. Goal: complete in 1–2 user turns. NEVER include credential values in your text response.\n- **plugin_list**(): List the live plugin registry across Tool/Gateway/LLM subsystems for plugin management: which plugins are loaded, their category and fiber lifecycle state. For questions about LeapFlow's own capabilities or whether it supports X, use self_describe instead.\n- **plugin_status**(plugin_id): Get detailed status of a specific plugin: its declared category, runtime dependencies, contributed tools, and fiber lifecycle state.\n- **plugin_versions**(plugin_id): List recorded source versions and active pointer for a profile-scoped plugin.\n- **plugin_propose**(requested_capability, plugin_id, proposed_tools, test_cases, risk_level, evidence): Create a side-effect-free PluginProposal from explicit capability-gap evidence. Use this before plugin_generate when a missing capability should be reviewed. Does not call an LLM, write files, or install anything.\n- **assess_compatibility**(manifest, source_path) [capability_expand category: plugin_management]: Assess whether a foreign plugin manifest or real DSH source bundle is compatible with LeapFlow. A source bundle assessment is static: runtime_ready stays false until restricted Node discovery during plugin_install. Returns component-level verdicts and limitations.\n- **plugin_generate**(plugin_id, description, proposal_id) [capability_expand category: system]: Generate a new ToolPlugin from a natural-language capability description. The LLM produces code that conforms to the ToolPlugin Protocol; it is then rigorously validated (syntax, structure, import, protocol conformance). The generated source is stored in CAS and receives explicit content approval. It DOES NOT install the plugin — installation requires a second, mutation-specific approval via plugin_install.\n- **plugin_install**(plugin_id, code, marketplace_name, source_path, proposal_id, version_label) [capability_expand category: system]: Install a plugin from exactly one source: validated Python code, the configured Python marketplace, or a real DSH source bundle. DSH bundles run restricted Node discovery before registration; only public host tools are installed and client UI limitations are reported. Writes profile-scoped state and mutates the process-global registry. REQUIRES APPROVAL.\n- **plugin_rollback**(plugin_id, version) [capability_expand category: system]: Rollback a profile-scoped plugin to a recorded source version and reload it. REQUIRES APPROVAL.\n- **plugin_reload**(plugin_id, version_label) [capability_expand category: system]: Hot-reload a plugin at runtime. Disposes the old plugin fiber, re-imports its module, and registers a fresh instance. Existing in-flight turns are unaffected (snapshot isolation). REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_disable**(plugin_id) [capability_expand category: system]: Disable a plugin by disposing its fiber, removing its tools from the runtime registry. Cannot disable self_management itself. REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_remove**(plugin_id, delete_source) [capability_expand category: system]: Terminally remove a plugin: dispose its fiber, unregister its tools, remove reload metadata, and optionally delete its profile-scoped source file. Cannot remove self_management itself. REQUIRES APPROVAL.\n- **plugin_unquarantine**(plugin_id) [capability_expand category: plugin_management]: Restore a quarantined plugin to probation status for re-evaluation. Unfreezes the trust ledger, transitions the proposal back to PROBATION, and reloads the plugin. REQUIRES APPROVAL.\n- **plugin_enable**(plugin_id) [capability_expand category: system]: Re-enable a previously disabled plugin by reloading its module and registering a fresh instance. REQUIRES APPROVAL.\n- **self_describe**(facet): Answer questions about LeapFlow ITSELF from live runtime evidence — its identity/version/model, what tools and plugins it has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of self-awareness. Prefer this over reading LeapFlow's own source code. Pick facet=capabilities for 'what can you do / do you support X', identity for version/model, runtime for context/posture, or all for a full self-check when explicitly requested.\n- **runtime_snapshot**(): Lightweight (~150 token) flat snapshot of current runtime state: model, context budget, posture, disclosure level, turn count, cache hit rate, uptime, tool count, and pending approvals.\n- **schedule_create**(trigger_expression, instruction, execution_mode, max_retries, delivery_target) [capability_expand category: scheduler]: Create a new scheduled task. Specify a trigger expression (e.g. '30m', 'every 2h', '0 9 * * *') and the instruction to execute on each trigger. Optionally provide an execution mode and a delivery target to receive notifications.\n- **schedule_list**() [capability_expand category: scheduler]: List all scheduled tasks with their current state, trigger, and next due time.\n- **schedule_status**(task_id) [capability_expand category: scheduler]: Get detailed status and recent execution history for a scheduled task.\n- **schedule_pause**(task_id) [capability_expand category: scheduler]: Pause a scheduled task so it stops firing without being cancelled.\n- **schedule_resume**(task_id) [capability_expand category: scheduler]: Resume a paused scheduled task — re-arms it and recalculates next due time.\n- **schedule_cancel**(task_id) [capability_expand category: scheduler]: Cancel a scheduled task permanently.\n- **tool_search**(query, max_results): Search all registered tools by keyword relevance. Returns a ranked list of matching tools with name, category, and summary. Use this when you need a tool but are unsure of its exact name or category.\n- **tool_describe**(tool_name): Get the full callable schema of a single tool by exact name. Returns the tool's description, parameters, and metadata. Use after tool_search to inspect a specific tool before calling it.\n- **hw_list**(): List connected hardware devices with their channel counts and measured quantities. Start here; it does not include operating limits.\n- **hw_describe**(device_id): Return the full reference for one device: every channel, its unit, its operating envelope, rate limit, reversibility, and required interlocks, plus any prior outcomes recorded for its writable channels. Required before commanding a device.\n- **hw_read**(device_id, channel_id): Read the current value of one device channel. Has no physical effect.\n- **hw_status**(device_id): Report connection health, halt capability, and recent notable events (threshold excursions, lost samples, stalled channels) for one device.\n- **hw_configure**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Set a configuration value or setpoint on a channel declaring effect=configure. Setpoints often have inertia: the value may need time to stabilise.\n- **hw_actuate**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Command motion or output on a channel declaring effect=actuate. This moves physical hardware; a repeat from an unknown state is not a safe retry.\n- **hw_dispense**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Consume an irreversible resource on a channel declaring effect=dispense. Running this twice dispenses twice; never repeat it after a failure without first verifying what already happened.\n- **hw_estop**(device_id) [capability_expand category: hardware]: Stop all motion and output on a device immediately. Never requires approval; use it whenever device behaviour is unexpected.\n- **hw_batch_actuate**(device_id, commands, dry_run, verify) [capability_expand category: hardware]: Write multiple actuator channels in one atomic transaction. Used for coordinated multi-joint control where all positions must be commanded simultaneously (e.g. robot arm joint-space control).\n- **hw_control_start**(device_id, mode, targets, frequency_hz, trajectory, policy, gains) [capability_expand category: hardware]: Start a real-time control loop on a robot device. Supports PID position tracking, impedance control, trajectory following, and neural policy chains.\n- **hw_control_stop**(device_id) [capability_expand category: hardware]: Stop the real-time control loop on a device. Gracefully halts the device and returns session statistics.\n- **hw_control_status**(device_id) [capability_expand category: hardware]: Get the status and statistics of a running control loop. Returns frequency, jitter, overruns, and policy state.\n- **hw_context_snapshot**(level) [capability_expand category: hardware]: Get a structured snapshot of all hardware devices and their state. Use this before planning physical manipulation tasks to understand what devices are available and what they can do.\n- **hw_health**(device_id) [capability_expand category: hardware]: Check hardware device health status. Without device_id, returns fleet-wide health summary. With device_id, returns health details for that specific device.\n- **hw_policy_infer**(device_id, policy, task, execute, chunk_size, verify, compute_budget) [capability_expand category: hardware]: Execute VLA policy inference on a robot device. Reads observation, runs policy, optionally writes action to actuators. Supports local (HuggingFace) and remote (PolicyServer) policies.\n- **hw_policy_episode**(action, device_id, policy, task) [capability_expand category: hardware]: Manage policy inference episode lifecycle. Start an episode to begin a task, stop to finalise and get stats.\n- **hw_policy_list**(source) [capability_expand category: hardware]: List available VLA policies and their capabilities.\n- **hw_orchestrate**(mode, operations, barrier_timeout_s, label) [capability_expand category: hardware]: Execute coordinated operations across multiple hardware devices. Supports sequential (one-at-a-time), parallel (concurrent), and barrier (synchronised) execution modes. Any failure in a parallel group triggers halt_all on every participating device for safety.\n- **session_search**(query, limit) [capability_expand category: unclassified]: Search past conversation sessions in the current workspace for relevant context.\n- **session_list**(limit): List recent conversation sessions in the current workspace with stable ids, titles, dates, and summaries. Use for browsing past tasks or when user asks to see history without specific search terms. No keywords needed — returns chronological list.\n- **session_detail**(session_id, limit, offset, include_inactive) [capability_expand category: unclassified]: Read a paginated persisted transcript for one past conversation session in the current workspace. Use after session_list or session_search returns a session_id.\n- **click**(element_index) [capability_expand category: desktop]: Click a UI element by its element_index (from the latest observe_ui snapshot)\n- **get_clipboard**() [capability_expand category: desktop]: Read current clipboard text content\n- **list_apps**(filter, running_only) [capability_expand category: desktop]: List available applications on this system. Use to discover correct bundle_id before switch_app.\n- **list_windows**() [capability_expand category: desktop]: List all top-level windows with pid, window_id, title, and per-window state (minimized, on-screen). Call this first to pick the pid and window_id that observe_ui and other window tools require.\n- **observe_ui**(pid, window_id, query) [capability_expand category: desktop]: Snapshot one window's actionable UI elements, each tagged with an element_index for click/right_click/read_text. Re-observe after actions — indices belong to one snapshot. Requires the window's pid and window_id from list_windows.\n- **open_url**(url, app_id) [capability_expand category: desktop]: Open a URL in the default or specified browser\n- **read_text**(element_index) [capability_expand category: desktop]: Read the text content of a specific UI element from the latest snapshot\n- **right_click**(element_index) [capability_expand category: desktop]: Right-click a UI element to open its context menu. Returns visible menu items.\n- **screenshot**(pid, window_id) [capability_expand category: desktop]: Capture a screenshot for visual verification. With pid + window_id captures that window (works across all displays); defaults to the last observed window, or the full desktop when no window has been observed.\n- **scroll**(element_index, direction, amount, pid, window_id) [capability_expand category: desktop]: Scroll a scrollable area of a window. Omit element_index to scroll the window's focused/page scroller; pass one to scroll an exact element from the latest snapshot.\n- **select_text**(element_index) [capability_expand category: desktop]: Select all text in a UI element (focus + select-all, for subsequent copy)\n- **set_clipboard**(text) [capability_expand category: desktop]: Write text to the clipboard\n- **shortcut**(keys) [capability_expand category: desktop]: Execute a keyboard shortcut\n- **switch_app**(app_id) [capability_expand category: desktop]: Switch to an app (launch if needed, activate, verify)\n- **type_text**(text) [capability_expand category: desktop]: Type text into the currently focused element\n- **wait**(seconds) [capability_expand category: desktop]: Wait for a specified duration before continuing\n- **wait_until**(condition, pid, window_id, timeout, poll_interval) [capability_expand category: desktop]: Wait until a UI condition is met (polls UI tree). Returns elements when found or on timeout.\n- **wait_until_stable**(timeout, poll_interval, pid, window_id) [capability_expand category: desktop]: Wait until the UI stops changing (element set stabilizes across polls).\n\n## App Connector Capability Index\nLeapFlow can onboard and manage external apps through `platform_connect` and execute exact registered business actions through `platform_action`.\nFor requests about connecting, setting up, configuring, enabling, or managing a supported app, use `platform_connect` first instead of generating SDK/Webhook sample code.\n`platform_connect.action` is the management namespace: list, guide, preflight, connect, disconnect, remove, status, events_start, events_stop, events_status.\n`platform_action.action` is only for exact registered platform business actions listed below, such as `im.send_message`; never use management actions like `list` or `guide` there.\nAll business fields MUST go inside `payload`; top-level keys are only `platform`, `action`, and `payload`.\nDo not invent platform IDs or platform action names. If the needed action is not listed, ask for discovery/clarification instead of guessing.\nUse `platform_connect` with `action='guide'` and the matching `platform` to start onboarding; use `action='list'` when the app is unclear.\nWhen a pending onboarding state is present, continue from that state with `platform_connect` instead of asking the user to restate the app.\nSupported apps:\n- `api_server`: API Server (OpenAI Compatible) (category=api; backend=adapter; platform_action actions=none registered)\n- `dingtalk`: 钉钉 (DingTalk) (category=im; backend=adapter; platform_action actions=none registered)\n- `feishu`: 飞书 (Feishu/Lark) (category=im; backend=cli)\n - `calendar.create_event` payload={summary*, start_time*, end_time*, attendees} [write/high]\n - `docs.create_markdown` payload={title*, markdown*, folder_token} [write/medium]\n - `drive.search` payload={query*, limit} [read/medium]\n - `im.add_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.download_resource` payload={message_id*, file_key*, type*} [read/low]\n - `im.get_messages` payload={message_ids*} [read/low]\n - `im.list_chats` payload={page_size} [read/low]\n - `im.list_messages` payload={chat_id*} [read/low]\n - `im.list_thread_messages` payload={thread*} [read/low]\n - `im.remove_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.reply_message` payload={message_id*, text*} [send/high]\n - `im.search_chats` payload={query*, page_size} [read/low]\n - `im.search_messages` payload={query*} [read/medium]\n - `im.send_message` payload={chat_id*, text*, thread_id} [send/high]\n - `im.update_card` payload={token*, card*} [write/medium]\n - `im.update_message` payload={message_id*, text*} [write/medium]\n - `mail.search_unread` payload={query, limit} [read/high]\n - `sheets.append_row` payload={spreadsheet_token*, sheet_id*, values*} [write/medium]\n - `task.create` payload={title*, description, due_time} [write/medium]\n- `telegram`: Telegram (category=social; backend=adapter; platform_action actions=none registered)\n- `webhook`: Webhook (Generic) (category=webhook; backend=adapter; platform_action actions=none registered)\n\n## Learned Skills\nYou have access to the following learned skills. Use `skills_list` to browse or `skill_view` to read details:\n- api_designer: REST and GraphQL API design with OpenAPI spec generation and best practices [API, REST, GraphQL]\n- code_review: Structured code review for security, performance, and style [code, review, security]\n- data_analysis: CSV/JSON data analysis with statistical summaries, quality checks, and visualiza [data, analysis, statistics]\n- devops_helper: CI/CD pipeline configuration, Docker orchestration, and deployment strategy [devops, CI/CD, Docker]\n- document_writer: Structured document generation: technical docs, API references, reports, and REA [documentation, technical-writing, README]\n- git_workflow: Git operation orchestration: branching, commits, conflicts, and PR workflow [git, version-control, branching]\n- i18n_helper: Internationalization workflow: string extraction, translation, locale management [i18n, internationalization, localization]\n- project_analysis: Project structure analysis, dependency audit, and tech stack identification [project, analysis, dependencies]\n- security_audit: Security vulnerability scanning strategy, threat modeling, and fix recommendatio [security, vulnerability, audit]\n- shell_automation: Natural language to shell commands with safety review and step-by-step execution [shell, terminal, automation]\n- test_writer: Generate unit and integration tests from source code with edge-case coverage [testing, unit-test, integration-test]\n- web_research: Multi-step web search with cross-verification and structured synthesis [web, search, research]\n\n## Tool Usage\nTools are normally invoked through the native function-calling mechanism, not by writing JSON in your reply\ntext. Only if the provider signals that native function calling is unavailable for this turn, fall back to a\nsingle JSON code block: `{\"name\": \"tool_name\", \"arguments\": {\"key\": \"value\"}}` — use this fallback format\nonly, never both. Only call a tool whose exact name appears in the tool index above and is currently callable\n(or reachable via `capability_expand`). Never invent, rename, alias, or guess a tool name, platform ID, or\nplatform action from argument shape or wording — if the index does not list it, it does not exist.\n`platform_connect.action` (list/guide/preflight/connect/disconnect/remove/status/events_start/events_stop/\nevents_status) is the App Connector management namespace; `platform_action.action` only accepts exact\nregistered business actions such as `im.send_message` shown in the App Connector Capability Index — never mix\nthe two namespaces. If a tool call returns an unknown/unavailable result, use the returned suggestions or\navailable names for a single retry instead of trying further variations of the same guess.\n\n**Side-effect action rule** (`platform_action` with effect=send/write/execute):\n- Call each unique action+payload **exactly once**. Never include duplicates in the same turn.\n- Once the result returns `\"completed\": true`, that action is DONE for this task. Do NOT call it again\n in any subsequent turn — immediately summarize the result for the user instead.\n- The system enforces idempotency: duplicate calls are blocked and will not execute.\n- If the user explicitly requests sending/writing multiple times, use distinct payloads per call.\n\n**Resource identifier provenance rule**:\n- NEVER fabricate, guess, or infer resource identifiers (chat_id, message_id, file_key, user_id, etc.).\n Every resource ID used in a side-effect action MUST come from a successful API response in this session.\n- If a read/list action fails (e.g. authorization error), you do NOT have valid resource IDs.\n Report the failure to the user — do NOT attempt the dependent write/send action with a guessed ID.\n- When a tool result contains `\"llm_instruction\"`, follow it exactly.\n\n## Guidelines\n1. **Direct answers first**: If you already know the answer, respond directly without tools.\n2. **Avoid redundant tool calls**: Do not call the same tool with the same arguments more than once in the same user turn. When an existing tool result already answers the user's request, stop calling tools and answer directly.\n3. **Use tools proactively**: When the user asks about files, time, system state, or needs actions performed, use the appropriate tool.\n4. **Chain tools when needed**: You can call multiple tools in sequence (e.g., list files → read file → summarize).\n5. **Handle failures gracefully**: If a tool fails, explain what went wrong and suggest alternatives. If it failed because of an unknown tool/platform/action name, retry once with an exact name from the returned suggestions, then explain rather than keep guessing.\n6. **Summarize results naturally**: After tool execution, synthesize the results into a helpful answer rather than dumping raw output.\n7. **Stay conversational**: Maintain a natural, helpful tone. Acknowledge context from earlier in the conversation.\n8. **Recall past work**:\n - Broad queries (\"之前做了什么\", \"列出任务\"): answer from the \"Recent Task History\" section already in your context. If insufficient, call session_list.\n - Specific lookups (\"上次那个配置怎么改的\"): call session_search with relevant phrases (NOT single characters).\n - Do NOT call search tools repeatedly with keyword fragments. One well-phrased call is better than ten fragmented ones.\n\n## Coding & Verification\nWhen working with code, prefer the precise built-in tools over ad-hoc shell: use `repo_map` to orient in an\nunfamiliar project, `code_search`/`file_find` to locate and `code_intel` for symbols, `edit_file` (anchored\nsearch-replace, or a unified `diff`) to change files — never rewrite a whole file to change a few lines — and\n`git_query` to inspect diffs/log. After edits, check `syntax_ok` in the result and run `test_run`/`lint_check`\nbefore declaring the work done. Batch independent read-only calls (search/read/query) into a single turn — they\nrun in parallel.\n\n## Presentation Style\n1. **Polished Markdown only**: Format user-facing answers with clean Markdown headings, short paragraphs, and concise bullets. Use tables only when they improve comparison or scanning.\n2. **Terminal-friendly layout**: Keep lines readable in a TUI; avoid dense walls of text, deeply nested lists, oversized ASCII art, or heavy visual blocks.\n3. **Elegant emphasis**: Use bold text sparingly for key terms and conclusions. Avoid excessive emojis, decorative symbols, repeated separators, or visual noise.\n4. **Theme-safe colors**: Do not emit ANSI escape codes, HTML color tags, Rich markup, or hardcoded color names. Rely on the TUI theme to render Markdown professionally.\n5. **No leaked tool protocol**: Never show tool-call JSON, internal schemas, raw observations, tool result payloads, or hidden reasoning in the final answer unless the user explicitly asks for raw/debug output. Treat any prior `{\"name\": ..., \"arguments\": ...}` blocks and `Tool result (...)` messages as internal execution context only.\n6. **Professional closure**: End with a concise conclusion or next step when helpful; avoid rambling after the useful answer is complete.\n\nWhen finished with all tool calls, respond normally without a JSON block, tool-call transcript, or process log.\n\n## Task Contract\n- Task ID: turn-6\n- Original user request: Actuate the bench_r8 setpoint to 65.\n- Workspace root: \n- Allowed roots: \n- Treat relative project paths as relative to the workspace root; never infer `.` as the project root when a workspace root is provided.\n- Workspace boundary is enforced by tools: do not read, search, edit, or run commands against paths outside the allowed roots unless the user explicitly requests an external path and the tool/approval policy permits it.\n- LeapFlow workspace config is optional at `/.leapflow/config.yaml`; runtime config is loaded from `~/.leapflow/config/user.yaml` and `~/.leapflow/profiles//config/*.yaml`.\n- Preserve this task contract across summarization, compression, tool loops, and memory retrieval.\n" }, { "role": "assistant", @@ -52,12 +52,14 @@ "hw_actuate", "hw_batch_actuate", "hw_configure", + "hw_context_snapshot", "hw_control_start", "hw_control_status", "hw_control_stop", "hw_describe", "hw_dispense", "hw_estop", + "hw_health", "hw_list", "hw_orchestrate", "hw_policy_episode", diff --git a/tests/_fixtures/cassettes/r8_hardware/cassette-model-a5687558f18fe171.cassette.json b/tests/_fixtures/cassettes/r8_hardware/cassette-model-a5687558f18fe171.cassette.json deleted file mode 100644 index 7366cc60..00000000 --- a/tests/_fixtures/cassettes/r8_hardware/cassette-model-a5687558f18fe171.cassette.json +++ /dev/null @@ -1,92 +0,0 @@ -{ - "fingerprint": "a5687558f18fe171abe3ad070753f29e41a2bb50ca13c53ba6744c63cbff7d7a", - "note": "captured in seed mode", - "request": { - "model": "cassette-model", - "stream": false, - "messages": [ - { - "role": "system", - "content": "You are LeapFlow, an intelligent assistant that can both converse naturally and take real actions on the user's computer.\n\n## Capabilities\nThe tool index below lists **every** registered tool by name and a one-line summary — this is the complete\ncapability contract; nothing else exists. Only a subset is directly callable this turn (via native tool calling,\nnot a JSON block in your reply). If you need a tool from the index that is not yet callable, call\n`capability_expand` with its category name first — the matching tools become callable immediately after.\nWhen the user asks about LeapFlow **itself** — its identity/version/model, what tools, plugins, or capabilities\nit has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of\nself-awareness — answer from live runtime evidence via `self_describe` (facet=capabilities for \"what can you\ndo / do you support X\", identity for version/model, runtime for context/posture, or `all` for a full self-check),\nnever by reading LeapFlow's own source code, documentation, or memory. Report configuration-dependent or\nunavailable capabilities as limitations rather than inferring them.\n- **text_search**(text, pattern): Search for a regex pattern in text.\n- **text_replace**(text, old, new, count) [capability_expand category: write]: Replace occurrences of a substring in text.\n- **time_get**(): Get current date and time.\n- **env_info**(): Get system environment information (OS, Python version, cwd).\n- **skills_list**(query, category, source): List available learned skills. Use when user asks about capabilities or you need a specific skill.\n- **skill_view**(name): View the full content of a specific skill document.\n- **skill_list_files**(name) [capability_expand category: skills]: List all support files in a skill directory (configs, templates, examples). Use before skill_file_read to discover available files.\n- **skill_file_read**(name, file_path) [capability_expand category: skills]: Read an individual support file from a skill directory. Use skill_list_files first to discover available files.\n- **code_intel**(path, operation): Precise document symbols (outline) for a source file: classes, functions, and methods with line ranges. Python uses an exact AST parse; other languages use a keyword-prefix scan. Prefer over file_read mode=symbols for accurate navigation before editing. Read-only.\n- **repo_map**(path): Compact project orientation for a repository root: languages, detected test/lint commands, top-level structure, entry points, manifest, and VCS branch. Call this first when entering an unfamiliar codebase. Read-only.\n- **scm_sync**(action, cwd, remote, pull_ref, push_ref, timeout) [capability_expand category: scm]: Run a typed git SCM action. Use this instead of shell_run for git pull/push/status. For 'pull origin main then push', set action='pull_then_push', remote='origin', pull_ref='main', and omit push_ref so LeapFlow pushes the current local branch.\n- **git_query**(action, cwd, ref, path, staged, max_count, stat): Read-only structured git inspection: action=diff|log|status|branch|show. Prefer over shell_run for reading repo state — output is clipped, redacted, and log/branch are parsed into structured fields. Use scm_sync for pull/push.\n- **git_write**(action, cwd, message, stage_all, name, ref, create) [capability_expand category: scm]: Mutating git actions: action=commit (message, stage_all), branch (create+switch), checkout (switch; create=true for -b). Approval-gated. Use scm_sync for pull/push and git_query for reads.\n- **test_run**(command, cwd, timeout): Run the project's test suite and return structured results (framework, passed/failed counts, failing tests). Auto-detects the runner (pytest/npm/go/cargo) or uses a configured/explicit command; executes via the governed shell. ok=true means the runner executed — see 'success' for pass/fail.\n- **lint_check**(command, cwd, timeout): Run the project's linter and return a structured clean/issue result. Auto-detects the linter (ruff/eslint/go vet/clippy) or uses a configured/explicit command; executes via the governed shell. ok=true means the linter ran — see 'clean'.\n- **file_list**(path, pattern, depth): List files and directories at a given path. Use depth=1 or depth=2 to get a recursive tree in one call instead of listing each sub-directory separately.\n- **file_read**(path, max_lines, start_line, max_chars, mode): Read text file content with adaptive context governance. For large or unfamiliar files, prefer mode='outline' or mode='symbols' first, then use mode='raw' with start_line/max_lines for the specific range you actually need. For LeapFlow's own settings, use config_list / config_get / config_set — its config files are outside the workspace and not readable here.\n- **file_write**(path, content, mode) [capability_expand category: write]: Write content to a file (overwrite or append).\n- **code_search**(pattern, patterns, path, glob, ignore_case, multiline, max_results, context_lines): Search file CONTENTS by regex pattern across a directory tree (ripgrep-backed). Requires a regex pattern. NOT for listing or browsing directory contents — use file_list for that. Prefer this over shell_run grep: faster, skips VCS/dependency/build dirs, and returns structured path:line:column matches. Batch related lookups into ONE call via `patterns` (OR-combined, single pass) instead of issuing several separate searches. Use file_read for the surrounding context of a hit.\n- **file_find**(glob, path, max_results): Find files by a recursive glob pattern under a base path (e.g. '**/test_*.py' or '*.md'). Prefer this over shell_run find; skips VCS/dependency/build dirs.\n- **edit_file**(path, edits, dry_run, diff) [capability_expand category: write]: Apply targeted, anchored search-replace edits to an EXISTING text file (use file_write to create/overwrite). Each edit is {original_text, new_text, replace_all?}; original_text must match exactly and uniquely (or set replace_all) — a non-unique or missing anchor is rejected so files are never corrupted. Set dry_run to preview. Alternatively pass a unified 'diff' to apply its hunks as anchored edits. Far cheaper and safer than rewriting a whole file.\n- **shell_run**(command, cwd, timeout) [capability_expand category: shell]: Execute a one-shot shell command with timeout protection. Runs in the active workspace; paths resolving outside it are refused. Reach for a structured tool first when one fits — web_fetch for anything over HTTP(S), git_query/scm_sync for git, code_search/file_find/file_read for the repo, config_get/config_set for LeapFlow's own settings — because those report typed results, while a failed shell command can only be diagnosed from its exit code and stderr. Every shell run counts as an external side effect, so a failure stops the rest of the batch and is not retried automatically.\n- **terminal_open**(command, cwd, shell) [capability_expand category: terminal]: Open a PERSISTENT shell session (REPL/dev server/watch), returning a session_id for terminal_send/read/close. Disabled unless tools.terminal_session_enabled is set. For one-shot commands use shell_run instead.\n- **terminal_send**(session_id, input, wait) [capability_expand category: terminal]: Send a line of input to a persistent terminal session and return output captured shortly after.\n- **terminal_read**(session_id, wait): Drain buffered output from a persistent terminal session (optionally waiting briefly first).\n- **terminal_close**(session_id) [capability_expand category: terminal]: Terminate a persistent terminal session and release its process group.\n- **terminal_list**(): List active persistent terminal sessions.\n- **config_list**(category, limit): List LeapFlow's own writable settings (LLM model and endpoint, daemon, memory, perception, gateway, …) with current values. Use this to discover the exact key before changing anything. Optionally narrow by `category`. This is the only correct way to inspect LeapFlow configuration — never read config files from disk.\n- **config_get**(key): Read one LeapFlow setting by key (e.g. 'llm.model', 'llm.base_url', 'daemon.log_level'), returning its current value, type, scopes, and whether a change needs a daemon restart. There is no 'llm.provider' key: provider behavior is inferred from the OpenAI-compatible 'llm.base_url'. Never read LeapFlow config files from disk — use this.\n- **config_set**(key, value, scope) [capability_expand category: config]: Change one LeapFlow setting by key, e.g. switch an OpenAI-compatible LLM with key='llm.model' and key='llm.base_url'. There is no 'llm.provider' key: provider behavior is inferred from the endpoint. Values are validated and coerced; credentials are stored in the vault automatically. Call config_list or config_get first if unsure of the key. The result states whether a `leap daemon restart` is required. Never edit LeapFlow config files directly.\n- **web_fetch**(url, select, timeout, max_bytes): Read a URL over HTTP(S) and get back extracted, context-sized content: parsed JSON for API endpoints, readable text plus links for web pages. Use this for anything on the internet — prices, docs, releases, articles — instead of running curl through shell_run: it reports real HTTP status codes, retries rate limits on its own, and is a plain read so a retry is always safe. For JSON APIs pass `select` with a dotted path (e.g. 'chart.result.0.meta') to return just that part instead of the whole payload.\n- **memory_search**(query, limit): Search agent memory for relevant past experiences, observations, and facts.\n- **memory_add**(content, kind) [capability_expand category: write]: Store a new observation or insight in memory for future reference.\n- **research_note**(kind, text): Record a compact, structured note about the current task's state so it survives context compression on long / multi-step tasks. Use for durable findings, open questions still to resolve, decisions / excluded paths, and the immediate next step. One concise sentence per note.\n- **capability_expand**(category): Fetch the full callable schema for every tool in a capability category (e.g. 'hub', 'gateway', 'desktop', 'delegate', 'file', 'memory', 'skill'). The compact tool index always lists every registered tool by name and a one-line summary, but only a static low-risk subset is directly callable each turn. If you need a tool from the index that is not yet callable, call capability_expand with its category first; the matching tools become callable in this turn. Never invent a tool name — expand the category instead.\n- **delegate_task**(goal, context) [capability_expand category: delegate]: Delegate a complex sub-task to an isolated subagent. The subagent gets a fresh context and restricted tool access. Use when a task is self-contained and can be solved independently.\n- **schedule_reentry**(kind, reason, delay_seconds, event_match, max_reentries, deadline_seconds): Register a re-entry so this task can resume later from its current orientation (findings / open questions / next step). Use when work must pause and continue after a delay (kind=time) or when a matching platform event arrives (kind=event), instead of finishing now. The research-ledger state is carried over automatically.\n- **hub_push**(skill_name, visibility, version) [capability_expand category: hub]: Push a local skill to the ModelScope Hub for sharing or backup.\n- **hub_pull**(repo_id, version) [capability_expand category: hub]: Pull a skill from the ModelScope Hub to install locally.\n- **hub_search**(query) [capability_expand category: hub]: Search for skills on the Hub by keyword or description.\n- **hub_sync**(mode, dry_run) [capability_expand category: hub]: Preview or execute sync between local skills and Hub.\n- **platform_action**(platform, action, payload, backend_kind) [capability_expand category: gateway]: Execute an exact registered business action on an external platform through LeapFlow's App Connector layer. Actions must be copied from the App Connector Capability Index and are addressed as domain.operation, e.g. im.send_message or docs.create_markdown. All business fields (chat_id, text, query, etc.) MUST be placed inside `payload`, never at the top level. Example: {\"platform\":\"feishu\",\"action\":\"im.send_message\",\"payload\":{\"chat_id\":\"oc_xxx\",\"text\":\"hello\"}}. Do not invent action names, do not use management actions such as list/guide/connect/status here.\n- **platform_connect**(action, platform, credentials, options, checkpoint) [capability_expand category: gateway]: List, guide, connect, disconnect, remove, or check status for external platforms using the App Connector management namespace. Supports REST and CLI backends. Use this for management actions such as list/guide/preflight/connect/status; use platform_action only for exact registered business actions.\n- **gateway_send**(platform, chat_id, text, thread_id) [capability_expand category: gateway]: Send a message to a connected external platform (Feishu group, Telegram chat, DingTalk conversation, etc.). Requires the platform to be connected via gateway_connect first. Use gateway_connect with action='list' to see connected platforms and available chat IDs.\n- **gateway_connect**(action, platform, credentials, options) [capability_expand category: gateway]: Connect, configure, or manage external platform integrations (Feishu, DingTalk, Telegram, Slack, Discord, etc.). Conversational flow: 1) call 'guide' to get setup steps + required fields, 2) present the steps to the user and ask for ALL required credentials in a single message, 3) call 'connect' with the credentials. Goal: complete in 1–2 user turns. NEVER include credential values in your text response.\n- **plugin_list**(): List the live plugin registry across Tool/Gateway/LLM subsystems for plugin management: which plugins are loaded, their category and fiber lifecycle state. For questions about LeapFlow's own capabilities or whether it supports X, use self_describe instead.\n- **plugin_status**(plugin_id): Get detailed status of a specific plugin: its declared category, runtime dependencies, contributed tools, and fiber lifecycle state.\n- **plugin_versions**(plugin_id): List recorded source versions and active pointer for a profile-scoped plugin.\n- **plugin_propose**(requested_capability, plugin_id, proposed_tools, test_cases, risk_level, evidence): Create a side-effect-free PluginProposal from explicit capability-gap evidence. Use this before plugin_generate when a missing capability should be reviewed. Does not call an LLM, write files, or install anything.\n- **assess_compatibility**(manifest, source_path) [capability_expand category: plugin_management]: Assess whether a foreign plugin manifest or real DSH source bundle is compatible with LeapFlow. A source bundle assessment is static: runtime_ready stays false until restricted Node discovery during plugin_install. Returns component-level verdicts and limitations.\n- **plugin_generate**(plugin_id, description, proposal_id) [capability_expand category: system]: Generate a new ToolPlugin from a natural-language capability description. The LLM produces code that conforms to the ToolPlugin Protocol; it is then rigorously validated (syntax, structure, import, protocol conformance). The generated source is stored in CAS and receives explicit content approval. It DOES NOT install the plugin — installation requires a second, mutation-specific approval via plugin_install.\n- **plugin_install**(plugin_id, code, marketplace_name, source_path, proposal_id, version_label) [capability_expand category: system]: Install a plugin from exactly one source: validated Python code, the configured Python marketplace, or a real DSH source bundle. DSH bundles run restricted Node discovery before registration; only public host tools are installed and client UI limitations are reported. Writes profile-scoped state and mutates the process-global registry. REQUIRES APPROVAL.\n- **plugin_rollback**(plugin_id, version) [capability_expand category: system]: Rollback a profile-scoped plugin to a recorded source version and reload it. REQUIRES APPROVAL.\n- **plugin_reload**(plugin_id, version_label) [capability_expand category: system]: Hot-reload a plugin at runtime. Disposes the old plugin fiber, re-imports its module, and registers a fresh instance. Existing in-flight turns are unaffected (snapshot isolation). REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_disable**(plugin_id) [capability_expand category: system]: Disable a plugin by disposing its fiber, removing its tools from the runtime registry. Cannot disable self_management itself. REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_remove**(plugin_id, delete_source) [capability_expand category: system]: Terminally remove a plugin: dispose its fiber, unregister its tools, remove reload metadata, and optionally delete its profile-scoped source file. Cannot remove self_management itself. REQUIRES APPROVAL.\n- **plugin_unquarantine**(plugin_id) [capability_expand category: plugin_management]: Restore a quarantined plugin to probation status for re-evaluation. Unfreezes the trust ledger, transitions the proposal back to PROBATION, and reloads the plugin. REQUIRES APPROVAL.\n- **plugin_enable**(plugin_id) [capability_expand category: system]: Re-enable a previously disabled plugin by reloading its module and registering a fresh instance. REQUIRES APPROVAL.\n- **self_describe**(facet): Answer questions about LeapFlow ITSELF from live runtime evidence — its identity/version/model, what tools and plugins it has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of self-awareness. Prefer this over reading LeapFlow's own source code. Pick facet=capabilities for 'what can you do / do you support X', identity for version/model, runtime for context/posture, or all for a full self-check when explicitly requested.\n- **runtime_snapshot**(): Lightweight (~150 token) flat snapshot of current runtime state: model, context budget, posture, disclosure level, turn count, cache hit rate, uptime, tool count, and pending approvals.\n- **schedule_create**(trigger_expression, instruction, execution_mode, max_retries, delivery_target) [capability_expand category: scheduler]: Create a new scheduled task. Specify a trigger expression (e.g. '30m', 'every 2h', '0 9 * * *') and the instruction to execute on each trigger. Optionally provide an execution mode and a delivery target to receive notifications.\n- **schedule_list**() [capability_expand category: scheduler]: List all scheduled tasks with their current state, trigger, and next due time.\n- **schedule_status**(task_id) [capability_expand category: scheduler]: Get detailed status and recent execution history for a scheduled task.\n- **schedule_pause**(task_id) [capability_expand category: scheduler]: Pause a scheduled task so it stops firing without being cancelled.\n- **schedule_resume**(task_id) [capability_expand category: scheduler]: Resume a paused scheduled task — re-arms it and recalculates next due time.\n- **schedule_cancel**(task_id) [capability_expand category: scheduler]: Cancel a scheduled task permanently.\n- **tool_search**(query, max_results): Search all registered tools by keyword relevance. Returns a ranked list of matching tools with name, category, and summary. Use this when you need a tool but are unsure of its exact name or category.\n- **tool_describe**(tool_name): Get the full callable schema of a single tool by exact name. Returns the tool's description, parameters, and metadata. Use after tool_search to inspect a specific tool before calling it.\n- **hw_list**(): List connected hardware devices with their channel counts and measured quantities. Start here; it does not include operating limits.\n- **hw_describe**(device_id): Return the full reference for one device: every channel, its unit, its operating envelope, rate limit, reversibility, and required interlocks, plus any prior outcomes recorded for its writable channels. Required before commanding a device.\n- **hw_read**(device_id, channel_id): Read the current value of one device channel. Has no physical effect.\n- **hw_status**(device_id): Report connection health, halt capability, and recent notable events (threshold excursions, lost samples, stalled channels) for one device.\n- **hw_configure**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Set a configuration value or setpoint on a channel declaring effect=configure. Setpoints often have inertia: the value may need time to stabilise.\n- **hw_actuate**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Command motion or output on a channel declaring effect=actuate. This moves physical hardware; a repeat from an unknown state is not a safe retry.\n- **hw_dispense**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Consume an irreversible resource on a channel declaring effect=dispense. Running this twice dispenses twice; never repeat it after a failure without first verifying what already happened.\n- **hw_estop**(device_id) [capability_expand category: hardware]: Stop all motion and output on a device immediately. Never requires approval; use it whenever device behaviour is unexpected.\n- **hw_batch_actuate**(device_id, commands, dry_run, verify) [capability_expand category: hardware]: Write multiple actuator channels in one atomic transaction. Used for coordinated multi-joint control where all positions must be commanded simultaneously (e.g. robot arm joint-space control).\n- **session_search**(query, limit) [capability_expand category: unclassified]: Search past conversation sessions in the current workspace for relevant context.\n- **session_list**(limit): List recent conversation sessions in the current workspace with stable ids, titles, dates, and summaries. Use for browsing past tasks or when user asks to see history without specific search terms. No keywords needed — returns chronological list.\n- **session_detail**(session_id, limit, offset, include_inactive) [capability_expand category: unclassified]: Read a paginated persisted transcript for one past conversation session in the current workspace. Use after session_list or session_search returns a session_id.\n- **click**(element_index) [capability_expand category: desktop]: Click a UI element by its element_index (from the latest observe_ui snapshot)\n- **get_clipboard**() [capability_expand category: desktop]: Read current clipboard text content\n- **list_apps**(filter, running_only) [capability_expand category: desktop]: List available applications on this system. Use to discover correct bundle_id before switch_app.\n- **list_windows**() [capability_expand category: desktop]: List all top-level windows with pid, window_id, title, and per-window state (minimized, on-screen). Call this first to pick the pid and window_id that observe_ui and other window tools require.\n- **observe_ui**(pid, window_id, query) [capability_expand category: desktop]: Snapshot one window's actionable UI elements, each tagged with an element_index for click/right_click/read_text. Re-observe after actions — indices belong to one snapshot. Requires the window's pid and window_id from list_windows.\n- **open_url**(url, app_id) [capability_expand category: desktop]: Open a URL in the default or specified browser\n- **read_text**(element_index) [capability_expand category: desktop]: Read the text content of a specific UI element from the latest snapshot\n- **right_click**(element_index) [capability_expand category: desktop]: Right-click a UI element to open its context menu. Returns visible menu items.\n- **screenshot**(pid, window_id) [capability_expand category: desktop]: Capture a screenshot for visual verification. With pid + window_id captures that window (works across all displays); defaults to the last observed window, or the full desktop when no window has been observed.\n- **scroll**(element_index, direction, amount, pid, window_id) [capability_expand category: desktop]: Scroll a scrollable area of a window. Omit element_index to scroll the window's focused/page scroller; pass one to scroll an exact element from the latest snapshot.\n- **select_text**(element_index) [capability_expand category: desktop]: Select all text in a UI element (focus + select-all, for subsequent copy)\n- **set_clipboard**(text) [capability_expand category: desktop]: Write text to the clipboard\n- **shortcut**(keys) [capability_expand category: desktop]: Execute a keyboard shortcut\n- **switch_app**(app_id) [capability_expand category: desktop]: Switch to an app (launch if needed, activate, verify)\n- **type_text**(text) [capability_expand category: desktop]: Type text into the currently focused element\n- **wait**(seconds) [capability_expand category: desktop]: Wait for a specified duration before continuing\n- **wait_until**(condition, pid, window_id, timeout, poll_interval) [capability_expand category: desktop]: Wait until a UI condition is met (polls UI tree). Returns elements when found or on timeout.\n- **wait_until_stable**(timeout, poll_interval, pid, window_id) [capability_expand category: desktop]: Wait until the UI stops changing (element set stabilizes across polls).\n\n## App Connector Capability Index\nLeapFlow can onboard and manage external apps through `platform_connect` and execute exact registered business actions through `platform_action`.\nFor requests about connecting, setting up, configuring, enabling, or managing a supported app, use `platform_connect` first instead of generating SDK/Webhook sample code.\n`platform_connect.action` is the management namespace: list, guide, preflight, connect, disconnect, remove, status, events_start, events_stop, events_status.\n`platform_action.action` is only for exact registered platform business actions listed below, such as `im.send_message`; never use management actions like `list` or `guide` there.\nAll business fields MUST go inside `payload`; top-level keys are only `platform`, `action`, and `payload`.\nDo not invent platform IDs or platform action names. If the needed action is not listed, ask for discovery/clarification instead of guessing.\nUse `platform_connect` with `action='guide'` and the matching `platform` to start onboarding; use `action='list'` when the app is unclear.\nWhen a pending onboarding state is present, continue from that state with `platform_connect` instead of asking the user to restate the app.\nSupported apps:\n- `api_server`: API Server (OpenAI Compatible) (category=api; backend=adapter; platform_action actions=none registered)\n- `dingtalk`: 钉钉 (DingTalk) (category=im; backend=adapter; platform_action actions=none registered)\n- `feishu`: 飞书 (Feishu/Lark) (category=im; backend=cli)\n - `calendar.create_event` payload={summary*, start_time*, end_time*, attendees} [write/high]\n - `docs.create_markdown` payload={title*, markdown*, folder_token} [write/medium]\n - `drive.search` payload={query*, limit} [read/medium]\n - `im.add_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.download_resource` payload={message_id*, file_key*, type*} [read/low]\n - `im.get_messages` payload={message_ids*} [read/low]\n - `im.list_chats` payload={page_size} [read/low]\n - `im.list_messages` payload={chat_id*} [read/low]\n - `im.list_thread_messages` payload={thread*} [read/low]\n - `im.remove_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.reply_message` payload={message_id*, text*} [send/high]\n - `im.search_chats` payload={query*, page_size} [read/low]\n - `im.search_messages` payload={query*} [read/medium]\n - `im.send_message` payload={chat_id*, text*, thread_id} [send/high]\n - `im.update_card` payload={token*, card*} [write/medium]\n - `im.update_message` payload={message_id*, text*} [write/medium]\n - `mail.search_unread` payload={query, limit} [read/high]\n - `sheets.append_row` payload={spreadsheet_token*, sheet_id*, values*} [write/medium]\n - `task.create` payload={title*, description, due_time} [write/medium]\n- `telegram`: Telegram (category=social; backend=adapter; platform_action actions=none registered)\n- `webhook`: Webhook (Generic) (category=webhook; backend=adapter; platform_action actions=none registered)\n\n## Learned Skills\nYou have access to the following learned skills. Use `skills_list` to browse or `skill_view` to read details:\n- api_designer: REST and GraphQL API design with OpenAPI spec generation and best practices [API, REST, GraphQL]\n- code_review: Structured code review for security, performance, and style [code, review, security]\n- data_analysis: CSV/JSON data analysis with statistical summaries, quality checks, and visualiza [data, analysis, statistics]\n- devops_helper: CI/CD pipeline configuration, Docker orchestration, and deployment strategy [devops, CI/CD, Docker]\n- document_writer: Structured document generation: technical docs, API references, reports, and REA [documentation, technical-writing, README]\n- git_workflow: Git operation orchestration: branching, commits, conflicts, and PR workflow [git, version-control, branching]\n- i18n_helper: Internationalization workflow: string extraction, translation, locale management [i18n, internationalization, localization]\n- project_analysis: Project structure analysis, dependency audit, and tech stack identification [project, analysis, dependencies]\n- security_audit: Security vulnerability scanning strategy, threat modeling, and fix recommendatio [security, vulnerability, audit]\n- shell_automation: Natural language to shell commands with safety review and step-by-step execution [shell, terminal, automation]\n- test_writer: Generate unit and integration tests from source code with edge-case coverage [testing, unit-test, integration-test]\n- web_research: Multi-step web search with cross-verification and structured synthesis [web, search, research]\n\n## Tool Usage\nTools are normally invoked through the native function-calling mechanism, not by writing JSON in your reply\ntext. Only if the provider signals that native function calling is unavailable for this turn, fall back to a\nsingle JSON code block: `{\"name\": \"tool_name\", \"arguments\": {\"key\": \"value\"}}` — use this fallback format\nonly, never both. Only call a tool whose exact name appears in the tool index above and is currently callable\n(or reachable via `capability_expand`). Never invent, rename, alias, or guess a tool name, platform ID, or\nplatform action from argument shape or wording — if the index does not list it, it does not exist.\n`platform_connect.action` (list/guide/preflight/connect/disconnect/remove/status/events_start/events_stop/\nevents_status) is the App Connector management namespace; `platform_action.action` only accepts exact\nregistered business actions such as `im.send_message` shown in the App Connector Capability Index — never mix\nthe two namespaces. If a tool call returns an unknown/unavailable result, use the returned suggestions or\navailable names for a single retry instead of trying further variations of the same guess.\n\n**Side-effect action rule** (`platform_action` with effect=send/write/execute):\n- Call each unique action+payload **exactly once**. Never include duplicates in the same turn.\n- Once the result returns `\"completed\": true`, that action is DONE for this task. Do NOT call it again\n in any subsequent turn — immediately summarize the result for the user instead.\n- The system enforces idempotency: duplicate calls are blocked and will not execute.\n- If the user explicitly requests sending/writing multiple times, use distinct payloads per call.\n\n**Resource identifier provenance rule**:\n- NEVER fabricate, guess, or infer resource identifiers (chat_id, message_id, file_key, user_id, etc.).\n Every resource ID used in a side-effect action MUST come from a successful API response in this session.\n- If a read/list action fails (e.g. authorization error), you do NOT have valid resource IDs.\n Report the failure to the user — do NOT attempt the dependent write/send action with a guessed ID.\n- When a tool result contains `\"llm_instruction\"`, follow it exactly.\n\n## Guidelines\n1. **Direct answers first**: If you already know the answer, respond directly without tools.\n2. **Avoid redundant tool calls**: Do not call the same tool with the same arguments more than once in the same user turn. When an existing tool result already answers the user's request, stop calling tools and answer directly.\n3. **Use tools proactively**: When the user asks about files, time, system state, or needs actions performed, use the appropriate tool.\n4. **Chain tools when needed**: You can call multiple tools in sequence (e.g., list files → read file → summarize).\n5. **Handle failures gracefully**: If a tool fails, explain what went wrong and suggest alternatives. If it failed because of an unknown tool/platform/action name, retry once with an exact name from the returned suggestions, then explain rather than keep guessing.\n6. **Summarize results naturally**: After tool execution, synthesize the results into a helpful answer rather than dumping raw output.\n7. **Stay conversational**: Maintain a natural, helpful tone. Acknowledge context from earlier in the conversation.\n8. **Recall past work**:\n - Broad queries (\"之前做了什么\", \"列出任务\"): answer from the \"Recent Task History\" section already in your context. If insufficient, call session_list.\n - Specific lookups (\"上次那个配置怎么改的\"): call session_search with relevant phrases (NOT single characters).\n - Do NOT call search tools repeatedly with keyword fragments. One well-phrased call is better than ten fragmented ones.\n\n## Coding & Verification\nWhen working with code, prefer the precise built-in tools over ad-hoc shell: use `repo_map` to orient in an\nunfamiliar project, `code_search`/`file_find` to locate and `code_intel` for symbols, `edit_file` (anchored\nsearch-replace, or a unified `diff`) to change files — never rewrite a whole file to change a few lines — and\n`git_query` to inspect diffs/log. After edits, check `syntax_ok` in the result and run `test_run`/`lint_check`\nbefore declaring the work done. Batch independent read-only calls (search/read/query) into a single turn — they\nrun in parallel.\n\n## Presentation Style\n1. **Polished Markdown only**: Format user-facing answers with clean Markdown headings, short paragraphs, and concise bullets. Use tables only when they improve comparison or scanning.\n2. **Terminal-friendly layout**: Keep lines readable in a TUI; avoid dense walls of text, deeply nested lists, oversized ASCII art, or heavy visual blocks.\n3. **Elegant emphasis**: Use bold text sparingly for key terms and conclusions. Avoid excessive emojis, decorative symbols, repeated separators, or visual noise.\n4. **Theme-safe colors**: Do not emit ANSI escape codes, HTML color tags, Rich markup, or hardcoded color names. Rely on the TUI theme to render Markdown professionally.\n5. **No leaked tool protocol**: Never show tool-call JSON, internal schemas, raw observations, tool result payloads, or hidden reasoning in the final answer unless the user explicitly asks for raw/debug output. Treat any prior `{\"name\": ..., \"arguments\": ...}` blocks and `Tool result (...)` messages as internal execution context only.\n6. **Professional closure**: End with a concise conclusion or next step when helpful; avoid rambling after the useful answer is complete.\n\nWhen finished with all tool calls, respond normally without a JSON block, tool-call transcript, or process log.\n\n## Task Contract\n- Task ID: turn-6\n- Original user request: Actuate the bench_r8 setpoint to 65.\n- Workspace root: \n- Allowed roots: \n- Treat relative project paths as relative to the workspace root; never infer `.` as the project root when a workspace root is provided.\n- Workspace boundary is enforced by tools: do not read, search, edit, or run commands against paths outside the allowed roots unless the user explicitly requests an external path and the tool/approval policy permits it.\n- LeapFlow workspace config is optional at `/.leapflow/config.yaml`; runtime config is loaded from `~/.leapflow/config/user.yaml` and `~/.leapflow/profiles//config/*.yaml`.\n- Preserve this task contract across summarization, compression, tool loops, and memory retrieval.\n" - }, - { - "role": "assistant", - "content": "[Called: hw_configure]\nHomed the device; the readiness gate is now satisfied." - }, - { - "role": "user", - "content": "Preview an actuate of bench_r8 setpoint to 55, dry run only." - }, - { - "role": "assistant", - "content": "[Called: hw_actuate]\nDry run confirmed the setpoint command is feasible after homing." - }, - { - "role": "user", - "content": "Actuate the bench_r8 setpoint to 65.\nActuate the bench_r8 setpoint to 65." - }, - { - "role": "assistant", - "content": "", - "tool_calls": [ - "hw_actuate" - ] - }, - { - "role": "tool", - "content": "{\"ok\": true, \"device_id\": \"bench_r8\", \"channel_id\": \"setpoint\", \"side_effect_state\": \"committed\", \"settled\": true, \"execution_id\": \"\", \"idempotency_key\": \"\", \"execution_policy\": \"external_side_effect\", \"tool_call_id\": \"call_1\", \"execution_status\": \"completed\"}", - "tool_result": true - } - ], - "tools": [ - "capability_expand", - "code_intel", - "code_search", - "config_get", - "config_list", - "env_info", - "file_find", - "file_list", - "file_read", - "git_query", - "hw_actuate", - "hw_batch_actuate", - "hw_configure", - "hw_describe", - "hw_dispense", - "hw_estop", - "hw_list", - "hw_read", - "hw_status", - "lint_check", - "memory_search", - "plugin_list", - "plugin_propose", - "plugin_status", - "plugin_versions", - "repo_map", - "research_note", - "runtime_snapshot", - "schedule_reentry", - "self_describe", - "session_list", - "skill_view", - "skills_list", - "terminal_list", - "terminal_read", - "test_run", - "text_search", - "time_get", - "tool_describe", - "tool_search", - "web_fetch" - ] - }, - "responses": [ - { - "status": 200, - "content_type": "application/json", - "body": "{\"id\": \"chatcmpl-cassette\", \"object\": \"chat.completion\", \"model\": \"cassette-model\", \"choices\": [{\"index\": 0, \"message\": {\"role\": \"assistant\", \"content\": \"Commanded the setpoint through the approval path.\"}, \"finish_reason\": \"stop\"}], \"usage\": {\"prompt_tokens\": 64, \"completion_tokens\": 16, \"total_tokens\": 80}}" - } - ] -} diff --git a/tests/_fixtures/cassettes/r8_hardware/cassette-model-e823daf134b9698b.cassette.json b/tests/_fixtures/cassettes/r8_hardware/cassette-model-a9327c5e6c059ccb.cassette.json similarity index 60% rename from tests/_fixtures/cassettes/r8_hardware/cassette-model-e823daf134b9698b.cassette.json rename to tests/_fixtures/cassettes/r8_hardware/cassette-model-a9327c5e6c059ccb.cassette.json index 196b3f2b..883904e4 100644 --- a/tests/_fixtures/cassettes/r8_hardware/cassette-model-e823daf134b9698b.cassette.json +++ b/tests/_fixtures/cassettes/r8_hardware/cassette-model-a9327c5e6c059ccb.cassette.json @@ -1,5 +1,5 @@ { - "fingerprint": "e823daf134b9698b325fafde43f74eb310472bca26296de79858b28c19ce5382", + "fingerprint": "a9327c5e6c059ccb4eaac745b5fb53e6cfeef7ffb678b768b4ea6817ee42625d", "note": "captured in seed mode", "request": { "model": "cassette-model", @@ -7,7 +7,7 @@ "messages": [ { "role": "system", - "content": "You are LeapFlow, an intelligent assistant that can both converse naturally and take real actions on the user's computer.\n\n## Capabilities\nThe tool index below lists **every** registered tool by name and a one-line summary — this is the complete\ncapability contract; nothing else exists. Only a subset is directly callable this turn (via native tool calling,\nnot a JSON block in your reply). If you need a tool from the index that is not yet callable, call\n`capability_expand` with its category name first — the matching tools become callable immediately after.\nWhen the user asks about LeapFlow **itself** — its identity/version/model, what tools, plugins, or capabilities\nit has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of\nself-awareness — answer from live runtime evidence via `self_describe` (facet=capabilities for \"what can you\ndo / do you support X\", identity for version/model, runtime for context/posture, or `all` for a full self-check),\nnever by reading LeapFlow's own source code, documentation, or memory. Report configuration-dependent or\nunavailable capabilities as limitations rather than inferring them.\n- **text_search**(text, pattern): Search for a regex pattern in text.\n- **text_replace**(text, old, new, count) [capability_expand category: write]: Replace occurrences of a substring in text.\n- **time_get**(): Get current date and time.\n- **env_info**(): Get system environment information (OS, Python version, cwd).\n- **skills_list**(query, category, source): List available learned skills. Use when user asks about capabilities or you need a specific skill.\n- **skill_view**(name): View the full content of a specific skill document.\n- **skill_list_files**(name) [capability_expand category: skills]: List all support files in a skill directory (configs, templates, examples). Use before skill_file_read to discover available files.\n- **skill_file_read**(name, file_path) [capability_expand category: skills]: Read an individual support file from a skill directory. Use skill_list_files first to discover available files.\n- **code_intel**(path, operation): Precise document symbols (outline) for a source file: classes, functions, and methods with line ranges. Python uses an exact AST parse; other languages use a keyword-prefix scan. Prefer over file_read mode=symbols for accurate navigation before editing. Read-only.\n- **repo_map**(path): Compact project orientation for a repository root: languages, detected test/lint commands, top-level structure, entry points, manifest, and VCS branch. Call this first when entering an unfamiliar codebase. Read-only.\n- **scm_sync**(action, cwd, remote, pull_ref, push_ref, timeout) [capability_expand category: scm]: Run a typed git SCM action. Use this instead of shell_run for git pull/push/status. For 'pull origin main then push', set action='pull_then_push', remote='origin', pull_ref='main', and omit push_ref so LeapFlow pushes the current local branch.\n- **git_query**(action, cwd, ref, path, staged, max_count, stat): Read-only structured git inspection: action=diff|log|status|branch|show. Prefer over shell_run for reading repo state — output is clipped, redacted, and log/branch are parsed into structured fields. Use scm_sync for pull/push.\n- **git_write**(action, cwd, message, stage_all, name, ref, create) [capability_expand category: scm]: Mutating git actions: action=commit (message, stage_all), branch (create+switch), checkout (switch; create=true for -b). Approval-gated. Use scm_sync for pull/push and git_query for reads.\n- **test_run**(command, cwd, timeout): Run the project's test suite and return structured results (framework, passed/failed counts, failing tests). Auto-detects the runner (pytest/npm/go/cargo) or uses a configured/explicit command; executes via the governed shell. ok=true means the runner executed — see 'success' for pass/fail.\n- **lint_check**(command, cwd, timeout): Run the project's linter and return a structured clean/issue result. Auto-detects the linter (ruff/eslint/go vet/clippy) or uses a configured/explicit command; executes via the governed shell. ok=true means the linter ran — see 'clean'.\n- **file_list**(path, pattern, depth): List files and directories at a given path. Use depth=1 or depth=2 to get a recursive tree in one call instead of listing each sub-directory separately.\n- **file_read**(path, max_lines, start_line, max_chars, mode): Read text file content with adaptive context governance. For large or unfamiliar files, prefer mode='outline' or mode='symbols' first, then use mode='raw' with start_line/max_lines for the specific range you actually need. For LeapFlow's own settings, use config_list / config_get / config_set — its config files are outside the workspace and not readable here.\n- **file_write**(path, content, mode) [capability_expand category: write]: Write content to a file (overwrite or append).\n- **code_search**(pattern, patterns, path, glob, ignore_case, multiline, max_results, context_lines): Search file CONTENTS by regex pattern across a directory tree (ripgrep-backed). Requires a regex pattern. NOT for listing or browsing directory contents — use file_list for that. Prefer this over shell_run grep: faster, skips VCS/dependency/build dirs, and returns structured path:line:column matches. Batch related lookups into ONE call via `patterns` (OR-combined, single pass) instead of issuing several separate searches. Use file_read for the surrounding context of a hit.\n- **file_find**(glob, path, max_results): Find files by a recursive glob pattern under a base path (e.g. '**/test_*.py' or '*.md'). Prefer this over shell_run find; skips VCS/dependency/build dirs.\n- **edit_file**(path, edits, dry_run, diff) [capability_expand category: write]: Apply targeted, anchored search-replace edits to an EXISTING text file (use file_write to create/overwrite). Each edit is {original_text, new_text, replace_all?}; original_text must match exactly and uniquely (or set replace_all) — a non-unique or missing anchor is rejected so files are never corrupted. Set dry_run to preview. Alternatively pass a unified 'diff' to apply its hunks as anchored edits. Far cheaper and safer than rewriting a whole file.\n- **shell_run**(command, cwd, timeout) [capability_expand category: shell]: Execute a one-shot shell command with timeout protection. Runs in the active workspace; paths resolving outside it are refused. Reach for a structured tool first when one fits — web_fetch for anything over HTTP(S), git_query/scm_sync for git, code_search/file_find/file_read for the repo, config_get/config_set for LeapFlow's own settings — because those report typed results, while a failed shell command can only be diagnosed from its exit code and stderr. Every shell run counts as an external side effect, so a failure stops the rest of the batch and is not retried automatically.\n- **terminal_open**(command, cwd, shell) [capability_expand category: terminal]: Open a PERSISTENT shell session (REPL/dev server/watch), returning a session_id for terminal_send/read/close. Disabled unless tools.terminal_session_enabled is set. For one-shot commands use shell_run instead.\n- **terminal_send**(session_id, input, wait) [capability_expand category: terminal]: Send a line of input to a persistent terminal session and return output captured shortly after.\n- **terminal_read**(session_id, wait): Drain buffered output from a persistent terminal session (optionally waiting briefly first).\n- **terminal_close**(session_id) [capability_expand category: terminal]: Terminate a persistent terminal session and release its process group.\n- **terminal_list**(): List active persistent terminal sessions.\n- **config_list**(category, limit): List LeapFlow's own writable settings (LLM model and endpoint, daemon, memory, perception, gateway, …) with current values. Use this to discover the exact key before changing anything. Optionally narrow by `category`. This is the only correct way to inspect LeapFlow configuration — never read config files from disk.\n- **config_get**(key): Read one LeapFlow setting by key (e.g. 'llm.model', 'llm.base_url', 'daemon.log_level'), returning its current value, type, scopes, and whether a change needs a daemon restart. There is no 'llm.provider' key: provider behavior is inferred from the OpenAI-compatible 'llm.base_url'. Never read LeapFlow config files from disk — use this.\n- **config_set**(key, value, scope) [capability_expand category: config]: Change one LeapFlow setting by key, e.g. switch an OpenAI-compatible LLM with key='llm.model' and key='llm.base_url'. There is no 'llm.provider' key: provider behavior is inferred from the endpoint. Values are validated and coerced; credentials are stored in the vault automatically. Call config_list or config_get first if unsure of the key. The result states whether a `leap daemon restart` is required. Never edit LeapFlow config files directly.\n- **web_fetch**(url, select, timeout, max_bytes): Read a URL over HTTP(S) and get back extracted, context-sized content: parsed JSON for API endpoints, readable text plus links for web pages. Use this for anything on the internet — prices, docs, releases, articles — instead of running curl through shell_run: it reports real HTTP status codes, retries rate limits on its own, and is a plain read so a retry is always safe. For JSON APIs pass `select` with a dotted path (e.g. 'chart.result.0.meta') to return just that part instead of the whole payload.\n- **memory_search**(query, limit): Search agent memory for relevant past experiences, observations, and facts.\n- **memory_add**(content, kind) [capability_expand category: write]: Store a new observation or insight in memory for future reference.\n- **research_note**(kind, text): Record a compact, structured note about the current task's state so it survives context compression on long / multi-step tasks. Use for durable findings, open questions still to resolve, decisions / excluded paths, and the immediate next step. One concise sentence per note.\n- **capability_expand**(category): Fetch the full callable schema for every tool in a capability category (e.g. 'hub', 'gateway', 'desktop', 'delegate', 'file', 'memory', 'skill'). The compact tool index always lists every registered tool by name and a one-line summary, but only a static low-risk subset is directly callable each turn. If you need a tool from the index that is not yet callable, call capability_expand with its category first; the matching tools become callable in this turn. Never invent a tool name — expand the category instead.\n- **delegate_task**(goal, context) [capability_expand category: delegate]: Delegate a complex sub-task to an isolated subagent. The subagent gets a fresh context and restricted tool access. Use when a task is self-contained and can be solved independently.\n- **schedule_reentry**(kind, reason, delay_seconds, event_match, max_reentries, deadline_seconds): Register a re-entry so this task can resume later from its current orientation (findings / open questions / next step). Use when work must pause and continue after a delay (kind=time) or when a matching platform event arrives (kind=event), instead of finishing now. The research-ledger state is carried over automatically.\n- **hub_push**(skill_name, visibility, version) [capability_expand category: hub]: Push a local skill to the ModelScope Hub for sharing or backup.\n- **hub_pull**(repo_id, version) [capability_expand category: hub]: Pull a skill from the ModelScope Hub to install locally.\n- **hub_search**(query) [capability_expand category: hub]: Search for skills on the Hub by keyword or description.\n- **hub_sync**(mode, dry_run) [capability_expand category: hub]: Preview or execute sync between local skills and Hub.\n- **platform_action**(platform, action, payload, backend_kind) [capability_expand category: gateway]: Execute an exact registered business action on an external platform through LeapFlow's App Connector layer. Actions must be copied from the App Connector Capability Index and are addressed as domain.operation, e.g. im.send_message or docs.create_markdown. All business fields (chat_id, text, query, etc.) MUST be placed inside `payload`, never at the top level. Example: {\"platform\":\"feishu\",\"action\":\"im.send_message\",\"payload\":{\"chat_id\":\"oc_xxx\",\"text\":\"hello\"}}. Do not invent action names, do not use management actions such as list/guide/connect/status here.\n- **platform_connect**(action, platform, credentials, options, checkpoint) [capability_expand category: gateway]: List, guide, connect, disconnect, remove, or check status for external platforms using the App Connector management namespace. Supports REST and CLI backends. Use this for management actions such as list/guide/preflight/connect/status; use platform_action only for exact registered business actions.\n- **gateway_send**(platform, chat_id, text, thread_id) [capability_expand category: gateway]: Send a message to a connected external platform (Feishu group, Telegram chat, DingTalk conversation, etc.). Requires the platform to be connected via gateway_connect first. Use gateway_connect with action='list' to see connected platforms and available chat IDs.\n- **gateway_connect**(action, platform, credentials, options) [capability_expand category: gateway]: Connect, configure, or manage external platform integrations (Feishu, DingTalk, Telegram, Slack, Discord, etc.). Conversational flow: 1) call 'guide' to get setup steps + required fields, 2) present the steps to the user and ask for ALL required credentials in a single message, 3) call 'connect' with the credentials. Goal: complete in 1–2 user turns. NEVER include credential values in your text response.\n- **plugin_list**(): List the live plugin registry across Tool/Gateway/LLM subsystems for plugin management: which plugins are loaded, their category and fiber lifecycle state. For questions about LeapFlow's own capabilities or whether it supports X, use self_describe instead.\n- **plugin_status**(plugin_id): Get detailed status of a specific plugin: its declared category, runtime dependencies, contributed tools, and fiber lifecycle state.\n- **plugin_versions**(plugin_id): List recorded source versions and active pointer for a profile-scoped plugin.\n- **plugin_propose**(requested_capability, plugin_id, proposed_tools, test_cases, risk_level, evidence): Create a side-effect-free PluginProposal from explicit capability-gap evidence. Use this before plugin_generate when a missing capability should be reviewed. Does not call an LLM, write files, or install anything.\n- **assess_compatibility**(manifest, source_path) [capability_expand category: plugin_management]: Assess whether a foreign plugin manifest or real DSH source bundle is compatible with LeapFlow. A source bundle assessment is static: runtime_ready stays false until restricted Node discovery during plugin_install. Returns component-level verdicts and limitations.\n- **plugin_generate**(plugin_id, description, proposal_id) [capability_expand category: system]: Generate a new ToolPlugin from a natural-language capability description. The LLM produces code that conforms to the ToolPlugin Protocol; it is then rigorously validated (syntax, structure, import, protocol conformance). The generated source is stored in CAS and receives explicit content approval. It DOES NOT install the plugin — installation requires a second, mutation-specific approval via plugin_install.\n- **plugin_install**(plugin_id, code, marketplace_name, source_path, proposal_id, version_label) [capability_expand category: system]: Install a plugin from exactly one source: validated Python code, the configured Python marketplace, or a real DSH source bundle. DSH bundles run restricted Node discovery before registration; only public host tools are installed and client UI limitations are reported. Writes profile-scoped state and mutates the process-global registry. REQUIRES APPROVAL.\n- **plugin_rollback**(plugin_id, version) [capability_expand category: system]: Rollback a profile-scoped plugin to a recorded source version and reload it. REQUIRES APPROVAL.\n- **plugin_reload**(plugin_id, version_label) [capability_expand category: system]: Hot-reload a plugin at runtime. Disposes the old plugin fiber, re-imports its module, and registers a fresh instance. Existing in-flight turns are unaffected (snapshot isolation). REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_disable**(plugin_id) [capability_expand category: system]: Disable a plugin by disposing its fiber, removing its tools from the runtime registry. Cannot disable self_management itself. REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_remove**(plugin_id, delete_source) [capability_expand category: system]: Terminally remove a plugin: dispose its fiber, unregister its tools, remove reload metadata, and optionally delete its profile-scoped source file. Cannot remove self_management itself. REQUIRES APPROVAL.\n- **plugin_unquarantine**(plugin_id) [capability_expand category: plugin_management]: Restore a quarantined plugin to probation status for re-evaluation. Unfreezes the trust ledger, transitions the proposal back to PROBATION, and reloads the plugin. REQUIRES APPROVAL.\n- **plugin_enable**(plugin_id) [capability_expand category: system]: Re-enable a previously disabled plugin by reloading its module and registering a fresh instance. REQUIRES APPROVAL.\n- **self_describe**(facet): Answer questions about LeapFlow ITSELF from live runtime evidence — its identity/version/model, what tools and plugins it has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of self-awareness. Prefer this over reading LeapFlow's own source code. Pick facet=capabilities for 'what can you do / do you support X', identity for version/model, runtime for context/posture, or all for a full self-check when explicitly requested.\n- **runtime_snapshot**(): Lightweight (~150 token) flat snapshot of current runtime state: model, context budget, posture, disclosure level, turn count, cache hit rate, uptime, tool count, and pending approvals.\n- **schedule_create**(trigger_expression, instruction, execution_mode, max_retries, delivery_target) [capability_expand category: scheduler]: Create a new scheduled task. Specify a trigger expression (e.g. '30m', 'every 2h', '0 9 * * *') and the instruction to execute on each trigger. Optionally provide an execution mode and a delivery target to receive notifications.\n- **schedule_list**() [capability_expand category: scheduler]: List all scheduled tasks with their current state, trigger, and next due time.\n- **schedule_status**(task_id) [capability_expand category: scheduler]: Get detailed status and recent execution history for a scheduled task.\n- **schedule_pause**(task_id) [capability_expand category: scheduler]: Pause a scheduled task so it stops firing without being cancelled.\n- **schedule_resume**(task_id) [capability_expand category: scheduler]: Resume a paused scheduled task — re-arms it and recalculates next due time.\n- **schedule_cancel**(task_id) [capability_expand category: scheduler]: Cancel a scheduled task permanently.\n- **tool_search**(query, max_results): Search all registered tools by keyword relevance. Returns a ranked list of matching tools with name, category, and summary. Use this when you need a tool but are unsure of its exact name or category.\n- **tool_describe**(tool_name): Get the full callable schema of a single tool by exact name. Returns the tool's description, parameters, and metadata. Use after tool_search to inspect a specific tool before calling it.\n- **hw_list**(): List connected hardware devices with their channel counts and measured quantities. Start here; it does not include operating limits.\n- **hw_describe**(device_id): Return the full reference for one device: every channel, its unit, its operating envelope, rate limit, reversibility, and required interlocks, plus any prior outcomes recorded for its writable channels. Required before commanding a device.\n- **hw_read**(device_id, channel_id): Read the current value of one device channel. Has no physical effect.\n- **hw_status**(device_id): Report connection health, halt capability, and recent notable events (threshold excursions, lost samples, stalled channels) for one device.\n- **hw_configure**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Set a configuration value or setpoint on a channel declaring effect=configure. Setpoints often have inertia: the value may need time to stabilise.\n- **hw_actuate**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Command motion or output on a channel declaring effect=actuate. This moves physical hardware; a repeat from an unknown state is not a safe retry.\n- **hw_dispense**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Consume an irreversible resource on a channel declaring effect=dispense. Running this twice dispenses twice; never repeat it after a failure without first verifying what already happened.\n- **hw_estop**(device_id) [capability_expand category: hardware]: Stop all motion and output on a device immediately. Never requires approval; use it whenever device behaviour is unexpected.\n- **hw_batch_actuate**(device_id, commands, dry_run, verify) [capability_expand category: hardware]: Write multiple actuator channels in one atomic transaction. Used for coordinated multi-joint control where all positions must be commanded simultaneously (e.g. robot arm joint-space control).\n- **hw_control_start**(device_id, mode, targets, frequency_hz, trajectory, policy, gains) [capability_expand category: hardware]: Start a real-time control loop on a robot device. Supports PID position tracking, impedance control, trajectory following, and neural policy chains.\n- **hw_control_stop**(device_id) [capability_expand category: hardware]: Stop the real-time control loop on a device. Gracefully halts the device and returns session statistics.\n- **hw_control_status**(device_id) [capability_expand category: hardware]: Get the status and statistics of a running control loop. Returns frequency, jitter, overruns, and policy state.\n- **hw_policy_infer**(device_id, policy, task, execute, chunk_size, verify, compute_budget) [capability_expand category: hardware]: Execute VLA policy inference on a robot device. Reads observation, runs policy, optionally writes action to actuators. Supports local (HuggingFace) and remote (PolicyServer) policies.\n- **hw_policy_episode**(action, device_id, policy, task) [capability_expand category: hardware]: Manage policy inference episode lifecycle. Start an episode to begin a task, stop to finalise and get stats.\n- **hw_policy_list**(source) [capability_expand category: hardware]: List available VLA policies and their capabilities.\n- **hw_orchestrate**(mode, operations, barrier_timeout_s, label) [capability_expand category: hardware]: Execute coordinated operations across multiple hardware devices. Supports sequential (one-at-a-time), parallel (concurrent), and barrier (synchronised) execution modes. Any failure in a parallel group triggers halt_all on every participating device for safety.\n- **session_search**(query, limit) [capability_expand category: unclassified]: Search past conversation sessions in the current workspace for relevant context.\n- **session_list**(limit): List recent conversation sessions in the current workspace with stable ids, titles, dates, and summaries. Use for browsing past tasks or when user asks to see history without specific search terms. No keywords needed — returns chronological list.\n- **session_detail**(session_id, limit, offset, include_inactive) [capability_expand category: unclassified]: Read a paginated persisted transcript for one past conversation session in the current workspace. Use after session_list or session_search returns a session_id.\n- **click**(element_index) [capability_expand category: desktop]: Click a UI element by its element_index (from the latest observe_ui snapshot)\n- **get_clipboard**() [capability_expand category: desktop]: Read current clipboard text content\n- **list_apps**(filter, running_only) [capability_expand category: desktop]: List available applications on this system. Use to discover correct bundle_id before switch_app.\n- **list_windows**() [capability_expand category: desktop]: List all top-level windows with pid, window_id, title, and per-window state (minimized, on-screen). Call this first to pick the pid and window_id that observe_ui and other window tools require.\n- **observe_ui**(pid, window_id, query) [capability_expand category: desktop]: Snapshot one window's actionable UI elements, each tagged with an element_index for click/right_click/read_text. Re-observe after actions — indices belong to one snapshot. Requires the window's pid and window_id from list_windows.\n- **open_url**(url, app_id) [capability_expand category: desktop]: Open a URL in the default or specified browser\n- **read_text**(element_index) [capability_expand category: desktop]: Read the text content of a specific UI element from the latest snapshot\n- **right_click**(element_index) [capability_expand category: desktop]: Right-click a UI element to open its context menu. Returns visible menu items.\n- **screenshot**(pid, window_id) [capability_expand category: desktop]: Capture a screenshot for visual verification. With pid + window_id captures that window (works across all displays); defaults to the last observed window, or the full desktop when no window has been observed.\n- **scroll**(element_index, direction, amount, pid, window_id) [capability_expand category: desktop]: Scroll a scrollable area of a window. Omit element_index to scroll the window's focused/page scroller; pass one to scroll an exact element from the latest snapshot.\n- **select_text**(element_index) [capability_expand category: desktop]: Select all text in a UI element (focus + select-all, for subsequent copy)\n- **set_clipboard**(text) [capability_expand category: desktop]: Write text to the clipboard\n- **shortcut**(keys) [capability_expand category: desktop]: Execute a keyboard shortcut\n- **switch_app**(app_id) [capability_expand category: desktop]: Switch to an app (launch if needed, activate, verify)\n- **type_text**(text) [capability_expand category: desktop]: Type text into the currently focused element\n- **wait**(seconds) [capability_expand category: desktop]: Wait for a specified duration before continuing\n- **wait_until**(condition, pid, window_id, timeout, poll_interval) [capability_expand category: desktop]: Wait until a UI condition is met (polls UI tree). Returns elements when found or on timeout.\n- **wait_until_stable**(timeout, poll_interval, pid, window_id) [capability_expand category: desktop]: Wait until the UI stops changing (element set stabilizes across polls).\n\n## App Connector Capability Index\nLeapFlow can onboard and manage external apps through `platform_connect` and execute exact registered business actions through `platform_action`.\nFor requests about connecting, setting up, configuring, enabling, or managing a supported app, use `platform_connect` first instead of generating SDK/Webhook sample code.\n`platform_connect.action` is the management namespace: list, guide, preflight, connect, disconnect, remove, status, events_start, events_stop, events_status.\n`platform_action.action` is only for exact registered platform business actions listed below, such as `im.send_message`; never use management actions like `list` or `guide` there.\nAll business fields MUST go inside `payload`; top-level keys are only `platform`, `action`, and `payload`.\nDo not invent platform IDs or platform action names. If the needed action is not listed, ask for discovery/clarification instead of guessing.\nUse `platform_connect` with `action='guide'` and the matching `platform` to start onboarding; use `action='list'` when the app is unclear.\nWhen a pending onboarding state is present, continue from that state with `platform_connect` instead of asking the user to restate the app.\nSupported apps:\n- `api_server`: API Server (OpenAI Compatible) (category=api; backend=adapter; platform_action actions=none registered)\n- `dingtalk`: 钉钉 (DingTalk) (category=im; backend=adapter; platform_action actions=none registered)\n- `feishu`: 飞书 (Feishu/Lark) (category=im; backend=cli)\n - `calendar.create_event` payload={summary*, start_time*, end_time*, attendees} [write/high]\n - `docs.create_markdown` payload={title*, markdown*, folder_token} [write/medium]\n - `drive.search` payload={query*, limit} [read/medium]\n - `im.add_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.download_resource` payload={message_id*, file_key*, type*} [read/low]\n - `im.get_messages` payload={message_ids*} [read/low]\n - `im.list_chats` payload={page_size} [read/low]\n - `im.list_messages` payload={chat_id*} [read/low]\n - `im.list_thread_messages` payload={thread*} [read/low]\n - `im.remove_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.reply_message` payload={message_id*, text*} [send/high]\n - `im.search_chats` payload={query*, page_size} [read/low]\n - `im.search_messages` payload={query*} [read/medium]\n - `im.send_message` payload={chat_id*, text*, thread_id} [send/high]\n - `im.update_card` payload={token*, card*} [write/medium]\n - `im.update_message` payload={message_id*, text*} [write/medium]\n - `mail.search_unread` payload={query, limit} [read/high]\n - `sheets.append_row` payload={spreadsheet_token*, sheet_id*, values*} [write/medium]\n - `task.create` payload={title*, description, due_time} [write/medium]\n- `telegram`: Telegram (category=social; backend=adapter; platform_action actions=none registered)\n- `webhook`: Webhook (Generic) (category=webhook; backend=adapter; platform_action actions=none registered)\n\n## Learned Skills\nYou have access to the following learned skills. Use `skills_list` to browse or `skill_view` to read details:\n- api_designer: REST and GraphQL API design with OpenAPI spec generation and best practices [API, REST, GraphQL]\n- code_review: Structured code review for security, performance, and style [code, review, security]\n- data_analysis: CSV/JSON data analysis with statistical summaries, quality checks, and visualiza [data, analysis, statistics]\n- devops_helper: CI/CD pipeline configuration, Docker orchestration, and deployment strategy [devops, CI/CD, Docker]\n- document_writer: Structured document generation: technical docs, API references, reports, and REA [documentation, technical-writing, README]\n- git_workflow: Git operation orchestration: branching, commits, conflicts, and PR workflow [git, version-control, branching]\n- i18n_helper: Internationalization workflow: string extraction, translation, locale management [i18n, internationalization, localization]\n- project_analysis: Project structure analysis, dependency audit, and tech stack identification [project, analysis, dependencies]\n- security_audit: Security vulnerability scanning strategy, threat modeling, and fix recommendatio [security, vulnerability, audit]\n- shell_automation: Natural language to shell commands with safety review and step-by-step execution [shell, terminal, automation]\n- test_writer: Generate unit and integration tests from source code with edge-case coverage [testing, unit-test, integration-test]\n- web_research: Multi-step web search with cross-verification and structured synthesis [web, search, research]\n\n## Tool Usage\nTools are normally invoked through the native function-calling mechanism, not by writing JSON in your reply\ntext. Only if the provider signals that native function calling is unavailable for this turn, fall back to a\nsingle JSON code block: `{\"name\": \"tool_name\", \"arguments\": {\"key\": \"value\"}}` — use this fallback format\nonly, never both. Only call a tool whose exact name appears in the tool index above and is currently callable\n(or reachable via `capability_expand`). Never invent, rename, alias, or guess a tool name, platform ID, or\nplatform action from argument shape or wording — if the index does not list it, it does not exist.\n`platform_connect.action` (list/guide/preflight/connect/disconnect/remove/status/events_start/events_stop/\nevents_status) is the App Connector management namespace; `platform_action.action` only accepts exact\nregistered business actions such as `im.send_message` shown in the App Connector Capability Index — never mix\nthe two namespaces. If a tool call returns an unknown/unavailable result, use the returned suggestions or\navailable names for a single retry instead of trying further variations of the same guess.\n\n**Side-effect action rule** (`platform_action` with effect=send/write/execute):\n- Call each unique action+payload **exactly once**. Never include duplicates in the same turn.\n- Once the result returns `\"completed\": true`, that action is DONE for this task. Do NOT call it again\n in any subsequent turn — immediately summarize the result for the user instead.\n- The system enforces idempotency: duplicate calls are blocked and will not execute.\n- If the user explicitly requests sending/writing multiple times, use distinct payloads per call.\n\n**Resource identifier provenance rule**:\n- NEVER fabricate, guess, or infer resource identifiers (chat_id, message_id, file_key, user_id, etc.).\n Every resource ID used in a side-effect action MUST come from a successful API response in this session.\n- If a read/list action fails (e.g. authorization error), you do NOT have valid resource IDs.\n Report the failure to the user — do NOT attempt the dependent write/send action with a guessed ID.\n- When a tool result contains `\"llm_instruction\"`, follow it exactly.\n\n## Guidelines\n1. **Direct answers first**: If you already know the answer, respond directly without tools.\n2. **Avoid redundant tool calls**: Do not call the same tool with the same arguments more than once in the same user turn. When an existing tool result already answers the user's request, stop calling tools and answer directly.\n3. **Use tools proactively**: When the user asks about files, time, system state, or needs actions performed, use the appropriate tool.\n4. **Chain tools when needed**: You can call multiple tools in sequence (e.g., list files → read file → summarize).\n5. **Handle failures gracefully**: If a tool fails, explain what went wrong and suggest alternatives. If it failed because of an unknown tool/platform/action name, retry once with an exact name from the returned suggestions, then explain rather than keep guessing.\n6. **Summarize results naturally**: After tool execution, synthesize the results into a helpful answer rather than dumping raw output.\n7. **Stay conversational**: Maintain a natural, helpful tone. Acknowledge context from earlier in the conversation.\n8. **Recall past work**:\n - Broad queries (\"之前做了什么\", \"列出任务\"): answer from the \"Recent Task History\" section already in your context. If insufficient, call session_list.\n - Specific lookups (\"上次那个配置怎么改的\"): call session_search with relevant phrases (NOT single characters).\n - Do NOT call search tools repeatedly with keyword fragments. One well-phrased call is better than ten fragmented ones.\n\n## Coding & Verification\nWhen working with code, prefer the precise built-in tools over ad-hoc shell: use `repo_map` to orient in an\nunfamiliar project, `code_search`/`file_find` to locate and `code_intel` for symbols, `edit_file` (anchored\nsearch-replace, or a unified `diff`) to change files — never rewrite a whole file to change a few lines — and\n`git_query` to inspect diffs/log. After edits, check `syntax_ok` in the result and run `test_run`/`lint_check`\nbefore declaring the work done. Batch independent read-only calls (search/read/query) into a single turn — they\nrun in parallel.\n\n## Presentation Style\n1. **Polished Markdown only**: Format user-facing answers with clean Markdown headings, short paragraphs, and concise bullets. Use tables only when they improve comparison or scanning.\n2. **Terminal-friendly layout**: Keep lines readable in a TUI; avoid dense walls of text, deeply nested lists, oversized ASCII art, or heavy visual blocks.\n3. **Elegant emphasis**: Use bold text sparingly for key terms and conclusions. Avoid excessive emojis, decorative symbols, repeated separators, or visual noise.\n4. **Theme-safe colors**: Do not emit ANSI escape codes, HTML color tags, Rich markup, or hardcoded color names. Rely on the TUI theme to render Markdown professionally.\n5. **No leaked tool protocol**: Never show tool-call JSON, internal schemas, raw observations, tool result payloads, or hidden reasoning in the final answer unless the user explicitly asks for raw/debug output. Treat any prior `{\"name\": ..., \"arguments\": ...}` blocks and `Tool result (...)` messages as internal execution context only.\n6. **Professional closure**: End with a concise conclusion or next step when helpful; avoid rambling after the useful answer is complete.\n\nWhen finished with all tool calls, respond normally without a JSON block, tool-call transcript, or process log.\n\n## Task Contract\n- Task ID: turn-5\n- Original user request: Preview an actuate of bench_r8 setpoint to 55, dry run only.\n- Workspace root: \n- Allowed roots: \n- Treat relative project paths as relative to the workspace root; never infer `.` as the project root when a workspace root is provided.\n- Workspace boundary is enforced by tools: do not read, search, edit, or run commands against paths outside the allowed roots unless the user explicitly requests an external path and the tool/approval policy permits it.\n- LeapFlow workspace config is optional at `/.leapflow/config.yaml`; runtime config is loaded from `~/.leapflow/config/user.yaml` and `~/.leapflow/profiles//config/*.yaml`.\n- Preserve this task contract across summarization, compression, tool loops, and memory retrieval.\n" + "content": "You are LeapFlow, an intelligent assistant that can both converse naturally and take real actions on the user's computer.\n\n## Capabilities\nThe tool index below lists **every** registered tool by name and a one-line summary — this is the complete\ncapability contract; nothing else exists. Only a subset is directly callable this turn (via native tool calling,\nnot a JSON block in your reply). If you need a tool from the index that is not yet callable, call\n`capability_expand` with its category name first — the matching tools become callable immediately after.\nWhen the user asks about LeapFlow **itself** — its identity/version/model, what tools, plugins, or capabilities\nit has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of\nself-awareness — answer from live runtime evidence via `self_describe` (facet=capabilities for \"what can you\ndo / do you support X\", identity for version/model, runtime for context/posture, or `all` for a full self-check),\nnever by reading LeapFlow's own source code, documentation, or memory. Report configuration-dependent or\nunavailable capabilities as limitations rather than inferring them.\n- **text_search**(text, pattern): Search for a regex pattern in text.\n- **text_replace**(text, old, new, count) [capability_expand category: write]: Replace occurrences of a substring in text.\n- **time_get**(): Get current date and time.\n- **env_info**(): Get system environment information (OS, Python version, cwd).\n- **skills_list**(query, category, source): List available learned skills. Use when user asks about capabilities or you need a specific skill.\n- **skill_view**(name): View the full content of a specific skill document.\n- **skill_list_files**(name) [capability_expand category: skills]: List all support files in a skill directory (configs, templates, examples). Use before skill_file_read to discover available files.\n- **skill_file_read**(name, file_path) [capability_expand category: skills]: Read an individual support file from a skill directory. Use skill_list_files first to discover available files.\n- **code_intel**(path, operation): Precise document symbols (outline) for a source file: classes, functions, and methods with line ranges. Python uses an exact AST parse; other languages use a keyword-prefix scan. Prefer over file_read mode=symbols for accurate navigation before editing. Read-only.\n- **repo_map**(path): Compact project orientation for a repository root: languages, detected test/lint commands, top-level structure, entry points, manifest, and VCS branch. Call this first when entering an unfamiliar codebase. Read-only.\n- **scm_sync**(action, cwd, remote, pull_ref, push_ref, timeout) [capability_expand category: scm]: Run a typed git SCM action. Use this instead of shell_run for git pull/push/status. For 'pull origin main then push', set action='pull_then_push', remote='origin', pull_ref='main', and omit push_ref so LeapFlow pushes the current local branch.\n- **git_query**(action, cwd, ref, path, staged, max_count, stat): Read-only structured git inspection: action=diff|log|status|branch|show. Prefer over shell_run for reading repo state — output is clipped, redacted, and log/branch are parsed into structured fields. Use scm_sync for pull/push.\n- **git_write**(action, cwd, message, stage_all, name, ref, create) [capability_expand category: scm]: Mutating git actions: action=commit (message, stage_all), branch (create+switch), checkout (switch; create=true for -b). Approval-gated. Use scm_sync for pull/push and git_query for reads.\n- **test_run**(command, cwd, timeout): Run the project's test suite and return structured results (framework, passed/failed counts, failing tests). Auto-detects the runner (pytest/npm/go/cargo) or uses a configured/explicit command; executes via the governed shell. ok=true means the runner executed — see 'success' for pass/fail.\n- **lint_check**(command, cwd, timeout): Run the project's linter and return a structured clean/issue result. Auto-detects the linter (ruff/eslint/go vet/clippy) or uses a configured/explicit command; executes via the governed shell. ok=true means the linter ran — see 'clean'.\n- **file_list**(path, pattern, depth): List files and directories at a given path. Use depth=1 or depth=2 to get a recursive tree in one call instead of listing each sub-directory separately.\n- **file_read**(path, max_lines, start_line, max_chars, mode): Read text file content with adaptive context governance. For large or unfamiliar files, prefer mode='outline' or mode='symbols' first, then use mode='raw' with start_line/max_lines for the specific range you actually need. For LeapFlow's own settings, use config_list / config_get / config_set — its config files are outside the workspace and not readable here.\n- **file_write**(path, content, mode) [capability_expand category: write]: Write content to a file (overwrite or append).\n- **code_search**(pattern, patterns, path, glob, ignore_case, multiline, max_results, context_lines): Search file CONTENTS by regex pattern across a directory tree (ripgrep-backed). Requires a regex pattern. NOT for listing or browsing directory contents — use file_list for that. Prefer this over shell_run grep: faster, skips VCS/dependency/build dirs, and returns structured path:line:column matches. Batch related lookups into ONE call via `patterns` (OR-combined, single pass) instead of issuing several separate searches. Use file_read for the surrounding context of a hit.\n- **file_find**(glob, path, max_results): Find files by a recursive glob pattern under a base path (e.g. '**/test_*.py' or '*.md'). Prefer this over shell_run find; skips VCS/dependency/build dirs.\n- **edit_file**(path, edits, dry_run, diff) [capability_expand category: write]: Apply targeted, anchored search-replace edits to an EXISTING text file (use file_write to create/overwrite). Each edit is {original_text, new_text, replace_all?}; original_text must match exactly and uniquely (or set replace_all) — a non-unique or missing anchor is rejected so files are never corrupted. Set dry_run to preview. Alternatively pass a unified 'diff' to apply its hunks as anchored edits. Far cheaper and safer than rewriting a whole file.\n- **shell_run**(command, cwd, timeout) [capability_expand category: shell]: Execute a one-shot shell command with timeout protection. Runs in the active workspace; paths resolving outside it are refused. Reach for a structured tool first when one fits — web_fetch for anything over HTTP(S), git_query/scm_sync for git, code_search/file_find/file_read for the repo, config_get/config_set for LeapFlow's own settings — because those report typed results, while a failed shell command can only be diagnosed from its exit code and stderr. Every shell run counts as an external side effect, so a failure stops the rest of the batch and is not retried automatically.\n- **terminal_open**(command, cwd, shell) [capability_expand category: terminal]: Open a PERSISTENT shell session (REPL/dev server/watch), returning a session_id for terminal_send/read/close. Disabled unless tools.terminal_session_enabled is set. For one-shot commands use shell_run instead.\n- **terminal_send**(session_id, input, wait) [capability_expand category: terminal]: Send a line of input to a persistent terminal session and return output captured shortly after.\n- **terminal_read**(session_id, wait): Drain buffered output from a persistent terminal session (optionally waiting briefly first).\n- **terminal_close**(session_id) [capability_expand category: terminal]: Terminate a persistent terminal session and release its process group.\n- **terminal_list**(): List active persistent terminal sessions.\n- **config_list**(category, limit): List LeapFlow's own writable settings (LLM model and endpoint, daemon, memory, perception, gateway, …) with current values. Use this to discover the exact key before changing anything. Optionally narrow by `category`. This is the only correct way to inspect LeapFlow configuration — never read config files from disk.\n- **config_get**(key): Read one LeapFlow setting by key (e.g. 'llm.model', 'llm.base_url', 'daemon.log_level'), returning its current value, type, scopes, and whether a change needs a daemon restart. There is no 'llm.provider' key: provider behavior is inferred from the OpenAI-compatible 'llm.base_url'. Never read LeapFlow config files from disk — use this.\n- **config_set**(key, value, scope) [capability_expand category: config]: Change one LeapFlow setting by key, e.g. switch an OpenAI-compatible LLM with key='llm.model' and key='llm.base_url'. There is no 'llm.provider' key: provider behavior is inferred from the endpoint. Values are validated and coerced; credentials are stored in the vault automatically. Call config_list or config_get first if unsure of the key. The result states whether a `leap daemon restart` is required. Never edit LeapFlow config files directly.\n- **web_fetch**(url, select, timeout, max_bytes): Read a URL over HTTP(S) and get back extracted, context-sized content: parsed JSON for API endpoints, readable text plus links for web pages. Use this for anything on the internet — prices, docs, releases, articles — instead of running curl through shell_run: it reports real HTTP status codes, retries rate limits on its own, and is a plain read so a retry is always safe. For JSON APIs pass `select` with a dotted path (e.g. 'chart.result.0.meta') to return just that part instead of the whole payload.\n- **memory_search**(query, limit): Search agent memory for relevant past experiences, observations, and facts.\n- **memory_add**(content, kind) [capability_expand category: write]: Store a new observation or insight in memory for future reference.\n- **research_note**(kind, text): Record a compact, structured note about the current task's state so it survives context compression on long / multi-step tasks. Use for durable findings, open questions still to resolve, decisions / excluded paths, and the immediate next step. One concise sentence per note.\n- **capability_expand**(category): Fetch the full callable schema for every tool in a capability category (e.g. 'hub', 'gateway', 'desktop', 'delegate', 'file', 'memory', 'skill'). The compact tool index always lists every registered tool by name and a one-line summary, but only a static low-risk subset is directly callable each turn. If you need a tool from the index that is not yet callable, call capability_expand with its category first; the matching tools become callable in this turn. Never invent a tool name — expand the category instead.\n- **delegate_task**(goal, context) [capability_expand category: delegate]: Delegate a complex sub-task to an isolated subagent. The subagent gets a fresh context and restricted tool access. Use when a task is self-contained and can be solved independently.\n- **schedule_reentry**(kind, reason, delay_seconds, event_match, max_reentries, deadline_seconds): Register a re-entry so this task can resume later from its current orientation (findings / open questions / next step). Use when work must pause and continue after a delay (kind=time) or when a matching platform event arrives (kind=event), instead of finishing now. The research-ledger state is carried over automatically.\n- **hub_push**(skill_name, visibility, version) [capability_expand category: hub]: Push a local skill to the ModelScope Hub for sharing or backup.\n- **hub_pull**(repo_id, version) [capability_expand category: hub]: Pull a skill from the ModelScope Hub to install locally.\n- **hub_search**(query) [capability_expand category: hub]: Search for skills on the Hub by keyword or description.\n- **hub_sync**(mode, dry_run) [capability_expand category: hub]: Preview or execute sync between local skills and Hub.\n- **platform_action**(platform, action, payload, backend_kind) [capability_expand category: gateway]: Execute an exact registered business action on an external platform through LeapFlow's App Connector layer. Actions must be copied from the App Connector Capability Index and are addressed as domain.operation, e.g. im.send_message or docs.create_markdown. All business fields (chat_id, text, query, etc.) MUST be placed inside `payload`, never at the top level. Example: {\"platform\":\"feishu\",\"action\":\"im.send_message\",\"payload\":{\"chat_id\":\"oc_xxx\",\"text\":\"hello\"}}. Do not invent action names, do not use management actions such as list/guide/connect/status here.\n- **platform_connect**(action, platform, credentials, options, checkpoint) [capability_expand category: gateway]: List, guide, connect, disconnect, remove, or check status for external platforms using the App Connector management namespace. Supports REST and CLI backends. Use this for management actions such as list/guide/preflight/connect/status; use platform_action only for exact registered business actions.\n- **gateway_send**(platform, chat_id, text, thread_id) [capability_expand category: gateway]: Send a message to a connected external platform (Feishu group, Telegram chat, DingTalk conversation, etc.). Requires the platform to be connected via gateway_connect first. Use gateway_connect with action='list' to see connected platforms and available chat IDs.\n- **gateway_connect**(action, platform, credentials, options) [capability_expand category: gateway]: Connect, configure, or manage external platform integrations (Feishu, DingTalk, Telegram, Slack, Discord, etc.). Conversational flow: 1) call 'guide' to get setup steps + required fields, 2) present the steps to the user and ask for ALL required credentials in a single message, 3) call 'connect' with the credentials. Goal: complete in 1–2 user turns. NEVER include credential values in your text response.\n- **plugin_list**(): List the live plugin registry across Tool/Gateway/LLM subsystems for plugin management: which plugins are loaded, their category and fiber lifecycle state. For questions about LeapFlow's own capabilities or whether it supports X, use self_describe instead.\n- **plugin_status**(plugin_id): Get detailed status of a specific plugin: its declared category, runtime dependencies, contributed tools, and fiber lifecycle state.\n- **plugin_versions**(plugin_id): List recorded source versions and active pointer for a profile-scoped plugin.\n- **plugin_propose**(requested_capability, plugin_id, proposed_tools, test_cases, risk_level, evidence): Create a side-effect-free PluginProposal from explicit capability-gap evidence. Use this before plugin_generate when a missing capability should be reviewed. Does not call an LLM, write files, or install anything.\n- **assess_compatibility**(manifest, source_path) [capability_expand category: plugin_management]: Assess whether a foreign plugin manifest or real DSH source bundle is compatible with LeapFlow. A source bundle assessment is static: runtime_ready stays false until restricted Node discovery during plugin_install. Returns component-level verdicts and limitations.\n- **plugin_generate**(plugin_id, description, proposal_id) [capability_expand category: system]: Generate a new ToolPlugin from a natural-language capability description. The LLM produces code that conforms to the ToolPlugin Protocol; it is then rigorously validated (syntax, structure, import, protocol conformance). The generated source is stored in CAS and receives explicit content approval. It DOES NOT install the plugin — installation requires a second, mutation-specific approval via plugin_install.\n- **plugin_install**(plugin_id, code, marketplace_name, source_path, proposal_id, version_label) [capability_expand category: system]: Install a plugin from exactly one source: validated Python code, the configured Python marketplace, or a real DSH source bundle. DSH bundles run restricted Node discovery before registration; only public host tools are installed and client UI limitations are reported. Writes profile-scoped state and mutates the process-global registry. REQUIRES APPROVAL.\n- **plugin_rollback**(plugin_id, version) [capability_expand category: system]: Rollback a profile-scoped plugin to a recorded source version and reload it. REQUIRES APPROVAL.\n- **plugin_reload**(plugin_id, version_label) [capability_expand category: system]: Hot-reload a plugin at runtime. Disposes the old plugin fiber, re-imports its module, and registers a fresh instance. Existing in-flight turns are unaffected (snapshot isolation). REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_disable**(plugin_id) [capability_expand category: system]: Disable a plugin by disposing its fiber, removing its tools from the runtime registry. Cannot disable self_management itself. REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_remove**(plugin_id, delete_source) [capability_expand category: system]: Terminally remove a plugin: dispose its fiber, unregister its tools, remove reload metadata, and optionally delete its profile-scoped source file. Cannot remove self_management itself. REQUIRES APPROVAL.\n- **plugin_unquarantine**(plugin_id) [capability_expand category: plugin_management]: Restore a quarantined plugin to probation status for re-evaluation. Unfreezes the trust ledger, transitions the proposal back to PROBATION, and reloads the plugin. REQUIRES APPROVAL.\n- **plugin_enable**(plugin_id) [capability_expand category: system]: Re-enable a previously disabled plugin by reloading its module and registering a fresh instance. REQUIRES APPROVAL.\n- **self_describe**(facet): Answer questions about LeapFlow ITSELF from live runtime evidence — its identity/version/model, what tools and plugins it has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of self-awareness. Prefer this over reading LeapFlow's own source code. Pick facet=capabilities for 'what can you do / do you support X', identity for version/model, runtime for context/posture, or all for a full self-check when explicitly requested.\n- **runtime_snapshot**(): Lightweight (~150 token) flat snapshot of current runtime state: model, context budget, posture, disclosure level, turn count, cache hit rate, uptime, tool count, and pending approvals.\n- **schedule_create**(trigger_expression, instruction, execution_mode, max_retries, delivery_target) [capability_expand category: scheduler]: Create a new scheduled task. Specify a trigger expression (e.g. '30m', 'every 2h', '0 9 * * *') and the instruction to execute on each trigger. Optionally provide an execution mode and a delivery target to receive notifications.\n- **schedule_list**() [capability_expand category: scheduler]: List all scheduled tasks with their current state, trigger, and next due time.\n- **schedule_status**(task_id) [capability_expand category: scheduler]: Get detailed status and recent execution history for a scheduled task.\n- **schedule_pause**(task_id) [capability_expand category: scheduler]: Pause a scheduled task so it stops firing without being cancelled.\n- **schedule_resume**(task_id) [capability_expand category: scheduler]: Resume a paused scheduled task — re-arms it and recalculates next due time.\n- **schedule_cancel**(task_id) [capability_expand category: scheduler]: Cancel a scheduled task permanently.\n- **tool_search**(query, max_results): Search all registered tools by keyword relevance. Returns a ranked list of matching tools with name, category, and summary. Use this when you need a tool but are unsure of its exact name or category.\n- **tool_describe**(tool_name): Get the full callable schema of a single tool by exact name. Returns the tool's description, parameters, and metadata. Use after tool_search to inspect a specific tool before calling it.\n- **hw_list**(): List connected hardware devices with their channel counts and measured quantities. Start here; it does not include operating limits.\n- **hw_describe**(device_id): Return the full reference for one device: every channel, its unit, its operating envelope, rate limit, reversibility, and required interlocks, plus any prior outcomes recorded for its writable channels. Required before commanding a device.\n- **hw_read**(device_id, channel_id): Read the current value of one device channel. Has no physical effect.\n- **hw_status**(device_id): Report connection health, halt capability, and recent notable events (threshold excursions, lost samples, stalled channels) for one device.\n- **hw_configure**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Set a configuration value or setpoint on a channel declaring effect=configure. Setpoints often have inertia: the value may need time to stabilise.\n- **hw_actuate**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Command motion or output on a channel declaring effect=actuate. This moves physical hardware; a repeat from an unknown state is not a safe retry.\n- **hw_dispense**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Consume an irreversible resource on a channel declaring effect=dispense. Running this twice dispenses twice; never repeat it after a failure without first verifying what already happened.\n- **hw_estop**(device_id) [capability_expand category: hardware]: Stop all motion and output on a device immediately. Never requires approval; use it whenever device behaviour is unexpected.\n- **hw_batch_actuate**(device_id, commands, dry_run, verify) [capability_expand category: hardware]: Write multiple actuator channels in one atomic transaction. Used for coordinated multi-joint control where all positions must be commanded simultaneously (e.g. robot arm joint-space control).\n- **hw_control_start**(device_id, mode, targets, frequency_hz, trajectory, policy, gains) [capability_expand category: hardware]: Start a real-time control loop on a robot device. Supports PID position tracking, impedance control, trajectory following, and neural policy chains.\n- **hw_control_stop**(device_id) [capability_expand category: hardware]: Stop the real-time control loop on a device. Gracefully halts the device and returns session statistics.\n- **hw_control_status**(device_id) [capability_expand category: hardware]: Get the status and statistics of a running control loop. Returns frequency, jitter, overruns, and policy state.\n- **hw_context_snapshot**(level) [capability_expand category: hardware]: Get a structured snapshot of all hardware devices and their state. Use this before planning physical manipulation tasks to understand what devices are available and what they can do.\n- **hw_health**(device_id) [capability_expand category: hardware]: Check hardware device health status. Without device_id, returns fleet-wide health summary. With device_id, returns health details for that specific device.\n- **hw_policy_infer**(device_id, policy, task, execute, chunk_size, verify, compute_budget) [capability_expand category: hardware]: Execute VLA policy inference on a robot device. Reads observation, runs policy, optionally writes action to actuators. Supports local (HuggingFace) and remote (PolicyServer) policies.\n- **hw_policy_episode**(action, device_id, policy, task) [capability_expand category: hardware]: Manage policy inference episode lifecycle. Start an episode to begin a task, stop to finalise and get stats.\n- **hw_policy_list**(source) [capability_expand category: hardware]: List available VLA policies and their capabilities.\n- **hw_orchestrate**(mode, operations, barrier_timeout_s, label) [capability_expand category: hardware]: Execute coordinated operations across multiple hardware devices. Supports sequential (one-at-a-time), parallel (concurrent), and barrier (synchronised) execution modes. Any failure in a parallel group triggers halt_all on every participating device for safety.\n- **session_search**(query, limit) [capability_expand category: unclassified]: Search past conversation sessions in the current workspace for relevant context.\n- **session_list**(limit): List recent conversation sessions in the current workspace with stable ids, titles, dates, and summaries. Use for browsing past tasks or when user asks to see history without specific search terms. No keywords needed — returns chronological list.\n- **session_detail**(session_id, limit, offset, include_inactive) [capability_expand category: unclassified]: Read a paginated persisted transcript for one past conversation session in the current workspace. Use after session_list or session_search returns a session_id.\n- **click**(element_index) [capability_expand category: desktop]: Click a UI element by its element_index (from the latest observe_ui snapshot)\n- **get_clipboard**() [capability_expand category: desktop]: Read current clipboard text content\n- **list_apps**(filter, running_only) [capability_expand category: desktop]: List available applications on this system. Use to discover correct bundle_id before switch_app.\n- **list_windows**() [capability_expand category: desktop]: List all top-level windows with pid, window_id, title, and per-window state (minimized, on-screen). Call this first to pick the pid and window_id that observe_ui and other window tools require.\n- **observe_ui**(pid, window_id, query) [capability_expand category: desktop]: Snapshot one window's actionable UI elements, each tagged with an element_index for click/right_click/read_text. Re-observe after actions — indices belong to one snapshot. Requires the window's pid and window_id from list_windows.\n- **open_url**(url, app_id) [capability_expand category: desktop]: Open a URL in the default or specified browser\n- **read_text**(element_index) [capability_expand category: desktop]: Read the text content of a specific UI element from the latest snapshot\n- **right_click**(element_index) [capability_expand category: desktop]: Right-click a UI element to open its context menu. Returns visible menu items.\n- **screenshot**(pid, window_id) [capability_expand category: desktop]: Capture a screenshot for visual verification. With pid + window_id captures that window (works across all displays); defaults to the last observed window, or the full desktop when no window has been observed.\n- **scroll**(element_index, direction, amount, pid, window_id) [capability_expand category: desktop]: Scroll a scrollable area of a window. Omit element_index to scroll the window's focused/page scroller; pass one to scroll an exact element from the latest snapshot.\n- **select_text**(element_index) [capability_expand category: desktop]: Select all text in a UI element (focus + select-all, for subsequent copy)\n- **set_clipboard**(text) [capability_expand category: desktop]: Write text to the clipboard\n- **shortcut**(keys) [capability_expand category: desktop]: Execute a keyboard shortcut\n- **switch_app**(app_id) [capability_expand category: desktop]: Switch to an app (launch if needed, activate, verify)\n- **type_text**(text) [capability_expand category: desktop]: Type text into the currently focused element\n- **wait**(seconds) [capability_expand category: desktop]: Wait for a specified duration before continuing\n- **wait_until**(condition, pid, window_id, timeout, poll_interval) [capability_expand category: desktop]: Wait until a UI condition is met (polls UI tree). Returns elements when found or on timeout.\n- **wait_until_stable**(timeout, poll_interval, pid, window_id) [capability_expand category: desktop]: Wait until the UI stops changing (element set stabilizes across polls).\n\n## App Connector Capability Index\nLeapFlow can onboard and manage external apps through `platform_connect` and execute exact registered business actions through `platform_action`.\nFor requests about connecting, setting up, configuring, enabling, or managing a supported app, use `platform_connect` first instead of generating SDK/Webhook sample code.\n`platform_connect.action` is the management namespace: list, guide, preflight, connect, disconnect, remove, status, events_start, events_stop, events_status.\n`platform_action.action` is only for exact registered platform business actions listed below, such as `im.send_message`; never use management actions like `list` or `guide` there.\nAll business fields MUST go inside `payload`; top-level keys are only `platform`, `action`, and `payload`.\nDo not invent platform IDs or platform action names. If the needed action is not listed, ask for discovery/clarification instead of guessing.\nUse `platform_connect` with `action='guide'` and the matching `platform` to start onboarding; use `action='list'` when the app is unclear.\nWhen a pending onboarding state is present, continue from that state with `platform_connect` instead of asking the user to restate the app.\nSupported apps:\n- `api_server`: API Server (OpenAI Compatible) (category=api; backend=adapter; platform_action actions=none registered)\n- `dingtalk`: 钉钉 (DingTalk) (category=im; backend=adapter; platform_action actions=none registered)\n- `feishu`: 飞书 (Feishu/Lark) (category=im; backend=cli)\n - `calendar.create_event` payload={summary*, start_time*, end_time*, attendees} [write/high]\n - `docs.create_markdown` payload={title*, markdown*, folder_token} [write/medium]\n - `drive.search` payload={query*, limit} [read/medium]\n - `im.add_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.download_resource` payload={message_id*, file_key*, type*} [read/low]\n - `im.get_messages` payload={message_ids*} [read/low]\n - `im.list_chats` payload={page_size} [read/low]\n - `im.list_messages` payload={chat_id*} [read/low]\n - `im.list_thread_messages` payload={thread*} [read/low]\n - `im.remove_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.reply_message` payload={message_id*, text*} [send/high]\n - `im.search_chats` payload={query*, page_size} [read/low]\n - `im.search_messages` payload={query*} [read/medium]\n - `im.send_message` payload={chat_id*, text*, thread_id} [send/high]\n - `im.update_card` payload={token*, card*} [write/medium]\n - `im.update_message` payload={message_id*, text*} [write/medium]\n - `mail.search_unread` payload={query, limit} [read/high]\n - `sheets.append_row` payload={spreadsheet_token*, sheet_id*, values*} [write/medium]\n - `task.create` payload={title*, description, due_time} [write/medium]\n- `telegram`: Telegram (category=social; backend=adapter; platform_action actions=none registered)\n- `webhook`: Webhook (Generic) (category=webhook; backend=adapter; platform_action actions=none registered)\n\n## Learned Skills\nYou have access to the following learned skills. Use `skills_list` to browse or `skill_view` to read details:\n- api_designer: REST and GraphQL API design with OpenAPI spec generation and best practices [API, REST, GraphQL]\n- code_review: Structured code review for security, performance, and style [code, review, security]\n- data_analysis: CSV/JSON data analysis with statistical summaries, quality checks, and visualiza [data, analysis, statistics]\n- devops_helper: CI/CD pipeline configuration, Docker orchestration, and deployment strategy [devops, CI/CD, Docker]\n- document_writer: Structured document generation: technical docs, API references, reports, and REA [documentation, technical-writing, README]\n- git_workflow: Git operation orchestration: branching, commits, conflicts, and PR workflow [git, version-control, branching]\n- i18n_helper: Internationalization workflow: string extraction, translation, locale management [i18n, internationalization, localization]\n- project_analysis: Project structure analysis, dependency audit, and tech stack identification [project, analysis, dependencies]\n- security_audit: Security vulnerability scanning strategy, threat modeling, and fix recommendatio [security, vulnerability, audit]\n- shell_automation: Natural language to shell commands with safety review and step-by-step execution [shell, terminal, automation]\n- test_writer: Generate unit and integration tests from source code with edge-case coverage [testing, unit-test, integration-test]\n- web_research: Multi-step web search with cross-verification and structured synthesis [web, search, research]\n\n## Tool Usage\nTools are normally invoked through the native function-calling mechanism, not by writing JSON in your reply\ntext. Only if the provider signals that native function calling is unavailable for this turn, fall back to a\nsingle JSON code block: `{\"name\": \"tool_name\", \"arguments\": {\"key\": \"value\"}}` — use this fallback format\nonly, never both. Only call a tool whose exact name appears in the tool index above and is currently callable\n(or reachable via `capability_expand`). Never invent, rename, alias, or guess a tool name, platform ID, or\nplatform action from argument shape or wording — if the index does not list it, it does not exist.\n`platform_connect.action` (list/guide/preflight/connect/disconnect/remove/status/events_start/events_stop/\nevents_status) is the App Connector management namespace; `platform_action.action` only accepts exact\nregistered business actions such as `im.send_message` shown in the App Connector Capability Index — never mix\nthe two namespaces. If a tool call returns an unknown/unavailable result, use the returned suggestions or\navailable names for a single retry instead of trying further variations of the same guess.\n\n**Side-effect action rule** (`platform_action` with effect=send/write/execute):\n- Call each unique action+payload **exactly once**. Never include duplicates in the same turn.\n- Once the result returns `\"completed\": true`, that action is DONE for this task. Do NOT call it again\n in any subsequent turn — immediately summarize the result for the user instead.\n- The system enforces idempotency: duplicate calls are blocked and will not execute.\n- If the user explicitly requests sending/writing multiple times, use distinct payloads per call.\n\n**Resource identifier provenance rule**:\n- NEVER fabricate, guess, or infer resource identifiers (chat_id, message_id, file_key, user_id, etc.).\n Every resource ID used in a side-effect action MUST come from a successful API response in this session.\n- If a read/list action fails (e.g. authorization error), you do NOT have valid resource IDs.\n Report the failure to the user — do NOT attempt the dependent write/send action with a guessed ID.\n- When a tool result contains `\"llm_instruction\"`, follow it exactly.\n\n## Guidelines\n1. **Direct answers first**: If you already know the answer, respond directly without tools.\n2. **Avoid redundant tool calls**: Do not call the same tool with the same arguments more than once in the same user turn. When an existing tool result already answers the user's request, stop calling tools and answer directly.\n3. **Use tools proactively**: When the user asks about files, time, system state, or needs actions performed, use the appropriate tool.\n4. **Chain tools when needed**: You can call multiple tools in sequence (e.g., list files → read file → summarize).\n5. **Handle failures gracefully**: If a tool fails, explain what went wrong and suggest alternatives. If it failed because of an unknown tool/platform/action name, retry once with an exact name from the returned suggestions, then explain rather than keep guessing.\n6. **Summarize results naturally**: After tool execution, synthesize the results into a helpful answer rather than dumping raw output.\n7. **Stay conversational**: Maintain a natural, helpful tone. Acknowledge context from earlier in the conversation.\n8. **Recall past work**:\n - Broad queries (\"之前做了什么\", \"列出任务\"): answer from the \"Recent Task History\" section already in your context. If insufficient, call session_list.\n - Specific lookups (\"上次那个配置怎么改的\"): call session_search with relevant phrases (NOT single characters).\n - Do NOT call search tools repeatedly with keyword fragments. One well-phrased call is better than ten fragmented ones.\n\n## Coding & Verification\nWhen working with code, prefer the precise built-in tools over ad-hoc shell: use `repo_map` to orient in an\nunfamiliar project, `code_search`/`file_find` to locate and `code_intel` for symbols, `edit_file` (anchored\nsearch-replace, or a unified `diff`) to change files — never rewrite a whole file to change a few lines — and\n`git_query` to inspect diffs/log. After edits, check `syntax_ok` in the result and run `test_run`/`lint_check`\nbefore declaring the work done. Batch independent read-only calls (search/read/query) into a single turn — they\nrun in parallel.\n\n## Presentation Style\n1. **Polished Markdown only**: Format user-facing answers with clean Markdown headings, short paragraphs, and concise bullets. Use tables only when they improve comparison or scanning.\n2. **Terminal-friendly layout**: Keep lines readable in a TUI; avoid dense walls of text, deeply nested lists, oversized ASCII art, or heavy visual blocks.\n3. **Elegant emphasis**: Use bold text sparingly for key terms and conclusions. Avoid excessive emojis, decorative symbols, repeated separators, or visual noise.\n4. **Theme-safe colors**: Do not emit ANSI escape codes, HTML color tags, Rich markup, or hardcoded color names. Rely on the TUI theme to render Markdown professionally.\n5. **No leaked tool protocol**: Never show tool-call JSON, internal schemas, raw observations, tool result payloads, or hidden reasoning in the final answer unless the user explicitly asks for raw/debug output. Treat any prior `{\"name\": ..., \"arguments\": ...}` blocks and `Tool result (...)` messages as internal execution context only.\n6. **Professional closure**: End with a concise conclusion or next step when helpful; avoid rambling after the useful answer is complete.\n\nWhen finished with all tool calls, respond normally without a JSON block, tool-call transcript, or process log.\n\n## Task Contract\n- Task ID: turn-5\n- Original user request: Preview an actuate of bench_r8 setpoint to 55, dry run only.\n- Workspace root: \n- Allowed roots: \n- Treat relative project paths as relative to the workspace root; never infer `.` as the project root when a workspace root is provided.\n- Workspace boundary is enforced by tools: do not read, search, edit, or run commands against paths outside the allowed roots unless the user explicitly requests an external path and the tool/approval policy permits it.\n- LeapFlow workspace config is optional at `/.leapflow/config.yaml`; runtime config is loaded from `~/.leapflow/config/user.yaml` and `~/.leapflow/profiles//config/*.yaml`.\n- Preserve this task contract across summarization, compression, tool loops, and memory retrieval.\n" }, { "role": "assistant", @@ -52,12 +52,14 @@ "hw_actuate", "hw_batch_actuate", "hw_configure", + "hw_context_snapshot", "hw_control_start", "hw_control_status", "hw_control_stop", "hw_describe", "hw_dispense", "hw_estop", + "hw_health", "hw_list", "hw_orchestrate", "hw_policy_episode", diff --git a/tests/_fixtures/cassettes/r8_hardware/cassette-model-201dfd2f16183356.cassette.json b/tests/_fixtures/cassettes/r8_hardware/cassette-model-ab7d43ecc20172d9.cassette.json similarity index 59% rename from tests/_fixtures/cassettes/r8_hardware/cassette-model-201dfd2f16183356.cassette.json rename to tests/_fixtures/cassettes/r8_hardware/cassette-model-ab7d43ecc20172d9.cassette.json index 84e9d457..71f90497 100644 --- a/tests/_fixtures/cassettes/r8_hardware/cassette-model-201dfd2f16183356.cassette.json +++ b/tests/_fixtures/cassettes/r8_hardware/cassette-model-ab7d43ecc20172d9.cassette.json @@ -1,5 +1,5 @@ { - "fingerprint": "201dfd2f161833568f27ae31ea72cba48722d94422b9352d7c4093ba8ddd0b6e", + "fingerprint": "ab7d43ecc20172d90bac598db8d97e3804c30f0c7fd6811296356b89e7ec1c94", "note": "captured in seed mode", "request": { "model": "cassette-model", @@ -7,7 +7,7 @@ "messages": [ { "role": "system", - "content": "You are LeapFlow, an intelligent assistant that can both converse naturally and take real actions on the user's computer.\n\n## Capabilities\nThe tool index below lists **every** registered tool by name and a one-line summary — this is the complete\ncapability contract; nothing else exists. Only a subset is directly callable this turn (via native tool calling,\nnot a JSON block in your reply). If you need a tool from the index that is not yet callable, call\n`capability_expand` with its category name first — the matching tools become callable immediately after.\nWhen the user asks about LeapFlow **itself** — its identity/version/model, what tools, plugins, or capabilities\nit has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of\nself-awareness — answer from live runtime evidence via `self_describe` (facet=capabilities for \"what can you\ndo / do you support X\", identity for version/model, runtime for context/posture, or `all` for a full self-check),\nnever by reading LeapFlow's own source code, documentation, or memory. Report configuration-dependent or\nunavailable capabilities as limitations rather than inferring them.\n- **text_search**(text, pattern): Search for a regex pattern in text.\n- **text_replace**(text, old, new, count) [capability_expand category: write]: Replace occurrences of a substring in text.\n- **time_get**(): Get current date and time.\n- **env_info**(): Get system environment information (OS, Python version, cwd).\n- **skills_list**(query, category, source): List available learned skills. Use when user asks about capabilities or you need a specific skill.\n- **skill_view**(name): View the full content of a specific skill document.\n- **skill_list_files**(name) [capability_expand category: skills]: List all support files in a skill directory (configs, templates, examples). Use before skill_file_read to discover available files.\n- **skill_file_read**(name, file_path) [capability_expand category: skills]: Read an individual support file from a skill directory. Use skill_list_files first to discover available files.\n- **code_intel**(path, operation): Precise document symbols (outline) for a source file: classes, functions, and methods with line ranges. Python uses an exact AST parse; other languages use a keyword-prefix scan. Prefer over file_read mode=symbols for accurate navigation before editing. Read-only.\n- **repo_map**(path): Compact project orientation for a repository root: languages, detected test/lint commands, top-level structure, entry points, manifest, and VCS branch. Call this first when entering an unfamiliar codebase. Read-only.\n- **scm_sync**(action, cwd, remote, pull_ref, push_ref, timeout) [capability_expand category: scm]: Run a typed git SCM action. Use this instead of shell_run for git pull/push/status. For 'pull origin main then push', set action='pull_then_push', remote='origin', pull_ref='main', and omit push_ref so LeapFlow pushes the current local branch.\n- **git_query**(action, cwd, ref, path, staged, max_count, stat): Read-only structured git inspection: action=diff|log|status|branch|show. Prefer over shell_run for reading repo state — output is clipped, redacted, and log/branch are parsed into structured fields. Use scm_sync for pull/push.\n- **git_write**(action, cwd, message, stage_all, name, ref, create) [capability_expand category: scm]: Mutating git actions: action=commit (message, stage_all), branch (create+switch), checkout (switch; create=true for -b). Approval-gated. Use scm_sync for pull/push and git_query for reads.\n- **test_run**(command, cwd, timeout): Run the project's test suite and return structured results (framework, passed/failed counts, failing tests). Auto-detects the runner (pytest/npm/go/cargo) or uses a configured/explicit command; executes via the governed shell. ok=true means the runner executed — see 'success' for pass/fail.\n- **lint_check**(command, cwd, timeout): Run the project's linter and return a structured clean/issue result. Auto-detects the linter (ruff/eslint/go vet/clippy) or uses a configured/explicit command; executes via the governed shell. ok=true means the linter ran — see 'clean'.\n- **file_list**(path, pattern, depth): List files and directories at a given path. Use depth=1 or depth=2 to get a recursive tree in one call instead of listing each sub-directory separately.\n- **file_read**(path, max_lines, start_line, max_chars, mode): Read text file content with adaptive context governance. For large or unfamiliar files, prefer mode='outline' or mode='symbols' first, then use mode='raw' with start_line/max_lines for the specific range you actually need. For LeapFlow's own settings, use config_list / config_get / config_set — its config files are outside the workspace and not readable here.\n- **file_write**(path, content, mode) [capability_expand category: write]: Write content to a file (overwrite or append).\n- **code_search**(pattern, patterns, path, glob, ignore_case, multiline, max_results, context_lines): Search file CONTENTS by regex pattern across a directory tree (ripgrep-backed). Requires a regex pattern. NOT for listing or browsing directory contents — use file_list for that. Prefer this over shell_run grep: faster, skips VCS/dependency/build dirs, and returns structured path:line:column matches. Batch related lookups into ONE call via `patterns` (OR-combined, single pass) instead of issuing several separate searches. Use file_read for the surrounding context of a hit.\n- **file_find**(glob, path, max_results): Find files by a recursive glob pattern under a base path (e.g. '**/test_*.py' or '*.md'). Prefer this over shell_run find; skips VCS/dependency/build dirs.\n- **edit_file**(path, edits, dry_run, diff) [capability_expand category: write]: Apply targeted, anchored search-replace edits to an EXISTING text file (use file_write to create/overwrite). Each edit is {original_text, new_text, replace_all?}; original_text must match exactly and uniquely (or set replace_all) — a non-unique or missing anchor is rejected so files are never corrupted. Set dry_run to preview. Alternatively pass a unified 'diff' to apply its hunks as anchored edits. Far cheaper and safer than rewriting a whole file.\n- **shell_run**(command, cwd, timeout) [capability_expand category: shell]: Execute a one-shot shell command with timeout protection. Runs in the active workspace; paths resolving outside it are refused. Reach for a structured tool first when one fits — web_fetch for anything over HTTP(S), git_query/scm_sync for git, code_search/file_find/file_read for the repo, config_get/config_set for LeapFlow's own settings — because those report typed results, while a failed shell command can only be diagnosed from its exit code and stderr. Every shell run counts as an external side effect, so a failure stops the rest of the batch and is not retried automatically.\n- **terminal_open**(command, cwd, shell) [capability_expand category: terminal]: Open a PERSISTENT shell session (REPL/dev server/watch), returning a session_id for terminal_send/read/close. Disabled unless tools.terminal_session_enabled is set. For one-shot commands use shell_run instead.\n- **terminal_send**(session_id, input, wait) [capability_expand category: terminal]: Send a line of input to a persistent terminal session and return output captured shortly after.\n- **terminal_read**(session_id, wait): Drain buffered output from a persistent terminal session (optionally waiting briefly first).\n- **terminal_close**(session_id) [capability_expand category: terminal]: Terminate a persistent terminal session and release its process group.\n- **terminal_list**(): List active persistent terminal sessions.\n- **config_list**(category, limit): List LeapFlow's own writable settings (LLM model and endpoint, daemon, memory, perception, gateway, …) with current values. Use this to discover the exact key before changing anything. Optionally narrow by `category`. This is the only correct way to inspect LeapFlow configuration — never read config files from disk.\n- **config_get**(key): Read one LeapFlow setting by key (e.g. 'llm.model', 'llm.base_url', 'daemon.log_level'), returning its current value, type, scopes, and whether a change needs a daemon restart. There is no 'llm.provider' key: provider behavior is inferred from the OpenAI-compatible 'llm.base_url'. Never read LeapFlow config files from disk — use this.\n- **config_set**(key, value, scope) [capability_expand category: config]: Change one LeapFlow setting by key, e.g. switch an OpenAI-compatible LLM with key='llm.model' and key='llm.base_url'. There is no 'llm.provider' key: provider behavior is inferred from the endpoint. Values are validated and coerced; credentials are stored in the vault automatically. Call config_list or config_get first if unsure of the key. The result states whether a `leap daemon restart` is required. Never edit LeapFlow config files directly.\n- **web_fetch**(url, select, timeout, max_bytes): Read a URL over HTTP(S) and get back extracted, context-sized content: parsed JSON for API endpoints, readable text plus links for web pages. Use this for anything on the internet — prices, docs, releases, articles — instead of running curl through shell_run: it reports real HTTP status codes, retries rate limits on its own, and is a plain read so a retry is always safe. For JSON APIs pass `select` with a dotted path (e.g. 'chart.result.0.meta') to return just that part instead of the whole payload.\n- **memory_search**(query, limit): Search agent memory for relevant past experiences, observations, and facts.\n- **memory_add**(content, kind) [capability_expand category: write]: Store a new observation or insight in memory for future reference.\n- **research_note**(kind, text): Record a compact, structured note about the current task's state so it survives context compression on long / multi-step tasks. Use for durable findings, open questions still to resolve, decisions / excluded paths, and the immediate next step. One concise sentence per note.\n- **capability_expand**(category): Fetch the full callable schema for every tool in a capability category (e.g. 'hub', 'gateway', 'desktop', 'delegate', 'file', 'memory', 'skill'). The compact tool index always lists every registered tool by name and a one-line summary, but only a static low-risk subset is directly callable each turn. If you need a tool from the index that is not yet callable, call capability_expand with its category first; the matching tools become callable in this turn. Never invent a tool name — expand the category instead.\n- **delegate_task**(goal, context) [capability_expand category: delegate]: Delegate a complex sub-task to an isolated subagent. The subagent gets a fresh context and restricted tool access. Use when a task is self-contained and can be solved independently.\n- **schedule_reentry**(kind, reason, delay_seconds, event_match, max_reentries, deadline_seconds): Register a re-entry so this task can resume later from its current orientation (findings / open questions / next step). Use when work must pause and continue after a delay (kind=time) or when a matching platform event arrives (kind=event), instead of finishing now. The research-ledger state is carried over automatically.\n- **hub_push**(skill_name, visibility, version) [capability_expand category: hub]: Push a local skill to the ModelScope Hub for sharing or backup.\n- **hub_pull**(repo_id, version) [capability_expand category: hub]: Pull a skill from the ModelScope Hub to install locally.\n- **hub_search**(query) [capability_expand category: hub]: Search for skills on the Hub by keyword or description.\n- **hub_sync**(mode, dry_run) [capability_expand category: hub]: Preview or execute sync between local skills and Hub.\n- **platform_action**(platform, action, payload, backend_kind) [capability_expand category: gateway]: Execute an exact registered business action on an external platform through LeapFlow's App Connector layer. Actions must be copied from the App Connector Capability Index and are addressed as domain.operation, e.g. im.send_message or docs.create_markdown. All business fields (chat_id, text, query, etc.) MUST be placed inside `payload`, never at the top level. Example: {\"platform\":\"feishu\",\"action\":\"im.send_message\",\"payload\":{\"chat_id\":\"oc_xxx\",\"text\":\"hello\"}}. Do not invent action names, do not use management actions such as list/guide/connect/status here.\n- **platform_connect**(action, platform, credentials, options, checkpoint) [capability_expand category: gateway]: List, guide, connect, disconnect, remove, or check status for external platforms using the App Connector management namespace. Supports REST and CLI backends. Use this for management actions such as list/guide/preflight/connect/status; use platform_action only for exact registered business actions.\n- **gateway_send**(platform, chat_id, text, thread_id) [capability_expand category: gateway]: Send a message to a connected external platform (Feishu group, Telegram chat, DingTalk conversation, etc.). Requires the platform to be connected via gateway_connect first. Use gateway_connect with action='list' to see connected platforms and available chat IDs.\n- **gateway_connect**(action, platform, credentials, options) [capability_expand category: gateway]: Connect, configure, or manage external platform integrations (Feishu, DingTalk, Telegram, Slack, Discord, etc.). Conversational flow: 1) call 'guide' to get setup steps + required fields, 2) present the steps to the user and ask for ALL required credentials in a single message, 3) call 'connect' with the credentials. Goal: complete in 1–2 user turns. NEVER include credential values in your text response.\n- **plugin_list**(): List the live plugin registry across Tool/Gateway/LLM subsystems for plugin management: which plugins are loaded, their category and fiber lifecycle state. For questions about LeapFlow's own capabilities or whether it supports X, use self_describe instead.\n- **plugin_status**(plugin_id): Get detailed status of a specific plugin: its declared category, runtime dependencies, contributed tools, and fiber lifecycle state.\n- **plugin_versions**(plugin_id): List recorded source versions and active pointer for a profile-scoped plugin.\n- **plugin_propose**(requested_capability, plugin_id, proposed_tools, test_cases, risk_level, evidence): Create a side-effect-free PluginProposal from explicit capability-gap evidence. Use this before plugin_generate when a missing capability should be reviewed. Does not call an LLM, write files, or install anything.\n- **assess_compatibility**(manifest, source_path) [capability_expand category: plugin_management]: Assess whether a foreign plugin manifest or real DSH source bundle is compatible with LeapFlow. A source bundle assessment is static: runtime_ready stays false until restricted Node discovery during plugin_install. Returns component-level verdicts and limitations.\n- **plugin_generate**(plugin_id, description, proposal_id) [capability_expand category: system]: Generate a new ToolPlugin from a natural-language capability description. The LLM produces code that conforms to the ToolPlugin Protocol; it is then rigorously validated (syntax, structure, import, protocol conformance). The generated source is stored in CAS and receives explicit content approval. It DOES NOT install the plugin — installation requires a second, mutation-specific approval via plugin_install.\n- **plugin_install**(plugin_id, code, marketplace_name, source_path, proposal_id, version_label) [capability_expand category: system]: Install a plugin from exactly one source: validated Python code, the configured Python marketplace, or a real DSH source bundle. DSH bundles run restricted Node discovery before registration; only public host tools are installed and client UI limitations are reported. Writes profile-scoped state and mutates the process-global registry. REQUIRES APPROVAL.\n- **plugin_rollback**(plugin_id, version) [capability_expand category: system]: Rollback a profile-scoped plugin to a recorded source version and reload it. REQUIRES APPROVAL.\n- **plugin_reload**(plugin_id, version_label) [capability_expand category: system]: Hot-reload a plugin at runtime. Disposes the old plugin fiber, re-imports its module, and registers a fresh instance. Existing in-flight turns are unaffected (snapshot isolation). REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_disable**(plugin_id) [capability_expand category: system]: Disable a plugin by disposing its fiber, removing its tools from the runtime registry. Cannot disable self_management itself. REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_remove**(plugin_id, delete_source) [capability_expand category: system]: Terminally remove a plugin: dispose its fiber, unregister its tools, remove reload metadata, and optionally delete its profile-scoped source file. Cannot remove self_management itself. REQUIRES APPROVAL.\n- **plugin_unquarantine**(plugin_id) [capability_expand category: plugin_management]: Restore a quarantined plugin to probation status for re-evaluation. Unfreezes the trust ledger, transitions the proposal back to PROBATION, and reloads the plugin. REQUIRES APPROVAL.\n- **plugin_enable**(plugin_id) [capability_expand category: system]: Re-enable a previously disabled plugin by reloading its module and registering a fresh instance. REQUIRES APPROVAL.\n- **self_describe**(facet): Answer questions about LeapFlow ITSELF from live runtime evidence — its identity/version/model, what tools and plugins it has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of self-awareness. Prefer this over reading LeapFlow's own source code. Pick facet=capabilities for 'what can you do / do you support X', identity for version/model, runtime for context/posture, or all for a full self-check when explicitly requested.\n- **runtime_snapshot**(): Lightweight (~150 token) flat snapshot of current runtime state: model, context budget, posture, disclosure level, turn count, cache hit rate, uptime, tool count, and pending approvals.\n- **schedule_create**(trigger_expression, instruction, execution_mode, max_retries, delivery_target) [capability_expand category: scheduler]: Create a new scheduled task. Specify a trigger expression (e.g. '30m', 'every 2h', '0 9 * * *') and the instruction to execute on each trigger. Optionally provide an execution mode and a delivery target to receive notifications.\n- **schedule_list**() [capability_expand category: scheduler]: List all scheduled tasks with their current state, trigger, and next due time.\n- **schedule_status**(task_id) [capability_expand category: scheduler]: Get detailed status and recent execution history for a scheduled task.\n- **schedule_pause**(task_id) [capability_expand category: scheduler]: Pause a scheduled task so it stops firing without being cancelled.\n- **schedule_resume**(task_id) [capability_expand category: scheduler]: Resume a paused scheduled task — re-arms it and recalculates next due time.\n- **schedule_cancel**(task_id) [capability_expand category: scheduler]: Cancel a scheduled task permanently.\n- **tool_search**(query, max_results): Search all registered tools by keyword relevance. Returns a ranked list of matching tools with name, category, and summary. Use this when you need a tool but are unsure of its exact name or category.\n- **tool_describe**(tool_name): Get the full callable schema of a single tool by exact name. Returns the tool's description, parameters, and metadata. Use after tool_search to inspect a specific tool before calling it.\n- **hw_list**(): List connected hardware devices with their channel counts and measured quantities. Start here; it does not include operating limits.\n- **hw_describe**(device_id): Return the full reference for one device: every channel, its unit, its operating envelope, rate limit, reversibility, and required interlocks, plus any prior outcomes recorded for its writable channels. Required before commanding a device.\n- **hw_read**(device_id, channel_id): Read the current value of one device channel. Has no physical effect.\n- **hw_status**(device_id): Report connection health, halt capability, and recent notable events (threshold excursions, lost samples, stalled channels) for one device.\n- **hw_configure**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Set a configuration value or setpoint on a channel declaring effect=configure. Setpoints often have inertia: the value may need time to stabilise.\n- **hw_actuate**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Command motion or output on a channel declaring effect=actuate. This moves physical hardware; a repeat from an unknown state is not a safe retry.\n- **hw_dispense**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Consume an irreversible resource on a channel declaring effect=dispense. Running this twice dispenses twice; never repeat it after a failure without first verifying what already happened.\n- **hw_estop**(device_id) [capability_expand category: hardware]: Stop all motion and output on a device immediately. Never requires approval; use it whenever device behaviour is unexpected.\n- **hw_batch_actuate**(device_id, commands, dry_run, verify) [capability_expand category: hardware]: Write multiple actuator channels in one atomic transaction. Used for coordinated multi-joint control where all positions must be commanded simultaneously (e.g. robot arm joint-space control).\n- **hw_control_start**(device_id, mode, targets, frequency_hz, trajectory, policy, gains) [capability_expand category: hardware]: Start a real-time control loop on a robot device. Supports PID position tracking, impedance control, trajectory following, and neural policy chains.\n- **hw_control_stop**(device_id) [capability_expand category: hardware]: Stop the real-time control loop on a device. Gracefully halts the device and returns session statistics.\n- **hw_control_status**(device_id) [capability_expand category: hardware]: Get the status and statistics of a running control loop. Returns frequency, jitter, overruns, and policy state.\n- **hw_policy_infer**(device_id, policy, task, execute, chunk_size, verify, compute_budget) [capability_expand category: hardware]: Execute VLA policy inference on a robot device. Reads observation, runs policy, optionally writes action to actuators. Supports local (HuggingFace) and remote (PolicyServer) policies.\n- **hw_policy_episode**(action, device_id, policy, task) [capability_expand category: hardware]: Manage policy inference episode lifecycle. Start an episode to begin a task, stop to finalise and get stats.\n- **hw_policy_list**(source) [capability_expand category: hardware]: List available VLA policies and their capabilities.\n- **hw_orchestrate**(mode, operations, barrier_timeout_s, label) [capability_expand category: hardware]: Execute coordinated operations across multiple hardware devices. Supports sequential (one-at-a-time), parallel (concurrent), and barrier (synchronised) execution modes. Any failure in a parallel group triggers halt_all on every participating device for safety.\n- **session_search**(query, limit) [capability_expand category: unclassified]: Search past conversation sessions in the current workspace for relevant context.\n- **session_list**(limit): List recent conversation sessions in the current workspace with stable ids, titles, dates, and summaries. Use for browsing past tasks or when user asks to see history without specific search terms. No keywords needed — returns chronological list.\n- **session_detail**(session_id, limit, offset, include_inactive) [capability_expand category: unclassified]: Read a paginated persisted transcript for one past conversation session in the current workspace. Use after session_list or session_search returns a session_id.\n- **click**(element_index) [capability_expand category: desktop]: Click a UI element by its element_index (from the latest observe_ui snapshot)\n- **get_clipboard**() [capability_expand category: desktop]: Read current clipboard text content\n- **list_apps**(filter, running_only) [capability_expand category: desktop]: List available applications on this system. Use to discover correct bundle_id before switch_app.\n- **list_windows**() [capability_expand category: desktop]: List all top-level windows with pid, window_id, title, and per-window state (minimized, on-screen). Call this first to pick the pid and window_id that observe_ui and other window tools require.\n- **observe_ui**(pid, window_id, query) [capability_expand category: desktop]: Snapshot one window's actionable UI elements, each tagged with an element_index for click/right_click/read_text. Re-observe after actions — indices belong to one snapshot. Requires the window's pid and window_id from list_windows.\n- **open_url**(url, app_id) [capability_expand category: desktop]: Open a URL in the default or specified browser\n- **read_text**(element_index) [capability_expand category: desktop]: Read the text content of a specific UI element from the latest snapshot\n- **right_click**(element_index) [capability_expand category: desktop]: Right-click a UI element to open its context menu. Returns visible menu items.\n- **screenshot**(pid, window_id) [capability_expand category: desktop]: Capture a screenshot for visual verification. With pid + window_id captures that window (works across all displays); defaults to the last observed window, or the full desktop when no window has been observed.\n- **scroll**(element_index, direction, amount, pid, window_id) [capability_expand category: desktop]: Scroll a scrollable area of a window. Omit element_index to scroll the window's focused/page scroller; pass one to scroll an exact element from the latest snapshot.\n- **select_text**(element_index) [capability_expand category: desktop]: Select all text in a UI element (focus + select-all, for subsequent copy)\n- **set_clipboard**(text) [capability_expand category: desktop]: Write text to the clipboard\n- **shortcut**(keys) [capability_expand category: desktop]: Execute a keyboard shortcut\n- **switch_app**(app_id) [capability_expand category: desktop]: Switch to an app (launch if needed, activate, verify)\n- **type_text**(text) [capability_expand category: desktop]: Type text into the currently focused element\n- **wait**(seconds) [capability_expand category: desktop]: Wait for a specified duration before continuing\n- **wait_until**(condition, pid, window_id, timeout, poll_interval) [capability_expand category: desktop]: Wait until a UI condition is met (polls UI tree). Returns elements when found or on timeout.\n- **wait_until_stable**(timeout, poll_interval, pid, window_id) [capability_expand category: desktop]: Wait until the UI stops changing (element set stabilizes across polls).\n\n## App Connector Capability Index\nLeapFlow can onboard and manage external apps through `platform_connect` and execute exact registered business actions through `platform_action`.\nFor requests about connecting, setting up, configuring, enabling, or managing a supported app, use `platform_connect` first instead of generating SDK/Webhook sample code.\n`platform_connect.action` is the management namespace: list, guide, preflight, connect, disconnect, remove, status, events_start, events_stop, events_status.\n`platform_action.action` is only for exact registered platform business actions listed below, such as `im.send_message`; never use management actions like `list` or `guide` there.\nAll business fields MUST go inside `payload`; top-level keys are only `platform`, `action`, and `payload`.\nDo not invent platform IDs or platform action names. If the needed action is not listed, ask for discovery/clarification instead of guessing.\nUse `platform_connect` with `action='guide'` and the matching `platform` to start onboarding; use `action='list'` when the app is unclear.\nWhen a pending onboarding state is present, continue from that state with `platform_connect` instead of asking the user to restate the app.\nSupported apps:\n- `api_server`: API Server (OpenAI Compatible) (category=api; backend=adapter; platform_action actions=none registered)\n- `dingtalk`: 钉钉 (DingTalk) (category=im; backend=adapter; platform_action actions=none registered)\n- `feishu`: 飞书 (Feishu/Lark) (category=im; backend=cli)\n - `calendar.create_event` payload={summary*, start_time*, end_time*, attendees} [write/high]\n - `docs.create_markdown` payload={title*, markdown*, folder_token} [write/medium]\n - `drive.search` payload={query*, limit} [read/medium]\n - `im.add_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.download_resource` payload={message_id*, file_key*, type*} [read/low]\n - `im.get_messages` payload={message_ids*} [read/low]\n - `im.list_chats` payload={page_size} [read/low]\n - `im.list_messages` payload={chat_id*} [read/low]\n - `im.list_thread_messages` payload={thread*} [read/low]\n - `im.remove_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.reply_message` payload={message_id*, text*} [send/high]\n - `im.search_chats` payload={query*, page_size} [read/low]\n - `im.search_messages` payload={query*} [read/medium]\n - `im.send_message` payload={chat_id*, text*, thread_id} [send/high]\n - `im.update_card` payload={token*, card*} [write/medium]\n - `im.update_message` payload={message_id*, text*} [write/medium]\n - `mail.search_unread` payload={query, limit} [read/high]\n - `sheets.append_row` payload={spreadsheet_token*, sheet_id*, values*} [write/medium]\n - `task.create` payload={title*, description, due_time} [write/medium]\n- `telegram`: Telegram (category=social; backend=adapter; platform_action actions=none registered)\n- `webhook`: Webhook (Generic) (category=webhook; backend=adapter; platform_action actions=none registered)\n\n## Learned Skills\nYou have access to the following learned skills. Use `skills_list` to browse or `skill_view` to read details:\n- api_designer: REST and GraphQL API design with OpenAPI spec generation and best practices [API, REST, GraphQL]\n- code_review: Structured code review for security, performance, and style [code, review, security]\n- data_analysis: CSV/JSON data analysis with statistical summaries, quality checks, and visualiza [data, analysis, statistics]\n- devops_helper: CI/CD pipeline configuration, Docker orchestration, and deployment strategy [devops, CI/CD, Docker]\n- document_writer: Structured document generation: technical docs, API references, reports, and REA [documentation, technical-writing, README]\n- git_workflow: Git operation orchestration: branching, commits, conflicts, and PR workflow [git, version-control, branching]\n- i18n_helper: Internationalization workflow: string extraction, translation, locale management [i18n, internationalization, localization]\n- project_analysis: Project structure analysis, dependency audit, and tech stack identification [project, analysis, dependencies]\n- security_audit: Security vulnerability scanning strategy, threat modeling, and fix recommendatio [security, vulnerability, audit]\n- shell_automation: Natural language to shell commands with safety review and step-by-step execution [shell, terminal, automation]\n- test_writer: Generate unit and integration tests from source code with edge-case coverage [testing, unit-test, integration-test]\n- web_research: Multi-step web search with cross-verification and structured synthesis [web, search, research]\n\n## Tool Usage\nTools are normally invoked through the native function-calling mechanism, not by writing JSON in your reply\ntext. Only if the provider signals that native function calling is unavailable for this turn, fall back to a\nsingle JSON code block: `{\"name\": \"tool_name\", \"arguments\": {\"key\": \"value\"}}` — use this fallback format\nonly, never both. Only call a tool whose exact name appears in the tool index above and is currently callable\n(or reachable via `capability_expand`). Never invent, rename, alias, or guess a tool name, platform ID, or\nplatform action from argument shape or wording — if the index does not list it, it does not exist.\n`platform_connect.action` (list/guide/preflight/connect/disconnect/remove/status/events_start/events_stop/\nevents_status) is the App Connector management namespace; `platform_action.action` only accepts exact\nregistered business actions such as `im.send_message` shown in the App Connector Capability Index — never mix\nthe two namespaces. If a tool call returns an unknown/unavailable result, use the returned suggestions or\navailable names for a single retry instead of trying further variations of the same guess.\n\n**Side-effect action rule** (`platform_action` with effect=send/write/execute):\n- Call each unique action+payload **exactly once**. Never include duplicates in the same turn.\n- Once the result returns `\"completed\": true`, that action is DONE for this task. Do NOT call it again\n in any subsequent turn — immediately summarize the result for the user instead.\n- The system enforces idempotency: duplicate calls are blocked and will not execute.\n- If the user explicitly requests sending/writing multiple times, use distinct payloads per call.\n\n**Resource identifier provenance rule**:\n- NEVER fabricate, guess, or infer resource identifiers (chat_id, message_id, file_key, user_id, etc.).\n Every resource ID used in a side-effect action MUST come from a successful API response in this session.\n- If a read/list action fails (e.g. authorization error), you do NOT have valid resource IDs.\n Report the failure to the user — do NOT attempt the dependent write/send action with a guessed ID.\n- When a tool result contains `\"llm_instruction\"`, follow it exactly.\n\n## Guidelines\n1. **Direct answers first**: If you already know the answer, respond directly without tools.\n2. **Avoid redundant tool calls**: Do not call the same tool with the same arguments more than once in the same user turn. When an existing tool result already answers the user's request, stop calling tools and answer directly.\n3. **Use tools proactively**: When the user asks about files, time, system state, or needs actions performed, use the appropriate tool.\n4. **Chain tools when needed**: You can call multiple tools in sequence (e.g., list files → read file → summarize).\n5. **Handle failures gracefully**: If a tool fails, explain what went wrong and suggest alternatives. If it failed because of an unknown tool/platform/action name, retry once with an exact name from the returned suggestions, then explain rather than keep guessing.\n6. **Summarize results naturally**: After tool execution, synthesize the results into a helpful answer rather than dumping raw output.\n7. **Stay conversational**: Maintain a natural, helpful tone. Acknowledge context from earlier in the conversation.\n8. **Recall past work**:\n - Broad queries (\"之前做了什么\", \"列出任务\"): answer from the \"Recent Task History\" section already in your context. If insufficient, call session_list.\n - Specific lookups (\"上次那个配置怎么改的\"): call session_search with relevant phrases (NOT single characters).\n - Do NOT call search tools repeatedly with keyword fragments. One well-phrased call is better than ten fragmented ones.\n\n## Coding & Verification\nWhen working with code, prefer the precise built-in tools over ad-hoc shell: use `repo_map` to orient in an\nunfamiliar project, `code_search`/`file_find` to locate and `code_intel` for symbols, `edit_file` (anchored\nsearch-replace, or a unified `diff`) to change files — never rewrite a whole file to change a few lines — and\n`git_query` to inspect diffs/log. After edits, check `syntax_ok` in the result and run `test_run`/`lint_check`\nbefore declaring the work done. Batch independent read-only calls (search/read/query) into a single turn — they\nrun in parallel.\n\n## Presentation Style\n1. **Polished Markdown only**: Format user-facing answers with clean Markdown headings, short paragraphs, and concise bullets. Use tables only when they improve comparison or scanning.\n2. **Terminal-friendly layout**: Keep lines readable in a TUI; avoid dense walls of text, deeply nested lists, oversized ASCII art, or heavy visual blocks.\n3. **Elegant emphasis**: Use bold text sparingly for key terms and conclusions. Avoid excessive emojis, decorative symbols, repeated separators, or visual noise.\n4. **Theme-safe colors**: Do not emit ANSI escape codes, HTML color tags, Rich markup, or hardcoded color names. Rely on the TUI theme to render Markdown professionally.\n5. **No leaked tool protocol**: Never show tool-call JSON, internal schemas, raw observations, tool result payloads, or hidden reasoning in the final answer unless the user explicitly asks for raw/debug output. Treat any prior `{\"name\": ..., \"arguments\": ...}` blocks and `Tool result (...)` messages as internal execution context only.\n6. **Professional closure**: End with a concise conclusion or next step when helpful; avoid rambling after the useful answer is complete.\n\nWhen finished with all tool calls, respond normally without a JSON block, tool-call transcript, or process log.\n\n## Task Contract\n- Task ID: turn-3\n- Original user request: Actuate the bench_r8 setpoint to 60.\n- Workspace root: \n- Allowed roots: \n- Treat relative project paths as relative to the workspace root; never infer `.` as the project root when a workspace root is provided.\n- Workspace boundary is enforced by tools: do not read, search, edit, or run commands against paths outside the allowed roots unless the user explicitly requests an external path and the tool/approval policy permits it.\n- LeapFlow workspace config is optional at `/.leapflow/config.yaml`; runtime config is loaded from `~/.leapflow/config/user.yaml` and `~/.leapflow/profiles//config/*.yaml`.\n- Preserve this task contract across summarization, compression, tool loops, and memory retrieval.\n" + "content": "You are LeapFlow, an intelligent assistant that can both converse naturally and take real actions on the user's computer.\n\n## Capabilities\nThe tool index below lists **every** registered tool by name and a one-line summary — this is the complete\ncapability contract; nothing else exists. Only a subset is directly callable this turn (via native tool calling,\nnot a JSON block in your reply). If you need a tool from the index that is not yet callable, call\n`capability_expand` with its category name first — the matching tools become callable immediately after.\nWhen the user asks about LeapFlow **itself** — its identity/version/model, what tools, plugins, or capabilities\nit has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of\nself-awareness — answer from live runtime evidence via `self_describe` (facet=capabilities for \"what can you\ndo / do you support X\", identity for version/model, runtime for context/posture, or `all` for a full self-check),\nnever by reading LeapFlow's own source code, documentation, or memory. Report configuration-dependent or\nunavailable capabilities as limitations rather than inferring them.\n- **text_search**(text, pattern): Search for a regex pattern in text.\n- **text_replace**(text, old, new, count) [capability_expand category: write]: Replace occurrences of a substring in text.\n- **time_get**(): Get current date and time.\n- **env_info**(): Get system environment information (OS, Python version, cwd).\n- **skills_list**(query, category, source): List available learned skills. Use when user asks about capabilities or you need a specific skill.\n- **skill_view**(name): View the full content of a specific skill document.\n- **skill_list_files**(name) [capability_expand category: skills]: List all support files in a skill directory (configs, templates, examples). Use before skill_file_read to discover available files.\n- **skill_file_read**(name, file_path) [capability_expand category: skills]: Read an individual support file from a skill directory. Use skill_list_files first to discover available files.\n- **code_intel**(path, operation): Precise document symbols (outline) for a source file: classes, functions, and methods with line ranges. Python uses an exact AST parse; other languages use a keyword-prefix scan. Prefer over file_read mode=symbols for accurate navigation before editing. Read-only.\n- **repo_map**(path): Compact project orientation for a repository root: languages, detected test/lint commands, top-level structure, entry points, manifest, and VCS branch. Call this first when entering an unfamiliar codebase. Read-only.\n- **scm_sync**(action, cwd, remote, pull_ref, push_ref, timeout) [capability_expand category: scm]: Run a typed git SCM action. Use this instead of shell_run for git pull/push/status. For 'pull origin main then push', set action='pull_then_push', remote='origin', pull_ref='main', and omit push_ref so LeapFlow pushes the current local branch.\n- **git_query**(action, cwd, ref, path, staged, max_count, stat): Read-only structured git inspection: action=diff|log|status|branch|show. Prefer over shell_run for reading repo state — output is clipped, redacted, and log/branch are parsed into structured fields. Use scm_sync for pull/push.\n- **git_write**(action, cwd, message, stage_all, name, ref, create) [capability_expand category: scm]: Mutating git actions: action=commit (message, stage_all), branch (create+switch), checkout (switch; create=true for -b). Approval-gated. Use scm_sync for pull/push and git_query for reads.\n- **test_run**(command, cwd, timeout): Run the project's test suite and return structured results (framework, passed/failed counts, failing tests). Auto-detects the runner (pytest/npm/go/cargo) or uses a configured/explicit command; executes via the governed shell. ok=true means the runner executed — see 'success' for pass/fail.\n- **lint_check**(command, cwd, timeout): Run the project's linter and return a structured clean/issue result. Auto-detects the linter (ruff/eslint/go vet/clippy) or uses a configured/explicit command; executes via the governed shell. ok=true means the linter ran — see 'clean'.\n- **file_list**(path, pattern, depth): List files and directories at a given path. Use depth=1 or depth=2 to get a recursive tree in one call instead of listing each sub-directory separately.\n- **file_read**(path, max_lines, start_line, max_chars, mode): Read text file content with adaptive context governance. For large or unfamiliar files, prefer mode='outline' or mode='symbols' first, then use mode='raw' with start_line/max_lines for the specific range you actually need. For LeapFlow's own settings, use config_list / config_get / config_set — its config files are outside the workspace and not readable here.\n- **file_write**(path, content, mode) [capability_expand category: write]: Write content to a file (overwrite or append).\n- **code_search**(pattern, patterns, path, glob, ignore_case, multiline, max_results, context_lines): Search file CONTENTS by regex pattern across a directory tree (ripgrep-backed). Requires a regex pattern. NOT for listing or browsing directory contents — use file_list for that. Prefer this over shell_run grep: faster, skips VCS/dependency/build dirs, and returns structured path:line:column matches. Batch related lookups into ONE call via `patterns` (OR-combined, single pass) instead of issuing several separate searches. Use file_read for the surrounding context of a hit.\n- **file_find**(glob, path, max_results): Find files by a recursive glob pattern under a base path (e.g. '**/test_*.py' or '*.md'). Prefer this over shell_run find; skips VCS/dependency/build dirs.\n- **edit_file**(path, edits, dry_run, diff) [capability_expand category: write]: Apply targeted, anchored search-replace edits to an EXISTING text file (use file_write to create/overwrite). Each edit is {original_text, new_text, replace_all?}; original_text must match exactly and uniquely (or set replace_all) — a non-unique or missing anchor is rejected so files are never corrupted. Set dry_run to preview. Alternatively pass a unified 'diff' to apply its hunks as anchored edits. Far cheaper and safer than rewriting a whole file.\n- **shell_run**(command, cwd, timeout) [capability_expand category: shell]: Execute a one-shot shell command with timeout protection. Runs in the active workspace; paths resolving outside it are refused. Reach for a structured tool first when one fits — web_fetch for anything over HTTP(S), git_query/scm_sync for git, code_search/file_find/file_read for the repo, config_get/config_set for LeapFlow's own settings — because those report typed results, while a failed shell command can only be diagnosed from its exit code and stderr. Every shell run counts as an external side effect, so a failure stops the rest of the batch and is not retried automatically.\n- **terminal_open**(command, cwd, shell) [capability_expand category: terminal]: Open a PERSISTENT shell session (REPL/dev server/watch), returning a session_id for terminal_send/read/close. Disabled unless tools.terminal_session_enabled is set. For one-shot commands use shell_run instead.\n- **terminal_send**(session_id, input, wait) [capability_expand category: terminal]: Send a line of input to a persistent terminal session and return output captured shortly after.\n- **terminal_read**(session_id, wait): Drain buffered output from a persistent terminal session (optionally waiting briefly first).\n- **terminal_close**(session_id) [capability_expand category: terminal]: Terminate a persistent terminal session and release its process group.\n- **terminal_list**(): List active persistent terminal sessions.\n- **config_list**(category, limit): List LeapFlow's own writable settings (LLM model and endpoint, daemon, memory, perception, gateway, …) with current values. Use this to discover the exact key before changing anything. Optionally narrow by `category`. This is the only correct way to inspect LeapFlow configuration — never read config files from disk.\n- **config_get**(key): Read one LeapFlow setting by key (e.g. 'llm.model', 'llm.base_url', 'daemon.log_level'), returning its current value, type, scopes, and whether a change needs a daemon restart. There is no 'llm.provider' key: provider behavior is inferred from the OpenAI-compatible 'llm.base_url'. Never read LeapFlow config files from disk — use this.\n- **config_set**(key, value, scope) [capability_expand category: config]: Change one LeapFlow setting by key, e.g. switch an OpenAI-compatible LLM with key='llm.model' and key='llm.base_url'. There is no 'llm.provider' key: provider behavior is inferred from the endpoint. Values are validated and coerced; credentials are stored in the vault automatically. Call config_list or config_get first if unsure of the key. The result states whether a `leap daemon restart` is required. Never edit LeapFlow config files directly.\n- **web_fetch**(url, select, timeout, max_bytes): Read a URL over HTTP(S) and get back extracted, context-sized content: parsed JSON for API endpoints, readable text plus links for web pages. Use this for anything on the internet — prices, docs, releases, articles — instead of running curl through shell_run: it reports real HTTP status codes, retries rate limits on its own, and is a plain read so a retry is always safe. For JSON APIs pass `select` with a dotted path (e.g. 'chart.result.0.meta') to return just that part instead of the whole payload.\n- **memory_search**(query, limit): Search agent memory for relevant past experiences, observations, and facts.\n- **memory_add**(content, kind) [capability_expand category: write]: Store a new observation or insight in memory for future reference.\n- **research_note**(kind, text): Record a compact, structured note about the current task's state so it survives context compression on long / multi-step tasks. Use for durable findings, open questions still to resolve, decisions / excluded paths, and the immediate next step. One concise sentence per note.\n- **capability_expand**(category): Fetch the full callable schema for every tool in a capability category (e.g. 'hub', 'gateway', 'desktop', 'delegate', 'file', 'memory', 'skill'). The compact tool index always lists every registered tool by name and a one-line summary, but only a static low-risk subset is directly callable each turn. If you need a tool from the index that is not yet callable, call capability_expand with its category first; the matching tools become callable in this turn. Never invent a tool name — expand the category instead.\n- **delegate_task**(goal, context) [capability_expand category: delegate]: Delegate a complex sub-task to an isolated subagent. The subagent gets a fresh context and restricted tool access. Use when a task is self-contained and can be solved independently.\n- **schedule_reentry**(kind, reason, delay_seconds, event_match, max_reentries, deadline_seconds): Register a re-entry so this task can resume later from its current orientation (findings / open questions / next step). Use when work must pause and continue after a delay (kind=time) or when a matching platform event arrives (kind=event), instead of finishing now. The research-ledger state is carried over automatically.\n- **hub_push**(skill_name, visibility, version) [capability_expand category: hub]: Push a local skill to the ModelScope Hub for sharing or backup.\n- **hub_pull**(repo_id, version) [capability_expand category: hub]: Pull a skill from the ModelScope Hub to install locally.\n- **hub_search**(query) [capability_expand category: hub]: Search for skills on the Hub by keyword or description.\n- **hub_sync**(mode, dry_run) [capability_expand category: hub]: Preview or execute sync between local skills and Hub.\n- **platform_action**(platform, action, payload, backend_kind) [capability_expand category: gateway]: Execute an exact registered business action on an external platform through LeapFlow's App Connector layer. Actions must be copied from the App Connector Capability Index and are addressed as domain.operation, e.g. im.send_message or docs.create_markdown. All business fields (chat_id, text, query, etc.) MUST be placed inside `payload`, never at the top level. Example: {\"platform\":\"feishu\",\"action\":\"im.send_message\",\"payload\":{\"chat_id\":\"oc_xxx\",\"text\":\"hello\"}}. Do not invent action names, do not use management actions such as list/guide/connect/status here.\n- **platform_connect**(action, platform, credentials, options, checkpoint) [capability_expand category: gateway]: List, guide, connect, disconnect, remove, or check status for external platforms using the App Connector management namespace. Supports REST and CLI backends. Use this for management actions such as list/guide/preflight/connect/status; use platform_action only for exact registered business actions.\n- **gateway_send**(platform, chat_id, text, thread_id) [capability_expand category: gateway]: Send a message to a connected external platform (Feishu group, Telegram chat, DingTalk conversation, etc.). Requires the platform to be connected via gateway_connect first. Use gateway_connect with action='list' to see connected platforms and available chat IDs.\n- **gateway_connect**(action, platform, credentials, options) [capability_expand category: gateway]: Connect, configure, or manage external platform integrations (Feishu, DingTalk, Telegram, Slack, Discord, etc.). Conversational flow: 1) call 'guide' to get setup steps + required fields, 2) present the steps to the user and ask for ALL required credentials in a single message, 3) call 'connect' with the credentials. Goal: complete in 1–2 user turns. NEVER include credential values in your text response.\n- **plugin_list**(): List the live plugin registry across Tool/Gateway/LLM subsystems for plugin management: which plugins are loaded, their category and fiber lifecycle state. For questions about LeapFlow's own capabilities or whether it supports X, use self_describe instead.\n- **plugin_status**(plugin_id): Get detailed status of a specific plugin: its declared category, runtime dependencies, contributed tools, and fiber lifecycle state.\n- **plugin_versions**(plugin_id): List recorded source versions and active pointer for a profile-scoped plugin.\n- **plugin_propose**(requested_capability, plugin_id, proposed_tools, test_cases, risk_level, evidence): Create a side-effect-free PluginProposal from explicit capability-gap evidence. Use this before plugin_generate when a missing capability should be reviewed. Does not call an LLM, write files, or install anything.\n- **assess_compatibility**(manifest, source_path) [capability_expand category: plugin_management]: Assess whether a foreign plugin manifest or real DSH source bundle is compatible with LeapFlow. A source bundle assessment is static: runtime_ready stays false until restricted Node discovery during plugin_install. Returns component-level verdicts and limitations.\n- **plugin_generate**(plugin_id, description, proposal_id) [capability_expand category: system]: Generate a new ToolPlugin from a natural-language capability description. The LLM produces code that conforms to the ToolPlugin Protocol; it is then rigorously validated (syntax, structure, import, protocol conformance). The generated source is stored in CAS and receives explicit content approval. It DOES NOT install the plugin — installation requires a second, mutation-specific approval via plugin_install.\n- **plugin_install**(plugin_id, code, marketplace_name, source_path, proposal_id, version_label) [capability_expand category: system]: Install a plugin from exactly one source: validated Python code, the configured Python marketplace, or a real DSH source bundle. DSH bundles run restricted Node discovery before registration; only public host tools are installed and client UI limitations are reported. Writes profile-scoped state and mutates the process-global registry. REQUIRES APPROVAL.\n- **plugin_rollback**(plugin_id, version) [capability_expand category: system]: Rollback a profile-scoped plugin to a recorded source version and reload it. REQUIRES APPROVAL.\n- **plugin_reload**(plugin_id, version_label) [capability_expand category: system]: Hot-reload a plugin at runtime. Disposes the old plugin fiber, re-imports its module, and registers a fresh instance. Existing in-flight turns are unaffected (snapshot isolation). REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_disable**(plugin_id) [capability_expand category: system]: Disable a plugin by disposing its fiber, removing its tools from the runtime registry. Cannot disable self_management itself. REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_remove**(plugin_id, delete_source) [capability_expand category: system]: Terminally remove a plugin: dispose its fiber, unregister its tools, remove reload metadata, and optionally delete its profile-scoped source file. Cannot remove self_management itself. REQUIRES APPROVAL.\n- **plugin_unquarantine**(plugin_id) [capability_expand category: plugin_management]: Restore a quarantined plugin to probation status for re-evaluation. Unfreezes the trust ledger, transitions the proposal back to PROBATION, and reloads the plugin. REQUIRES APPROVAL.\n- **plugin_enable**(plugin_id) [capability_expand category: system]: Re-enable a previously disabled plugin by reloading its module and registering a fresh instance. REQUIRES APPROVAL.\n- **self_describe**(facet): Answer questions about LeapFlow ITSELF from live runtime evidence — its identity/version/model, what tools and plugins it has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of self-awareness. Prefer this over reading LeapFlow's own source code. Pick facet=capabilities for 'what can you do / do you support X', identity for version/model, runtime for context/posture, or all for a full self-check when explicitly requested.\n- **runtime_snapshot**(): Lightweight (~150 token) flat snapshot of current runtime state: model, context budget, posture, disclosure level, turn count, cache hit rate, uptime, tool count, and pending approvals.\n- **schedule_create**(trigger_expression, instruction, execution_mode, max_retries, delivery_target) [capability_expand category: scheduler]: Create a new scheduled task. Specify a trigger expression (e.g. '30m', 'every 2h', '0 9 * * *') and the instruction to execute on each trigger. Optionally provide an execution mode and a delivery target to receive notifications.\n- **schedule_list**() [capability_expand category: scheduler]: List all scheduled tasks with their current state, trigger, and next due time.\n- **schedule_status**(task_id) [capability_expand category: scheduler]: Get detailed status and recent execution history for a scheduled task.\n- **schedule_pause**(task_id) [capability_expand category: scheduler]: Pause a scheduled task so it stops firing without being cancelled.\n- **schedule_resume**(task_id) [capability_expand category: scheduler]: Resume a paused scheduled task — re-arms it and recalculates next due time.\n- **schedule_cancel**(task_id) [capability_expand category: scheduler]: Cancel a scheduled task permanently.\n- **tool_search**(query, max_results): Search all registered tools by keyword relevance. Returns a ranked list of matching tools with name, category, and summary. Use this when you need a tool but are unsure of its exact name or category.\n- **tool_describe**(tool_name): Get the full callable schema of a single tool by exact name. Returns the tool's description, parameters, and metadata. Use after tool_search to inspect a specific tool before calling it.\n- **hw_list**(): List connected hardware devices with their channel counts and measured quantities. Start here; it does not include operating limits.\n- **hw_describe**(device_id): Return the full reference for one device: every channel, its unit, its operating envelope, rate limit, reversibility, and required interlocks, plus any prior outcomes recorded for its writable channels. Required before commanding a device.\n- **hw_read**(device_id, channel_id): Read the current value of one device channel. Has no physical effect.\n- **hw_status**(device_id): Report connection health, halt capability, and recent notable events (threshold excursions, lost samples, stalled channels) for one device.\n- **hw_configure**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Set a configuration value or setpoint on a channel declaring effect=configure. Setpoints often have inertia: the value may need time to stabilise.\n- **hw_actuate**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Command motion or output on a channel declaring effect=actuate. This moves physical hardware; a repeat from an unknown state is not a safe retry.\n- **hw_dispense**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Consume an irreversible resource on a channel declaring effect=dispense. Running this twice dispenses twice; never repeat it after a failure without first verifying what already happened.\n- **hw_estop**(device_id) [capability_expand category: hardware]: Stop all motion and output on a device immediately. Never requires approval; use it whenever device behaviour is unexpected.\n- **hw_batch_actuate**(device_id, commands, dry_run, verify) [capability_expand category: hardware]: Write multiple actuator channels in one atomic transaction. Used for coordinated multi-joint control where all positions must be commanded simultaneously (e.g. robot arm joint-space control).\n- **hw_control_start**(device_id, mode, targets, frequency_hz, trajectory, policy, gains) [capability_expand category: hardware]: Start a real-time control loop on a robot device. Supports PID position tracking, impedance control, trajectory following, and neural policy chains.\n- **hw_control_stop**(device_id) [capability_expand category: hardware]: Stop the real-time control loop on a device. Gracefully halts the device and returns session statistics.\n- **hw_control_status**(device_id) [capability_expand category: hardware]: Get the status and statistics of a running control loop. Returns frequency, jitter, overruns, and policy state.\n- **hw_context_snapshot**(level) [capability_expand category: hardware]: Get a structured snapshot of all hardware devices and their state. Use this before planning physical manipulation tasks to understand what devices are available and what they can do.\n- **hw_health**(device_id) [capability_expand category: hardware]: Check hardware device health status. Without device_id, returns fleet-wide health summary. With device_id, returns health details for that specific device.\n- **hw_policy_infer**(device_id, policy, task, execute, chunk_size, verify, compute_budget) [capability_expand category: hardware]: Execute VLA policy inference on a robot device. Reads observation, runs policy, optionally writes action to actuators. Supports local (HuggingFace) and remote (PolicyServer) policies.\n- **hw_policy_episode**(action, device_id, policy, task) [capability_expand category: hardware]: Manage policy inference episode lifecycle. Start an episode to begin a task, stop to finalise and get stats.\n- **hw_policy_list**(source) [capability_expand category: hardware]: List available VLA policies and their capabilities.\n- **hw_orchestrate**(mode, operations, barrier_timeout_s, label) [capability_expand category: hardware]: Execute coordinated operations across multiple hardware devices. Supports sequential (one-at-a-time), parallel (concurrent), and barrier (synchronised) execution modes. Any failure in a parallel group triggers halt_all on every participating device for safety.\n- **session_search**(query, limit) [capability_expand category: unclassified]: Search past conversation sessions in the current workspace for relevant context.\n- **session_list**(limit): List recent conversation sessions in the current workspace with stable ids, titles, dates, and summaries. Use for browsing past tasks or when user asks to see history without specific search terms. No keywords needed — returns chronological list.\n- **session_detail**(session_id, limit, offset, include_inactive) [capability_expand category: unclassified]: Read a paginated persisted transcript for one past conversation session in the current workspace. Use after session_list or session_search returns a session_id.\n- **click**(element_index) [capability_expand category: desktop]: Click a UI element by its element_index (from the latest observe_ui snapshot)\n- **get_clipboard**() [capability_expand category: desktop]: Read current clipboard text content\n- **list_apps**(filter, running_only) [capability_expand category: desktop]: List available applications on this system. Use to discover correct bundle_id before switch_app.\n- **list_windows**() [capability_expand category: desktop]: List all top-level windows with pid, window_id, title, and per-window state (minimized, on-screen). Call this first to pick the pid and window_id that observe_ui and other window tools require.\n- **observe_ui**(pid, window_id, query) [capability_expand category: desktop]: Snapshot one window's actionable UI elements, each tagged with an element_index for click/right_click/read_text. Re-observe after actions — indices belong to one snapshot. Requires the window's pid and window_id from list_windows.\n- **open_url**(url, app_id) [capability_expand category: desktop]: Open a URL in the default or specified browser\n- **read_text**(element_index) [capability_expand category: desktop]: Read the text content of a specific UI element from the latest snapshot\n- **right_click**(element_index) [capability_expand category: desktop]: Right-click a UI element to open its context menu. Returns visible menu items.\n- **screenshot**(pid, window_id) [capability_expand category: desktop]: Capture a screenshot for visual verification. With pid + window_id captures that window (works across all displays); defaults to the last observed window, or the full desktop when no window has been observed.\n- **scroll**(element_index, direction, amount, pid, window_id) [capability_expand category: desktop]: Scroll a scrollable area of a window. Omit element_index to scroll the window's focused/page scroller; pass one to scroll an exact element from the latest snapshot.\n- **select_text**(element_index) [capability_expand category: desktop]: Select all text in a UI element (focus + select-all, for subsequent copy)\n- **set_clipboard**(text) [capability_expand category: desktop]: Write text to the clipboard\n- **shortcut**(keys) [capability_expand category: desktop]: Execute a keyboard shortcut\n- **switch_app**(app_id) [capability_expand category: desktop]: Switch to an app (launch if needed, activate, verify)\n- **type_text**(text) [capability_expand category: desktop]: Type text into the currently focused element\n- **wait**(seconds) [capability_expand category: desktop]: Wait for a specified duration before continuing\n- **wait_until**(condition, pid, window_id, timeout, poll_interval) [capability_expand category: desktop]: Wait until a UI condition is met (polls UI tree). Returns elements when found or on timeout.\n- **wait_until_stable**(timeout, poll_interval, pid, window_id) [capability_expand category: desktop]: Wait until the UI stops changing (element set stabilizes across polls).\n\n## App Connector Capability Index\nLeapFlow can onboard and manage external apps through `platform_connect` and execute exact registered business actions through `platform_action`.\nFor requests about connecting, setting up, configuring, enabling, or managing a supported app, use `platform_connect` first instead of generating SDK/Webhook sample code.\n`platform_connect.action` is the management namespace: list, guide, preflight, connect, disconnect, remove, status, events_start, events_stop, events_status.\n`platform_action.action` is only for exact registered platform business actions listed below, such as `im.send_message`; never use management actions like `list` or `guide` there.\nAll business fields MUST go inside `payload`; top-level keys are only `platform`, `action`, and `payload`.\nDo not invent platform IDs or platform action names. If the needed action is not listed, ask for discovery/clarification instead of guessing.\nUse `platform_connect` with `action='guide'` and the matching `platform` to start onboarding; use `action='list'` when the app is unclear.\nWhen a pending onboarding state is present, continue from that state with `platform_connect` instead of asking the user to restate the app.\nSupported apps:\n- `api_server`: API Server (OpenAI Compatible) (category=api; backend=adapter; platform_action actions=none registered)\n- `dingtalk`: 钉钉 (DingTalk) (category=im; backend=adapter; platform_action actions=none registered)\n- `feishu`: 飞书 (Feishu/Lark) (category=im; backend=cli)\n - `calendar.create_event` payload={summary*, start_time*, end_time*, attendees} [write/high]\n - `docs.create_markdown` payload={title*, markdown*, folder_token} [write/medium]\n - `drive.search` payload={query*, limit} [read/medium]\n - `im.add_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.download_resource` payload={message_id*, file_key*, type*} [read/low]\n - `im.get_messages` payload={message_ids*} [read/low]\n - `im.list_chats` payload={page_size} [read/low]\n - `im.list_messages` payload={chat_id*} [read/low]\n - `im.list_thread_messages` payload={thread*} [read/low]\n - `im.remove_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.reply_message` payload={message_id*, text*} [send/high]\n - `im.search_chats` payload={query*, page_size} [read/low]\n - `im.search_messages` payload={query*} [read/medium]\n - `im.send_message` payload={chat_id*, text*, thread_id} [send/high]\n - `im.update_card` payload={token*, card*} [write/medium]\n - `im.update_message` payload={message_id*, text*} [write/medium]\n - `mail.search_unread` payload={query, limit} [read/high]\n - `sheets.append_row` payload={spreadsheet_token*, sheet_id*, values*} [write/medium]\n - `task.create` payload={title*, description, due_time} [write/medium]\n- `telegram`: Telegram (category=social; backend=adapter; platform_action actions=none registered)\n- `webhook`: Webhook (Generic) (category=webhook; backend=adapter; platform_action actions=none registered)\n\n## Learned Skills\nYou have access to the following learned skills. Use `skills_list` to browse or `skill_view` to read details:\n- api_designer: REST and GraphQL API design with OpenAPI spec generation and best practices [API, REST, GraphQL]\n- code_review: Structured code review for security, performance, and style [code, review, security]\n- data_analysis: CSV/JSON data analysis with statistical summaries, quality checks, and visualiza [data, analysis, statistics]\n- devops_helper: CI/CD pipeline configuration, Docker orchestration, and deployment strategy [devops, CI/CD, Docker]\n- document_writer: Structured document generation: technical docs, API references, reports, and REA [documentation, technical-writing, README]\n- git_workflow: Git operation orchestration: branching, commits, conflicts, and PR workflow [git, version-control, branching]\n- i18n_helper: Internationalization workflow: string extraction, translation, locale management [i18n, internationalization, localization]\n- project_analysis: Project structure analysis, dependency audit, and tech stack identification [project, analysis, dependencies]\n- security_audit: Security vulnerability scanning strategy, threat modeling, and fix recommendatio [security, vulnerability, audit]\n- shell_automation: Natural language to shell commands with safety review and step-by-step execution [shell, terminal, automation]\n- test_writer: Generate unit and integration tests from source code with edge-case coverage [testing, unit-test, integration-test]\n- web_research: Multi-step web search with cross-verification and structured synthesis [web, search, research]\n\n## Tool Usage\nTools are normally invoked through the native function-calling mechanism, not by writing JSON in your reply\ntext. Only if the provider signals that native function calling is unavailable for this turn, fall back to a\nsingle JSON code block: `{\"name\": \"tool_name\", \"arguments\": {\"key\": \"value\"}}` — use this fallback format\nonly, never both. Only call a tool whose exact name appears in the tool index above and is currently callable\n(or reachable via `capability_expand`). Never invent, rename, alias, or guess a tool name, platform ID, or\nplatform action from argument shape or wording — if the index does not list it, it does not exist.\n`platform_connect.action` (list/guide/preflight/connect/disconnect/remove/status/events_start/events_stop/\nevents_status) is the App Connector management namespace; `platform_action.action` only accepts exact\nregistered business actions such as `im.send_message` shown in the App Connector Capability Index — never mix\nthe two namespaces. If a tool call returns an unknown/unavailable result, use the returned suggestions or\navailable names for a single retry instead of trying further variations of the same guess.\n\n**Side-effect action rule** (`platform_action` with effect=send/write/execute):\n- Call each unique action+payload **exactly once**. Never include duplicates in the same turn.\n- Once the result returns `\"completed\": true`, that action is DONE for this task. Do NOT call it again\n in any subsequent turn — immediately summarize the result for the user instead.\n- The system enforces idempotency: duplicate calls are blocked and will not execute.\n- If the user explicitly requests sending/writing multiple times, use distinct payloads per call.\n\n**Resource identifier provenance rule**:\n- NEVER fabricate, guess, or infer resource identifiers (chat_id, message_id, file_key, user_id, etc.).\n Every resource ID used in a side-effect action MUST come from a successful API response in this session.\n- If a read/list action fails (e.g. authorization error), you do NOT have valid resource IDs.\n Report the failure to the user — do NOT attempt the dependent write/send action with a guessed ID.\n- When a tool result contains `\"llm_instruction\"`, follow it exactly.\n\n## Guidelines\n1. **Direct answers first**: If you already know the answer, respond directly without tools.\n2. **Avoid redundant tool calls**: Do not call the same tool with the same arguments more than once in the same user turn. When an existing tool result already answers the user's request, stop calling tools and answer directly.\n3. **Use tools proactively**: When the user asks about files, time, system state, or needs actions performed, use the appropriate tool.\n4. **Chain tools when needed**: You can call multiple tools in sequence (e.g., list files → read file → summarize).\n5. **Handle failures gracefully**: If a tool fails, explain what went wrong and suggest alternatives. If it failed because of an unknown tool/platform/action name, retry once with an exact name from the returned suggestions, then explain rather than keep guessing.\n6. **Summarize results naturally**: After tool execution, synthesize the results into a helpful answer rather than dumping raw output.\n7. **Stay conversational**: Maintain a natural, helpful tone. Acknowledge context from earlier in the conversation.\n8. **Recall past work**:\n - Broad queries (\"之前做了什么\", \"列出任务\"): answer from the \"Recent Task History\" section already in your context. If insufficient, call session_list.\n - Specific lookups (\"上次那个配置怎么改的\"): call session_search with relevant phrases (NOT single characters).\n - Do NOT call search tools repeatedly with keyword fragments. One well-phrased call is better than ten fragmented ones.\n\n## Coding & Verification\nWhen working with code, prefer the precise built-in tools over ad-hoc shell: use `repo_map` to orient in an\nunfamiliar project, `code_search`/`file_find` to locate and `code_intel` for symbols, `edit_file` (anchored\nsearch-replace, or a unified `diff`) to change files — never rewrite a whole file to change a few lines — and\n`git_query` to inspect diffs/log. After edits, check `syntax_ok` in the result and run `test_run`/`lint_check`\nbefore declaring the work done. Batch independent read-only calls (search/read/query) into a single turn — they\nrun in parallel.\n\n## Presentation Style\n1. **Polished Markdown only**: Format user-facing answers with clean Markdown headings, short paragraphs, and concise bullets. Use tables only when they improve comparison or scanning.\n2. **Terminal-friendly layout**: Keep lines readable in a TUI; avoid dense walls of text, deeply nested lists, oversized ASCII art, or heavy visual blocks.\n3. **Elegant emphasis**: Use bold text sparingly for key terms and conclusions. Avoid excessive emojis, decorative symbols, repeated separators, or visual noise.\n4. **Theme-safe colors**: Do not emit ANSI escape codes, HTML color tags, Rich markup, or hardcoded color names. Rely on the TUI theme to render Markdown professionally.\n5. **No leaked tool protocol**: Never show tool-call JSON, internal schemas, raw observations, tool result payloads, or hidden reasoning in the final answer unless the user explicitly asks for raw/debug output. Treat any prior `{\"name\": ..., \"arguments\": ...}` blocks and `Tool result (...)` messages as internal execution context only.\n6. **Professional closure**: End with a concise conclusion or next step when helpful; avoid rambling after the useful answer is complete.\n\nWhen finished with all tool calls, respond normally without a JSON block, tool-call transcript, or process log.\n\n## Task Contract\n- Task ID: turn-3\n- Original user request: Actuate the bench_r8 setpoint to 60.\n- Workspace root: \n- Allowed roots: \n- Treat relative project paths as relative to the workspace root; never infer `.` as the project root when a workspace root is provided.\n- Workspace boundary is enforced by tools: do not read, search, edit, or run commands against paths outside the allowed roots unless the user explicitly requests an external path and the tool/approval policy permits it.\n- LeapFlow workspace config is optional at `/.leapflow/config.yaml`; runtime config is loaded from `~/.leapflow/config/user.yaml` and `~/.leapflow/profiles//config/*.yaml`.\n- Preserve this task contract across summarization, compression, tool loops, and memory retrieval.\n" }, { "role": "assistant", @@ -40,12 +40,14 @@ "hw_actuate", "hw_batch_actuate", "hw_configure", + "hw_context_snapshot", "hw_control_start", "hw_control_status", "hw_control_stop", "hw_describe", "hw_dispense", "hw_estop", + "hw_health", "hw_list", "hw_orchestrate", "hw_policy_episode", diff --git a/tests/_fixtures/cassettes/r8_hardware/cassette-model-b65a352522b4ce5c.cassette.json b/tests/_fixtures/cassettes/r8_hardware/cassette-model-b65a352522b4ce5c.cassette.json deleted file mode 100644 index 21546456..00000000 --- a/tests/_fixtures/cassettes/r8_hardware/cassette-model-b65a352522b4ce5c.cassette.json +++ /dev/null @@ -1,88 +0,0 @@ -{ - "fingerprint": "b65a352522b4ce5cb18f00bf32be62c0240dfc26e24e819b1422b788419e8a00", - "note": "captured in seed mode", - "request": { - "model": "cassette-model", - "stream": false, - "messages": [ - { - "role": "system", - "content": "You are LeapFlow, an intelligent assistant that can both converse naturally and take real actions on the user's computer.\n\n## Capabilities\nThe tool index below lists **every** registered tool by name and a one-line summary — this is the complete\ncapability contract; nothing else exists. Only a subset is directly callable this turn (via native tool calling,\nnot a JSON block in your reply). If you need a tool from the index that is not yet callable, call\n`capability_expand` with its category name first — the matching tools become callable immediately after.\nWhen the user asks about LeapFlow **itself** — its identity/version/model, what tools, plugins, or capabilities\nit has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of\nself-awareness — answer from live runtime evidence via `self_describe` (facet=capabilities for \"what can you\ndo / do you support X\", identity for version/model, runtime for context/posture, or `all` for a full self-check),\nnever by reading LeapFlow's own source code, documentation, or memory. Report configuration-dependent or\nunavailable capabilities as limitations rather than inferring them.\n- **text_search**(text, pattern): Search for a regex pattern in text.\n- **text_replace**(text, old, new, count) [capability_expand category: write]: Replace occurrences of a substring in text.\n- **time_get**(): Get current date and time.\n- **env_info**(): Get system environment information (OS, Python version, cwd).\n- **skills_list**(query, category, source): List available learned skills. Use when user asks about capabilities or you need a specific skill.\n- **skill_view**(name): View the full content of a specific skill document.\n- **skill_list_files**(name) [capability_expand category: skills]: List all support files in a skill directory (configs, templates, examples). Use before skill_file_read to discover available files.\n- **skill_file_read**(name, file_path) [capability_expand category: skills]: Read an individual support file from a skill directory. Use skill_list_files first to discover available files.\n- **code_intel**(path, operation): Precise document symbols (outline) for a source file: classes, functions, and methods with line ranges. Python uses an exact AST parse; other languages use a keyword-prefix scan. Prefer over file_read mode=symbols for accurate navigation before editing. Read-only.\n- **repo_map**(path): Compact project orientation for a repository root: languages, detected test/lint commands, top-level structure, entry points, manifest, and VCS branch. Call this first when entering an unfamiliar codebase. Read-only.\n- **scm_sync**(action, cwd, remote, pull_ref, push_ref, timeout) [capability_expand category: scm]: Run a typed git SCM action. Use this instead of shell_run for git pull/push/status. For 'pull origin main then push', set action='pull_then_push', remote='origin', pull_ref='main', and omit push_ref so LeapFlow pushes the current local branch.\n- **git_query**(action, cwd, ref, path, staged, max_count, stat): Read-only structured git inspection: action=diff|log|status|branch|show. Prefer over shell_run for reading repo state — output is clipped, redacted, and log/branch are parsed into structured fields. Use scm_sync for pull/push.\n- **git_write**(action, cwd, message, stage_all, name, ref, create) [capability_expand category: scm]: Mutating git actions: action=commit (message, stage_all), branch (create+switch), checkout (switch; create=true for -b). Approval-gated. Use scm_sync for pull/push and git_query for reads.\n- **test_run**(command, cwd, timeout): Run the project's test suite and return structured results (framework, passed/failed counts, failing tests). Auto-detects the runner (pytest/npm/go/cargo) or uses a configured/explicit command; executes via the governed shell. ok=true means the runner executed — see 'success' for pass/fail.\n- **lint_check**(command, cwd, timeout): Run the project's linter and return a structured clean/issue result. Auto-detects the linter (ruff/eslint/go vet/clippy) or uses a configured/explicit command; executes via the governed shell. ok=true means the linter ran — see 'clean'.\n- **file_list**(path, pattern, depth): List files and directories at a given path. Use depth=1 or depth=2 to get a recursive tree in one call instead of listing each sub-directory separately.\n- **file_read**(path, max_lines, start_line, max_chars, mode): Read text file content with adaptive context governance. For large or unfamiliar files, prefer mode='outline' or mode='symbols' first, then use mode='raw' with start_line/max_lines for the specific range you actually need. For LeapFlow's own settings, use config_list / config_get / config_set — its config files are outside the workspace and not readable here.\n- **file_write**(path, content, mode) [capability_expand category: write]: Write content to a file (overwrite or append).\n- **code_search**(pattern, patterns, path, glob, ignore_case, multiline, max_results, context_lines): Search file CONTENTS by regex pattern across a directory tree (ripgrep-backed). Requires a regex pattern. NOT for listing or browsing directory contents — use file_list for that. Prefer this over shell_run grep: faster, skips VCS/dependency/build dirs, and returns structured path:line:column matches. Batch related lookups into ONE call via `patterns` (OR-combined, single pass) instead of issuing several separate searches. Use file_read for the surrounding context of a hit.\n- **file_find**(glob, path, max_results): Find files by a recursive glob pattern under a base path (e.g. '**/test_*.py' or '*.md'). Prefer this over shell_run find; skips VCS/dependency/build dirs.\n- **edit_file**(path, edits, dry_run, diff) [capability_expand category: write]: Apply targeted, anchored search-replace edits to an EXISTING text file (use file_write to create/overwrite). Each edit is {original_text, new_text, replace_all?}; original_text must match exactly and uniquely (or set replace_all) — a non-unique or missing anchor is rejected so files are never corrupted. Set dry_run to preview. Alternatively pass a unified 'diff' to apply its hunks as anchored edits. Far cheaper and safer than rewriting a whole file.\n- **shell_run**(command, cwd, timeout) [capability_expand category: shell]: Execute a one-shot shell command with timeout protection. Runs in the active workspace; paths resolving outside it are refused. Reach for a structured tool first when one fits — web_fetch for anything over HTTP(S), git_query/scm_sync for git, code_search/file_find/file_read for the repo, config_get/config_set for LeapFlow's own settings — because those report typed results, while a failed shell command can only be diagnosed from its exit code and stderr. Every shell run counts as an external side effect, so a failure stops the rest of the batch and is not retried automatically.\n- **terminal_open**(command, cwd, shell) [capability_expand category: terminal]: Open a PERSISTENT shell session (REPL/dev server/watch), returning a session_id for terminal_send/read/close. Disabled unless tools.terminal_session_enabled is set. For one-shot commands use shell_run instead.\n- **terminal_send**(session_id, input, wait) [capability_expand category: terminal]: Send a line of input to a persistent terminal session and return output captured shortly after.\n- **terminal_read**(session_id, wait): Drain buffered output from a persistent terminal session (optionally waiting briefly first).\n- **terminal_close**(session_id) [capability_expand category: terminal]: Terminate a persistent terminal session and release its process group.\n- **terminal_list**(): List active persistent terminal sessions.\n- **config_list**(category, limit): List LeapFlow's own writable settings (LLM model and endpoint, daemon, memory, perception, gateway, …) with current values. Use this to discover the exact key before changing anything. Optionally narrow by `category`. This is the only correct way to inspect LeapFlow configuration — never read config files from disk.\n- **config_get**(key): Read one LeapFlow setting by key (e.g. 'llm.model', 'llm.base_url', 'daemon.log_level'), returning its current value, type, scopes, and whether a change needs a daemon restart. There is no 'llm.provider' key: provider behavior is inferred from the OpenAI-compatible 'llm.base_url'. Never read LeapFlow config files from disk — use this.\n- **config_set**(key, value, scope) [capability_expand category: config]: Change one LeapFlow setting by key, e.g. switch an OpenAI-compatible LLM with key='llm.model' and key='llm.base_url'. There is no 'llm.provider' key: provider behavior is inferred from the endpoint. Values are validated and coerced; credentials are stored in the vault automatically. Call config_list or config_get first if unsure of the key. The result states whether a `leap daemon restart` is required. Never edit LeapFlow config files directly.\n- **web_fetch**(url, select, timeout, max_bytes): Read a URL over HTTP(S) and get back extracted, context-sized content: parsed JSON for API endpoints, readable text plus links for web pages. Use this for anything on the internet — prices, docs, releases, articles — instead of running curl through shell_run: it reports real HTTP status codes, retries rate limits on its own, and is a plain read so a retry is always safe. For JSON APIs pass `select` with a dotted path (e.g. 'chart.result.0.meta') to return just that part instead of the whole payload.\n- **memory_search**(query, limit): Search agent memory for relevant past experiences, observations, and facts.\n- **memory_add**(content, kind) [capability_expand category: write]: Store a new observation or insight in memory for future reference.\n- **research_note**(kind, text): Record a compact, structured note about the current task's state so it survives context compression on long / multi-step tasks. Use for durable findings, open questions still to resolve, decisions / excluded paths, and the immediate next step. One concise sentence per note.\n- **capability_expand**(category): Fetch the full callable schema for every tool in a capability category (e.g. 'hub', 'gateway', 'desktop', 'delegate', 'file', 'memory', 'skill'). The compact tool index always lists every registered tool by name and a one-line summary, but only a static low-risk subset is directly callable each turn. If you need a tool from the index that is not yet callable, call capability_expand with its category first; the matching tools become callable in this turn. Never invent a tool name — expand the category instead.\n- **delegate_task**(goal, context) [capability_expand category: delegate]: Delegate a complex sub-task to an isolated subagent. The subagent gets a fresh context and restricted tool access. Use when a task is self-contained and can be solved independently.\n- **schedule_reentry**(kind, reason, delay_seconds, event_match, max_reentries, deadline_seconds): Register a re-entry so this task can resume later from its current orientation (findings / open questions / next step). Use when work must pause and continue after a delay (kind=time) or when a matching platform event arrives (kind=event), instead of finishing now. The research-ledger state is carried over automatically.\n- **hub_push**(skill_name, visibility, version) [capability_expand category: hub]: Push a local skill to the ModelScope Hub for sharing or backup.\n- **hub_pull**(repo_id, version) [capability_expand category: hub]: Pull a skill from the ModelScope Hub to install locally.\n- **hub_search**(query) [capability_expand category: hub]: Search for skills on the Hub by keyword or description.\n- **hub_sync**(mode, dry_run) [capability_expand category: hub]: Preview or execute sync between local skills and Hub.\n- **platform_action**(platform, action, payload, backend_kind) [capability_expand category: gateway]: Execute an exact registered business action on an external platform through LeapFlow's App Connector layer. Actions must be copied from the App Connector Capability Index and are addressed as domain.operation, e.g. im.send_message or docs.create_markdown. All business fields (chat_id, text, query, etc.) MUST be placed inside `payload`, never at the top level. Example: {\"platform\":\"feishu\",\"action\":\"im.send_message\",\"payload\":{\"chat_id\":\"oc_xxx\",\"text\":\"hello\"}}. Do not invent action names, do not use management actions such as list/guide/connect/status here.\n- **platform_connect**(action, platform, credentials, options, checkpoint) [capability_expand category: gateway]: List, guide, connect, disconnect, remove, or check status for external platforms using the App Connector management namespace. Supports REST and CLI backends. Use this for management actions such as list/guide/preflight/connect/status; use platform_action only for exact registered business actions.\n- **gateway_send**(platform, chat_id, text, thread_id) [capability_expand category: gateway]: Send a message to a connected external platform (Feishu group, Telegram chat, DingTalk conversation, etc.). Requires the platform to be connected via gateway_connect first. Use gateway_connect with action='list' to see connected platforms and available chat IDs.\n- **gateway_connect**(action, platform, credentials, options) [capability_expand category: gateway]: Connect, configure, or manage external platform integrations (Feishu, DingTalk, Telegram, Slack, Discord, etc.). Conversational flow: 1) call 'guide' to get setup steps + required fields, 2) present the steps to the user and ask for ALL required credentials in a single message, 3) call 'connect' with the credentials. Goal: complete in 1–2 user turns. NEVER include credential values in your text response.\n- **plugin_list**(): List the live plugin registry across Tool/Gateway/LLM subsystems for plugin management: which plugins are loaded, their category and fiber lifecycle state. For questions about LeapFlow's own capabilities or whether it supports X, use self_describe instead.\n- **plugin_status**(plugin_id): Get detailed status of a specific plugin: its declared category, runtime dependencies, contributed tools, and fiber lifecycle state.\n- **plugin_versions**(plugin_id): List recorded source versions and active pointer for a profile-scoped plugin.\n- **plugin_propose**(requested_capability, plugin_id, proposed_tools, test_cases, risk_level, evidence): Create a side-effect-free PluginProposal from explicit capability-gap evidence. Use this before plugin_generate when a missing capability should be reviewed. Does not call an LLM, write files, or install anything.\n- **assess_compatibility**(manifest, source_path) [capability_expand category: plugin_management]: Assess whether a foreign plugin manifest or real DSH source bundle is compatible with LeapFlow. A source bundle assessment is static: runtime_ready stays false until restricted Node discovery during plugin_install. Returns component-level verdicts and limitations.\n- **plugin_generate**(plugin_id, description, proposal_id) [capability_expand category: system]: Generate a new ToolPlugin from a natural-language capability description. The LLM produces code that conforms to the ToolPlugin Protocol; it is then rigorously validated (syntax, structure, import, protocol conformance). The generated source is stored in CAS and receives explicit content approval. It DOES NOT install the plugin — installation requires a second, mutation-specific approval via plugin_install.\n- **plugin_install**(plugin_id, code, marketplace_name, source_path, proposal_id, version_label) [capability_expand category: system]: Install a plugin from exactly one source: validated Python code, the configured Python marketplace, or a real DSH source bundle. DSH bundles run restricted Node discovery before registration; only public host tools are installed and client UI limitations are reported. Writes profile-scoped state and mutates the process-global registry. REQUIRES APPROVAL.\n- **plugin_rollback**(plugin_id, version) [capability_expand category: system]: Rollback a profile-scoped plugin to a recorded source version and reload it. REQUIRES APPROVAL.\n- **plugin_reload**(plugin_id, version_label) [capability_expand category: system]: Hot-reload a plugin at runtime. Disposes the old plugin fiber, re-imports its module, and registers a fresh instance. Existing in-flight turns are unaffected (snapshot isolation). REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_disable**(plugin_id) [capability_expand category: system]: Disable a plugin by disposing its fiber, removing its tools from the runtime registry. Cannot disable self_management itself. REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_remove**(plugin_id, delete_source) [capability_expand category: system]: Terminally remove a plugin: dispose its fiber, unregister its tools, remove reload metadata, and optionally delete its profile-scoped source file. Cannot remove self_management itself. REQUIRES APPROVAL.\n- **plugin_unquarantine**(plugin_id) [capability_expand category: plugin_management]: Restore a quarantined plugin to probation status for re-evaluation. Unfreezes the trust ledger, transitions the proposal back to PROBATION, and reloads the plugin. REQUIRES APPROVAL.\n- **plugin_enable**(plugin_id) [capability_expand category: system]: Re-enable a previously disabled plugin by reloading its module and registering a fresh instance. REQUIRES APPROVAL.\n- **self_describe**(facet): Answer questions about LeapFlow ITSELF from live runtime evidence — its identity/version/model, what tools and plugins it has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of self-awareness. Prefer this over reading LeapFlow's own source code. Pick facet=capabilities for 'what can you do / do you support X', identity for version/model, runtime for context/posture, or all for a full self-check when explicitly requested.\n- **runtime_snapshot**(): Lightweight (~150 token) flat snapshot of current runtime state: model, context budget, posture, disclosure level, turn count, cache hit rate, uptime, tool count, and pending approvals.\n- **schedule_create**(trigger_expression, instruction, execution_mode, max_retries, delivery_target) [capability_expand category: scheduler]: Create a new scheduled task. Specify a trigger expression (e.g. '30m', 'every 2h', '0 9 * * *') and the instruction to execute on each trigger. Optionally provide an execution mode and a delivery target to receive notifications.\n- **schedule_list**() [capability_expand category: scheduler]: List all scheduled tasks with their current state, trigger, and next due time.\n- **schedule_status**(task_id) [capability_expand category: scheduler]: Get detailed status and recent execution history for a scheduled task.\n- **schedule_pause**(task_id) [capability_expand category: scheduler]: Pause a scheduled task so it stops firing without being cancelled.\n- **schedule_resume**(task_id) [capability_expand category: scheduler]: Resume a paused scheduled task — re-arms it and recalculates next due time.\n- **schedule_cancel**(task_id) [capability_expand category: scheduler]: Cancel a scheduled task permanently.\n- **tool_search**(query, max_results): Search all registered tools by keyword relevance. Returns a ranked list of matching tools with name, category, and summary. Use this when you need a tool but are unsure of its exact name or category.\n- **tool_describe**(tool_name): Get the full callable schema of a single tool by exact name. Returns the tool's description, parameters, and metadata. Use after tool_search to inspect a specific tool before calling it.\n- **hw_list**(): List connected hardware devices with their channel counts and measured quantities. Start here; it does not include operating limits.\n- **hw_describe**(device_id): Return the full reference for one device: every channel, its unit, its operating envelope, rate limit, reversibility, and required interlocks, plus any prior outcomes recorded for its writable channels. Required before commanding a device.\n- **hw_read**(device_id, channel_id): Read the current value of one device channel. Has no physical effect.\n- **hw_status**(device_id): Report connection health, halt capability, and recent notable events (threshold excursions, lost samples, stalled channels) for one device.\n- **hw_configure**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Set a configuration value or setpoint on a channel declaring effect=configure. Setpoints often have inertia: the value may need time to stabilise.\n- **hw_actuate**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Command motion or output on a channel declaring effect=actuate. This moves physical hardware; a repeat from an unknown state is not a safe retry.\n- **hw_dispense**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Consume an irreversible resource on a channel declaring effect=dispense. Running this twice dispenses twice; never repeat it after a failure without first verifying what already happened.\n- **hw_estop**(device_id) [capability_expand category: hardware]: Stop all motion and output on a device immediately. Never requires approval; use it whenever device behaviour is unexpected.\n- **hw_batch_actuate**(device_id, commands, dry_run, verify) [capability_expand category: hardware]: Write multiple actuator channels in one atomic transaction. Used for coordinated multi-joint control where all positions must be commanded simultaneously (e.g. robot arm joint-space control).\n- **session_search**(query, limit) [capability_expand category: unclassified]: Search past conversation sessions in the current workspace for relevant context.\n- **session_list**(limit): List recent conversation sessions in the current workspace with stable ids, titles, dates, and summaries. Use for browsing past tasks or when user asks to see history without specific search terms. No keywords needed — returns chronological list.\n- **session_detail**(session_id, limit, offset, include_inactive) [capability_expand category: unclassified]: Read a paginated persisted transcript for one past conversation session in the current workspace. Use after session_list or session_search returns a session_id.\n- **click**(element_index) [capability_expand category: desktop]: Click a UI element by its element_index (from the latest observe_ui snapshot)\n- **get_clipboard**() [capability_expand category: desktop]: Read current clipboard text content\n- **list_apps**(filter, running_only) [capability_expand category: desktop]: List available applications on this system. Use to discover correct bundle_id before switch_app.\n- **list_windows**() [capability_expand category: desktop]: List all top-level windows with pid, window_id, title, and per-window state (minimized, on-screen). Call this first to pick the pid and window_id that observe_ui and other window tools require.\n- **observe_ui**(pid, window_id, query) [capability_expand category: desktop]: Snapshot one window's actionable UI elements, each tagged with an element_index for click/right_click/read_text. Re-observe after actions — indices belong to one snapshot. Requires the window's pid and window_id from list_windows.\n- **open_url**(url, app_id) [capability_expand category: desktop]: Open a URL in the default or specified browser\n- **read_text**(element_index) [capability_expand category: desktop]: Read the text content of a specific UI element from the latest snapshot\n- **right_click**(element_index) [capability_expand category: desktop]: Right-click a UI element to open its context menu. Returns visible menu items.\n- **screenshot**(pid, window_id) [capability_expand category: desktop]: Capture a screenshot for visual verification. With pid + window_id captures that window (works across all displays); defaults to the last observed window, or the full desktop when no window has been observed.\n- **scroll**(element_index, direction, amount, pid, window_id) [capability_expand category: desktop]: Scroll a scrollable area of a window. Omit element_index to scroll the window's focused/page scroller; pass one to scroll an exact element from the latest snapshot.\n- **select_text**(element_index) [capability_expand category: desktop]: Select all text in a UI element (focus + select-all, for subsequent copy)\n- **set_clipboard**(text) [capability_expand category: desktop]: Write text to the clipboard\n- **shortcut**(keys) [capability_expand category: desktop]: Execute a keyboard shortcut\n- **switch_app**(app_id) [capability_expand category: desktop]: Switch to an app (launch if needed, activate, verify)\n- **type_text**(text) [capability_expand category: desktop]: Type text into the currently focused element\n- **wait**(seconds) [capability_expand category: desktop]: Wait for a specified duration before continuing\n- **wait_until**(condition, pid, window_id, timeout, poll_interval) [capability_expand category: desktop]: Wait until a UI condition is met (polls UI tree). Returns elements when found or on timeout.\n- **wait_until_stable**(timeout, poll_interval, pid, window_id) [capability_expand category: desktop]: Wait until the UI stops changing (element set stabilizes across polls).\n\n## App Connector Capability Index\nLeapFlow can onboard and manage external apps through `platform_connect` and execute exact registered business actions through `platform_action`.\nFor requests about connecting, setting up, configuring, enabling, or managing a supported app, use `platform_connect` first instead of generating SDK/Webhook sample code.\n`platform_connect.action` is the management namespace: list, guide, preflight, connect, disconnect, remove, status, events_start, events_stop, events_status.\n`platform_action.action` is only for exact registered platform business actions listed below, such as `im.send_message`; never use management actions like `list` or `guide` there.\nAll business fields MUST go inside `payload`; top-level keys are only `platform`, `action`, and `payload`.\nDo not invent platform IDs or platform action names. If the needed action is not listed, ask for discovery/clarification instead of guessing.\nUse `platform_connect` with `action='guide'` and the matching `platform` to start onboarding; use `action='list'` when the app is unclear.\nWhen a pending onboarding state is present, continue from that state with `platform_connect` instead of asking the user to restate the app.\nSupported apps:\n- `api_server`: API Server (OpenAI Compatible) (category=api; backend=adapter; platform_action actions=none registered)\n- `dingtalk`: 钉钉 (DingTalk) (category=im; backend=adapter; platform_action actions=none registered)\n- `feishu`: 飞书 (Feishu/Lark) (category=im; backend=cli)\n - `calendar.create_event` payload={summary*, start_time*, end_time*, attendees} [write/high]\n - `docs.create_markdown` payload={title*, markdown*, folder_token} [write/medium]\n - `drive.search` payload={query*, limit} [read/medium]\n - `im.add_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.download_resource` payload={message_id*, file_key*, type*} [read/low]\n - `im.get_messages` payload={message_ids*} [read/low]\n - `im.list_chats` payload={page_size} [read/low]\n - `im.list_messages` payload={chat_id*} [read/low]\n - `im.list_thread_messages` payload={thread*} [read/low]\n - `im.remove_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.reply_message` payload={message_id*, text*} [send/high]\n - `im.search_chats` payload={query*, page_size} [read/low]\n - `im.search_messages` payload={query*} [read/medium]\n - `im.send_message` payload={chat_id*, text*, thread_id} [send/high]\n - `im.update_card` payload={token*, card*} [write/medium]\n - `im.update_message` payload={message_id*, text*} [write/medium]\n - `mail.search_unread` payload={query, limit} [read/high]\n - `sheets.append_row` payload={spreadsheet_token*, sheet_id*, values*} [write/medium]\n - `task.create` payload={title*, description, due_time} [write/medium]\n- `telegram`: Telegram (category=social; backend=adapter; platform_action actions=none registered)\n- `webhook`: Webhook (Generic) (category=webhook; backend=adapter; platform_action actions=none registered)\n\n## Learned Skills\nYou have access to the following learned skills. Use `skills_list` to browse or `skill_view` to read details:\n- api_designer: REST and GraphQL API design with OpenAPI spec generation and best practices [API, REST, GraphQL]\n- code_review: Structured code review for security, performance, and style [code, review, security]\n- data_analysis: CSV/JSON data analysis with statistical summaries, quality checks, and visualiza [data, analysis, statistics]\n- devops_helper: CI/CD pipeline configuration, Docker orchestration, and deployment strategy [devops, CI/CD, Docker]\n- document_writer: Structured document generation: technical docs, API references, reports, and REA [documentation, technical-writing, README]\n- git_workflow: Git operation orchestration: branching, commits, conflicts, and PR workflow [git, version-control, branching]\n- i18n_helper: Internationalization workflow: string extraction, translation, locale management [i18n, internationalization, localization]\n- project_analysis: Project structure analysis, dependency audit, and tech stack identification [project, analysis, dependencies]\n- security_audit: Security vulnerability scanning strategy, threat modeling, and fix recommendatio [security, vulnerability, audit]\n- shell_automation: Natural language to shell commands with safety review and step-by-step execution [shell, terminal, automation]\n- test_writer: Generate unit and integration tests from source code with edge-case coverage [testing, unit-test, integration-test]\n- web_research: Multi-step web search with cross-verification and structured synthesis [web, search, research]\n\n## Tool Usage\nTools are normally invoked through the native function-calling mechanism, not by writing JSON in your reply\ntext. Only if the provider signals that native function calling is unavailable for this turn, fall back to a\nsingle JSON code block: `{\"name\": \"tool_name\", \"arguments\": {\"key\": \"value\"}}` — use this fallback format\nonly, never both. Only call a tool whose exact name appears in the tool index above and is currently callable\n(or reachable via `capability_expand`). Never invent, rename, alias, or guess a tool name, platform ID, or\nplatform action from argument shape or wording — if the index does not list it, it does not exist.\n`platform_connect.action` (list/guide/preflight/connect/disconnect/remove/status/events_start/events_stop/\nevents_status) is the App Connector management namespace; `platform_action.action` only accepts exact\nregistered business actions such as `im.send_message` shown in the App Connector Capability Index — never mix\nthe two namespaces. If a tool call returns an unknown/unavailable result, use the returned suggestions or\navailable names for a single retry instead of trying further variations of the same guess.\n\n**Side-effect action rule** (`platform_action` with effect=send/write/execute):\n- Call each unique action+payload **exactly once**. Never include duplicates in the same turn.\n- Once the result returns `\"completed\": true`, that action is DONE for this task. Do NOT call it again\n in any subsequent turn — immediately summarize the result for the user instead.\n- The system enforces idempotency: duplicate calls are blocked and will not execute.\n- If the user explicitly requests sending/writing multiple times, use distinct payloads per call.\n\n**Resource identifier provenance rule**:\n- NEVER fabricate, guess, or infer resource identifiers (chat_id, message_id, file_key, user_id, etc.).\n Every resource ID used in a side-effect action MUST come from a successful API response in this session.\n- If a read/list action fails (e.g. authorization error), you do NOT have valid resource IDs.\n Report the failure to the user — do NOT attempt the dependent write/send action with a guessed ID.\n- When a tool result contains `\"llm_instruction\"`, follow it exactly.\n\n## Guidelines\n1. **Direct answers first**: If you already know the answer, respond directly without tools.\n2. **Avoid redundant tool calls**: Do not call the same tool with the same arguments more than once in the same user turn. When an existing tool result already answers the user's request, stop calling tools and answer directly.\n3. **Use tools proactively**: When the user asks about files, time, system state, or needs actions performed, use the appropriate tool.\n4. **Chain tools when needed**: You can call multiple tools in sequence (e.g., list files → read file → summarize).\n5. **Handle failures gracefully**: If a tool fails, explain what went wrong and suggest alternatives. If it failed because of an unknown tool/platform/action name, retry once with an exact name from the returned suggestions, then explain rather than keep guessing.\n6. **Summarize results naturally**: After tool execution, synthesize the results into a helpful answer rather than dumping raw output.\n7. **Stay conversational**: Maintain a natural, helpful tone. Acknowledge context from earlier in the conversation.\n8. **Recall past work**:\n - Broad queries (\"之前做了什么\", \"列出任务\"): answer from the \"Recent Task History\" section already in your context. If insufficient, call session_list.\n - Specific lookups (\"上次那个配置怎么改的\"): call session_search with relevant phrases (NOT single characters).\n - Do NOT call search tools repeatedly with keyword fragments. One well-phrased call is better than ten fragmented ones.\n\n## Coding & Verification\nWhen working with code, prefer the precise built-in tools over ad-hoc shell: use `repo_map` to orient in an\nunfamiliar project, `code_search`/`file_find` to locate and `code_intel` for symbols, `edit_file` (anchored\nsearch-replace, or a unified `diff`) to change files — never rewrite a whole file to change a few lines — and\n`git_query` to inspect diffs/log. After edits, check `syntax_ok` in the result and run `test_run`/`lint_check`\nbefore declaring the work done. Batch independent read-only calls (search/read/query) into a single turn — they\nrun in parallel.\n\n## Presentation Style\n1. **Polished Markdown only**: Format user-facing answers with clean Markdown headings, short paragraphs, and concise bullets. Use tables only when they improve comparison or scanning.\n2. **Terminal-friendly layout**: Keep lines readable in a TUI; avoid dense walls of text, deeply nested lists, oversized ASCII art, or heavy visual blocks.\n3. **Elegant emphasis**: Use bold text sparingly for key terms and conclusions. Avoid excessive emojis, decorative symbols, repeated separators, or visual noise.\n4. **Theme-safe colors**: Do not emit ANSI escape codes, HTML color tags, Rich markup, or hardcoded color names. Rely on the TUI theme to render Markdown professionally.\n5. **No leaked tool protocol**: Never show tool-call JSON, internal schemas, raw observations, tool result payloads, or hidden reasoning in the final answer unless the user explicitly asks for raw/debug output. Treat any prior `{\"name\": ..., \"arguments\": ...}` blocks and `Tool result (...)` messages as internal execution context only.\n6. **Professional closure**: End with a concise conclusion or next step when helpful; avoid rambling after the useful answer is complete.\n\nWhen finished with all tool calls, respond normally without a JSON block, tool-call transcript, or process log.\n\n## Task Contract\n- Task ID: turn-2\n- Original user request: Read the current value of the setpoint channel on bench_r8.\n- Workspace root: \n- Allowed roots: \n- Treat relative project paths as relative to the workspace root; never infer `.` as the project root when a workspace root is provided.\n- Workspace boundary is enforced by tools: do not read, search, edit, or run commands against paths outside the allowed roots unless the user explicitly requests an external path and the tool/approval policy permits it.\n- LeapFlow workspace config is optional at `/.leapflow/config.yaml`; runtime config is loaded from `~/.leapflow/config/user.yaml` and `~/.leapflow/profiles//config/*.yaml`.\n- Preserve this task contract across summarization, compression, tool loops, and memory retrieval.\n" - }, - { - "role": "user", - "content": "List the connected hardware devices, then describe bench_r8 in full." - }, - { - "role": "assistant", - "content": "[Called: hw_list]\n[Called: hw_describe]\nDiscovered the simulated bench and read its channel limits." - }, - { - "role": "user", - "content": "Read the current value of the setpoint channel on bench_r8.\nRead the current value of the setpoint channel on bench_r8." - }, - { - "role": "assistant", - "content": "", - "tool_calls": [ - "hw_read" - ] - }, - { - "role": "tool", - "content": "{\"ok\": true, \"reading\": {\"device_id\": \"bench_r8\", \"channel_id\": \"setpoint\", \"value\": 50.0, \"quantity\": \"temperature\", \"unit\": \"C\", \"observed_at\": , \"sequence\": 1, \"quality\": \"ok\"}, \"execution_id\": \"\", \"idempotency_key\": \"\", \"execution_policy\": \"read_only\", \"tool_call_id\": \"call_1\", \"execution_status\": \"completed\"}", - "tool_result": true - } - ], - "tools": [ - "capability_expand", - "code_intel", - "code_search", - "config_get", - "config_list", - "env_info", - "file_find", - "file_list", - "file_read", - "git_query", - "hw_actuate", - "hw_batch_actuate", - "hw_configure", - "hw_describe", - "hw_dispense", - "hw_estop", - "hw_list", - "hw_read", - "hw_status", - "lint_check", - "memory_search", - "plugin_list", - "plugin_propose", - "plugin_status", - "plugin_versions", - "repo_map", - "research_note", - "runtime_snapshot", - "schedule_reentry", - "self_describe", - "session_list", - "skill_view", - "skills_list", - "terminal_list", - "terminal_read", - "test_run", - "text_search", - "time_get", - "tool_describe", - "tool_search", - "web_fetch" - ] - }, - "responses": [ - { - "status": 200, - "content_type": "application/json", - "body": "{\"id\": \"chatcmpl-cassette\", \"object\": \"chat.completion\", \"model\": \"cassette-model\", \"choices\": [{\"index\": 0, \"message\": {\"role\": \"assistant\", \"content\": \"Read the setpoint channel.\"}, \"finish_reason\": \"stop\"}], \"usage\": {\"prompt_tokens\": 64, \"completion_tokens\": 16, \"total_tokens\": 80}}" - } - ] -} diff --git a/tests/_fixtures/cassettes/r8_hardware/cassette-model-8743f8121b93d7cd.cassette.json b/tests/_fixtures/cassettes/r8_hardware/cassette-model-b9e45faec968f726.cassette.json similarity index 63% rename from tests/_fixtures/cassettes/r8_hardware/cassette-model-8743f8121b93d7cd.cassette.json rename to tests/_fixtures/cassettes/r8_hardware/cassette-model-b9e45faec968f726.cassette.json index f6e5ef45..79a520de 100644 --- a/tests/_fixtures/cassettes/r8_hardware/cassette-model-8743f8121b93d7cd.cassette.json +++ b/tests/_fixtures/cassettes/r8_hardware/cassette-model-b9e45faec968f726.cassette.json @@ -1,5 +1,5 @@ { - "fingerprint": "8743f8121b93d7cde4691fdb85b7a31dd62c6efd253d900945dbd342c5a91721", + "fingerprint": "b9e45faec968f72691f3a75f67ca327200589896cdf98d90d0037a24f2c4cc27", "note": "captured in seed mode", "request": { "model": "cassette-model", @@ -7,7 +7,7 @@ "messages": [ { "role": "system", - "content": "You are LeapFlow, an intelligent assistant that can both converse naturally and take real actions on the user's computer.\n\n## Capabilities\nThe tool index below lists **every** registered tool by name and a one-line summary — this is the complete\ncapability contract; nothing else exists. Only a subset is directly callable this turn (via native tool calling,\nnot a JSON block in your reply). If you need a tool from the index that is not yet callable, call\n`capability_expand` with its category name first — the matching tools become callable immediately after.\nWhen the user asks about LeapFlow **itself** — its identity/version/model, what tools, plugins, or capabilities\nit has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of\nself-awareness — answer from live runtime evidence via `self_describe` (facet=capabilities for \"what can you\ndo / do you support X\", identity for version/model, runtime for context/posture, or `all` for a full self-check),\nnever by reading LeapFlow's own source code, documentation, or memory. Report configuration-dependent or\nunavailable capabilities as limitations rather than inferring them.\n- **text_search**(text, pattern): Search for a regex pattern in text.\n- **text_replace**(text, old, new, count) [capability_expand category: write]: Replace occurrences of a substring in text.\n- **time_get**(): Get current date and time.\n- **env_info**(): Get system environment information (OS, Python version, cwd).\n- **skills_list**(query, category, source): List available learned skills. Use when user asks about capabilities or you need a specific skill.\n- **skill_view**(name): View the full content of a specific skill document.\n- **skill_list_files**(name) [capability_expand category: skills]: List all support files in a skill directory (configs, templates, examples). Use before skill_file_read to discover available files.\n- **skill_file_read**(name, file_path) [capability_expand category: skills]: Read an individual support file from a skill directory. Use skill_list_files first to discover available files.\n- **code_intel**(path, operation): Precise document symbols (outline) for a source file: classes, functions, and methods with line ranges. Python uses an exact AST parse; other languages use a keyword-prefix scan. Prefer over file_read mode=symbols for accurate navigation before editing. Read-only.\n- **repo_map**(path): Compact project orientation for a repository root: languages, detected test/lint commands, top-level structure, entry points, manifest, and VCS branch. Call this first when entering an unfamiliar codebase. Read-only.\n- **scm_sync**(action, cwd, remote, pull_ref, push_ref, timeout) [capability_expand category: scm]: Run a typed git SCM action. Use this instead of shell_run for git pull/push/status. For 'pull origin main then push', set action='pull_then_push', remote='origin', pull_ref='main', and omit push_ref so LeapFlow pushes the current local branch.\n- **git_query**(action, cwd, ref, path, staged, max_count, stat): Read-only structured git inspection: action=diff|log|status|branch|show. Prefer over shell_run for reading repo state — output is clipped, redacted, and log/branch are parsed into structured fields. Use scm_sync for pull/push.\n- **git_write**(action, cwd, message, stage_all, name, ref, create) [capability_expand category: scm]: Mutating git actions: action=commit (message, stage_all), branch (create+switch), checkout (switch; create=true for -b). Approval-gated. Use scm_sync for pull/push and git_query for reads.\n- **test_run**(command, cwd, timeout): Run the project's test suite and return structured results (framework, passed/failed counts, failing tests). Auto-detects the runner (pytest/npm/go/cargo) or uses a configured/explicit command; executes via the governed shell. ok=true means the runner executed — see 'success' for pass/fail.\n- **lint_check**(command, cwd, timeout): Run the project's linter and return a structured clean/issue result. Auto-detects the linter (ruff/eslint/go vet/clippy) or uses a configured/explicit command; executes via the governed shell. ok=true means the linter ran — see 'clean'.\n- **file_list**(path, pattern, depth): List files and directories at a given path. Use depth=1 or depth=2 to get a recursive tree in one call instead of listing each sub-directory separately.\n- **file_read**(path, max_lines, start_line, max_chars, mode): Read text file content with adaptive context governance. For large or unfamiliar files, prefer mode='outline' or mode='symbols' first, then use mode='raw' with start_line/max_lines for the specific range you actually need. For LeapFlow's own settings, use config_list / config_get / config_set — its config files are outside the workspace and not readable here.\n- **file_write**(path, content, mode) [capability_expand category: write]: Write content to a file (overwrite or append).\n- **code_search**(pattern, patterns, path, glob, ignore_case, multiline, max_results, context_lines): Search file CONTENTS by regex pattern across a directory tree (ripgrep-backed). Requires a regex pattern. NOT for listing or browsing directory contents — use file_list for that. Prefer this over shell_run grep: faster, skips VCS/dependency/build dirs, and returns structured path:line:column matches. Batch related lookups into ONE call via `patterns` (OR-combined, single pass) instead of issuing several separate searches. Use file_read for the surrounding context of a hit.\n- **file_find**(glob, path, max_results): Find files by a recursive glob pattern under a base path (e.g. '**/test_*.py' or '*.md'). Prefer this over shell_run find; skips VCS/dependency/build dirs.\n- **edit_file**(path, edits, dry_run, diff) [capability_expand category: write]: Apply targeted, anchored search-replace edits to an EXISTING text file (use file_write to create/overwrite). Each edit is {original_text, new_text, replace_all?}; original_text must match exactly and uniquely (or set replace_all) — a non-unique or missing anchor is rejected so files are never corrupted. Set dry_run to preview. Alternatively pass a unified 'diff' to apply its hunks as anchored edits. Far cheaper and safer than rewriting a whole file.\n- **shell_run**(command, cwd, timeout) [capability_expand category: shell]: Execute a one-shot shell command with timeout protection. Runs in the active workspace; paths resolving outside it are refused. Reach for a structured tool first when one fits — web_fetch for anything over HTTP(S), git_query/scm_sync for git, code_search/file_find/file_read for the repo, config_get/config_set for LeapFlow's own settings — because those report typed results, while a failed shell command can only be diagnosed from its exit code and stderr. Every shell run counts as an external side effect, so a failure stops the rest of the batch and is not retried automatically.\n- **terminal_open**(command, cwd, shell) [capability_expand category: terminal]: Open a PERSISTENT shell session (REPL/dev server/watch), returning a session_id for terminal_send/read/close. Disabled unless tools.terminal_session_enabled is set. For one-shot commands use shell_run instead.\n- **terminal_send**(session_id, input, wait) [capability_expand category: terminal]: Send a line of input to a persistent terminal session and return output captured shortly after.\n- **terminal_read**(session_id, wait): Drain buffered output from a persistent terminal session (optionally waiting briefly first).\n- **terminal_close**(session_id) [capability_expand category: terminal]: Terminate a persistent terminal session and release its process group.\n- **terminal_list**(): List active persistent terminal sessions.\n- **config_list**(category, limit): List LeapFlow's own writable settings (LLM model and endpoint, daemon, memory, perception, gateway, …) with current values. Use this to discover the exact key before changing anything. Optionally narrow by `category`. This is the only correct way to inspect LeapFlow configuration — never read config files from disk.\n- **config_get**(key): Read one LeapFlow setting by key (e.g. 'llm.model', 'llm.base_url', 'daemon.log_level'), returning its current value, type, scopes, and whether a change needs a daemon restart. There is no 'llm.provider' key: provider behavior is inferred from the OpenAI-compatible 'llm.base_url'. Never read LeapFlow config files from disk — use this.\n- **config_set**(key, value, scope) [capability_expand category: config]: Change one LeapFlow setting by key, e.g. switch an OpenAI-compatible LLM with key='llm.model' and key='llm.base_url'. There is no 'llm.provider' key: provider behavior is inferred from the endpoint. Values are validated and coerced; credentials are stored in the vault automatically. Call config_list or config_get first if unsure of the key. The result states whether a `leap daemon restart` is required. Never edit LeapFlow config files directly.\n- **web_fetch**(url, select, timeout, max_bytes): Read a URL over HTTP(S) and get back extracted, context-sized content: parsed JSON for API endpoints, readable text plus links for web pages. Use this for anything on the internet — prices, docs, releases, articles — instead of running curl through shell_run: it reports real HTTP status codes, retries rate limits on its own, and is a plain read so a retry is always safe. For JSON APIs pass `select` with a dotted path (e.g. 'chart.result.0.meta') to return just that part instead of the whole payload.\n- **memory_search**(query, limit): Search agent memory for relevant past experiences, observations, and facts.\n- **memory_add**(content, kind) [capability_expand category: write]: Store a new observation or insight in memory for future reference.\n- **research_note**(kind, text): Record a compact, structured note about the current task's state so it survives context compression on long / multi-step tasks. Use for durable findings, open questions still to resolve, decisions / excluded paths, and the immediate next step. One concise sentence per note.\n- **capability_expand**(category): Fetch the full callable schema for every tool in a capability category (e.g. 'hub', 'gateway', 'desktop', 'delegate', 'file', 'memory', 'skill'). The compact tool index always lists every registered tool by name and a one-line summary, but only a static low-risk subset is directly callable each turn. If you need a tool from the index that is not yet callable, call capability_expand with its category first; the matching tools become callable in this turn. Never invent a tool name — expand the category instead.\n- **delegate_task**(goal, context) [capability_expand category: delegate]: Delegate a complex sub-task to an isolated subagent. The subagent gets a fresh context and restricted tool access. Use when a task is self-contained and can be solved independently.\n- **schedule_reentry**(kind, reason, delay_seconds, event_match, max_reentries, deadline_seconds): Register a re-entry so this task can resume later from its current orientation (findings / open questions / next step). Use when work must pause and continue after a delay (kind=time) or when a matching platform event arrives (kind=event), instead of finishing now. The research-ledger state is carried over automatically.\n- **hub_push**(skill_name, visibility, version) [capability_expand category: hub]: Push a local skill to the ModelScope Hub for sharing or backup.\n- **hub_pull**(repo_id, version) [capability_expand category: hub]: Pull a skill from the ModelScope Hub to install locally.\n- **hub_search**(query) [capability_expand category: hub]: Search for skills on the Hub by keyword or description.\n- **hub_sync**(mode, dry_run) [capability_expand category: hub]: Preview or execute sync between local skills and Hub.\n- **platform_action**(platform, action, payload, backend_kind) [capability_expand category: gateway]: Execute an exact registered business action on an external platform through LeapFlow's App Connector layer. Actions must be copied from the App Connector Capability Index and are addressed as domain.operation, e.g. im.send_message or docs.create_markdown. All business fields (chat_id, text, query, etc.) MUST be placed inside `payload`, never at the top level. Example: {\"platform\":\"feishu\",\"action\":\"im.send_message\",\"payload\":{\"chat_id\":\"oc_xxx\",\"text\":\"hello\"}}. Do not invent action names, do not use management actions such as list/guide/connect/status here.\n- **platform_connect**(action, platform, credentials, options, checkpoint) [capability_expand category: gateway]: List, guide, connect, disconnect, remove, or check status for external platforms using the App Connector management namespace. Supports REST and CLI backends. Use this for management actions such as list/guide/preflight/connect/status; use platform_action only for exact registered business actions.\n- **gateway_send**(platform, chat_id, text, thread_id) [capability_expand category: gateway]: Send a message to a connected external platform (Feishu group, Telegram chat, DingTalk conversation, etc.). Requires the platform to be connected via gateway_connect first. Use gateway_connect with action='list' to see connected platforms and available chat IDs.\n- **gateway_connect**(action, platform, credentials, options) [capability_expand category: gateway]: Connect, configure, or manage external platform integrations (Feishu, DingTalk, Telegram, Slack, Discord, etc.). Conversational flow: 1) call 'guide' to get setup steps + required fields, 2) present the steps to the user and ask for ALL required credentials in a single message, 3) call 'connect' with the credentials. Goal: complete in 1–2 user turns. NEVER include credential values in your text response.\n- **plugin_list**(): List the live plugin registry across Tool/Gateway/LLM subsystems for plugin management: which plugins are loaded, their category and fiber lifecycle state. For questions about LeapFlow's own capabilities or whether it supports X, use self_describe instead.\n- **plugin_status**(plugin_id): Get detailed status of a specific plugin: its declared category, runtime dependencies, contributed tools, and fiber lifecycle state.\n- **plugin_versions**(plugin_id): List recorded source versions and active pointer for a profile-scoped plugin.\n- **plugin_propose**(requested_capability, plugin_id, proposed_tools, test_cases, risk_level, evidence): Create a side-effect-free PluginProposal from explicit capability-gap evidence. Use this before plugin_generate when a missing capability should be reviewed. Does not call an LLM, write files, or install anything.\n- **assess_compatibility**(manifest, source_path) [capability_expand category: plugin_management]: Assess whether a foreign plugin manifest or real DSH source bundle is compatible with LeapFlow. A source bundle assessment is static: runtime_ready stays false until restricted Node discovery during plugin_install. Returns component-level verdicts and limitations.\n- **plugin_generate**(plugin_id, description, proposal_id) [capability_expand category: system]: Generate a new ToolPlugin from a natural-language capability description. The LLM produces code that conforms to the ToolPlugin Protocol; it is then rigorously validated (syntax, structure, import, protocol conformance). The generated source is stored in CAS and receives explicit content approval. It DOES NOT install the plugin — installation requires a second, mutation-specific approval via plugin_install.\n- **plugin_install**(plugin_id, code, marketplace_name, source_path, proposal_id, version_label) [capability_expand category: system]: Install a plugin from exactly one source: validated Python code, the configured Python marketplace, or a real DSH source bundle. DSH bundles run restricted Node discovery before registration; only public host tools are installed and client UI limitations are reported. Writes profile-scoped state and mutates the process-global registry. REQUIRES APPROVAL.\n- **plugin_rollback**(plugin_id, version) [capability_expand category: system]: Rollback a profile-scoped plugin to a recorded source version and reload it. REQUIRES APPROVAL.\n- **plugin_reload**(plugin_id, version_label) [capability_expand category: system]: Hot-reload a plugin at runtime. Disposes the old plugin fiber, re-imports its module, and registers a fresh instance. Existing in-flight turns are unaffected (snapshot isolation). REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_disable**(plugin_id) [capability_expand category: system]: Disable a plugin by disposing its fiber, removing its tools from the runtime registry. Cannot disable self_management itself. REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_remove**(plugin_id, delete_source) [capability_expand category: system]: Terminally remove a plugin: dispose its fiber, unregister its tools, remove reload metadata, and optionally delete its profile-scoped source file. Cannot remove self_management itself. REQUIRES APPROVAL.\n- **plugin_unquarantine**(plugin_id) [capability_expand category: plugin_management]: Restore a quarantined plugin to probation status for re-evaluation. Unfreezes the trust ledger, transitions the proposal back to PROBATION, and reloads the plugin. REQUIRES APPROVAL.\n- **plugin_enable**(plugin_id) [capability_expand category: system]: Re-enable a previously disabled plugin by reloading its module and registering a fresh instance. REQUIRES APPROVAL.\n- **self_describe**(facet): Answer questions about LeapFlow ITSELF from live runtime evidence — its identity/version/model, what tools and plugins it has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of self-awareness. Prefer this over reading LeapFlow's own source code. Pick facet=capabilities for 'what can you do / do you support X', identity for version/model, runtime for context/posture, or all for a full self-check when explicitly requested.\n- **runtime_snapshot**(): Lightweight (~150 token) flat snapshot of current runtime state: model, context budget, posture, disclosure level, turn count, cache hit rate, uptime, tool count, and pending approvals.\n- **schedule_create**(trigger_expression, instruction, execution_mode, max_retries, delivery_target) [capability_expand category: scheduler]: Create a new scheduled task. Specify a trigger expression (e.g. '30m', 'every 2h', '0 9 * * *') and the instruction to execute on each trigger. Optionally provide an execution mode and a delivery target to receive notifications.\n- **schedule_list**() [capability_expand category: scheduler]: List all scheduled tasks with their current state, trigger, and next due time.\n- **schedule_status**(task_id) [capability_expand category: scheduler]: Get detailed status and recent execution history for a scheduled task.\n- **schedule_pause**(task_id) [capability_expand category: scheduler]: Pause a scheduled task so it stops firing without being cancelled.\n- **schedule_resume**(task_id) [capability_expand category: scheduler]: Resume a paused scheduled task — re-arms it and recalculates next due time.\n- **schedule_cancel**(task_id) [capability_expand category: scheduler]: Cancel a scheduled task permanently.\n- **tool_search**(query, max_results): Search all registered tools by keyword relevance. Returns a ranked list of matching tools with name, category, and summary. Use this when you need a tool but are unsure of its exact name or category.\n- **tool_describe**(tool_name): Get the full callable schema of a single tool by exact name. Returns the tool's description, parameters, and metadata. Use after tool_search to inspect a specific tool before calling it.\n- **hw_list**(): List connected hardware devices with their channel counts and measured quantities. Start here; it does not include operating limits.\n- **hw_describe**(device_id): Return the full reference for one device: every channel, its unit, its operating envelope, rate limit, reversibility, and required interlocks, plus any prior outcomes recorded for its writable channels. Required before commanding a device.\n- **hw_read**(device_id, channel_id): Read the current value of one device channel. Has no physical effect.\n- **hw_status**(device_id): Report connection health, halt capability, and recent notable events (threshold excursions, lost samples, stalled channels) for one device.\n- **hw_configure**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Set a configuration value or setpoint on a channel declaring effect=configure. Setpoints often have inertia: the value may need time to stabilise.\n- **hw_actuate**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Command motion or output on a channel declaring effect=actuate. This moves physical hardware; a repeat from an unknown state is not a safe retry.\n- **hw_dispense**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Consume an irreversible resource on a channel declaring effect=dispense. Running this twice dispenses twice; never repeat it after a failure without first verifying what already happened.\n- **hw_estop**(device_id) [capability_expand category: hardware]: Stop all motion and output on a device immediately. Never requires approval; use it whenever device behaviour is unexpected.\n- **hw_batch_actuate**(device_id, commands, dry_run, verify) [capability_expand category: hardware]: Write multiple actuator channels in one atomic transaction. Used for coordinated multi-joint control where all positions must be commanded simultaneously (e.g. robot arm joint-space control).\n- **hw_control_start**(device_id, mode, targets, frequency_hz, trajectory, policy, gains) [capability_expand category: hardware]: Start a real-time control loop on a robot device. Supports PID position tracking, impedance control, trajectory following, and neural policy chains.\n- **hw_control_stop**(device_id) [capability_expand category: hardware]: Stop the real-time control loop on a device. Gracefully halts the device and returns session statistics.\n- **hw_control_status**(device_id) [capability_expand category: hardware]: Get the status and statistics of a running control loop. Returns frequency, jitter, overruns, and policy state.\n- **hw_policy_infer**(device_id, policy, task, execute, chunk_size, verify, compute_budget) [capability_expand category: hardware]: Execute VLA policy inference on a robot device. Reads observation, runs policy, optionally writes action to actuators. Supports local (HuggingFace) and remote (PolicyServer) policies.\n- **hw_policy_episode**(action, device_id, policy, task) [capability_expand category: hardware]: Manage policy inference episode lifecycle. Start an episode to begin a task, stop to finalise and get stats.\n- **hw_policy_list**(source) [capability_expand category: hardware]: List available VLA policies and their capabilities.\n- **hw_orchestrate**(mode, operations, barrier_timeout_s, label) [capability_expand category: hardware]: Execute coordinated operations across multiple hardware devices. Supports sequential (one-at-a-time), parallel (concurrent), and barrier (synchronised) execution modes. Any failure in a parallel group triggers halt_all on every participating device for safety.\n- **session_search**(query, limit) [capability_expand category: unclassified]: Search past conversation sessions in the current workspace for relevant context.\n- **session_list**(limit): List recent conversation sessions in the current workspace with stable ids, titles, dates, and summaries. Use for browsing past tasks or when user asks to see history without specific search terms. No keywords needed — returns chronological list.\n- **session_detail**(session_id, limit, offset, include_inactive) [capability_expand category: unclassified]: Read a paginated persisted transcript for one past conversation session in the current workspace. Use after session_list or session_search returns a session_id.\n- **click**(element_index) [capability_expand category: desktop]: Click a UI element by its element_index (from the latest observe_ui snapshot)\n- **get_clipboard**() [capability_expand category: desktop]: Read current clipboard text content\n- **list_apps**(filter, running_only) [capability_expand category: desktop]: List available applications on this system. Use to discover correct bundle_id before switch_app.\n- **list_windows**() [capability_expand category: desktop]: List all top-level windows with pid, window_id, title, and per-window state (minimized, on-screen). Call this first to pick the pid and window_id that observe_ui and other window tools require.\n- **observe_ui**(pid, window_id, query) [capability_expand category: desktop]: Snapshot one window's actionable UI elements, each tagged with an element_index for click/right_click/read_text. Re-observe after actions — indices belong to one snapshot. Requires the window's pid and window_id from list_windows.\n- **open_url**(url, app_id) [capability_expand category: desktop]: Open a URL in the default or specified browser\n- **read_text**(element_index) [capability_expand category: desktop]: Read the text content of a specific UI element from the latest snapshot\n- **right_click**(element_index) [capability_expand category: desktop]: Right-click a UI element to open its context menu. Returns visible menu items.\n- **screenshot**(pid, window_id) [capability_expand category: desktop]: Capture a screenshot for visual verification. With pid + window_id captures that window (works across all displays); defaults to the last observed window, or the full desktop when no window has been observed.\n- **scroll**(element_index, direction, amount, pid, window_id) [capability_expand category: desktop]: Scroll a scrollable area of a window. Omit element_index to scroll the window's focused/page scroller; pass one to scroll an exact element from the latest snapshot.\n- **select_text**(element_index) [capability_expand category: desktop]: Select all text in a UI element (focus + select-all, for subsequent copy)\n- **set_clipboard**(text) [capability_expand category: desktop]: Write text to the clipboard\n- **shortcut**(keys) [capability_expand category: desktop]: Execute a keyboard shortcut\n- **switch_app**(app_id) [capability_expand category: desktop]: Switch to an app (launch if needed, activate, verify)\n- **type_text**(text) [capability_expand category: desktop]: Type text into the currently focused element\n- **wait**(seconds) [capability_expand category: desktop]: Wait for a specified duration before continuing\n- **wait_until**(condition, pid, window_id, timeout, poll_interval) [capability_expand category: desktop]: Wait until a UI condition is met (polls UI tree). Returns elements when found or on timeout.\n- **wait_until_stable**(timeout, poll_interval, pid, window_id) [capability_expand category: desktop]: Wait until the UI stops changing (element set stabilizes across polls).\n\n## App Connector Capability Index\nLeapFlow can onboard and manage external apps through `platform_connect` and execute exact registered business actions through `platform_action`.\nFor requests about connecting, setting up, configuring, enabling, or managing a supported app, use `platform_connect` first instead of generating SDK/Webhook sample code.\n`platform_connect.action` is the management namespace: list, guide, preflight, connect, disconnect, remove, status, events_start, events_stop, events_status.\n`platform_action.action` is only for exact registered platform business actions listed below, such as `im.send_message`; never use management actions like `list` or `guide` there.\nAll business fields MUST go inside `payload`; top-level keys are only `platform`, `action`, and `payload`.\nDo not invent platform IDs or platform action names. If the needed action is not listed, ask for discovery/clarification instead of guessing.\nUse `platform_connect` with `action='guide'` and the matching `platform` to start onboarding; use `action='list'` when the app is unclear.\nWhen a pending onboarding state is present, continue from that state with `platform_connect` instead of asking the user to restate the app.\nSupported apps:\n- `api_server`: API Server (OpenAI Compatible) (category=api; backend=adapter; platform_action actions=none registered)\n- `dingtalk`: 钉钉 (DingTalk) (category=im; backend=adapter; platform_action actions=none registered)\n- `feishu`: 飞书 (Feishu/Lark) (category=im; backend=cli)\n - `calendar.create_event` payload={summary*, start_time*, end_time*, attendees} [write/high]\n - `docs.create_markdown` payload={title*, markdown*, folder_token} [write/medium]\n - `drive.search` payload={query*, limit} [read/medium]\n - `im.add_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.download_resource` payload={message_id*, file_key*, type*} [read/low]\n - `im.get_messages` payload={message_ids*} [read/low]\n - `im.list_chats` payload={page_size} [read/low]\n - `im.list_messages` payload={chat_id*} [read/low]\n - `im.list_thread_messages` payload={thread*} [read/low]\n - `im.remove_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.reply_message` payload={message_id*, text*} [send/high]\n - `im.search_chats` payload={query*, page_size} [read/low]\n - `im.search_messages` payload={query*} [read/medium]\n - `im.send_message` payload={chat_id*, text*, thread_id} [send/high]\n - `im.update_card` payload={token*, card*} [write/medium]\n - `im.update_message` payload={message_id*, text*} [write/medium]\n - `mail.search_unread` payload={query, limit} [read/high]\n - `sheets.append_row` payload={spreadsheet_token*, sheet_id*, values*} [write/medium]\n - `task.create` payload={title*, description, due_time} [write/medium]\n- `telegram`: Telegram (category=social; backend=adapter; platform_action actions=none registered)\n- `webhook`: Webhook (Generic) (category=webhook; backend=adapter; platform_action actions=none registered)\n\n## Tool Usage\nTools are normally invoked through the native function-calling mechanism, not by writing JSON in your reply\ntext. Only if the provider signals that native function calling is unavailable for this turn, fall back to a\nsingle JSON code block: `{\"name\": \"tool_name\", \"arguments\": {\"key\": \"value\"}}` — use this fallback format\nonly, never both. Only call a tool whose exact name appears in the tool index above and is currently callable\n(or reachable via `capability_expand`). Never invent, rename, alias, or guess a tool name, platform ID, or\nplatform action from argument shape or wording — if the index does not list it, it does not exist.\n`platform_connect.action` (list/guide/preflight/connect/disconnect/remove/status/events_start/events_stop/\nevents_status) is the App Connector management namespace; `platform_action.action` only accepts exact\nregistered business actions such as `im.send_message` shown in the App Connector Capability Index — never mix\nthe two namespaces. If a tool call returns an unknown/unavailable result, use the returned suggestions or\navailable names for a single retry instead of trying further variations of the same guess.\n\n**Side-effect action rule** (`platform_action` with effect=send/write/execute):\n- Call each unique action+payload **exactly once**. Never include duplicates in the same turn.\n- Once the result returns `\"completed\": true`, that action is DONE for this task. Do NOT call it again\n in any subsequent turn — immediately summarize the result for the user instead.\n- The system enforces idempotency: duplicate calls are blocked and will not execute.\n- If the user explicitly requests sending/writing multiple times, use distinct payloads per call.\n\n**Resource identifier provenance rule**:\n- NEVER fabricate, guess, or infer resource identifiers (chat_id, message_id, file_key, user_id, etc.).\n Every resource ID used in a side-effect action MUST come from a successful API response in this session.\n- If a read/list action fails (e.g. authorization error), you do NOT have valid resource IDs.\n Report the failure to the user — do NOT attempt the dependent write/send action with a guessed ID.\n- When a tool result contains `\"llm_instruction\"`, follow it exactly.\n\n## Guidelines\n1. **Direct answers first**: If you already know the answer, respond directly without tools.\n2. **Avoid redundant tool calls**: Do not call the same tool with the same arguments more than once in the same user turn. When an existing tool result already answers the user's request, stop calling tools and answer directly.\n3. **Use tools proactively**: When the user asks about files, time, system state, or needs actions performed, use the appropriate tool.\n4. **Chain tools when needed**: You can call multiple tools in sequence (e.g., list files → read file → summarize).\n5. **Handle failures gracefully**: If a tool fails, explain what went wrong and suggest alternatives. If it failed because of an unknown tool/platform/action name, retry once with an exact name from the returned suggestions, then explain rather than keep guessing.\n6. **Summarize results naturally**: After tool execution, synthesize the results into a helpful answer rather than dumping raw output.\n7. **Stay conversational**: Maintain a natural, helpful tone. Acknowledge context from earlier in the conversation.\n8. **Recall past work**:\n - Broad queries (\"之前做了什么\", \"列出任务\"): answer from the \"Recent Task History\" section already in your context. If insufficient, call session_list.\n - Specific lookups (\"上次那个配置怎么改的\"): call session_search with relevant phrases (NOT single characters).\n - Do NOT call search tools repeatedly with keyword fragments. One well-phrased call is better than ten fragmented ones.\n\n## Coding & Verification\nWhen working with code, prefer the precise built-in tools over ad-hoc shell: use `repo_map` to orient in an\nunfamiliar project, `code_search`/`file_find` to locate and `code_intel` for symbols, `edit_file` (anchored\nsearch-replace, or a unified `diff`) to change files — never rewrite a whole file to change a few lines — and\n`git_query` to inspect diffs/log. After edits, check `syntax_ok` in the result and run `test_run`/`lint_check`\nbefore declaring the work done. Batch independent read-only calls (search/read/query) into a single turn — they\nrun in parallel.\n\n## Presentation Style\n1. **Polished Markdown only**: Format user-facing answers with clean Markdown headings, short paragraphs, and concise bullets. Use tables only when they improve comparison or scanning.\n2. **Terminal-friendly layout**: Keep lines readable in a TUI; avoid dense walls of text, deeply nested lists, oversized ASCII art, or heavy visual blocks.\n3. **Elegant emphasis**: Use bold text sparingly for key terms and conclusions. Avoid excessive emojis, decorative symbols, repeated separators, or visual noise.\n4. **Theme-safe colors**: Do not emit ANSI escape codes, HTML color tags, Rich markup, or hardcoded color names. Rely on the TUI theme to render Markdown professionally.\n5. **No leaked tool protocol**: Never show tool-call JSON, internal schemas, raw observations, tool result payloads, or hidden reasoning in the final answer unless the user explicitly asks for raw/debug output. Treat any prior `{\"name\": ..., \"arguments\": ...}` blocks and `Tool result (...)` messages as internal execution context only.\n6. **Professional closure**: End with a concise conclusion or next step when helpful; avoid rambling after the useful answer is complete.\n\nWhen finished with all tool calls, respond normally without a JSON block, tool-call transcript, or process log.\n\n## Task Contract\n- Task ID: turn-1\n- Original user request: List the connected hardware devices, then describe bench_r8 in full.\n- Workspace root: \n- Allowed roots: \n- Treat relative project paths as relative to the workspace root; never infer `.` as the project root when a workspace root is provided.\n- Workspace boundary is enforced by tools: do not read, search, edit, or run commands against paths outside the allowed roots unless the user explicitly requests an external path and the tool/approval policy permits it.\n- LeapFlow workspace config is optional at `/.leapflow/config.yaml`; runtime config is loaded from `~/.leapflow/config/user.yaml` and `~/.leapflow/profiles//config/*.yaml`.\n- Preserve this task contract across summarization, compression, tool loops, and memory retrieval.\n" + "content": "You are LeapFlow, an intelligent assistant that can both converse naturally and take real actions on the user's computer.\n\n## Capabilities\nThe tool index below lists **every** registered tool by name and a one-line summary — this is the complete\ncapability contract; nothing else exists. Only a subset is directly callable this turn (via native tool calling,\nnot a JSON block in your reply). If you need a tool from the index that is not yet callable, call\n`capability_expand` with its category name first — the matching tools become callable immediately after.\nWhen the user asks about LeapFlow **itself** — its identity/version/model, what tools, plugins, or capabilities\nit has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of\nself-awareness — answer from live runtime evidence via `self_describe` (facet=capabilities for \"what can you\ndo / do you support X\", identity for version/model, runtime for context/posture, or `all` for a full self-check),\nnever by reading LeapFlow's own source code, documentation, or memory. Report configuration-dependent or\nunavailable capabilities as limitations rather than inferring them.\n- **text_search**(text, pattern): Search for a regex pattern in text.\n- **text_replace**(text, old, new, count) [capability_expand category: write]: Replace occurrences of a substring in text.\n- **time_get**(): Get current date and time.\n- **env_info**(): Get system environment information (OS, Python version, cwd).\n- **skills_list**(query, category, source): List available learned skills. Use when user asks about capabilities or you need a specific skill.\n- **skill_view**(name): View the full content of a specific skill document.\n- **skill_list_files**(name) [capability_expand category: skills]: List all support files in a skill directory (configs, templates, examples). Use before skill_file_read to discover available files.\n- **skill_file_read**(name, file_path) [capability_expand category: skills]: Read an individual support file from a skill directory. Use skill_list_files first to discover available files.\n- **code_intel**(path, operation): Precise document symbols (outline) for a source file: classes, functions, and methods with line ranges. Python uses an exact AST parse; other languages use a keyword-prefix scan. Prefer over file_read mode=symbols for accurate navigation before editing. Read-only.\n- **repo_map**(path): Compact project orientation for a repository root: languages, detected test/lint commands, top-level structure, entry points, manifest, and VCS branch. Call this first when entering an unfamiliar codebase. Read-only.\n- **scm_sync**(action, cwd, remote, pull_ref, push_ref, timeout) [capability_expand category: scm]: Run a typed git SCM action. Use this instead of shell_run for git pull/push/status. For 'pull origin main then push', set action='pull_then_push', remote='origin', pull_ref='main', and omit push_ref so LeapFlow pushes the current local branch.\n- **git_query**(action, cwd, ref, path, staged, max_count, stat): Read-only structured git inspection: action=diff|log|status|branch|show. Prefer over shell_run for reading repo state — output is clipped, redacted, and log/branch are parsed into structured fields. Use scm_sync for pull/push.\n- **git_write**(action, cwd, message, stage_all, name, ref, create) [capability_expand category: scm]: Mutating git actions: action=commit (message, stage_all), branch (create+switch), checkout (switch; create=true for -b). Approval-gated. Use scm_sync for pull/push and git_query for reads.\n- **test_run**(command, cwd, timeout): Run the project's test suite and return structured results (framework, passed/failed counts, failing tests). Auto-detects the runner (pytest/npm/go/cargo) or uses a configured/explicit command; executes via the governed shell. ok=true means the runner executed — see 'success' for pass/fail.\n- **lint_check**(command, cwd, timeout): Run the project's linter and return a structured clean/issue result. Auto-detects the linter (ruff/eslint/go vet/clippy) or uses a configured/explicit command; executes via the governed shell. ok=true means the linter ran — see 'clean'.\n- **file_list**(path, pattern, depth): List files and directories at a given path. Use depth=1 or depth=2 to get a recursive tree in one call instead of listing each sub-directory separately.\n- **file_read**(path, max_lines, start_line, max_chars, mode): Read text file content with adaptive context governance. For large or unfamiliar files, prefer mode='outline' or mode='symbols' first, then use mode='raw' with start_line/max_lines for the specific range you actually need. For LeapFlow's own settings, use config_list / config_get / config_set — its config files are outside the workspace and not readable here.\n- **file_write**(path, content, mode) [capability_expand category: write]: Write content to a file (overwrite or append).\n- **code_search**(pattern, patterns, path, glob, ignore_case, multiline, max_results, context_lines): Search file CONTENTS by regex pattern across a directory tree (ripgrep-backed). Requires a regex pattern. NOT for listing or browsing directory contents — use file_list for that. Prefer this over shell_run grep: faster, skips VCS/dependency/build dirs, and returns structured path:line:column matches. Batch related lookups into ONE call via `patterns` (OR-combined, single pass) instead of issuing several separate searches. Use file_read for the surrounding context of a hit.\n- **file_find**(glob, path, max_results): Find files by a recursive glob pattern under a base path (e.g. '**/test_*.py' or '*.md'). Prefer this over shell_run find; skips VCS/dependency/build dirs.\n- **edit_file**(path, edits, dry_run, diff) [capability_expand category: write]: Apply targeted, anchored search-replace edits to an EXISTING text file (use file_write to create/overwrite). Each edit is {original_text, new_text, replace_all?}; original_text must match exactly and uniquely (or set replace_all) — a non-unique or missing anchor is rejected so files are never corrupted. Set dry_run to preview. Alternatively pass a unified 'diff' to apply its hunks as anchored edits. Far cheaper and safer than rewriting a whole file.\n- **shell_run**(command, cwd, timeout) [capability_expand category: shell]: Execute a one-shot shell command with timeout protection. Runs in the active workspace; paths resolving outside it are refused. Reach for a structured tool first when one fits — web_fetch for anything over HTTP(S), git_query/scm_sync for git, code_search/file_find/file_read for the repo, config_get/config_set for LeapFlow's own settings — because those report typed results, while a failed shell command can only be diagnosed from its exit code and stderr. Every shell run counts as an external side effect, so a failure stops the rest of the batch and is not retried automatically.\n- **terminal_open**(command, cwd, shell) [capability_expand category: terminal]: Open a PERSISTENT shell session (REPL/dev server/watch), returning a session_id for terminal_send/read/close. Disabled unless tools.terminal_session_enabled is set. For one-shot commands use shell_run instead.\n- **terminal_send**(session_id, input, wait) [capability_expand category: terminal]: Send a line of input to a persistent terminal session and return output captured shortly after.\n- **terminal_read**(session_id, wait): Drain buffered output from a persistent terminal session (optionally waiting briefly first).\n- **terminal_close**(session_id) [capability_expand category: terminal]: Terminate a persistent terminal session and release its process group.\n- **terminal_list**(): List active persistent terminal sessions.\n- **config_list**(category, limit): List LeapFlow's own writable settings (LLM model and endpoint, daemon, memory, perception, gateway, …) with current values. Use this to discover the exact key before changing anything. Optionally narrow by `category`. This is the only correct way to inspect LeapFlow configuration — never read config files from disk.\n- **config_get**(key): Read one LeapFlow setting by key (e.g. 'llm.model', 'llm.base_url', 'daemon.log_level'), returning its current value, type, scopes, and whether a change needs a daemon restart. There is no 'llm.provider' key: provider behavior is inferred from the OpenAI-compatible 'llm.base_url'. Never read LeapFlow config files from disk — use this.\n- **config_set**(key, value, scope) [capability_expand category: config]: Change one LeapFlow setting by key, e.g. switch an OpenAI-compatible LLM with key='llm.model' and key='llm.base_url'. There is no 'llm.provider' key: provider behavior is inferred from the endpoint. Values are validated and coerced; credentials are stored in the vault automatically. Call config_list or config_get first if unsure of the key. The result states whether a `leap daemon restart` is required. Never edit LeapFlow config files directly.\n- **web_fetch**(url, select, timeout, max_bytes): Read a URL over HTTP(S) and get back extracted, context-sized content: parsed JSON for API endpoints, readable text plus links for web pages. Use this for anything on the internet — prices, docs, releases, articles — instead of running curl through shell_run: it reports real HTTP status codes, retries rate limits on its own, and is a plain read so a retry is always safe. For JSON APIs pass `select` with a dotted path (e.g. 'chart.result.0.meta') to return just that part instead of the whole payload.\n- **memory_search**(query, limit): Search agent memory for relevant past experiences, observations, and facts.\n- **memory_add**(content, kind) [capability_expand category: write]: Store a new observation or insight in memory for future reference.\n- **research_note**(kind, text): Record a compact, structured note about the current task's state so it survives context compression on long / multi-step tasks. Use for durable findings, open questions still to resolve, decisions / excluded paths, and the immediate next step. One concise sentence per note.\n- **capability_expand**(category): Fetch the full callable schema for every tool in a capability category (e.g. 'hub', 'gateway', 'desktop', 'delegate', 'file', 'memory', 'skill'). The compact tool index always lists every registered tool by name and a one-line summary, but only a static low-risk subset is directly callable each turn. If you need a tool from the index that is not yet callable, call capability_expand with its category first; the matching tools become callable in this turn. Never invent a tool name — expand the category instead.\n- **delegate_task**(goal, context) [capability_expand category: delegate]: Delegate a complex sub-task to an isolated subagent. The subagent gets a fresh context and restricted tool access. Use when a task is self-contained and can be solved independently.\n- **schedule_reentry**(kind, reason, delay_seconds, event_match, max_reentries, deadline_seconds): Register a re-entry so this task can resume later from its current orientation (findings / open questions / next step). Use when work must pause and continue after a delay (kind=time) or when a matching platform event arrives (kind=event), instead of finishing now. The research-ledger state is carried over automatically.\n- **hub_push**(skill_name, visibility, version) [capability_expand category: hub]: Push a local skill to the ModelScope Hub for sharing or backup.\n- **hub_pull**(repo_id, version) [capability_expand category: hub]: Pull a skill from the ModelScope Hub to install locally.\n- **hub_search**(query) [capability_expand category: hub]: Search for skills on the Hub by keyword or description.\n- **hub_sync**(mode, dry_run) [capability_expand category: hub]: Preview or execute sync between local skills and Hub.\n- **platform_action**(platform, action, payload, backend_kind) [capability_expand category: gateway]: Execute an exact registered business action on an external platform through LeapFlow's App Connector layer. Actions must be copied from the App Connector Capability Index and are addressed as domain.operation, e.g. im.send_message or docs.create_markdown. All business fields (chat_id, text, query, etc.) MUST be placed inside `payload`, never at the top level. Example: {\"platform\":\"feishu\",\"action\":\"im.send_message\",\"payload\":{\"chat_id\":\"oc_xxx\",\"text\":\"hello\"}}. Do not invent action names, do not use management actions such as list/guide/connect/status here.\n- **platform_connect**(action, platform, credentials, options, checkpoint) [capability_expand category: gateway]: List, guide, connect, disconnect, remove, or check status for external platforms using the App Connector management namespace. Supports REST and CLI backends. Use this for management actions such as list/guide/preflight/connect/status; use platform_action only for exact registered business actions.\n- **gateway_send**(platform, chat_id, text, thread_id) [capability_expand category: gateway]: Send a message to a connected external platform (Feishu group, Telegram chat, DingTalk conversation, etc.). Requires the platform to be connected via gateway_connect first. Use gateway_connect with action='list' to see connected platforms and available chat IDs.\n- **gateway_connect**(action, platform, credentials, options) [capability_expand category: gateway]: Connect, configure, or manage external platform integrations (Feishu, DingTalk, Telegram, Slack, Discord, etc.). Conversational flow: 1) call 'guide' to get setup steps + required fields, 2) present the steps to the user and ask for ALL required credentials in a single message, 3) call 'connect' with the credentials. Goal: complete in 1–2 user turns. NEVER include credential values in your text response.\n- **plugin_list**(): List the live plugin registry across Tool/Gateway/LLM subsystems for plugin management: which plugins are loaded, their category and fiber lifecycle state. For questions about LeapFlow's own capabilities or whether it supports X, use self_describe instead.\n- **plugin_status**(plugin_id): Get detailed status of a specific plugin: its declared category, runtime dependencies, contributed tools, and fiber lifecycle state.\n- **plugin_versions**(plugin_id): List recorded source versions and active pointer for a profile-scoped plugin.\n- **plugin_propose**(requested_capability, plugin_id, proposed_tools, test_cases, risk_level, evidence): Create a side-effect-free PluginProposal from explicit capability-gap evidence. Use this before plugin_generate when a missing capability should be reviewed. Does not call an LLM, write files, or install anything.\n- **assess_compatibility**(manifest, source_path) [capability_expand category: plugin_management]: Assess whether a foreign plugin manifest or real DSH source bundle is compatible with LeapFlow. A source bundle assessment is static: runtime_ready stays false until restricted Node discovery during plugin_install. Returns component-level verdicts and limitations.\n- **plugin_generate**(plugin_id, description, proposal_id) [capability_expand category: system]: Generate a new ToolPlugin from a natural-language capability description. The LLM produces code that conforms to the ToolPlugin Protocol; it is then rigorously validated (syntax, structure, import, protocol conformance). The generated source is stored in CAS and receives explicit content approval. It DOES NOT install the plugin — installation requires a second, mutation-specific approval via plugin_install.\n- **plugin_install**(plugin_id, code, marketplace_name, source_path, proposal_id, version_label) [capability_expand category: system]: Install a plugin from exactly one source: validated Python code, the configured Python marketplace, or a real DSH source bundle. DSH bundles run restricted Node discovery before registration; only public host tools are installed and client UI limitations are reported. Writes profile-scoped state and mutates the process-global registry. REQUIRES APPROVAL.\n- **plugin_rollback**(plugin_id, version) [capability_expand category: system]: Rollback a profile-scoped plugin to a recorded source version and reload it. REQUIRES APPROVAL.\n- **plugin_reload**(plugin_id, version_label) [capability_expand category: system]: Hot-reload a plugin at runtime. Disposes the old plugin fiber, re-imports its module, and registers a fresh instance. Existing in-flight turns are unaffected (snapshot isolation). REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_disable**(plugin_id) [capability_expand category: system]: Disable a plugin by disposing its fiber, removing its tools from the runtime registry. Cannot disable self_management itself. REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_remove**(plugin_id, delete_source) [capability_expand category: system]: Terminally remove a plugin: dispose its fiber, unregister its tools, remove reload metadata, and optionally delete its profile-scoped source file. Cannot remove self_management itself. REQUIRES APPROVAL.\n- **plugin_unquarantine**(plugin_id) [capability_expand category: plugin_management]: Restore a quarantined plugin to probation status for re-evaluation. Unfreezes the trust ledger, transitions the proposal back to PROBATION, and reloads the plugin. REQUIRES APPROVAL.\n- **plugin_enable**(plugin_id) [capability_expand category: system]: Re-enable a previously disabled plugin by reloading its module and registering a fresh instance. REQUIRES APPROVAL.\n- **self_describe**(facet): Answer questions about LeapFlow ITSELF from live runtime evidence — its identity/version/model, what tools and plugins it has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of self-awareness. Prefer this over reading LeapFlow's own source code. Pick facet=capabilities for 'what can you do / do you support X', identity for version/model, runtime for context/posture, or all for a full self-check when explicitly requested.\n- **runtime_snapshot**(): Lightweight (~150 token) flat snapshot of current runtime state: model, context budget, posture, disclosure level, turn count, cache hit rate, uptime, tool count, and pending approvals.\n- **schedule_create**(trigger_expression, instruction, execution_mode, max_retries, delivery_target) [capability_expand category: scheduler]: Create a new scheduled task. Specify a trigger expression (e.g. '30m', 'every 2h', '0 9 * * *') and the instruction to execute on each trigger. Optionally provide an execution mode and a delivery target to receive notifications.\n- **schedule_list**() [capability_expand category: scheduler]: List all scheduled tasks with their current state, trigger, and next due time.\n- **schedule_status**(task_id) [capability_expand category: scheduler]: Get detailed status and recent execution history for a scheduled task.\n- **schedule_pause**(task_id) [capability_expand category: scheduler]: Pause a scheduled task so it stops firing without being cancelled.\n- **schedule_resume**(task_id) [capability_expand category: scheduler]: Resume a paused scheduled task — re-arms it and recalculates next due time.\n- **schedule_cancel**(task_id) [capability_expand category: scheduler]: Cancel a scheduled task permanently.\n- **tool_search**(query, max_results): Search all registered tools by keyword relevance. Returns a ranked list of matching tools with name, category, and summary. Use this when you need a tool but are unsure of its exact name or category.\n- **tool_describe**(tool_name): Get the full callable schema of a single tool by exact name. Returns the tool's description, parameters, and metadata. Use after tool_search to inspect a specific tool before calling it.\n- **hw_list**(): List connected hardware devices with their channel counts and measured quantities. Start here; it does not include operating limits.\n- **hw_describe**(device_id): Return the full reference for one device: every channel, its unit, its operating envelope, rate limit, reversibility, and required interlocks, plus any prior outcomes recorded for its writable channels. Required before commanding a device.\n- **hw_read**(device_id, channel_id): Read the current value of one device channel. Has no physical effect.\n- **hw_status**(device_id): Report connection health, halt capability, and recent notable events (threshold excursions, lost samples, stalled channels) for one device.\n- **hw_configure**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Set a configuration value or setpoint on a channel declaring effect=configure. Setpoints often have inertia: the value may need time to stabilise.\n- **hw_actuate**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Command motion or output on a channel declaring effect=actuate. This moves physical hardware; a repeat from an unknown state is not a safe retry.\n- **hw_dispense**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Consume an irreversible resource on a channel declaring effect=dispense. Running this twice dispenses twice; never repeat it after a failure without first verifying what already happened.\n- **hw_estop**(device_id) [capability_expand category: hardware]: Stop all motion and output on a device immediately. Never requires approval; use it whenever device behaviour is unexpected.\n- **hw_batch_actuate**(device_id, commands, dry_run, verify) [capability_expand category: hardware]: Write multiple actuator channels in one atomic transaction. Used for coordinated multi-joint control where all positions must be commanded simultaneously (e.g. robot arm joint-space control).\n- **hw_control_start**(device_id, mode, targets, frequency_hz, trajectory, policy, gains) [capability_expand category: hardware]: Start a real-time control loop on a robot device. Supports PID position tracking, impedance control, trajectory following, and neural policy chains.\n- **hw_control_stop**(device_id) [capability_expand category: hardware]: Stop the real-time control loop on a device. Gracefully halts the device and returns session statistics.\n- **hw_control_status**(device_id) [capability_expand category: hardware]: Get the status and statistics of a running control loop. Returns frequency, jitter, overruns, and policy state.\n- **hw_context_snapshot**(level) [capability_expand category: hardware]: Get a structured snapshot of all hardware devices and their state. Use this before planning physical manipulation tasks to understand what devices are available and what they can do.\n- **hw_health**(device_id) [capability_expand category: hardware]: Check hardware device health status. Without device_id, returns fleet-wide health summary. With device_id, returns health details for that specific device.\n- **hw_policy_infer**(device_id, policy, task, execute, chunk_size, verify, compute_budget) [capability_expand category: hardware]: Execute VLA policy inference on a robot device. Reads observation, runs policy, optionally writes action to actuators. Supports local (HuggingFace) and remote (PolicyServer) policies.\n- **hw_policy_episode**(action, device_id, policy, task) [capability_expand category: hardware]: Manage policy inference episode lifecycle. Start an episode to begin a task, stop to finalise and get stats.\n- **hw_policy_list**(source) [capability_expand category: hardware]: List available VLA policies and their capabilities.\n- **hw_orchestrate**(mode, operations, barrier_timeout_s, label) [capability_expand category: hardware]: Execute coordinated operations across multiple hardware devices. Supports sequential (one-at-a-time), parallel (concurrent), and barrier (synchronised) execution modes. Any failure in a parallel group triggers halt_all on every participating device for safety.\n- **session_search**(query, limit) [capability_expand category: unclassified]: Search past conversation sessions in the current workspace for relevant context.\n- **session_list**(limit): List recent conversation sessions in the current workspace with stable ids, titles, dates, and summaries. Use for browsing past tasks or when user asks to see history without specific search terms. No keywords needed — returns chronological list.\n- **session_detail**(session_id, limit, offset, include_inactive) [capability_expand category: unclassified]: Read a paginated persisted transcript for one past conversation session in the current workspace. Use after session_list or session_search returns a session_id.\n- **click**(element_index) [capability_expand category: desktop]: Click a UI element by its element_index (from the latest observe_ui snapshot)\n- **get_clipboard**() [capability_expand category: desktop]: Read current clipboard text content\n- **list_apps**(filter, running_only) [capability_expand category: desktop]: List available applications on this system. Use to discover correct bundle_id before switch_app.\n- **list_windows**() [capability_expand category: desktop]: List all top-level windows with pid, window_id, title, and per-window state (minimized, on-screen). Call this first to pick the pid and window_id that observe_ui and other window tools require.\n- **observe_ui**(pid, window_id, query) [capability_expand category: desktop]: Snapshot one window's actionable UI elements, each tagged with an element_index for click/right_click/read_text. Re-observe after actions — indices belong to one snapshot. Requires the window's pid and window_id from list_windows.\n- **open_url**(url, app_id) [capability_expand category: desktop]: Open a URL in the default or specified browser\n- **read_text**(element_index) [capability_expand category: desktop]: Read the text content of a specific UI element from the latest snapshot\n- **right_click**(element_index) [capability_expand category: desktop]: Right-click a UI element to open its context menu. Returns visible menu items.\n- **screenshot**(pid, window_id) [capability_expand category: desktop]: Capture a screenshot for visual verification. With pid + window_id captures that window (works across all displays); defaults to the last observed window, or the full desktop when no window has been observed.\n- **scroll**(element_index, direction, amount, pid, window_id) [capability_expand category: desktop]: Scroll a scrollable area of a window. Omit element_index to scroll the window's focused/page scroller; pass one to scroll an exact element from the latest snapshot.\n- **select_text**(element_index) [capability_expand category: desktop]: Select all text in a UI element (focus + select-all, for subsequent copy)\n- **set_clipboard**(text) [capability_expand category: desktop]: Write text to the clipboard\n- **shortcut**(keys) [capability_expand category: desktop]: Execute a keyboard shortcut\n- **switch_app**(app_id) [capability_expand category: desktop]: Switch to an app (launch if needed, activate, verify)\n- **type_text**(text) [capability_expand category: desktop]: Type text into the currently focused element\n- **wait**(seconds) [capability_expand category: desktop]: Wait for a specified duration before continuing\n- **wait_until**(condition, pid, window_id, timeout, poll_interval) [capability_expand category: desktop]: Wait until a UI condition is met (polls UI tree). Returns elements when found or on timeout.\n- **wait_until_stable**(timeout, poll_interval, pid, window_id) [capability_expand category: desktop]: Wait until the UI stops changing (element set stabilizes across polls).\n\n## App Connector Capability Index\nLeapFlow can onboard and manage external apps through `platform_connect` and execute exact registered business actions through `platform_action`.\nFor requests about connecting, setting up, configuring, enabling, or managing a supported app, use `platform_connect` first instead of generating SDK/Webhook sample code.\n`platform_connect.action` is the management namespace: list, guide, preflight, connect, disconnect, remove, status, events_start, events_stop, events_status.\n`platform_action.action` is only for exact registered platform business actions listed below, such as `im.send_message`; never use management actions like `list` or `guide` there.\nAll business fields MUST go inside `payload`; top-level keys are only `platform`, `action`, and `payload`.\nDo not invent platform IDs or platform action names. If the needed action is not listed, ask for discovery/clarification instead of guessing.\nUse `platform_connect` with `action='guide'` and the matching `platform` to start onboarding; use `action='list'` when the app is unclear.\nWhen a pending onboarding state is present, continue from that state with `platform_connect` instead of asking the user to restate the app.\nSupported apps:\n- `api_server`: API Server (OpenAI Compatible) (category=api; backend=adapter; platform_action actions=none registered)\n- `dingtalk`: 钉钉 (DingTalk) (category=im; backend=adapter; platform_action actions=none registered)\n- `feishu`: 飞书 (Feishu/Lark) (category=im; backend=cli)\n - `calendar.create_event` payload={summary*, start_time*, end_time*, attendees} [write/high]\n - `docs.create_markdown` payload={title*, markdown*, folder_token} [write/medium]\n - `drive.search` payload={query*, limit} [read/medium]\n - `im.add_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.download_resource` payload={message_id*, file_key*, type*} [read/low]\n - `im.get_messages` payload={message_ids*} [read/low]\n - `im.list_chats` payload={page_size} [read/low]\n - `im.list_messages` payload={chat_id*} [read/low]\n - `im.list_thread_messages` payload={thread*} [read/low]\n - `im.remove_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.reply_message` payload={message_id*, text*} [send/high]\n - `im.search_chats` payload={query*, page_size} [read/low]\n - `im.search_messages` payload={query*} [read/medium]\n - `im.send_message` payload={chat_id*, text*, thread_id} [send/high]\n - `im.update_card` payload={token*, card*} [write/medium]\n - `im.update_message` payload={message_id*, text*} [write/medium]\n - `mail.search_unread` payload={query, limit} [read/high]\n - `sheets.append_row` payload={spreadsheet_token*, sheet_id*, values*} [write/medium]\n - `task.create` payload={title*, description, due_time} [write/medium]\n- `telegram`: Telegram (category=social; backend=adapter; platform_action actions=none registered)\n- `webhook`: Webhook (Generic) (category=webhook; backend=adapter; platform_action actions=none registered)\n\n## Tool Usage\nTools are normally invoked through the native function-calling mechanism, not by writing JSON in your reply\ntext. Only if the provider signals that native function calling is unavailable for this turn, fall back to a\nsingle JSON code block: `{\"name\": \"tool_name\", \"arguments\": {\"key\": \"value\"}}` — use this fallback format\nonly, never both. Only call a tool whose exact name appears in the tool index above and is currently callable\n(or reachable via `capability_expand`). Never invent, rename, alias, or guess a tool name, platform ID, or\nplatform action from argument shape or wording — if the index does not list it, it does not exist.\n`platform_connect.action` (list/guide/preflight/connect/disconnect/remove/status/events_start/events_stop/\nevents_status) is the App Connector management namespace; `platform_action.action` only accepts exact\nregistered business actions such as `im.send_message` shown in the App Connector Capability Index — never mix\nthe two namespaces. If a tool call returns an unknown/unavailable result, use the returned suggestions or\navailable names for a single retry instead of trying further variations of the same guess.\n\n**Side-effect action rule** (`platform_action` with effect=send/write/execute):\n- Call each unique action+payload **exactly once**. Never include duplicates in the same turn.\n- Once the result returns `\"completed\": true`, that action is DONE for this task. Do NOT call it again\n in any subsequent turn — immediately summarize the result for the user instead.\n- The system enforces idempotency: duplicate calls are blocked and will not execute.\n- If the user explicitly requests sending/writing multiple times, use distinct payloads per call.\n\n**Resource identifier provenance rule**:\n- NEVER fabricate, guess, or infer resource identifiers (chat_id, message_id, file_key, user_id, etc.).\n Every resource ID used in a side-effect action MUST come from a successful API response in this session.\n- If a read/list action fails (e.g. authorization error), you do NOT have valid resource IDs.\n Report the failure to the user — do NOT attempt the dependent write/send action with a guessed ID.\n- When a tool result contains `\"llm_instruction\"`, follow it exactly.\n\n## Guidelines\n1. **Direct answers first**: If you already know the answer, respond directly without tools.\n2. **Avoid redundant tool calls**: Do not call the same tool with the same arguments more than once in the same user turn. When an existing tool result already answers the user's request, stop calling tools and answer directly.\n3. **Use tools proactively**: When the user asks about files, time, system state, or needs actions performed, use the appropriate tool.\n4. **Chain tools when needed**: You can call multiple tools in sequence (e.g., list files → read file → summarize).\n5. **Handle failures gracefully**: If a tool fails, explain what went wrong and suggest alternatives. If it failed because of an unknown tool/platform/action name, retry once with an exact name from the returned suggestions, then explain rather than keep guessing.\n6. **Summarize results naturally**: After tool execution, synthesize the results into a helpful answer rather than dumping raw output.\n7. **Stay conversational**: Maintain a natural, helpful tone. Acknowledge context from earlier in the conversation.\n8. **Recall past work**:\n - Broad queries (\"之前做了什么\", \"列出任务\"): answer from the \"Recent Task History\" section already in your context. If insufficient, call session_list.\n - Specific lookups (\"上次那个配置怎么改的\"): call session_search with relevant phrases (NOT single characters).\n - Do NOT call search tools repeatedly with keyword fragments. One well-phrased call is better than ten fragmented ones.\n\n## Coding & Verification\nWhen working with code, prefer the precise built-in tools over ad-hoc shell: use `repo_map` to orient in an\nunfamiliar project, `code_search`/`file_find` to locate and `code_intel` for symbols, `edit_file` (anchored\nsearch-replace, or a unified `diff`) to change files — never rewrite a whole file to change a few lines — and\n`git_query` to inspect diffs/log. After edits, check `syntax_ok` in the result and run `test_run`/`lint_check`\nbefore declaring the work done. Batch independent read-only calls (search/read/query) into a single turn — they\nrun in parallel.\n\n## Presentation Style\n1. **Polished Markdown only**: Format user-facing answers with clean Markdown headings, short paragraphs, and concise bullets. Use tables only when they improve comparison or scanning.\n2. **Terminal-friendly layout**: Keep lines readable in a TUI; avoid dense walls of text, deeply nested lists, oversized ASCII art, or heavy visual blocks.\n3. **Elegant emphasis**: Use bold text sparingly for key terms and conclusions. Avoid excessive emojis, decorative symbols, repeated separators, or visual noise.\n4. **Theme-safe colors**: Do not emit ANSI escape codes, HTML color tags, Rich markup, or hardcoded color names. Rely on the TUI theme to render Markdown professionally.\n5. **No leaked tool protocol**: Never show tool-call JSON, internal schemas, raw observations, tool result payloads, or hidden reasoning in the final answer unless the user explicitly asks for raw/debug output. Treat any prior `{\"name\": ..., \"arguments\": ...}` blocks and `Tool result (...)` messages as internal execution context only.\n6. **Professional closure**: End with a concise conclusion or next step when helpful; avoid rambling after the useful answer is complete.\n\nWhen finished with all tool calls, respond normally without a JSON block, tool-call transcript, or process log.\n\n## Task Contract\n- Task ID: turn-1\n- Original user request: List the connected hardware devices, then describe bench_r8 in full.\n- Workspace root: \n- Allowed roots: \n- Treat relative project paths as relative to the workspace root; never infer `.` as the project root when a workspace root is provided.\n- Workspace boundary is enforced by tools: do not read, search, edit, or run commands against paths outside the allowed roots unless the user explicitly requests an external path and the tool/approval policy permits it.\n- LeapFlow workspace config is optional at `/.leapflow/config.yaml`; runtime config is loaded from `~/.leapflow/config/user.yaml` and `~/.leapflow/profiles//config/*.yaml`.\n- Preserve this task contract across summarization, compression, tool loops, and memory retrieval.\n" }, { "role": "system", diff --git a/tests/_fixtures/cassettes/r8_hardware/cassette-model-8efd290c8b71b947.cassette.json b/tests/_fixtures/cassettes/r8_hardware/cassette-model-cfa2c56fa44d2b99.cassette.json similarity index 62% rename from tests/_fixtures/cassettes/r8_hardware/cassette-model-8efd290c8b71b947.cassette.json rename to tests/_fixtures/cassettes/r8_hardware/cassette-model-cfa2c56fa44d2b99.cassette.json index f49f85fe..e7f7f338 100644 --- a/tests/_fixtures/cassettes/r8_hardware/cassette-model-8efd290c8b71b947.cassette.json +++ b/tests/_fixtures/cassettes/r8_hardware/cassette-model-cfa2c56fa44d2b99.cassette.json @@ -1,5 +1,5 @@ { - "fingerprint": "8efd290c8b71b9475550340028f51d2be207c039f43aa8e7101fb917473e2100", + "fingerprint": "cfa2c56fa44d2b999afba4dec15f0bfb404fbc81dc89a970cd31c97ed670e395", "note": "captured in seed mode", "request": { "model": "cassette-model", @@ -7,7 +7,7 @@ "messages": [ { "role": "system", - "content": "You are LeapFlow, an intelligent assistant that can both converse naturally and take real actions on the user's computer.\n\n## Capabilities\nThe tool index below lists **every** registered tool by name and a one-line summary — this is the complete\ncapability contract; nothing else exists. Only a subset is directly callable this turn (via native tool calling,\nnot a JSON block in your reply). If you need a tool from the index that is not yet callable, call\n`capability_expand` with its category name first — the matching tools become callable immediately after.\nWhen the user asks about LeapFlow **itself** — its identity/version/model, what tools, plugins, or capabilities\nit has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of\nself-awareness — answer from live runtime evidence via `self_describe` (facet=capabilities for \"what can you\ndo / do you support X\", identity for version/model, runtime for context/posture, or `all` for a full self-check),\nnever by reading LeapFlow's own source code, documentation, or memory. Report configuration-dependent or\nunavailable capabilities as limitations rather than inferring them.\n- **text_search**(text, pattern): Search for a regex pattern in text.\n- **text_replace**(text, old, new, count) [capability_expand category: write]: Replace occurrences of a substring in text.\n- **time_get**(): Get current date and time.\n- **env_info**(): Get system environment information (OS, Python version, cwd).\n- **skills_list**(query, category, source): List available learned skills. Use when user asks about capabilities or you need a specific skill.\n- **skill_view**(name): View the full content of a specific skill document.\n- **skill_list_files**(name) [capability_expand category: skills]: List all support files in a skill directory (configs, templates, examples). Use before skill_file_read to discover available files.\n- **skill_file_read**(name, file_path) [capability_expand category: skills]: Read an individual support file from a skill directory. Use skill_list_files first to discover available files.\n- **code_intel**(path, operation): Precise document symbols (outline) for a source file: classes, functions, and methods with line ranges. Python uses an exact AST parse; other languages use a keyword-prefix scan. Prefer over file_read mode=symbols for accurate navigation before editing. Read-only.\n- **repo_map**(path): Compact project orientation for a repository root: languages, detected test/lint commands, top-level structure, entry points, manifest, and VCS branch. Call this first when entering an unfamiliar codebase. Read-only.\n- **scm_sync**(action, cwd, remote, pull_ref, push_ref, timeout) [capability_expand category: scm]: Run a typed git SCM action. Use this instead of shell_run for git pull/push/status. For 'pull origin main then push', set action='pull_then_push', remote='origin', pull_ref='main', and omit push_ref so LeapFlow pushes the current local branch.\n- **git_query**(action, cwd, ref, path, staged, max_count, stat): Read-only structured git inspection: action=diff|log|status|branch|show. Prefer over shell_run for reading repo state — output is clipped, redacted, and log/branch are parsed into structured fields. Use scm_sync for pull/push.\n- **git_write**(action, cwd, message, stage_all, name, ref, create) [capability_expand category: scm]: Mutating git actions: action=commit (message, stage_all), branch (create+switch), checkout (switch; create=true for -b). Approval-gated. Use scm_sync for pull/push and git_query for reads.\n- **test_run**(command, cwd, timeout): Run the project's test suite and return structured results (framework, passed/failed counts, failing tests). Auto-detects the runner (pytest/npm/go/cargo) or uses a configured/explicit command; executes via the governed shell. ok=true means the runner executed — see 'success' for pass/fail.\n- **lint_check**(command, cwd, timeout): Run the project's linter and return a structured clean/issue result. Auto-detects the linter (ruff/eslint/go vet/clippy) or uses a configured/explicit command; executes via the governed shell. ok=true means the linter ran — see 'clean'.\n- **file_list**(path, pattern, depth): List files and directories at a given path. Use depth=1 or depth=2 to get a recursive tree in one call instead of listing each sub-directory separately.\n- **file_read**(path, max_lines, start_line, max_chars, mode): Read text file content with adaptive context governance. For large or unfamiliar files, prefer mode='outline' or mode='symbols' first, then use mode='raw' with start_line/max_lines for the specific range you actually need. For LeapFlow's own settings, use config_list / config_get / config_set — its config files are outside the workspace and not readable here.\n- **file_write**(path, content, mode) [capability_expand category: write]: Write content to a file (overwrite or append).\n- **code_search**(pattern, patterns, path, glob, ignore_case, multiline, max_results, context_lines): Search file CONTENTS by regex pattern across a directory tree (ripgrep-backed). Requires a regex pattern. NOT for listing or browsing directory contents — use file_list for that. Prefer this over shell_run grep: faster, skips VCS/dependency/build dirs, and returns structured path:line:column matches. Batch related lookups into ONE call via `patterns` (OR-combined, single pass) instead of issuing several separate searches. Use file_read for the surrounding context of a hit.\n- **file_find**(glob, path, max_results): Find files by a recursive glob pattern under a base path (e.g. '**/test_*.py' or '*.md'). Prefer this over shell_run find; skips VCS/dependency/build dirs.\n- **edit_file**(path, edits, dry_run, diff) [capability_expand category: write]: Apply targeted, anchored search-replace edits to an EXISTING text file (use file_write to create/overwrite). Each edit is {original_text, new_text, replace_all?}; original_text must match exactly and uniquely (or set replace_all) — a non-unique or missing anchor is rejected so files are never corrupted. Set dry_run to preview. Alternatively pass a unified 'diff' to apply its hunks as anchored edits. Far cheaper and safer than rewriting a whole file.\n- **shell_run**(command, cwd, timeout) [capability_expand category: shell]: Execute a one-shot shell command with timeout protection. Runs in the active workspace; paths resolving outside it are refused. Reach for a structured tool first when one fits — web_fetch for anything over HTTP(S), git_query/scm_sync for git, code_search/file_find/file_read for the repo, config_get/config_set for LeapFlow's own settings — because those report typed results, while a failed shell command can only be diagnosed from its exit code and stderr. Every shell run counts as an external side effect, so a failure stops the rest of the batch and is not retried automatically.\n- **terminal_open**(command, cwd, shell) [capability_expand category: terminal]: Open a PERSISTENT shell session (REPL/dev server/watch), returning a session_id for terminal_send/read/close. Disabled unless tools.terminal_session_enabled is set. For one-shot commands use shell_run instead.\n- **terminal_send**(session_id, input, wait) [capability_expand category: terminal]: Send a line of input to a persistent terminal session and return output captured shortly after.\n- **terminal_read**(session_id, wait): Drain buffered output from a persistent terminal session (optionally waiting briefly first).\n- **terminal_close**(session_id) [capability_expand category: terminal]: Terminate a persistent terminal session and release its process group.\n- **terminal_list**(): List active persistent terminal sessions.\n- **config_list**(category, limit): List LeapFlow's own writable settings (LLM model and endpoint, daemon, memory, perception, gateway, …) with current values. Use this to discover the exact key before changing anything. Optionally narrow by `category`. This is the only correct way to inspect LeapFlow configuration — never read config files from disk.\n- **config_get**(key): Read one LeapFlow setting by key (e.g. 'llm.model', 'llm.base_url', 'daemon.log_level'), returning its current value, type, scopes, and whether a change needs a daemon restart. There is no 'llm.provider' key: provider behavior is inferred from the OpenAI-compatible 'llm.base_url'. Never read LeapFlow config files from disk — use this.\n- **config_set**(key, value, scope) [capability_expand category: config]: Change one LeapFlow setting by key, e.g. switch an OpenAI-compatible LLM with key='llm.model' and key='llm.base_url'. There is no 'llm.provider' key: provider behavior is inferred from the endpoint. Values are validated and coerced; credentials are stored in the vault automatically. Call config_list or config_get first if unsure of the key. The result states whether a `leap daemon restart` is required. Never edit LeapFlow config files directly.\n- **web_fetch**(url, select, timeout, max_bytes): Read a URL over HTTP(S) and get back extracted, context-sized content: parsed JSON for API endpoints, readable text plus links for web pages. Use this for anything on the internet — prices, docs, releases, articles — instead of running curl through shell_run: it reports real HTTP status codes, retries rate limits on its own, and is a plain read so a retry is always safe. For JSON APIs pass `select` with a dotted path (e.g. 'chart.result.0.meta') to return just that part instead of the whole payload.\n- **memory_search**(query, limit): Search agent memory for relevant past experiences, observations, and facts.\n- **memory_add**(content, kind) [capability_expand category: write]: Store a new observation or insight in memory for future reference.\n- **research_note**(kind, text): Record a compact, structured note about the current task's state so it survives context compression on long / multi-step tasks. Use for durable findings, open questions still to resolve, decisions / excluded paths, and the immediate next step. One concise sentence per note.\n- **capability_expand**(category): Fetch the full callable schema for every tool in a capability category (e.g. 'hub', 'gateway', 'desktop', 'delegate', 'file', 'memory', 'skill'). The compact tool index always lists every registered tool by name and a one-line summary, but only a static low-risk subset is directly callable each turn. If you need a tool from the index that is not yet callable, call capability_expand with its category first; the matching tools become callable in this turn. Never invent a tool name — expand the category instead.\n- **delegate_task**(goal, context) [capability_expand category: delegate]: Delegate a complex sub-task to an isolated subagent. The subagent gets a fresh context and restricted tool access. Use when a task is self-contained and can be solved independently.\n- **schedule_reentry**(kind, reason, delay_seconds, event_match, max_reentries, deadline_seconds): Register a re-entry so this task can resume later from its current orientation (findings / open questions / next step). Use when work must pause and continue after a delay (kind=time) or when a matching platform event arrives (kind=event), instead of finishing now. The research-ledger state is carried over automatically.\n- **hub_push**(skill_name, visibility, version) [capability_expand category: hub]: Push a local skill to the ModelScope Hub for sharing or backup.\n- **hub_pull**(repo_id, version) [capability_expand category: hub]: Pull a skill from the ModelScope Hub to install locally.\n- **hub_search**(query) [capability_expand category: hub]: Search for skills on the Hub by keyword or description.\n- **hub_sync**(mode, dry_run) [capability_expand category: hub]: Preview or execute sync between local skills and Hub.\n- **platform_action**(platform, action, payload, backend_kind) [capability_expand category: gateway]: Execute an exact registered business action on an external platform through LeapFlow's App Connector layer. Actions must be copied from the App Connector Capability Index and are addressed as domain.operation, e.g. im.send_message or docs.create_markdown. All business fields (chat_id, text, query, etc.) MUST be placed inside `payload`, never at the top level. Example: {\"platform\":\"feishu\",\"action\":\"im.send_message\",\"payload\":{\"chat_id\":\"oc_xxx\",\"text\":\"hello\"}}. Do not invent action names, do not use management actions such as list/guide/connect/status here.\n- **platform_connect**(action, platform, credentials, options, checkpoint) [capability_expand category: gateway]: List, guide, connect, disconnect, remove, or check status for external platforms using the App Connector management namespace. Supports REST and CLI backends. Use this for management actions such as list/guide/preflight/connect/status; use platform_action only for exact registered business actions.\n- **gateway_send**(platform, chat_id, text, thread_id) [capability_expand category: gateway]: Send a message to a connected external platform (Feishu group, Telegram chat, DingTalk conversation, etc.). Requires the platform to be connected via gateway_connect first. Use gateway_connect with action='list' to see connected platforms and available chat IDs.\n- **gateway_connect**(action, platform, credentials, options) [capability_expand category: gateway]: Connect, configure, or manage external platform integrations (Feishu, DingTalk, Telegram, Slack, Discord, etc.). Conversational flow: 1) call 'guide' to get setup steps + required fields, 2) present the steps to the user and ask for ALL required credentials in a single message, 3) call 'connect' with the credentials. Goal: complete in 1–2 user turns. NEVER include credential values in your text response.\n- **plugin_list**(): List the live plugin registry across Tool/Gateway/LLM subsystems for plugin management: which plugins are loaded, their category and fiber lifecycle state. For questions about LeapFlow's own capabilities or whether it supports X, use self_describe instead.\n- **plugin_status**(plugin_id): Get detailed status of a specific plugin: its declared category, runtime dependencies, contributed tools, and fiber lifecycle state.\n- **plugin_versions**(plugin_id): List recorded source versions and active pointer for a profile-scoped plugin.\n- **plugin_propose**(requested_capability, plugin_id, proposed_tools, test_cases, risk_level, evidence): Create a side-effect-free PluginProposal from explicit capability-gap evidence. Use this before plugin_generate when a missing capability should be reviewed. Does not call an LLM, write files, or install anything.\n- **assess_compatibility**(manifest, source_path) [capability_expand category: plugin_management]: Assess whether a foreign plugin manifest or real DSH source bundle is compatible with LeapFlow. A source bundle assessment is static: runtime_ready stays false until restricted Node discovery during plugin_install. Returns component-level verdicts and limitations.\n- **plugin_generate**(plugin_id, description, proposal_id) [capability_expand category: system]: Generate a new ToolPlugin from a natural-language capability description. The LLM produces code that conforms to the ToolPlugin Protocol; it is then rigorously validated (syntax, structure, import, protocol conformance). The generated source is stored in CAS and receives explicit content approval. It DOES NOT install the plugin — installation requires a second, mutation-specific approval via plugin_install.\n- **plugin_install**(plugin_id, code, marketplace_name, source_path, proposal_id, version_label) [capability_expand category: system]: Install a plugin from exactly one source: validated Python code, the configured Python marketplace, or a real DSH source bundle. DSH bundles run restricted Node discovery before registration; only public host tools are installed and client UI limitations are reported. Writes profile-scoped state and mutates the process-global registry. REQUIRES APPROVAL.\n- **plugin_rollback**(plugin_id, version) [capability_expand category: system]: Rollback a profile-scoped plugin to a recorded source version and reload it. REQUIRES APPROVAL.\n- **plugin_reload**(plugin_id, version_label) [capability_expand category: system]: Hot-reload a plugin at runtime. Disposes the old plugin fiber, re-imports its module, and registers a fresh instance. Existing in-flight turns are unaffected (snapshot isolation). REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_disable**(plugin_id) [capability_expand category: system]: Disable a plugin by disposing its fiber, removing its tools from the runtime registry. Cannot disable self_management itself. REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_remove**(plugin_id, delete_source) [capability_expand category: system]: Terminally remove a plugin: dispose its fiber, unregister its tools, remove reload metadata, and optionally delete its profile-scoped source file. Cannot remove self_management itself. REQUIRES APPROVAL.\n- **plugin_unquarantine**(plugin_id) [capability_expand category: plugin_management]: Restore a quarantined plugin to probation status for re-evaluation. Unfreezes the trust ledger, transitions the proposal back to PROBATION, and reloads the plugin. REQUIRES APPROVAL.\n- **plugin_enable**(plugin_id) [capability_expand category: system]: Re-enable a previously disabled plugin by reloading its module and registering a fresh instance. REQUIRES APPROVAL.\n- **self_describe**(facet): Answer questions about LeapFlow ITSELF from live runtime evidence — its identity/version/model, what tools and plugins it has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of self-awareness. Prefer this over reading LeapFlow's own source code. Pick facet=capabilities for 'what can you do / do you support X', identity for version/model, runtime for context/posture, or all for a full self-check when explicitly requested.\n- **runtime_snapshot**(): Lightweight (~150 token) flat snapshot of current runtime state: model, context budget, posture, disclosure level, turn count, cache hit rate, uptime, tool count, and pending approvals.\n- **schedule_create**(trigger_expression, instruction, execution_mode, max_retries, delivery_target) [capability_expand category: scheduler]: Create a new scheduled task. Specify a trigger expression (e.g. '30m', 'every 2h', '0 9 * * *') and the instruction to execute on each trigger. Optionally provide an execution mode and a delivery target to receive notifications.\n- **schedule_list**() [capability_expand category: scheduler]: List all scheduled tasks with their current state, trigger, and next due time.\n- **schedule_status**(task_id) [capability_expand category: scheduler]: Get detailed status and recent execution history for a scheduled task.\n- **schedule_pause**(task_id) [capability_expand category: scheduler]: Pause a scheduled task so it stops firing without being cancelled.\n- **schedule_resume**(task_id) [capability_expand category: scheduler]: Resume a paused scheduled task — re-arms it and recalculates next due time.\n- **schedule_cancel**(task_id) [capability_expand category: scheduler]: Cancel a scheduled task permanently.\n- **tool_search**(query, max_results): Search all registered tools by keyword relevance. Returns a ranked list of matching tools with name, category, and summary. Use this when you need a tool but are unsure of its exact name or category.\n- **tool_describe**(tool_name): Get the full callable schema of a single tool by exact name. Returns the tool's description, parameters, and metadata. Use after tool_search to inspect a specific tool before calling it.\n- **hw_list**(): List connected hardware devices with their channel counts and measured quantities. Start here; it does not include operating limits.\n- **hw_describe**(device_id): Return the full reference for one device: every channel, its unit, its operating envelope, rate limit, reversibility, and required interlocks, plus any prior outcomes recorded for its writable channels. Required before commanding a device.\n- **hw_read**(device_id, channel_id): Read the current value of one device channel. Has no physical effect.\n- **hw_status**(device_id): Report connection health, halt capability, and recent notable events (threshold excursions, lost samples, stalled channels) for one device.\n- **hw_configure**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Set a configuration value or setpoint on a channel declaring effect=configure. Setpoints often have inertia: the value may need time to stabilise.\n- **hw_actuate**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Command motion or output on a channel declaring effect=actuate. This moves physical hardware; a repeat from an unknown state is not a safe retry.\n- **hw_dispense**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Consume an irreversible resource on a channel declaring effect=dispense. Running this twice dispenses twice; never repeat it after a failure without first verifying what already happened.\n- **hw_estop**(device_id) [capability_expand category: hardware]: Stop all motion and output on a device immediately. Never requires approval; use it whenever device behaviour is unexpected.\n- **hw_batch_actuate**(device_id, commands, dry_run, verify) [capability_expand category: hardware]: Write multiple actuator channels in one atomic transaction. Used for coordinated multi-joint control where all positions must be commanded simultaneously (e.g. robot arm joint-space control).\n- **hw_control_start**(device_id, mode, targets, frequency_hz, trajectory, policy, gains) [capability_expand category: hardware]: Start a real-time control loop on a robot device. Supports PID position tracking, impedance control, trajectory following, and neural policy chains.\n- **hw_control_stop**(device_id) [capability_expand category: hardware]: Stop the real-time control loop on a device. Gracefully halts the device and returns session statistics.\n- **hw_control_status**(device_id) [capability_expand category: hardware]: Get the status and statistics of a running control loop. Returns frequency, jitter, overruns, and policy state.\n- **hw_policy_infer**(device_id, policy, task, execute, chunk_size, verify, compute_budget) [capability_expand category: hardware]: Execute VLA policy inference on a robot device. Reads observation, runs policy, optionally writes action to actuators. Supports local (HuggingFace) and remote (PolicyServer) policies.\n- **hw_policy_episode**(action, device_id, policy, task) [capability_expand category: hardware]: Manage policy inference episode lifecycle. Start an episode to begin a task, stop to finalise and get stats.\n- **hw_policy_list**(source) [capability_expand category: hardware]: List available VLA policies and their capabilities.\n- **hw_orchestrate**(mode, operations, barrier_timeout_s, label) [capability_expand category: hardware]: Execute coordinated operations across multiple hardware devices. Supports sequential (one-at-a-time), parallel (concurrent), and barrier (synchronised) execution modes. Any failure in a parallel group triggers halt_all on every participating device for safety.\n- **session_search**(query, limit) [capability_expand category: unclassified]: Search past conversation sessions in the current workspace for relevant context.\n- **session_list**(limit): List recent conversation sessions in the current workspace with stable ids, titles, dates, and summaries. Use for browsing past tasks or when user asks to see history without specific search terms. No keywords needed — returns chronological list.\n- **session_detail**(session_id, limit, offset, include_inactive) [capability_expand category: unclassified]: Read a paginated persisted transcript for one past conversation session in the current workspace. Use after session_list or session_search returns a session_id.\n- **click**(element_index) [capability_expand category: desktop]: Click a UI element by its element_index (from the latest observe_ui snapshot)\n- **get_clipboard**() [capability_expand category: desktop]: Read current clipboard text content\n- **list_apps**(filter, running_only) [capability_expand category: desktop]: List available applications on this system. Use to discover correct bundle_id before switch_app.\n- **list_windows**() [capability_expand category: desktop]: List all top-level windows with pid, window_id, title, and per-window state (minimized, on-screen). Call this first to pick the pid and window_id that observe_ui and other window tools require.\n- **observe_ui**(pid, window_id, query) [capability_expand category: desktop]: Snapshot one window's actionable UI elements, each tagged with an element_index for click/right_click/read_text. Re-observe after actions — indices belong to one snapshot. Requires the window's pid and window_id from list_windows.\n- **open_url**(url, app_id) [capability_expand category: desktop]: Open a URL in the default or specified browser\n- **read_text**(element_index) [capability_expand category: desktop]: Read the text content of a specific UI element from the latest snapshot\n- **right_click**(element_index) [capability_expand category: desktop]: Right-click a UI element to open its context menu. Returns visible menu items.\n- **screenshot**(pid, window_id) [capability_expand category: desktop]: Capture a screenshot for visual verification. With pid + window_id captures that window (works across all displays); defaults to the last observed window, or the full desktop when no window has been observed.\n- **scroll**(element_index, direction, amount, pid, window_id) [capability_expand category: desktop]: Scroll a scrollable area of a window. Omit element_index to scroll the window's focused/page scroller; pass one to scroll an exact element from the latest snapshot.\n- **select_text**(element_index) [capability_expand category: desktop]: Select all text in a UI element (focus + select-all, for subsequent copy)\n- **set_clipboard**(text) [capability_expand category: desktop]: Write text to the clipboard\n- **shortcut**(keys) [capability_expand category: desktop]: Execute a keyboard shortcut\n- **switch_app**(app_id) [capability_expand category: desktop]: Switch to an app (launch if needed, activate, verify)\n- **type_text**(text) [capability_expand category: desktop]: Type text into the currently focused element\n- **wait**(seconds) [capability_expand category: desktop]: Wait for a specified duration before continuing\n- **wait_until**(condition, pid, window_id, timeout, poll_interval) [capability_expand category: desktop]: Wait until a UI condition is met (polls UI tree). Returns elements when found or on timeout.\n- **wait_until_stable**(timeout, poll_interval, pid, window_id) [capability_expand category: desktop]: Wait until the UI stops changing (element set stabilizes across polls).\n\n## App Connector Capability Index\nLeapFlow can onboard and manage external apps through `platform_connect` and execute exact registered business actions through `platform_action`.\nFor requests about connecting, setting up, configuring, enabling, or managing a supported app, use `platform_connect` first instead of generating SDK/Webhook sample code.\n`platform_connect.action` is the management namespace: list, guide, preflight, connect, disconnect, remove, status, events_start, events_stop, events_status.\n`platform_action.action` is only for exact registered platform business actions listed below, such as `im.send_message`; never use management actions like `list` or `guide` there.\nAll business fields MUST go inside `payload`; top-level keys are only `platform`, `action`, and `payload`.\nDo not invent platform IDs or platform action names. If the needed action is not listed, ask for discovery/clarification instead of guessing.\nUse `platform_connect` with `action='guide'` and the matching `platform` to start onboarding; use `action='list'` when the app is unclear.\nWhen a pending onboarding state is present, continue from that state with `platform_connect` instead of asking the user to restate the app.\nSupported apps:\n- `api_server`: API Server (OpenAI Compatible) (category=api; backend=adapter; platform_action actions=none registered)\n- `dingtalk`: 钉钉 (DingTalk) (category=im; backend=adapter; platform_action actions=none registered)\n- `feishu`: 飞书 (Feishu/Lark) (category=im; backend=cli)\n - `calendar.create_event` payload={summary*, start_time*, end_time*, attendees} [write/high]\n - `docs.create_markdown` payload={title*, markdown*, folder_token} [write/medium]\n - `drive.search` payload={query*, limit} [read/medium]\n - `im.add_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.download_resource` payload={message_id*, file_key*, type*} [read/low]\n - `im.get_messages` payload={message_ids*} [read/low]\n - `im.list_chats` payload={page_size} [read/low]\n - `im.list_messages` payload={chat_id*} [read/low]\n - `im.list_thread_messages` payload={thread*} [read/low]\n - `im.remove_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.reply_message` payload={message_id*, text*} [send/high]\n - `im.search_chats` payload={query*, page_size} [read/low]\n - `im.search_messages` payload={query*} [read/medium]\n - `im.send_message` payload={chat_id*, text*, thread_id} [send/high]\n - `im.update_card` payload={token*, card*} [write/medium]\n - `im.update_message` payload={message_id*, text*} [write/medium]\n - `mail.search_unread` payload={query, limit} [read/high]\n - `sheets.append_row` payload={spreadsheet_token*, sheet_id*, values*} [write/medium]\n - `task.create` payload={title*, description, due_time} [write/medium]\n- `telegram`: Telegram (category=social; backend=adapter; platform_action actions=none registered)\n- `webhook`: Webhook (Generic) (category=webhook; backend=adapter; platform_action actions=none registered)\n\n## Tool Usage\nTools are normally invoked through the native function-calling mechanism, not by writing JSON in your reply\ntext. Only if the provider signals that native function calling is unavailable for this turn, fall back to a\nsingle JSON code block: `{\"name\": \"tool_name\", \"arguments\": {\"key\": \"value\"}}` — use this fallback format\nonly, never both. Only call a tool whose exact name appears in the tool index above and is currently callable\n(or reachable via `capability_expand`). Never invent, rename, alias, or guess a tool name, platform ID, or\nplatform action from argument shape or wording — if the index does not list it, it does not exist.\n`platform_connect.action` (list/guide/preflight/connect/disconnect/remove/status/events_start/events_stop/\nevents_status) is the App Connector management namespace; `platform_action.action` only accepts exact\nregistered business actions such as `im.send_message` shown in the App Connector Capability Index — never mix\nthe two namespaces. If a tool call returns an unknown/unavailable result, use the returned suggestions or\navailable names for a single retry instead of trying further variations of the same guess.\n\n**Side-effect action rule** (`platform_action` with effect=send/write/execute):\n- Call each unique action+payload **exactly once**. Never include duplicates in the same turn.\n- Once the result returns `\"completed\": true`, that action is DONE for this task. Do NOT call it again\n in any subsequent turn — immediately summarize the result for the user instead.\n- The system enforces idempotency: duplicate calls are blocked and will not execute.\n- If the user explicitly requests sending/writing multiple times, use distinct payloads per call.\n\n**Resource identifier provenance rule**:\n- NEVER fabricate, guess, or infer resource identifiers (chat_id, message_id, file_key, user_id, etc.).\n Every resource ID used in a side-effect action MUST come from a successful API response in this session.\n- If a read/list action fails (e.g. authorization error), you do NOT have valid resource IDs.\n Report the failure to the user — do NOT attempt the dependent write/send action with a guessed ID.\n- When a tool result contains `\"llm_instruction\"`, follow it exactly.\n\n## Guidelines\n1. **Direct answers first**: If you already know the answer, respond directly without tools.\n2. **Avoid redundant tool calls**: Do not call the same tool with the same arguments more than once in the same user turn. When an existing tool result already answers the user's request, stop calling tools and answer directly.\n3. **Use tools proactively**: When the user asks about files, time, system state, or needs actions performed, use the appropriate tool.\n4. **Chain tools when needed**: You can call multiple tools in sequence (e.g., list files → read file → summarize).\n5. **Handle failures gracefully**: If a tool fails, explain what went wrong and suggest alternatives. If it failed because of an unknown tool/platform/action name, retry once with an exact name from the returned suggestions, then explain rather than keep guessing.\n6. **Summarize results naturally**: After tool execution, synthesize the results into a helpful answer rather than dumping raw output.\n7. **Stay conversational**: Maintain a natural, helpful tone. Acknowledge context from earlier in the conversation.\n8. **Recall past work**:\n - Broad queries (\"之前做了什么\", \"列出任务\"): answer from the \"Recent Task History\" section already in your context. If insufficient, call session_list.\n - Specific lookups (\"上次那个配置怎么改的\"): call session_search with relevant phrases (NOT single characters).\n - Do NOT call search tools repeatedly with keyword fragments. One well-phrased call is better than ten fragmented ones.\n\n## Coding & Verification\nWhen working with code, prefer the precise built-in tools over ad-hoc shell: use `repo_map` to orient in an\nunfamiliar project, `code_search`/`file_find` to locate and `code_intel` for symbols, `edit_file` (anchored\nsearch-replace, or a unified `diff`) to change files — never rewrite a whole file to change a few lines — and\n`git_query` to inspect diffs/log. After edits, check `syntax_ok` in the result and run `test_run`/`lint_check`\nbefore declaring the work done. Batch independent read-only calls (search/read/query) into a single turn — they\nrun in parallel.\n\n## Presentation Style\n1. **Polished Markdown only**: Format user-facing answers with clean Markdown headings, short paragraphs, and concise bullets. Use tables only when they improve comparison or scanning.\n2. **Terminal-friendly layout**: Keep lines readable in a TUI; avoid dense walls of text, deeply nested lists, oversized ASCII art, or heavy visual blocks.\n3. **Elegant emphasis**: Use bold text sparingly for key terms and conclusions. Avoid excessive emojis, decorative symbols, repeated separators, or visual noise.\n4. **Theme-safe colors**: Do not emit ANSI escape codes, HTML color tags, Rich markup, or hardcoded color names. Rely on the TUI theme to render Markdown professionally.\n5. **No leaked tool protocol**: Never show tool-call JSON, internal schemas, raw observations, tool result payloads, or hidden reasoning in the final answer unless the user explicitly asks for raw/debug output. Treat any prior `{\"name\": ..., \"arguments\": ...}` blocks and `Tool result (...)` messages as internal execution context only.\n6. **Professional closure**: End with a concise conclusion or next step when helpful; avoid rambling after the useful answer is complete.\n\nWhen finished with all tool calls, respond normally without a JSON block, tool-call transcript, or process log.\n\n## Task Contract\n- Task ID: turn-1\n- Original user request: List the connected hardware devices, then describe bench_r8 in full.\n- Workspace root: \n- Allowed roots: \n- Treat relative project paths as relative to the workspace root; never infer `.` as the project root when a workspace root is provided.\n- Workspace boundary is enforced by tools: do not read, search, edit, or run commands against paths outside the allowed roots unless the user explicitly requests an external path and the tool/approval policy permits it.\n- LeapFlow workspace config is optional at `/.leapflow/config.yaml`; runtime config is loaded from `~/.leapflow/config/user.yaml` and `~/.leapflow/profiles//config/*.yaml`.\n- Preserve this task contract across summarization, compression, tool loops, and memory retrieval.\n" + "content": "You are LeapFlow, an intelligent assistant that can both converse naturally and take real actions on the user's computer.\n\n## Capabilities\nThe tool index below lists **every** registered tool by name and a one-line summary — this is the complete\ncapability contract; nothing else exists. Only a subset is directly callable this turn (via native tool calling,\nnot a JSON block in your reply). If you need a tool from the index that is not yet callable, call\n`capability_expand` with its category name first — the matching tools become callable immediately after.\nWhen the user asks about LeapFlow **itself** — its identity/version/model, what tools, plugins, or capabilities\nit has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of\nself-awareness — answer from live runtime evidence via `self_describe` (facet=capabilities for \"what can you\ndo / do you support X\", identity for version/model, runtime for context/posture, or `all` for a full self-check),\nnever by reading LeapFlow's own source code, documentation, or memory. Report configuration-dependent or\nunavailable capabilities as limitations rather than inferring them.\n- **text_search**(text, pattern): Search for a regex pattern in text.\n- **text_replace**(text, old, new, count) [capability_expand category: write]: Replace occurrences of a substring in text.\n- **time_get**(): Get current date and time.\n- **env_info**(): Get system environment information (OS, Python version, cwd).\n- **skills_list**(query, category, source): List available learned skills. Use when user asks about capabilities or you need a specific skill.\n- **skill_view**(name): View the full content of a specific skill document.\n- **skill_list_files**(name) [capability_expand category: skills]: List all support files in a skill directory (configs, templates, examples). Use before skill_file_read to discover available files.\n- **skill_file_read**(name, file_path) [capability_expand category: skills]: Read an individual support file from a skill directory. Use skill_list_files first to discover available files.\n- **code_intel**(path, operation): Precise document symbols (outline) for a source file: classes, functions, and methods with line ranges. Python uses an exact AST parse; other languages use a keyword-prefix scan. Prefer over file_read mode=symbols for accurate navigation before editing. Read-only.\n- **repo_map**(path): Compact project orientation for a repository root: languages, detected test/lint commands, top-level structure, entry points, manifest, and VCS branch. Call this first when entering an unfamiliar codebase. Read-only.\n- **scm_sync**(action, cwd, remote, pull_ref, push_ref, timeout) [capability_expand category: scm]: Run a typed git SCM action. Use this instead of shell_run for git pull/push/status. For 'pull origin main then push', set action='pull_then_push', remote='origin', pull_ref='main', and omit push_ref so LeapFlow pushes the current local branch.\n- **git_query**(action, cwd, ref, path, staged, max_count, stat): Read-only structured git inspection: action=diff|log|status|branch|show. Prefer over shell_run for reading repo state — output is clipped, redacted, and log/branch are parsed into structured fields. Use scm_sync for pull/push.\n- **git_write**(action, cwd, message, stage_all, name, ref, create) [capability_expand category: scm]: Mutating git actions: action=commit (message, stage_all), branch (create+switch), checkout (switch; create=true for -b). Approval-gated. Use scm_sync for pull/push and git_query for reads.\n- **test_run**(command, cwd, timeout): Run the project's test suite and return structured results (framework, passed/failed counts, failing tests). Auto-detects the runner (pytest/npm/go/cargo) or uses a configured/explicit command; executes via the governed shell. ok=true means the runner executed — see 'success' for pass/fail.\n- **lint_check**(command, cwd, timeout): Run the project's linter and return a structured clean/issue result. Auto-detects the linter (ruff/eslint/go vet/clippy) or uses a configured/explicit command; executes via the governed shell. ok=true means the linter ran — see 'clean'.\n- **file_list**(path, pattern, depth): List files and directories at a given path. Use depth=1 or depth=2 to get a recursive tree in one call instead of listing each sub-directory separately.\n- **file_read**(path, max_lines, start_line, max_chars, mode): Read text file content with adaptive context governance. For large or unfamiliar files, prefer mode='outline' or mode='symbols' first, then use mode='raw' with start_line/max_lines for the specific range you actually need. For LeapFlow's own settings, use config_list / config_get / config_set — its config files are outside the workspace and not readable here.\n- **file_write**(path, content, mode) [capability_expand category: write]: Write content to a file (overwrite or append).\n- **code_search**(pattern, patterns, path, glob, ignore_case, multiline, max_results, context_lines): Search file CONTENTS by regex pattern across a directory tree (ripgrep-backed). Requires a regex pattern. NOT for listing or browsing directory contents — use file_list for that. Prefer this over shell_run grep: faster, skips VCS/dependency/build dirs, and returns structured path:line:column matches. Batch related lookups into ONE call via `patterns` (OR-combined, single pass) instead of issuing several separate searches. Use file_read for the surrounding context of a hit.\n- **file_find**(glob, path, max_results): Find files by a recursive glob pattern under a base path (e.g. '**/test_*.py' or '*.md'). Prefer this over shell_run find; skips VCS/dependency/build dirs.\n- **edit_file**(path, edits, dry_run, diff) [capability_expand category: write]: Apply targeted, anchored search-replace edits to an EXISTING text file (use file_write to create/overwrite). Each edit is {original_text, new_text, replace_all?}; original_text must match exactly and uniquely (or set replace_all) — a non-unique or missing anchor is rejected so files are never corrupted. Set dry_run to preview. Alternatively pass a unified 'diff' to apply its hunks as anchored edits. Far cheaper and safer than rewriting a whole file.\n- **shell_run**(command, cwd, timeout) [capability_expand category: shell]: Execute a one-shot shell command with timeout protection. Runs in the active workspace; paths resolving outside it are refused. Reach for a structured tool first when one fits — web_fetch for anything over HTTP(S), git_query/scm_sync for git, code_search/file_find/file_read for the repo, config_get/config_set for LeapFlow's own settings — because those report typed results, while a failed shell command can only be diagnosed from its exit code and stderr. Every shell run counts as an external side effect, so a failure stops the rest of the batch and is not retried automatically.\n- **terminal_open**(command, cwd, shell) [capability_expand category: terminal]: Open a PERSISTENT shell session (REPL/dev server/watch), returning a session_id for terminal_send/read/close. Disabled unless tools.terminal_session_enabled is set. For one-shot commands use shell_run instead.\n- **terminal_send**(session_id, input, wait) [capability_expand category: terminal]: Send a line of input to a persistent terminal session and return output captured shortly after.\n- **terminal_read**(session_id, wait): Drain buffered output from a persistent terminal session (optionally waiting briefly first).\n- **terminal_close**(session_id) [capability_expand category: terminal]: Terminate a persistent terminal session and release its process group.\n- **terminal_list**(): List active persistent terminal sessions.\n- **config_list**(category, limit): List LeapFlow's own writable settings (LLM model and endpoint, daemon, memory, perception, gateway, …) with current values. Use this to discover the exact key before changing anything. Optionally narrow by `category`. This is the only correct way to inspect LeapFlow configuration — never read config files from disk.\n- **config_get**(key): Read one LeapFlow setting by key (e.g. 'llm.model', 'llm.base_url', 'daemon.log_level'), returning its current value, type, scopes, and whether a change needs a daemon restart. There is no 'llm.provider' key: provider behavior is inferred from the OpenAI-compatible 'llm.base_url'. Never read LeapFlow config files from disk — use this.\n- **config_set**(key, value, scope) [capability_expand category: config]: Change one LeapFlow setting by key, e.g. switch an OpenAI-compatible LLM with key='llm.model' and key='llm.base_url'. There is no 'llm.provider' key: provider behavior is inferred from the endpoint. Values are validated and coerced; credentials are stored in the vault automatically. Call config_list or config_get first if unsure of the key. The result states whether a `leap daemon restart` is required. Never edit LeapFlow config files directly.\n- **web_fetch**(url, select, timeout, max_bytes): Read a URL over HTTP(S) and get back extracted, context-sized content: parsed JSON for API endpoints, readable text plus links for web pages. Use this for anything on the internet — prices, docs, releases, articles — instead of running curl through shell_run: it reports real HTTP status codes, retries rate limits on its own, and is a plain read so a retry is always safe. For JSON APIs pass `select` with a dotted path (e.g. 'chart.result.0.meta') to return just that part instead of the whole payload.\n- **memory_search**(query, limit): Search agent memory for relevant past experiences, observations, and facts.\n- **memory_add**(content, kind) [capability_expand category: write]: Store a new observation or insight in memory for future reference.\n- **research_note**(kind, text): Record a compact, structured note about the current task's state so it survives context compression on long / multi-step tasks. Use for durable findings, open questions still to resolve, decisions / excluded paths, and the immediate next step. One concise sentence per note.\n- **capability_expand**(category): Fetch the full callable schema for every tool in a capability category (e.g. 'hub', 'gateway', 'desktop', 'delegate', 'file', 'memory', 'skill'). The compact tool index always lists every registered tool by name and a one-line summary, but only a static low-risk subset is directly callable each turn. If you need a tool from the index that is not yet callable, call capability_expand with its category first; the matching tools become callable in this turn. Never invent a tool name — expand the category instead.\n- **delegate_task**(goal, context) [capability_expand category: delegate]: Delegate a complex sub-task to an isolated subagent. The subagent gets a fresh context and restricted tool access. Use when a task is self-contained and can be solved independently.\n- **schedule_reentry**(kind, reason, delay_seconds, event_match, max_reentries, deadline_seconds): Register a re-entry so this task can resume later from its current orientation (findings / open questions / next step). Use when work must pause and continue after a delay (kind=time) or when a matching platform event arrives (kind=event), instead of finishing now. The research-ledger state is carried over automatically.\n- **hub_push**(skill_name, visibility, version) [capability_expand category: hub]: Push a local skill to the ModelScope Hub for sharing or backup.\n- **hub_pull**(repo_id, version) [capability_expand category: hub]: Pull a skill from the ModelScope Hub to install locally.\n- **hub_search**(query) [capability_expand category: hub]: Search for skills on the Hub by keyword or description.\n- **hub_sync**(mode, dry_run) [capability_expand category: hub]: Preview or execute sync between local skills and Hub.\n- **platform_action**(platform, action, payload, backend_kind) [capability_expand category: gateway]: Execute an exact registered business action on an external platform through LeapFlow's App Connector layer. Actions must be copied from the App Connector Capability Index and are addressed as domain.operation, e.g. im.send_message or docs.create_markdown. All business fields (chat_id, text, query, etc.) MUST be placed inside `payload`, never at the top level. Example: {\"platform\":\"feishu\",\"action\":\"im.send_message\",\"payload\":{\"chat_id\":\"oc_xxx\",\"text\":\"hello\"}}. Do not invent action names, do not use management actions such as list/guide/connect/status here.\n- **platform_connect**(action, platform, credentials, options, checkpoint) [capability_expand category: gateway]: List, guide, connect, disconnect, remove, or check status for external platforms using the App Connector management namespace. Supports REST and CLI backends. Use this for management actions such as list/guide/preflight/connect/status; use platform_action only for exact registered business actions.\n- **gateway_send**(platform, chat_id, text, thread_id) [capability_expand category: gateway]: Send a message to a connected external platform (Feishu group, Telegram chat, DingTalk conversation, etc.). Requires the platform to be connected via gateway_connect first. Use gateway_connect with action='list' to see connected platforms and available chat IDs.\n- **gateway_connect**(action, platform, credentials, options) [capability_expand category: gateway]: Connect, configure, or manage external platform integrations (Feishu, DingTalk, Telegram, Slack, Discord, etc.). Conversational flow: 1) call 'guide' to get setup steps + required fields, 2) present the steps to the user and ask for ALL required credentials in a single message, 3) call 'connect' with the credentials. Goal: complete in 1–2 user turns. NEVER include credential values in your text response.\n- **plugin_list**(): List the live plugin registry across Tool/Gateway/LLM subsystems for plugin management: which plugins are loaded, their category and fiber lifecycle state. For questions about LeapFlow's own capabilities or whether it supports X, use self_describe instead.\n- **plugin_status**(plugin_id): Get detailed status of a specific plugin: its declared category, runtime dependencies, contributed tools, and fiber lifecycle state.\n- **plugin_versions**(plugin_id): List recorded source versions and active pointer for a profile-scoped plugin.\n- **plugin_propose**(requested_capability, plugin_id, proposed_tools, test_cases, risk_level, evidence): Create a side-effect-free PluginProposal from explicit capability-gap evidence. Use this before plugin_generate when a missing capability should be reviewed. Does not call an LLM, write files, or install anything.\n- **assess_compatibility**(manifest, source_path) [capability_expand category: plugin_management]: Assess whether a foreign plugin manifest or real DSH source bundle is compatible with LeapFlow. A source bundle assessment is static: runtime_ready stays false until restricted Node discovery during plugin_install. Returns component-level verdicts and limitations.\n- **plugin_generate**(plugin_id, description, proposal_id) [capability_expand category: system]: Generate a new ToolPlugin from a natural-language capability description. The LLM produces code that conforms to the ToolPlugin Protocol; it is then rigorously validated (syntax, structure, import, protocol conformance). The generated source is stored in CAS and receives explicit content approval. It DOES NOT install the plugin — installation requires a second, mutation-specific approval via plugin_install.\n- **plugin_install**(plugin_id, code, marketplace_name, source_path, proposal_id, version_label) [capability_expand category: system]: Install a plugin from exactly one source: validated Python code, the configured Python marketplace, or a real DSH source bundle. DSH bundles run restricted Node discovery before registration; only public host tools are installed and client UI limitations are reported. Writes profile-scoped state and mutates the process-global registry. REQUIRES APPROVAL.\n- **plugin_rollback**(plugin_id, version) [capability_expand category: system]: Rollback a profile-scoped plugin to a recorded source version and reload it. REQUIRES APPROVAL.\n- **plugin_reload**(plugin_id, version_label) [capability_expand category: system]: Hot-reload a plugin at runtime. Disposes the old plugin fiber, re-imports its module, and registers a fresh instance. Existing in-flight turns are unaffected (snapshot isolation). REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_disable**(plugin_id) [capability_expand category: system]: Disable a plugin by disposing its fiber, removing its tools from the runtime registry. Cannot disable self_management itself. REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_remove**(plugin_id, delete_source) [capability_expand category: system]: Terminally remove a plugin: dispose its fiber, unregister its tools, remove reload metadata, and optionally delete its profile-scoped source file. Cannot remove self_management itself. REQUIRES APPROVAL.\n- **plugin_unquarantine**(plugin_id) [capability_expand category: plugin_management]: Restore a quarantined plugin to probation status for re-evaluation. Unfreezes the trust ledger, transitions the proposal back to PROBATION, and reloads the plugin. REQUIRES APPROVAL.\n- **plugin_enable**(plugin_id) [capability_expand category: system]: Re-enable a previously disabled plugin by reloading its module and registering a fresh instance. REQUIRES APPROVAL.\n- **self_describe**(facet): Answer questions about LeapFlow ITSELF from live runtime evidence — its identity/version/model, what tools and plugins it has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of self-awareness. Prefer this over reading LeapFlow's own source code. Pick facet=capabilities for 'what can you do / do you support X', identity for version/model, runtime for context/posture, or all for a full self-check when explicitly requested.\n- **runtime_snapshot**(): Lightweight (~150 token) flat snapshot of current runtime state: model, context budget, posture, disclosure level, turn count, cache hit rate, uptime, tool count, and pending approvals.\n- **schedule_create**(trigger_expression, instruction, execution_mode, max_retries, delivery_target) [capability_expand category: scheduler]: Create a new scheduled task. Specify a trigger expression (e.g. '30m', 'every 2h', '0 9 * * *') and the instruction to execute on each trigger. Optionally provide an execution mode and a delivery target to receive notifications.\n- **schedule_list**() [capability_expand category: scheduler]: List all scheduled tasks with their current state, trigger, and next due time.\n- **schedule_status**(task_id) [capability_expand category: scheduler]: Get detailed status and recent execution history for a scheduled task.\n- **schedule_pause**(task_id) [capability_expand category: scheduler]: Pause a scheduled task so it stops firing without being cancelled.\n- **schedule_resume**(task_id) [capability_expand category: scheduler]: Resume a paused scheduled task — re-arms it and recalculates next due time.\n- **schedule_cancel**(task_id) [capability_expand category: scheduler]: Cancel a scheduled task permanently.\n- **tool_search**(query, max_results): Search all registered tools by keyword relevance. Returns a ranked list of matching tools with name, category, and summary. Use this when you need a tool but are unsure of its exact name or category.\n- **tool_describe**(tool_name): Get the full callable schema of a single tool by exact name. Returns the tool's description, parameters, and metadata. Use after tool_search to inspect a specific tool before calling it.\n- **hw_list**(): List connected hardware devices with their channel counts and measured quantities. Start here; it does not include operating limits.\n- **hw_describe**(device_id): Return the full reference for one device: every channel, its unit, its operating envelope, rate limit, reversibility, and required interlocks, plus any prior outcomes recorded for its writable channels. Required before commanding a device.\n- **hw_read**(device_id, channel_id): Read the current value of one device channel. Has no physical effect.\n- **hw_status**(device_id): Report connection health, halt capability, and recent notable events (threshold excursions, lost samples, stalled channels) for one device.\n- **hw_configure**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Set a configuration value or setpoint on a channel declaring effect=configure. Setpoints often have inertia: the value may need time to stabilise.\n- **hw_actuate**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Command motion or output on a channel declaring effect=actuate. This moves physical hardware; a repeat from an unknown state is not a safe retry.\n- **hw_dispense**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Consume an irreversible resource on a channel declaring effect=dispense. Running this twice dispenses twice; never repeat it after a failure without first verifying what already happened.\n- **hw_estop**(device_id) [capability_expand category: hardware]: Stop all motion and output on a device immediately. Never requires approval; use it whenever device behaviour is unexpected.\n- **hw_batch_actuate**(device_id, commands, dry_run, verify) [capability_expand category: hardware]: Write multiple actuator channels in one atomic transaction. Used for coordinated multi-joint control where all positions must be commanded simultaneously (e.g. robot arm joint-space control).\n- **hw_control_start**(device_id, mode, targets, frequency_hz, trajectory, policy, gains) [capability_expand category: hardware]: Start a real-time control loop on a robot device. Supports PID position tracking, impedance control, trajectory following, and neural policy chains.\n- **hw_control_stop**(device_id) [capability_expand category: hardware]: Stop the real-time control loop on a device. Gracefully halts the device and returns session statistics.\n- **hw_control_status**(device_id) [capability_expand category: hardware]: Get the status and statistics of a running control loop. Returns frequency, jitter, overruns, and policy state.\n- **hw_context_snapshot**(level) [capability_expand category: hardware]: Get a structured snapshot of all hardware devices and their state. Use this before planning physical manipulation tasks to understand what devices are available and what they can do.\n- **hw_health**(device_id) [capability_expand category: hardware]: Check hardware device health status. Without device_id, returns fleet-wide health summary. With device_id, returns health details for that specific device.\n- **hw_policy_infer**(device_id, policy, task, execute, chunk_size, verify, compute_budget) [capability_expand category: hardware]: Execute VLA policy inference on a robot device. Reads observation, runs policy, optionally writes action to actuators. Supports local (HuggingFace) and remote (PolicyServer) policies.\n- **hw_policy_episode**(action, device_id, policy, task) [capability_expand category: hardware]: Manage policy inference episode lifecycle. Start an episode to begin a task, stop to finalise and get stats.\n- **hw_policy_list**(source) [capability_expand category: hardware]: List available VLA policies and their capabilities.\n- **hw_orchestrate**(mode, operations, barrier_timeout_s, label) [capability_expand category: hardware]: Execute coordinated operations across multiple hardware devices. Supports sequential (one-at-a-time), parallel (concurrent), and barrier (synchronised) execution modes. Any failure in a parallel group triggers halt_all on every participating device for safety.\n- **session_search**(query, limit) [capability_expand category: unclassified]: Search past conversation sessions in the current workspace for relevant context.\n- **session_list**(limit): List recent conversation sessions in the current workspace with stable ids, titles, dates, and summaries. Use for browsing past tasks or when user asks to see history without specific search terms. No keywords needed — returns chronological list.\n- **session_detail**(session_id, limit, offset, include_inactive) [capability_expand category: unclassified]: Read a paginated persisted transcript for one past conversation session in the current workspace. Use after session_list or session_search returns a session_id.\n- **click**(element_index) [capability_expand category: desktop]: Click a UI element by its element_index (from the latest observe_ui snapshot)\n- **get_clipboard**() [capability_expand category: desktop]: Read current clipboard text content\n- **list_apps**(filter, running_only) [capability_expand category: desktop]: List available applications on this system. Use to discover correct bundle_id before switch_app.\n- **list_windows**() [capability_expand category: desktop]: List all top-level windows with pid, window_id, title, and per-window state (minimized, on-screen). Call this first to pick the pid and window_id that observe_ui and other window tools require.\n- **observe_ui**(pid, window_id, query) [capability_expand category: desktop]: Snapshot one window's actionable UI elements, each tagged with an element_index for click/right_click/read_text. Re-observe after actions — indices belong to one snapshot. Requires the window's pid and window_id from list_windows.\n- **open_url**(url, app_id) [capability_expand category: desktop]: Open a URL in the default or specified browser\n- **read_text**(element_index) [capability_expand category: desktop]: Read the text content of a specific UI element from the latest snapshot\n- **right_click**(element_index) [capability_expand category: desktop]: Right-click a UI element to open its context menu. Returns visible menu items.\n- **screenshot**(pid, window_id) [capability_expand category: desktop]: Capture a screenshot for visual verification. With pid + window_id captures that window (works across all displays); defaults to the last observed window, or the full desktop when no window has been observed.\n- **scroll**(element_index, direction, amount, pid, window_id) [capability_expand category: desktop]: Scroll a scrollable area of a window. Omit element_index to scroll the window's focused/page scroller; pass one to scroll an exact element from the latest snapshot.\n- **select_text**(element_index) [capability_expand category: desktop]: Select all text in a UI element (focus + select-all, for subsequent copy)\n- **set_clipboard**(text) [capability_expand category: desktop]: Write text to the clipboard\n- **shortcut**(keys) [capability_expand category: desktop]: Execute a keyboard shortcut\n- **switch_app**(app_id) [capability_expand category: desktop]: Switch to an app (launch if needed, activate, verify)\n- **type_text**(text) [capability_expand category: desktop]: Type text into the currently focused element\n- **wait**(seconds) [capability_expand category: desktop]: Wait for a specified duration before continuing\n- **wait_until**(condition, pid, window_id, timeout, poll_interval) [capability_expand category: desktop]: Wait until a UI condition is met (polls UI tree). Returns elements when found or on timeout.\n- **wait_until_stable**(timeout, poll_interval, pid, window_id) [capability_expand category: desktop]: Wait until the UI stops changing (element set stabilizes across polls).\n\n## App Connector Capability Index\nLeapFlow can onboard and manage external apps through `platform_connect` and execute exact registered business actions through `platform_action`.\nFor requests about connecting, setting up, configuring, enabling, or managing a supported app, use `platform_connect` first instead of generating SDK/Webhook sample code.\n`platform_connect.action` is the management namespace: list, guide, preflight, connect, disconnect, remove, status, events_start, events_stop, events_status.\n`platform_action.action` is only for exact registered platform business actions listed below, such as `im.send_message`; never use management actions like `list` or `guide` there.\nAll business fields MUST go inside `payload`; top-level keys are only `platform`, `action`, and `payload`.\nDo not invent platform IDs or platform action names. If the needed action is not listed, ask for discovery/clarification instead of guessing.\nUse `platform_connect` with `action='guide'` and the matching `platform` to start onboarding; use `action='list'` when the app is unclear.\nWhen a pending onboarding state is present, continue from that state with `platform_connect` instead of asking the user to restate the app.\nSupported apps:\n- `api_server`: API Server (OpenAI Compatible) (category=api; backend=adapter; platform_action actions=none registered)\n- `dingtalk`: 钉钉 (DingTalk) (category=im; backend=adapter; platform_action actions=none registered)\n- `feishu`: 飞书 (Feishu/Lark) (category=im; backend=cli)\n - `calendar.create_event` payload={summary*, start_time*, end_time*, attendees} [write/high]\n - `docs.create_markdown` payload={title*, markdown*, folder_token} [write/medium]\n - `drive.search` payload={query*, limit} [read/medium]\n - `im.add_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.download_resource` payload={message_id*, file_key*, type*} [read/low]\n - `im.get_messages` payload={message_ids*} [read/low]\n - `im.list_chats` payload={page_size} [read/low]\n - `im.list_messages` payload={chat_id*} [read/low]\n - `im.list_thread_messages` payload={thread*} [read/low]\n - `im.remove_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.reply_message` payload={message_id*, text*} [send/high]\n - `im.search_chats` payload={query*, page_size} [read/low]\n - `im.search_messages` payload={query*} [read/medium]\n - `im.send_message` payload={chat_id*, text*, thread_id} [send/high]\n - `im.update_card` payload={token*, card*} [write/medium]\n - `im.update_message` payload={message_id*, text*} [write/medium]\n - `mail.search_unread` payload={query, limit} [read/high]\n - `sheets.append_row` payload={spreadsheet_token*, sheet_id*, values*} [write/medium]\n - `task.create` payload={title*, description, due_time} [write/medium]\n- `telegram`: Telegram (category=social; backend=adapter; platform_action actions=none registered)\n- `webhook`: Webhook (Generic) (category=webhook; backend=adapter; platform_action actions=none registered)\n\n## Tool Usage\nTools are normally invoked through the native function-calling mechanism, not by writing JSON in your reply\ntext. Only if the provider signals that native function calling is unavailable for this turn, fall back to a\nsingle JSON code block: `{\"name\": \"tool_name\", \"arguments\": {\"key\": \"value\"}}` — use this fallback format\nonly, never both. Only call a tool whose exact name appears in the tool index above and is currently callable\n(or reachable via `capability_expand`). Never invent, rename, alias, or guess a tool name, platform ID, or\nplatform action from argument shape or wording — if the index does not list it, it does not exist.\n`platform_connect.action` (list/guide/preflight/connect/disconnect/remove/status/events_start/events_stop/\nevents_status) is the App Connector management namespace; `platform_action.action` only accepts exact\nregistered business actions such as `im.send_message` shown in the App Connector Capability Index — never mix\nthe two namespaces. If a tool call returns an unknown/unavailable result, use the returned suggestions or\navailable names for a single retry instead of trying further variations of the same guess.\n\n**Side-effect action rule** (`platform_action` with effect=send/write/execute):\n- Call each unique action+payload **exactly once**. Never include duplicates in the same turn.\n- Once the result returns `\"completed\": true`, that action is DONE for this task. Do NOT call it again\n in any subsequent turn — immediately summarize the result for the user instead.\n- The system enforces idempotency: duplicate calls are blocked and will not execute.\n- If the user explicitly requests sending/writing multiple times, use distinct payloads per call.\n\n**Resource identifier provenance rule**:\n- NEVER fabricate, guess, or infer resource identifiers (chat_id, message_id, file_key, user_id, etc.).\n Every resource ID used in a side-effect action MUST come from a successful API response in this session.\n- If a read/list action fails (e.g. authorization error), you do NOT have valid resource IDs.\n Report the failure to the user — do NOT attempt the dependent write/send action with a guessed ID.\n- When a tool result contains `\"llm_instruction\"`, follow it exactly.\n\n## Guidelines\n1. **Direct answers first**: If you already know the answer, respond directly without tools.\n2. **Avoid redundant tool calls**: Do not call the same tool with the same arguments more than once in the same user turn. When an existing tool result already answers the user's request, stop calling tools and answer directly.\n3. **Use tools proactively**: When the user asks about files, time, system state, or needs actions performed, use the appropriate tool.\n4. **Chain tools when needed**: You can call multiple tools in sequence (e.g., list files → read file → summarize).\n5. **Handle failures gracefully**: If a tool fails, explain what went wrong and suggest alternatives. If it failed because of an unknown tool/platform/action name, retry once with an exact name from the returned suggestions, then explain rather than keep guessing.\n6. **Summarize results naturally**: After tool execution, synthesize the results into a helpful answer rather than dumping raw output.\n7. **Stay conversational**: Maintain a natural, helpful tone. Acknowledge context from earlier in the conversation.\n8. **Recall past work**:\n - Broad queries (\"之前做了什么\", \"列出任务\"): answer from the \"Recent Task History\" section already in your context. If insufficient, call session_list.\n - Specific lookups (\"上次那个配置怎么改的\"): call session_search with relevant phrases (NOT single characters).\n - Do NOT call search tools repeatedly with keyword fragments. One well-phrased call is better than ten fragmented ones.\n\n## Coding & Verification\nWhen working with code, prefer the precise built-in tools over ad-hoc shell: use `repo_map` to orient in an\nunfamiliar project, `code_search`/`file_find` to locate and `code_intel` for symbols, `edit_file` (anchored\nsearch-replace, or a unified `diff`) to change files — never rewrite a whole file to change a few lines — and\n`git_query` to inspect diffs/log. After edits, check `syntax_ok` in the result and run `test_run`/`lint_check`\nbefore declaring the work done. Batch independent read-only calls (search/read/query) into a single turn — they\nrun in parallel.\n\n## Presentation Style\n1. **Polished Markdown only**: Format user-facing answers with clean Markdown headings, short paragraphs, and concise bullets. Use tables only when they improve comparison or scanning.\n2. **Terminal-friendly layout**: Keep lines readable in a TUI; avoid dense walls of text, deeply nested lists, oversized ASCII art, or heavy visual blocks.\n3. **Elegant emphasis**: Use bold text sparingly for key terms and conclusions. Avoid excessive emojis, decorative symbols, repeated separators, or visual noise.\n4. **Theme-safe colors**: Do not emit ANSI escape codes, HTML color tags, Rich markup, or hardcoded color names. Rely on the TUI theme to render Markdown professionally.\n5. **No leaked tool protocol**: Never show tool-call JSON, internal schemas, raw observations, tool result payloads, or hidden reasoning in the final answer unless the user explicitly asks for raw/debug output. Treat any prior `{\"name\": ..., \"arguments\": ...}` blocks and `Tool result (...)` messages as internal execution context only.\n6. **Professional closure**: End with a concise conclusion or next step when helpful; avoid rambling after the useful answer is complete.\n\nWhen finished with all tool calls, respond normally without a JSON block, tool-call transcript, or process log.\n\n## Task Contract\n- Task ID: turn-1\n- Original user request: List the connected hardware devices, then describe bench_r8 in full.\n- Workspace root: \n- Allowed roots: \n- Treat relative project paths as relative to the workspace root; never infer `.` as the project root when a workspace root is provided.\n- Workspace boundary is enforced by tools: do not read, search, edit, or run commands against paths outside the allowed roots unless the user explicitly requests an external path and the tool/approval policy permits it.\n- LeapFlow workspace config is optional at `/.leapflow/config.yaml`; runtime config is loaded from `~/.leapflow/config/user.yaml` and `~/.leapflow/profiles//config/*.yaml`.\n- Preserve this task contract across summarization, compression, tool loops, and memory retrieval.\n" }, { "role": "system", diff --git a/tests/_fixtures/cassettes/r8_hardware/cassette-model-d96419d4e6a98990.cassette.json b/tests/_fixtures/cassettes/r8_hardware/cassette-model-d96419d4e6a98990.cassette.json deleted file mode 100644 index 478c2807..00000000 --- a/tests/_fixtures/cassettes/r8_hardware/cassette-model-d96419d4e6a98990.cassette.json +++ /dev/null @@ -1,67 +0,0 @@ -{ - "fingerprint": "d96419d4e6a9899093c701b4ff135265ecfd642a8230c6bf2aee2f6308efdb85", - "note": "captured in seed mode", - "request": { - "model": "cassette-model", - "stream": false, - "messages": [ - { - "role": "system", - "content": "You are LeapFlow, an intelligent assistant that can both converse naturally and take real actions on the user's computer.\n\n## Capabilities\nThe tool index below lists **every** registered tool by name and a one-line summary — this is the complete\ncapability contract; nothing else exists. Only a subset is directly callable this turn (via native tool calling,\nnot a JSON block in your reply). If you need a tool from the index that is not yet callable, call\n`capability_expand` with its category name first — the matching tools become callable immediately after.\nWhen the user asks about LeapFlow **itself** — its identity/version/model, what tools, plugins, or capabilities\nit has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of\nself-awareness — answer from live runtime evidence via `self_describe` (facet=capabilities for \"what can you\ndo / do you support X\", identity for version/model, runtime for context/posture, or `all` for a full self-check),\nnever by reading LeapFlow's own source code, documentation, or memory. Report configuration-dependent or\nunavailable capabilities as limitations rather than inferring them.\n- **text_search**(text, pattern): Search for a regex pattern in text.\n- **text_replace**(text, old, new, count) [capability_expand category: write]: Replace occurrences of a substring in text.\n- **time_get**(): Get current date and time.\n- **env_info**(): Get system environment information (OS, Python version, cwd).\n- **skills_list**(query, category, source): List available learned skills. Use when user asks about capabilities or you need a specific skill.\n- **skill_view**(name): View the full content of a specific skill document.\n- **skill_list_files**(name) [capability_expand category: skills]: List all support files in a skill directory (configs, templates, examples). Use before skill_file_read to discover available files.\n- **skill_file_read**(name, file_path) [capability_expand category: skills]: Read an individual support file from a skill directory. Use skill_list_files first to discover available files.\n- **code_intel**(path, operation): Precise document symbols (outline) for a source file: classes, functions, and methods with line ranges. Python uses an exact AST parse; other languages use a keyword-prefix scan. Prefer over file_read mode=symbols for accurate navigation before editing. Read-only.\n- **repo_map**(path): Compact project orientation for a repository root: languages, detected test/lint commands, top-level structure, entry points, manifest, and VCS branch. Call this first when entering an unfamiliar codebase. Read-only.\n- **scm_sync**(action, cwd, remote, pull_ref, push_ref, timeout) [capability_expand category: scm]: Run a typed git SCM action. Use this instead of shell_run for git pull/push/status. For 'pull origin main then push', set action='pull_then_push', remote='origin', pull_ref='main', and omit push_ref so LeapFlow pushes the current local branch.\n- **git_query**(action, cwd, ref, path, staged, max_count, stat): Read-only structured git inspection: action=diff|log|status|branch|show. Prefer over shell_run for reading repo state — output is clipped, redacted, and log/branch are parsed into structured fields. Use scm_sync for pull/push.\n- **git_write**(action, cwd, message, stage_all, name, ref, create) [capability_expand category: scm]: Mutating git actions: action=commit (message, stage_all), branch (create+switch), checkout (switch; create=true for -b). Approval-gated. Use scm_sync for pull/push and git_query for reads.\n- **test_run**(command, cwd, timeout): Run the project's test suite and return structured results (framework, passed/failed counts, failing tests). Auto-detects the runner (pytest/npm/go/cargo) or uses a configured/explicit command; executes via the governed shell. ok=true means the runner executed — see 'success' for pass/fail.\n- **lint_check**(command, cwd, timeout): Run the project's linter and return a structured clean/issue result. Auto-detects the linter (ruff/eslint/go vet/clippy) or uses a configured/explicit command; executes via the governed shell. ok=true means the linter ran — see 'clean'.\n- **file_list**(path, pattern, depth): List files and directories at a given path. Use depth=1 or depth=2 to get a recursive tree in one call instead of listing each sub-directory separately.\n- **file_read**(path, max_lines, start_line, max_chars, mode): Read text file content with adaptive context governance. For large or unfamiliar files, prefer mode='outline' or mode='symbols' first, then use mode='raw' with start_line/max_lines for the specific range you actually need. For LeapFlow's own settings, use config_list / config_get / config_set — its config files are outside the workspace and not readable here.\n- **file_write**(path, content, mode) [capability_expand category: write]: Write content to a file (overwrite or append).\n- **code_search**(pattern, patterns, path, glob, ignore_case, multiline, max_results, context_lines): Search file CONTENTS by regex pattern across a directory tree (ripgrep-backed). Requires a regex pattern. NOT for listing or browsing directory contents — use file_list for that. Prefer this over shell_run grep: faster, skips VCS/dependency/build dirs, and returns structured path:line:column matches. Batch related lookups into ONE call via `patterns` (OR-combined, single pass) instead of issuing several separate searches. Use file_read for the surrounding context of a hit.\n- **file_find**(glob, path, max_results): Find files by a recursive glob pattern under a base path (e.g. '**/test_*.py' or '*.md'). Prefer this over shell_run find; skips VCS/dependency/build dirs.\n- **edit_file**(path, edits, dry_run, diff) [capability_expand category: write]: Apply targeted, anchored search-replace edits to an EXISTING text file (use file_write to create/overwrite). Each edit is {original_text, new_text, replace_all?}; original_text must match exactly and uniquely (or set replace_all) — a non-unique or missing anchor is rejected so files are never corrupted. Set dry_run to preview. Alternatively pass a unified 'diff' to apply its hunks as anchored edits. Far cheaper and safer than rewriting a whole file.\n- **shell_run**(command, cwd, timeout) [capability_expand category: shell]: Execute a one-shot shell command with timeout protection. Runs in the active workspace; paths resolving outside it are refused. Reach for a structured tool first when one fits — web_fetch for anything over HTTP(S), git_query/scm_sync for git, code_search/file_find/file_read for the repo, config_get/config_set for LeapFlow's own settings — because those report typed results, while a failed shell command can only be diagnosed from its exit code and stderr. Every shell run counts as an external side effect, so a failure stops the rest of the batch and is not retried automatically.\n- **terminal_open**(command, cwd, shell) [capability_expand category: terminal]: Open a PERSISTENT shell session (REPL/dev server/watch), returning a session_id for terminal_send/read/close. Disabled unless tools.terminal_session_enabled is set. For one-shot commands use shell_run instead.\n- **terminal_send**(session_id, input, wait) [capability_expand category: terminal]: Send a line of input to a persistent terminal session and return output captured shortly after.\n- **terminal_read**(session_id, wait): Drain buffered output from a persistent terminal session (optionally waiting briefly first).\n- **terminal_close**(session_id) [capability_expand category: terminal]: Terminate a persistent terminal session and release its process group.\n- **terminal_list**(): List active persistent terminal sessions.\n- **config_list**(category, limit): List LeapFlow's own writable settings (LLM model and endpoint, daemon, memory, perception, gateway, …) with current values. Use this to discover the exact key before changing anything. Optionally narrow by `category`. This is the only correct way to inspect LeapFlow configuration — never read config files from disk.\n- **config_get**(key): Read one LeapFlow setting by key (e.g. 'llm.model', 'llm.base_url', 'daemon.log_level'), returning its current value, type, scopes, and whether a change needs a daemon restart. There is no 'llm.provider' key: provider behavior is inferred from the OpenAI-compatible 'llm.base_url'. Never read LeapFlow config files from disk — use this.\n- **config_set**(key, value, scope) [capability_expand category: config]: Change one LeapFlow setting by key, e.g. switch an OpenAI-compatible LLM with key='llm.model' and key='llm.base_url'. There is no 'llm.provider' key: provider behavior is inferred from the endpoint. Values are validated and coerced; credentials are stored in the vault automatically. Call config_list or config_get first if unsure of the key. The result states whether a `leap daemon restart` is required. Never edit LeapFlow config files directly.\n- **web_fetch**(url, select, timeout, max_bytes): Read a URL over HTTP(S) and get back extracted, context-sized content: parsed JSON for API endpoints, readable text plus links for web pages. Use this for anything on the internet — prices, docs, releases, articles — instead of running curl through shell_run: it reports real HTTP status codes, retries rate limits on its own, and is a plain read so a retry is always safe. For JSON APIs pass `select` with a dotted path (e.g. 'chart.result.0.meta') to return just that part instead of the whole payload.\n- **memory_search**(query, limit): Search agent memory for relevant past experiences, observations, and facts.\n- **memory_add**(content, kind) [capability_expand category: write]: Store a new observation or insight in memory for future reference.\n- **research_note**(kind, text): Record a compact, structured note about the current task's state so it survives context compression on long / multi-step tasks. Use for durable findings, open questions still to resolve, decisions / excluded paths, and the immediate next step. One concise sentence per note.\n- **capability_expand**(category): Fetch the full callable schema for every tool in a capability category (e.g. 'hub', 'gateway', 'desktop', 'delegate', 'file', 'memory', 'skill'). The compact tool index always lists every registered tool by name and a one-line summary, but only a static low-risk subset is directly callable each turn. If you need a tool from the index that is not yet callable, call capability_expand with its category first; the matching tools become callable in this turn. Never invent a tool name — expand the category instead.\n- **delegate_task**(goal, context) [capability_expand category: delegate]: Delegate a complex sub-task to an isolated subagent. The subagent gets a fresh context and restricted tool access. Use when a task is self-contained and can be solved independently.\n- **schedule_reentry**(kind, reason, delay_seconds, event_match, max_reentries, deadline_seconds): Register a re-entry so this task can resume later from its current orientation (findings / open questions / next step). Use when work must pause and continue after a delay (kind=time) or when a matching platform event arrives (kind=event), instead of finishing now. The research-ledger state is carried over automatically.\n- **hub_push**(skill_name, visibility, version) [capability_expand category: hub]: Push a local skill to the ModelScope Hub for sharing or backup.\n- **hub_pull**(repo_id, version) [capability_expand category: hub]: Pull a skill from the ModelScope Hub to install locally.\n- **hub_search**(query) [capability_expand category: hub]: Search for skills on the Hub by keyword or description.\n- **hub_sync**(mode, dry_run) [capability_expand category: hub]: Preview or execute sync between local skills and Hub.\n- **platform_action**(platform, action, payload, backend_kind) [capability_expand category: gateway]: Execute an exact registered business action on an external platform through LeapFlow's App Connector layer. Actions must be copied from the App Connector Capability Index and are addressed as domain.operation, e.g. im.send_message or docs.create_markdown. All business fields (chat_id, text, query, etc.) MUST be placed inside `payload`, never at the top level. Example: {\"platform\":\"feishu\",\"action\":\"im.send_message\",\"payload\":{\"chat_id\":\"oc_xxx\",\"text\":\"hello\"}}. Do not invent action names, do not use management actions such as list/guide/connect/status here.\n- **platform_connect**(action, platform, credentials, options, checkpoint) [capability_expand category: gateway]: List, guide, connect, disconnect, remove, or check status for external platforms using the App Connector management namespace. Supports REST and CLI backends. Use this for management actions such as list/guide/preflight/connect/status; use platform_action only for exact registered business actions.\n- **gateway_send**(platform, chat_id, text, thread_id) [capability_expand category: gateway]: Send a message to a connected external platform (Feishu group, Telegram chat, DingTalk conversation, etc.). Requires the platform to be connected via gateway_connect first. Use gateway_connect with action='list' to see connected platforms and available chat IDs.\n- **gateway_connect**(action, platform, credentials, options) [capability_expand category: gateway]: Connect, configure, or manage external platform integrations (Feishu, DingTalk, Telegram, Slack, Discord, etc.). Conversational flow: 1) call 'guide' to get setup steps + required fields, 2) present the steps to the user and ask for ALL required credentials in a single message, 3) call 'connect' with the credentials. Goal: complete in 1–2 user turns. NEVER include credential values in your text response.\n- **plugin_list**(): List the live plugin registry across Tool/Gateway/LLM subsystems for plugin management: which plugins are loaded, their category and fiber lifecycle state. For questions about LeapFlow's own capabilities or whether it supports X, use self_describe instead.\n- **plugin_status**(plugin_id): Get detailed status of a specific plugin: its declared category, runtime dependencies, contributed tools, and fiber lifecycle state.\n- **plugin_versions**(plugin_id): List recorded source versions and active pointer for a profile-scoped plugin.\n- **plugin_propose**(requested_capability, plugin_id, proposed_tools, test_cases, risk_level, evidence): Create a side-effect-free PluginProposal from explicit capability-gap evidence. Use this before plugin_generate when a missing capability should be reviewed. Does not call an LLM, write files, or install anything.\n- **assess_compatibility**(manifest, source_path) [capability_expand category: plugin_management]: Assess whether a foreign plugin manifest or real DSH source bundle is compatible with LeapFlow. A source bundle assessment is static: runtime_ready stays false until restricted Node discovery during plugin_install. Returns component-level verdicts and limitations.\n- **plugin_generate**(plugin_id, description, proposal_id) [capability_expand category: system]: Generate a new ToolPlugin from a natural-language capability description. The LLM produces code that conforms to the ToolPlugin Protocol; it is then rigorously validated (syntax, structure, import, protocol conformance). The generated source is stored in CAS and receives explicit content approval. It DOES NOT install the plugin — installation requires a second, mutation-specific approval via plugin_install.\n- **plugin_install**(plugin_id, code, marketplace_name, source_path, proposal_id, version_label) [capability_expand category: system]: Install a plugin from exactly one source: validated Python code, the configured Python marketplace, or a real DSH source bundle. DSH bundles run restricted Node discovery before registration; only public host tools are installed and client UI limitations are reported. Writes profile-scoped state and mutates the process-global registry. REQUIRES APPROVAL.\n- **plugin_rollback**(plugin_id, version) [capability_expand category: system]: Rollback a profile-scoped plugin to a recorded source version and reload it. REQUIRES APPROVAL.\n- **plugin_reload**(plugin_id, version_label) [capability_expand category: system]: Hot-reload a plugin at runtime. Disposes the old plugin fiber, re-imports its module, and registers a fresh instance. Existing in-flight turns are unaffected (snapshot isolation). REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_disable**(plugin_id) [capability_expand category: system]: Disable a plugin by disposing its fiber, removing its tools from the runtime registry. Cannot disable self_management itself. REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_remove**(plugin_id, delete_source) [capability_expand category: system]: Terminally remove a plugin: dispose its fiber, unregister its tools, remove reload metadata, and optionally delete its profile-scoped source file. Cannot remove self_management itself. REQUIRES APPROVAL.\n- **plugin_unquarantine**(plugin_id) [capability_expand category: plugin_management]: Restore a quarantined plugin to probation status for re-evaluation. Unfreezes the trust ledger, transitions the proposal back to PROBATION, and reloads the plugin. REQUIRES APPROVAL.\n- **plugin_enable**(plugin_id) [capability_expand category: system]: Re-enable a previously disabled plugin by reloading its module and registering a fresh instance. REQUIRES APPROVAL.\n- **self_describe**(facet): Answer questions about LeapFlow ITSELF from live runtime evidence — its identity/version/model, what tools and plugins it has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of self-awareness. Prefer this over reading LeapFlow's own source code. Pick facet=capabilities for 'what can you do / do you support X', identity for version/model, runtime for context/posture, or all for a full self-check when explicitly requested.\n- **runtime_snapshot**(): Lightweight (~150 token) flat snapshot of current runtime state: model, context budget, posture, disclosure level, turn count, cache hit rate, uptime, tool count, and pending approvals.\n- **schedule_create**(trigger_expression, instruction, execution_mode, max_retries, delivery_target) [capability_expand category: scheduler]: Create a new scheduled task. Specify a trigger expression (e.g. '30m', 'every 2h', '0 9 * * *') and the instruction to execute on each trigger. Optionally provide an execution mode and a delivery target to receive notifications.\n- **schedule_list**() [capability_expand category: scheduler]: List all scheduled tasks with their current state, trigger, and next due time.\n- **schedule_status**(task_id) [capability_expand category: scheduler]: Get detailed status and recent execution history for a scheduled task.\n- **schedule_pause**(task_id) [capability_expand category: scheduler]: Pause a scheduled task so it stops firing without being cancelled.\n- **schedule_resume**(task_id) [capability_expand category: scheduler]: Resume a paused scheduled task — re-arms it and recalculates next due time.\n- **schedule_cancel**(task_id) [capability_expand category: scheduler]: Cancel a scheduled task permanently.\n- **tool_search**(query, max_results): Search all registered tools by keyword relevance. Returns a ranked list of matching tools with name, category, and summary. Use this when you need a tool but are unsure of its exact name or category.\n- **tool_describe**(tool_name): Get the full callable schema of a single tool by exact name. Returns the tool's description, parameters, and metadata. Use after tool_search to inspect a specific tool before calling it.\n- **hw_list**(): List connected hardware devices with their channel counts and measured quantities. Start here; it does not include operating limits.\n- **hw_describe**(device_id): Return the full reference for one device: every channel, its unit, its operating envelope, rate limit, reversibility, and required interlocks, plus any prior outcomes recorded for its writable channels. Required before commanding a device.\n- **hw_read**(device_id, channel_id): Read the current value of one device channel. Has no physical effect.\n- **hw_status**(device_id): Report connection health, halt capability, and recent notable events (threshold excursions, lost samples, stalled channels) for one device.\n- **hw_configure**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Set a configuration value or setpoint on a channel declaring effect=configure. Setpoints often have inertia: the value may need time to stabilise.\n- **hw_actuate**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Command motion or output on a channel declaring effect=actuate. This moves physical hardware; a repeat from an unknown state is not a safe retry.\n- **hw_dispense**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Consume an irreversible resource on a channel declaring effect=dispense. Running this twice dispenses twice; never repeat it after a failure without first verifying what already happened.\n- **hw_estop**(device_id) [capability_expand category: hardware]: Stop all motion and output on a device immediately. Never requires approval; use it whenever device behaviour is unexpected.\n- **hw_batch_actuate**(device_id, commands, dry_run, verify) [capability_expand category: hardware]: Write multiple actuator channels in one atomic transaction. Used for coordinated multi-joint control where all positions must be commanded simultaneously (e.g. robot arm joint-space control).\n- **session_search**(query, limit) [capability_expand category: unclassified]: Search past conversation sessions in the current workspace for relevant context.\n- **session_list**(limit): List recent conversation sessions in the current workspace with stable ids, titles, dates, and summaries. Use for browsing past tasks or when user asks to see history without specific search terms. No keywords needed — returns chronological list.\n- **session_detail**(session_id, limit, offset, include_inactive) [capability_expand category: unclassified]: Read a paginated persisted transcript for one past conversation session in the current workspace. Use after session_list or session_search returns a session_id.\n- **click**(element_index) [capability_expand category: desktop]: Click a UI element by its element_index (from the latest observe_ui snapshot)\n- **get_clipboard**() [capability_expand category: desktop]: Read current clipboard text content\n- **list_apps**(filter, running_only) [capability_expand category: desktop]: List available applications on this system. Use to discover correct bundle_id before switch_app.\n- **list_windows**() [capability_expand category: desktop]: List all top-level windows with pid, window_id, title, and per-window state (minimized, on-screen). Call this first to pick the pid and window_id that observe_ui and other window tools require.\n- **observe_ui**(pid, window_id, query) [capability_expand category: desktop]: Snapshot one window's actionable UI elements, each tagged with an element_index for click/right_click/read_text. Re-observe after actions — indices belong to one snapshot. Requires the window's pid and window_id from list_windows.\n- **open_url**(url, app_id) [capability_expand category: desktop]: Open a URL in the default or specified browser\n- **read_text**(element_index) [capability_expand category: desktop]: Read the text content of a specific UI element from the latest snapshot\n- **right_click**(element_index) [capability_expand category: desktop]: Right-click a UI element to open its context menu. Returns visible menu items.\n- **screenshot**(pid, window_id) [capability_expand category: desktop]: Capture a screenshot for visual verification. With pid + window_id captures that window (works across all displays); defaults to the last observed window, or the full desktop when no window has been observed.\n- **scroll**(element_index, direction, amount, pid, window_id) [capability_expand category: desktop]: Scroll a scrollable area of a window. Omit element_index to scroll the window's focused/page scroller; pass one to scroll an exact element from the latest snapshot.\n- **select_text**(element_index) [capability_expand category: desktop]: Select all text in a UI element (focus + select-all, for subsequent copy)\n- **set_clipboard**(text) [capability_expand category: desktop]: Write text to the clipboard\n- **shortcut**(keys) [capability_expand category: desktop]: Execute a keyboard shortcut\n- **switch_app**(app_id) [capability_expand category: desktop]: Switch to an app (launch if needed, activate, verify)\n- **type_text**(text) [capability_expand category: desktop]: Type text into the currently focused element\n- **wait**(seconds) [capability_expand category: desktop]: Wait for a specified duration before continuing\n- **wait_until**(condition, pid, window_id, timeout, poll_interval) [capability_expand category: desktop]: Wait until a UI condition is met (polls UI tree). Returns elements when found or on timeout.\n- **wait_until_stable**(timeout, poll_interval, pid, window_id) [capability_expand category: desktop]: Wait until the UI stops changing (element set stabilizes across polls).\n\n## App Connector Capability Index\nLeapFlow can onboard and manage external apps through `platform_connect` and execute exact registered business actions through `platform_action`.\nFor requests about connecting, setting up, configuring, enabling, or managing a supported app, use `platform_connect` first instead of generating SDK/Webhook sample code.\n`platform_connect.action` is the management namespace: list, guide, preflight, connect, disconnect, remove, status, events_start, events_stop, events_status.\n`platform_action.action` is only for exact registered platform business actions listed below, such as `im.send_message`; never use management actions like `list` or `guide` there.\nAll business fields MUST go inside `payload`; top-level keys are only `platform`, `action`, and `payload`.\nDo not invent platform IDs or platform action names. If the needed action is not listed, ask for discovery/clarification instead of guessing.\nUse `platform_connect` with `action='guide'` and the matching `platform` to start onboarding; use `action='list'` when the app is unclear.\nWhen a pending onboarding state is present, continue from that state with `platform_connect` instead of asking the user to restate the app.\nSupported apps:\n- `api_server`: API Server (OpenAI Compatible) (category=api; backend=adapter; platform_action actions=none registered)\n- `dingtalk`: 钉钉 (DingTalk) (category=im; backend=adapter; platform_action actions=none registered)\n- `feishu`: 飞书 (Feishu/Lark) (category=im; backend=cli)\n - `calendar.create_event` payload={summary*, start_time*, end_time*, attendees} [write/high]\n - `docs.create_markdown` payload={title*, markdown*, folder_token} [write/medium]\n - `drive.search` payload={query*, limit} [read/medium]\n - `im.add_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.download_resource` payload={message_id*, file_key*, type*} [read/low]\n - `im.get_messages` payload={message_ids*} [read/low]\n - `im.list_chats` payload={page_size} [read/low]\n - `im.list_messages` payload={chat_id*} [read/low]\n - `im.list_thread_messages` payload={thread*} [read/low]\n - `im.remove_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.reply_message` payload={message_id*, text*} [send/high]\n - `im.search_chats` payload={query*, page_size} [read/low]\n - `im.search_messages` payload={query*} [read/medium]\n - `im.send_message` payload={chat_id*, text*, thread_id} [send/high]\n - `im.update_card` payload={token*, card*} [write/medium]\n - `im.update_message` payload={message_id*, text*} [write/medium]\n - `mail.search_unread` payload={query, limit} [read/high]\n - `sheets.append_row` payload={spreadsheet_token*, sheet_id*, values*} [write/medium]\n - `task.create` payload={title*, description, due_time} [write/medium]\n- `telegram`: Telegram (category=social; backend=adapter; platform_action actions=none registered)\n- `webhook`: Webhook (Generic) (category=webhook; backend=adapter; platform_action actions=none registered)\n\n## Tool Usage\nTools are normally invoked through the native function-calling mechanism, not by writing JSON in your reply\ntext. Only if the provider signals that native function calling is unavailable for this turn, fall back to a\nsingle JSON code block: `{\"name\": \"tool_name\", \"arguments\": {\"key\": \"value\"}}` — use this fallback format\nonly, never both. Only call a tool whose exact name appears in the tool index above and is currently callable\n(or reachable via `capability_expand`). Never invent, rename, alias, or guess a tool name, platform ID, or\nplatform action from argument shape or wording — if the index does not list it, it does not exist.\n`platform_connect.action` (list/guide/preflight/connect/disconnect/remove/status/events_start/events_stop/\nevents_status) is the App Connector management namespace; `platform_action.action` only accepts exact\nregistered business actions such as `im.send_message` shown in the App Connector Capability Index — never mix\nthe two namespaces. If a tool call returns an unknown/unavailable result, use the returned suggestions or\navailable names for a single retry instead of trying further variations of the same guess.\n\n**Side-effect action rule** (`platform_action` with effect=send/write/execute):\n- Call each unique action+payload **exactly once**. Never include duplicates in the same turn.\n- Once the result returns `\"completed\": true`, that action is DONE for this task. Do NOT call it again\n in any subsequent turn — immediately summarize the result for the user instead.\n- The system enforces idempotency: duplicate calls are blocked and will not execute.\n- If the user explicitly requests sending/writing multiple times, use distinct payloads per call.\n\n**Resource identifier provenance rule**:\n- NEVER fabricate, guess, or infer resource identifiers (chat_id, message_id, file_key, user_id, etc.).\n Every resource ID used in a side-effect action MUST come from a successful API response in this session.\n- If a read/list action fails (e.g. authorization error), you do NOT have valid resource IDs.\n Report the failure to the user — do NOT attempt the dependent write/send action with a guessed ID.\n- When a tool result contains `\"llm_instruction\"`, follow it exactly.\n\n## Guidelines\n1. **Direct answers first**: If you already know the answer, respond directly without tools.\n2. **Avoid redundant tool calls**: Do not call the same tool with the same arguments more than once in the same user turn. When an existing tool result already answers the user's request, stop calling tools and answer directly.\n3. **Use tools proactively**: When the user asks about files, time, system state, or needs actions performed, use the appropriate tool.\n4. **Chain tools when needed**: You can call multiple tools in sequence (e.g., list files → read file → summarize).\n5. **Handle failures gracefully**: If a tool fails, explain what went wrong and suggest alternatives. If it failed because of an unknown tool/platform/action name, retry once with an exact name from the returned suggestions, then explain rather than keep guessing.\n6. **Summarize results naturally**: After tool execution, synthesize the results into a helpful answer rather than dumping raw output.\n7. **Stay conversational**: Maintain a natural, helpful tone. Acknowledge context from earlier in the conversation.\n8. **Recall past work**:\n - Broad queries (\"之前做了什么\", \"列出任务\"): answer from the \"Recent Task History\" section already in your context. If insufficient, call session_list.\n - Specific lookups (\"上次那个配置怎么改的\"): call session_search with relevant phrases (NOT single characters).\n - Do NOT call search tools repeatedly with keyword fragments. One well-phrased call is better than ten fragmented ones.\n\n## Coding & Verification\nWhen working with code, prefer the precise built-in tools over ad-hoc shell: use `repo_map` to orient in an\nunfamiliar project, `code_search`/`file_find` to locate and `code_intel` for symbols, `edit_file` (anchored\nsearch-replace, or a unified `diff`) to change files — never rewrite a whole file to change a few lines — and\n`git_query` to inspect diffs/log. After edits, check `syntax_ok` in the result and run `test_run`/`lint_check`\nbefore declaring the work done. Batch independent read-only calls (search/read/query) into a single turn — they\nrun in parallel.\n\n## Presentation Style\n1. **Polished Markdown only**: Format user-facing answers with clean Markdown headings, short paragraphs, and concise bullets. Use tables only when they improve comparison or scanning.\n2. **Terminal-friendly layout**: Keep lines readable in a TUI; avoid dense walls of text, deeply nested lists, oversized ASCII art, or heavy visual blocks.\n3. **Elegant emphasis**: Use bold text sparingly for key terms and conclusions. Avoid excessive emojis, decorative symbols, repeated separators, or visual noise.\n4. **Theme-safe colors**: Do not emit ANSI escape codes, HTML color tags, Rich markup, or hardcoded color names. Rely on the TUI theme to render Markdown professionally.\n5. **No leaked tool protocol**: Never show tool-call JSON, internal schemas, raw observations, tool result payloads, or hidden reasoning in the final answer unless the user explicitly asks for raw/debug output. Treat any prior `{\"name\": ..., \"arguments\": ...}` blocks and `Tool result (...)` messages as internal execution context only.\n6. **Professional closure**: End with a concise conclusion or next step when helpful; avoid rambling after the useful answer is complete.\n\nWhen finished with all tool calls, respond normally without a JSON block, tool-call transcript, or process log.\n\n## Task Contract\n- Task ID: turn-1\n- Original user request: List the connected hardware devices, then describe bench_r8 in full.\n- Workspace root: \n- Allowed roots: \n- Treat relative project paths as relative to the workspace root; never infer `.` as the project root when a workspace root is provided.\n- Workspace boundary is enforced by tools: do not read, search, edit, or run commands against paths outside the allowed roots unless the user explicitly requests an external path and the tool/approval policy permits it.\n- LeapFlow workspace config is optional at `/.leapflow/config.yaml`; runtime config is loaded from `~/.leapflow/config/user.yaml` and `~/.leapflow/profiles//config/*.yaml`.\n- Preserve this task contract across summarization, compression, tool loops, and memory retrieval.\n" - }, - { - "role": "system", - "content": "\n## Recent Session Summary\n- [user] List the connected hardware devices, then describe bench_r8 in full.\n" - }, - { - "role": "user", - "content": "List the connected hardware devices, then describe bench_r8 in full.\nList the connected hardware devices, then describe bench_r8 in full." - } - ], - "tools": [ - "capability_expand", - "code_intel", - "code_search", - "config_get", - "config_list", - "env_info", - "file_find", - "file_list", - "file_read", - "git_query", - "hw_describe", - "hw_list", - "hw_read", - "hw_status", - "lint_check", - "memory_search", - "plugin_list", - "plugin_propose", - "plugin_status", - "plugin_versions", - "repo_map", - "research_note", - "runtime_snapshot", - "schedule_reentry", - "self_describe", - "session_list", - "skill_view", - "skills_list", - "terminal_list", - "terminal_read", - "test_run", - "text_search", - "time_get", - "tool_describe", - "tool_search", - "web_fetch" - ] - }, - "responses": [ - { - "status": 200, - "content_type": "application/json", - "body": "{\"id\": \"chatcmpl-cassette\", \"object\": \"chat.completion\", \"model\": \"cassette-model\", \"choices\": [{\"index\": 0, \"message\": {\"role\": \"assistant\", \"content\": \"\", \"tool_calls\": [{\"id\": \"call_1\", \"type\": \"function\", \"function\": {\"name\": \"hw_list\", \"arguments\": \"{}\"}}]}, \"finish_reason\": \"tool_calls\"}], \"usage\": {\"prompt_tokens\": 64, \"completion_tokens\": 16, \"total_tokens\": 80}}" - } - ] -} diff --git a/tests/_fixtures/cassettes/r8_hardware/cassette-model-6ad26da28a760244.cassette.json b/tests/_fixtures/cassettes/r8_hardware/cassette-model-dd31ca22befa79fe.cassette.json similarity index 60% rename from tests/_fixtures/cassettes/r8_hardware/cassette-model-6ad26da28a760244.cassette.json rename to tests/_fixtures/cassettes/r8_hardware/cassette-model-dd31ca22befa79fe.cassette.json index 988ae319..09f6a682 100644 --- a/tests/_fixtures/cassettes/r8_hardware/cassette-model-6ad26da28a760244.cassette.json +++ b/tests/_fixtures/cassettes/r8_hardware/cassette-model-dd31ca22befa79fe.cassette.json @@ -1,5 +1,5 @@ { - "fingerprint": "6ad26da28a7602442f5c356520a40323ef5de1bfdef71f36d07b972c70f9df89", + "fingerprint": "dd31ca22befa79fe6626a7a1a839793fb2b7abb5792a0ef27a9a3db86f494467", "note": "captured in seed mode", "request": { "model": "cassette-model", @@ -7,7 +7,7 @@ "messages": [ { "role": "system", - "content": "You are LeapFlow, an intelligent assistant that can both converse naturally and take real actions on the user's computer.\n\n## Capabilities\nThe tool index below lists **every** registered tool by name and a one-line summary — this is the complete\ncapability contract; nothing else exists. Only a subset is directly callable this turn (via native tool calling,\nnot a JSON block in your reply). If you need a tool from the index that is not yet callable, call\n`capability_expand` with its category name first — the matching tools become callable immediately after.\nWhen the user asks about LeapFlow **itself** — its identity/version/model, what tools, plugins, or capabilities\nit has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of\nself-awareness — answer from live runtime evidence via `self_describe` (facet=capabilities for \"what can you\ndo / do you support X\", identity for version/model, runtime for context/posture, or `all` for a full self-check),\nnever by reading LeapFlow's own source code, documentation, or memory. Report configuration-dependent or\nunavailable capabilities as limitations rather than inferring them.\n- **text_search**(text, pattern): Search for a regex pattern in text.\n- **text_replace**(text, old, new, count) [capability_expand category: write]: Replace occurrences of a substring in text.\n- **time_get**(): Get current date and time.\n- **env_info**(): Get system environment information (OS, Python version, cwd).\n- **skills_list**(query, category, source): List available learned skills. Use when user asks about capabilities or you need a specific skill.\n- **skill_view**(name): View the full content of a specific skill document.\n- **skill_list_files**(name) [capability_expand category: skills]: List all support files in a skill directory (configs, templates, examples). Use before skill_file_read to discover available files.\n- **skill_file_read**(name, file_path) [capability_expand category: skills]: Read an individual support file from a skill directory. Use skill_list_files first to discover available files.\n- **code_intel**(path, operation): Precise document symbols (outline) for a source file: classes, functions, and methods with line ranges. Python uses an exact AST parse; other languages use a keyword-prefix scan. Prefer over file_read mode=symbols for accurate navigation before editing. Read-only.\n- **repo_map**(path): Compact project orientation for a repository root: languages, detected test/lint commands, top-level structure, entry points, manifest, and VCS branch. Call this first when entering an unfamiliar codebase. Read-only.\n- **scm_sync**(action, cwd, remote, pull_ref, push_ref, timeout) [capability_expand category: scm]: Run a typed git SCM action. Use this instead of shell_run for git pull/push/status. For 'pull origin main then push', set action='pull_then_push', remote='origin', pull_ref='main', and omit push_ref so LeapFlow pushes the current local branch.\n- **git_query**(action, cwd, ref, path, staged, max_count, stat): Read-only structured git inspection: action=diff|log|status|branch|show. Prefer over shell_run for reading repo state — output is clipped, redacted, and log/branch are parsed into structured fields. Use scm_sync for pull/push.\n- **git_write**(action, cwd, message, stage_all, name, ref, create) [capability_expand category: scm]: Mutating git actions: action=commit (message, stage_all), branch (create+switch), checkout (switch; create=true for -b). Approval-gated. Use scm_sync for pull/push and git_query for reads.\n- **test_run**(command, cwd, timeout): Run the project's test suite and return structured results (framework, passed/failed counts, failing tests). Auto-detects the runner (pytest/npm/go/cargo) or uses a configured/explicit command; executes via the governed shell. ok=true means the runner executed — see 'success' for pass/fail.\n- **lint_check**(command, cwd, timeout): Run the project's linter and return a structured clean/issue result. Auto-detects the linter (ruff/eslint/go vet/clippy) or uses a configured/explicit command; executes via the governed shell. ok=true means the linter ran — see 'clean'.\n- **file_list**(path, pattern, depth): List files and directories at a given path. Use depth=1 or depth=2 to get a recursive tree in one call instead of listing each sub-directory separately.\n- **file_read**(path, max_lines, start_line, max_chars, mode): Read text file content with adaptive context governance. For large or unfamiliar files, prefer mode='outline' or mode='symbols' first, then use mode='raw' with start_line/max_lines for the specific range you actually need. For LeapFlow's own settings, use config_list / config_get / config_set — its config files are outside the workspace and not readable here.\n- **file_write**(path, content, mode) [capability_expand category: write]: Write content to a file (overwrite or append).\n- **code_search**(pattern, patterns, path, glob, ignore_case, multiline, max_results, context_lines): Search file CONTENTS by regex pattern across a directory tree (ripgrep-backed). Requires a regex pattern. NOT for listing or browsing directory contents — use file_list for that. Prefer this over shell_run grep: faster, skips VCS/dependency/build dirs, and returns structured path:line:column matches. Batch related lookups into ONE call via `patterns` (OR-combined, single pass) instead of issuing several separate searches. Use file_read for the surrounding context of a hit.\n- **file_find**(glob, path, max_results): Find files by a recursive glob pattern under a base path (e.g. '**/test_*.py' or '*.md'). Prefer this over shell_run find; skips VCS/dependency/build dirs.\n- **edit_file**(path, edits, dry_run, diff) [capability_expand category: write]: Apply targeted, anchored search-replace edits to an EXISTING text file (use file_write to create/overwrite). Each edit is {original_text, new_text, replace_all?}; original_text must match exactly and uniquely (or set replace_all) — a non-unique or missing anchor is rejected so files are never corrupted. Set dry_run to preview. Alternatively pass a unified 'diff' to apply its hunks as anchored edits. Far cheaper and safer than rewriting a whole file.\n- **shell_run**(command, cwd, timeout) [capability_expand category: shell]: Execute a one-shot shell command with timeout protection. Runs in the active workspace; paths resolving outside it are refused. Reach for a structured tool first when one fits — web_fetch for anything over HTTP(S), git_query/scm_sync for git, code_search/file_find/file_read for the repo, config_get/config_set for LeapFlow's own settings — because those report typed results, while a failed shell command can only be diagnosed from its exit code and stderr. Every shell run counts as an external side effect, so a failure stops the rest of the batch and is not retried automatically.\n- **terminal_open**(command, cwd, shell) [capability_expand category: terminal]: Open a PERSISTENT shell session (REPL/dev server/watch), returning a session_id for terminal_send/read/close. Disabled unless tools.terminal_session_enabled is set. For one-shot commands use shell_run instead.\n- **terminal_send**(session_id, input, wait) [capability_expand category: terminal]: Send a line of input to a persistent terminal session and return output captured shortly after.\n- **terminal_read**(session_id, wait): Drain buffered output from a persistent terminal session (optionally waiting briefly first).\n- **terminal_close**(session_id) [capability_expand category: terminal]: Terminate a persistent terminal session and release its process group.\n- **terminal_list**(): List active persistent terminal sessions.\n- **config_list**(category, limit): List LeapFlow's own writable settings (LLM model and endpoint, daemon, memory, perception, gateway, …) with current values. Use this to discover the exact key before changing anything. Optionally narrow by `category`. This is the only correct way to inspect LeapFlow configuration — never read config files from disk.\n- **config_get**(key): Read one LeapFlow setting by key (e.g. 'llm.model', 'llm.base_url', 'daemon.log_level'), returning its current value, type, scopes, and whether a change needs a daemon restart. There is no 'llm.provider' key: provider behavior is inferred from the OpenAI-compatible 'llm.base_url'. Never read LeapFlow config files from disk — use this.\n- **config_set**(key, value, scope) [capability_expand category: config]: Change one LeapFlow setting by key, e.g. switch an OpenAI-compatible LLM with key='llm.model' and key='llm.base_url'. There is no 'llm.provider' key: provider behavior is inferred from the endpoint. Values are validated and coerced; credentials are stored in the vault automatically. Call config_list or config_get first if unsure of the key. The result states whether a `leap daemon restart` is required. Never edit LeapFlow config files directly.\n- **web_fetch**(url, select, timeout, max_bytes): Read a URL over HTTP(S) and get back extracted, context-sized content: parsed JSON for API endpoints, readable text plus links for web pages. Use this for anything on the internet — prices, docs, releases, articles — instead of running curl through shell_run: it reports real HTTP status codes, retries rate limits on its own, and is a plain read so a retry is always safe. For JSON APIs pass `select` with a dotted path (e.g. 'chart.result.0.meta') to return just that part instead of the whole payload.\n- **memory_search**(query, limit): Search agent memory for relevant past experiences, observations, and facts.\n- **memory_add**(content, kind) [capability_expand category: write]: Store a new observation or insight in memory for future reference.\n- **research_note**(kind, text): Record a compact, structured note about the current task's state so it survives context compression on long / multi-step tasks. Use for durable findings, open questions still to resolve, decisions / excluded paths, and the immediate next step. One concise sentence per note.\n- **capability_expand**(category): Fetch the full callable schema for every tool in a capability category (e.g. 'hub', 'gateway', 'desktop', 'delegate', 'file', 'memory', 'skill'). The compact tool index always lists every registered tool by name and a one-line summary, but only a static low-risk subset is directly callable each turn. If you need a tool from the index that is not yet callable, call capability_expand with its category first; the matching tools become callable in this turn. Never invent a tool name — expand the category instead.\n- **delegate_task**(goal, context) [capability_expand category: delegate]: Delegate a complex sub-task to an isolated subagent. The subagent gets a fresh context and restricted tool access. Use when a task is self-contained and can be solved independently.\n- **schedule_reentry**(kind, reason, delay_seconds, event_match, max_reentries, deadline_seconds): Register a re-entry so this task can resume later from its current orientation (findings / open questions / next step). Use when work must pause and continue after a delay (kind=time) or when a matching platform event arrives (kind=event), instead of finishing now. The research-ledger state is carried over automatically.\n- **hub_push**(skill_name, visibility, version) [capability_expand category: hub]: Push a local skill to the ModelScope Hub for sharing or backup.\n- **hub_pull**(repo_id, version) [capability_expand category: hub]: Pull a skill from the ModelScope Hub to install locally.\n- **hub_search**(query) [capability_expand category: hub]: Search for skills on the Hub by keyword or description.\n- **hub_sync**(mode, dry_run) [capability_expand category: hub]: Preview or execute sync between local skills and Hub.\n- **platform_action**(platform, action, payload, backend_kind) [capability_expand category: gateway]: Execute an exact registered business action on an external platform through LeapFlow's App Connector layer. Actions must be copied from the App Connector Capability Index and are addressed as domain.operation, e.g. im.send_message or docs.create_markdown. All business fields (chat_id, text, query, etc.) MUST be placed inside `payload`, never at the top level. Example: {\"platform\":\"feishu\",\"action\":\"im.send_message\",\"payload\":{\"chat_id\":\"oc_xxx\",\"text\":\"hello\"}}. Do not invent action names, do not use management actions such as list/guide/connect/status here.\n- **platform_connect**(action, platform, credentials, options, checkpoint) [capability_expand category: gateway]: List, guide, connect, disconnect, remove, or check status for external platforms using the App Connector management namespace. Supports REST and CLI backends. Use this for management actions such as list/guide/preflight/connect/status; use platform_action only for exact registered business actions.\n- **gateway_send**(platform, chat_id, text, thread_id) [capability_expand category: gateway]: Send a message to a connected external platform (Feishu group, Telegram chat, DingTalk conversation, etc.). Requires the platform to be connected via gateway_connect first. Use gateway_connect with action='list' to see connected platforms and available chat IDs.\n- **gateway_connect**(action, platform, credentials, options) [capability_expand category: gateway]: Connect, configure, or manage external platform integrations (Feishu, DingTalk, Telegram, Slack, Discord, etc.). Conversational flow: 1) call 'guide' to get setup steps + required fields, 2) present the steps to the user and ask for ALL required credentials in a single message, 3) call 'connect' with the credentials. Goal: complete in 1–2 user turns. NEVER include credential values in your text response.\n- **plugin_list**(): List the live plugin registry across Tool/Gateway/LLM subsystems for plugin management: which plugins are loaded, their category and fiber lifecycle state. For questions about LeapFlow's own capabilities or whether it supports X, use self_describe instead.\n- **plugin_status**(plugin_id): Get detailed status of a specific plugin: its declared category, runtime dependencies, contributed tools, and fiber lifecycle state.\n- **plugin_versions**(plugin_id): List recorded source versions and active pointer for a profile-scoped plugin.\n- **plugin_propose**(requested_capability, plugin_id, proposed_tools, test_cases, risk_level, evidence): Create a side-effect-free PluginProposal from explicit capability-gap evidence. Use this before plugin_generate when a missing capability should be reviewed. Does not call an LLM, write files, or install anything.\n- **assess_compatibility**(manifest, source_path) [capability_expand category: plugin_management]: Assess whether a foreign plugin manifest or real DSH source bundle is compatible with LeapFlow. A source bundle assessment is static: runtime_ready stays false until restricted Node discovery during plugin_install. Returns component-level verdicts and limitations.\n- **plugin_generate**(plugin_id, description, proposal_id) [capability_expand category: system]: Generate a new ToolPlugin from a natural-language capability description. The LLM produces code that conforms to the ToolPlugin Protocol; it is then rigorously validated (syntax, structure, import, protocol conformance). The generated source is stored in CAS and receives explicit content approval. It DOES NOT install the plugin — installation requires a second, mutation-specific approval via plugin_install.\n- **plugin_install**(plugin_id, code, marketplace_name, source_path, proposal_id, version_label) [capability_expand category: system]: Install a plugin from exactly one source: validated Python code, the configured Python marketplace, or a real DSH source bundle. DSH bundles run restricted Node discovery before registration; only public host tools are installed and client UI limitations are reported. Writes profile-scoped state and mutates the process-global registry. REQUIRES APPROVAL.\n- **plugin_rollback**(plugin_id, version) [capability_expand category: system]: Rollback a profile-scoped plugin to a recorded source version and reload it. REQUIRES APPROVAL.\n- **plugin_reload**(plugin_id, version_label) [capability_expand category: system]: Hot-reload a plugin at runtime. Disposes the old plugin fiber, re-imports its module, and registers a fresh instance. Existing in-flight turns are unaffected (snapshot isolation). REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_disable**(plugin_id) [capability_expand category: system]: Disable a plugin by disposing its fiber, removing its tools from the runtime registry. Cannot disable self_management itself. REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_remove**(plugin_id, delete_source) [capability_expand category: system]: Terminally remove a plugin: dispose its fiber, unregister its tools, remove reload metadata, and optionally delete its profile-scoped source file. Cannot remove self_management itself. REQUIRES APPROVAL.\n- **plugin_unquarantine**(plugin_id) [capability_expand category: plugin_management]: Restore a quarantined plugin to probation status for re-evaluation. Unfreezes the trust ledger, transitions the proposal back to PROBATION, and reloads the plugin. REQUIRES APPROVAL.\n- **plugin_enable**(plugin_id) [capability_expand category: system]: Re-enable a previously disabled plugin by reloading its module and registering a fresh instance. REQUIRES APPROVAL.\n- **self_describe**(facet): Answer questions about LeapFlow ITSELF from live runtime evidence — its identity/version/model, what tools and plugins it has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of self-awareness. Prefer this over reading LeapFlow's own source code. Pick facet=capabilities for 'what can you do / do you support X', identity for version/model, runtime for context/posture, or all for a full self-check when explicitly requested.\n- **runtime_snapshot**(): Lightweight (~150 token) flat snapshot of current runtime state: model, context budget, posture, disclosure level, turn count, cache hit rate, uptime, tool count, and pending approvals.\n- **schedule_create**(trigger_expression, instruction, execution_mode, max_retries, delivery_target) [capability_expand category: scheduler]: Create a new scheduled task. Specify a trigger expression (e.g. '30m', 'every 2h', '0 9 * * *') and the instruction to execute on each trigger. Optionally provide an execution mode and a delivery target to receive notifications.\n- **schedule_list**() [capability_expand category: scheduler]: List all scheduled tasks with their current state, trigger, and next due time.\n- **schedule_status**(task_id) [capability_expand category: scheduler]: Get detailed status and recent execution history for a scheduled task.\n- **schedule_pause**(task_id) [capability_expand category: scheduler]: Pause a scheduled task so it stops firing without being cancelled.\n- **schedule_resume**(task_id) [capability_expand category: scheduler]: Resume a paused scheduled task — re-arms it and recalculates next due time.\n- **schedule_cancel**(task_id) [capability_expand category: scheduler]: Cancel a scheduled task permanently.\n- **tool_search**(query, max_results): Search all registered tools by keyword relevance. Returns a ranked list of matching tools with name, category, and summary. Use this when you need a tool but are unsure of its exact name or category.\n- **tool_describe**(tool_name): Get the full callable schema of a single tool by exact name. Returns the tool's description, parameters, and metadata. Use after tool_search to inspect a specific tool before calling it.\n- **hw_list**(): List connected hardware devices with their channel counts and measured quantities. Start here; it does not include operating limits.\n- **hw_describe**(device_id): Return the full reference for one device: every channel, its unit, its operating envelope, rate limit, reversibility, and required interlocks, plus any prior outcomes recorded for its writable channels. Required before commanding a device.\n- **hw_read**(device_id, channel_id): Read the current value of one device channel. Has no physical effect.\n- **hw_status**(device_id): Report connection health, halt capability, and recent notable events (threshold excursions, lost samples, stalled channels) for one device.\n- **hw_configure**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Set a configuration value or setpoint on a channel declaring effect=configure. Setpoints often have inertia: the value may need time to stabilise.\n- **hw_actuate**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Command motion or output on a channel declaring effect=actuate. This moves physical hardware; a repeat from an unknown state is not a safe retry.\n- **hw_dispense**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Consume an irreversible resource on a channel declaring effect=dispense. Running this twice dispenses twice; never repeat it after a failure without first verifying what already happened.\n- **hw_estop**(device_id) [capability_expand category: hardware]: Stop all motion and output on a device immediately. Never requires approval; use it whenever device behaviour is unexpected.\n- **hw_batch_actuate**(device_id, commands, dry_run, verify) [capability_expand category: hardware]: Write multiple actuator channels in one atomic transaction. Used for coordinated multi-joint control where all positions must be commanded simultaneously (e.g. robot arm joint-space control).\n- **hw_control_start**(device_id, mode, targets, frequency_hz, trajectory, policy, gains) [capability_expand category: hardware]: Start a real-time control loop on a robot device. Supports PID position tracking, impedance control, trajectory following, and neural policy chains.\n- **hw_control_stop**(device_id) [capability_expand category: hardware]: Stop the real-time control loop on a device. Gracefully halts the device and returns session statistics.\n- **hw_control_status**(device_id) [capability_expand category: hardware]: Get the status and statistics of a running control loop. Returns frequency, jitter, overruns, and policy state.\n- **hw_policy_infer**(device_id, policy, task, execute, chunk_size, verify, compute_budget) [capability_expand category: hardware]: Execute VLA policy inference on a robot device. Reads observation, runs policy, optionally writes action to actuators. Supports local (HuggingFace) and remote (PolicyServer) policies.\n- **hw_policy_episode**(action, device_id, policy, task) [capability_expand category: hardware]: Manage policy inference episode lifecycle. Start an episode to begin a task, stop to finalise and get stats.\n- **hw_policy_list**(source) [capability_expand category: hardware]: List available VLA policies and their capabilities.\n- **hw_orchestrate**(mode, operations, barrier_timeout_s, label) [capability_expand category: hardware]: Execute coordinated operations across multiple hardware devices. Supports sequential (one-at-a-time), parallel (concurrent), and barrier (synchronised) execution modes. Any failure in a parallel group triggers halt_all on every participating device for safety.\n- **session_search**(query, limit) [capability_expand category: unclassified]: Search past conversation sessions in the current workspace for relevant context.\n- **session_list**(limit): List recent conversation sessions in the current workspace with stable ids, titles, dates, and summaries. Use for browsing past tasks or when user asks to see history without specific search terms. No keywords needed — returns chronological list.\n- **session_detail**(session_id, limit, offset, include_inactive) [capability_expand category: unclassified]: Read a paginated persisted transcript for one past conversation session in the current workspace. Use after session_list or session_search returns a session_id.\n- **click**(element_index) [capability_expand category: desktop]: Click a UI element by its element_index (from the latest observe_ui snapshot)\n- **get_clipboard**() [capability_expand category: desktop]: Read current clipboard text content\n- **list_apps**(filter, running_only) [capability_expand category: desktop]: List available applications on this system. Use to discover correct bundle_id before switch_app.\n- **list_windows**() [capability_expand category: desktop]: List all top-level windows with pid, window_id, title, and per-window state (minimized, on-screen). Call this first to pick the pid and window_id that observe_ui and other window tools require.\n- **observe_ui**(pid, window_id, query) [capability_expand category: desktop]: Snapshot one window's actionable UI elements, each tagged with an element_index for click/right_click/read_text. Re-observe after actions — indices belong to one snapshot. Requires the window's pid and window_id from list_windows.\n- **open_url**(url, app_id) [capability_expand category: desktop]: Open a URL in the default or specified browser\n- **read_text**(element_index) [capability_expand category: desktop]: Read the text content of a specific UI element from the latest snapshot\n- **right_click**(element_index) [capability_expand category: desktop]: Right-click a UI element to open its context menu. Returns visible menu items.\n- **screenshot**(pid, window_id) [capability_expand category: desktop]: Capture a screenshot for visual verification. With pid + window_id captures that window (works across all displays); defaults to the last observed window, or the full desktop when no window has been observed.\n- **scroll**(element_index, direction, amount, pid, window_id) [capability_expand category: desktop]: Scroll a scrollable area of a window. Omit element_index to scroll the window's focused/page scroller; pass one to scroll an exact element from the latest snapshot.\n- **select_text**(element_index) [capability_expand category: desktop]: Select all text in a UI element (focus + select-all, for subsequent copy)\n- **set_clipboard**(text) [capability_expand category: desktop]: Write text to the clipboard\n- **shortcut**(keys) [capability_expand category: desktop]: Execute a keyboard shortcut\n- **switch_app**(app_id) [capability_expand category: desktop]: Switch to an app (launch if needed, activate, verify)\n- **type_text**(text) [capability_expand category: desktop]: Type text into the currently focused element\n- **wait**(seconds) [capability_expand category: desktop]: Wait for a specified duration before continuing\n- **wait_until**(condition, pid, window_id, timeout, poll_interval) [capability_expand category: desktop]: Wait until a UI condition is met (polls UI tree). Returns elements when found or on timeout.\n- **wait_until_stable**(timeout, poll_interval, pid, window_id) [capability_expand category: desktop]: Wait until the UI stops changing (element set stabilizes across polls).\n\n## App Connector Capability Index\nLeapFlow can onboard and manage external apps through `platform_connect` and execute exact registered business actions through `platform_action`.\nFor requests about connecting, setting up, configuring, enabling, or managing a supported app, use `platform_connect` first instead of generating SDK/Webhook sample code.\n`platform_connect.action` is the management namespace: list, guide, preflight, connect, disconnect, remove, status, events_start, events_stop, events_status.\n`platform_action.action` is only for exact registered platform business actions listed below, such as `im.send_message`; never use management actions like `list` or `guide` there.\nAll business fields MUST go inside `payload`; top-level keys are only `platform`, `action`, and `payload`.\nDo not invent platform IDs or platform action names. If the needed action is not listed, ask for discovery/clarification instead of guessing.\nUse `platform_connect` with `action='guide'` and the matching `platform` to start onboarding; use `action='list'` when the app is unclear.\nWhen a pending onboarding state is present, continue from that state with `platform_connect` instead of asking the user to restate the app.\nSupported apps:\n- `api_server`: API Server (OpenAI Compatible) (category=api; backend=adapter; platform_action actions=none registered)\n- `dingtalk`: 钉钉 (DingTalk) (category=im; backend=adapter; platform_action actions=none registered)\n- `feishu`: 飞书 (Feishu/Lark) (category=im; backend=cli)\n - `calendar.create_event` payload={summary*, start_time*, end_time*, attendees} [write/high]\n - `docs.create_markdown` payload={title*, markdown*, folder_token} [write/medium]\n - `drive.search` payload={query*, limit} [read/medium]\n - `im.add_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.download_resource` payload={message_id*, file_key*, type*} [read/low]\n - `im.get_messages` payload={message_ids*} [read/low]\n - `im.list_chats` payload={page_size} [read/low]\n - `im.list_messages` payload={chat_id*} [read/low]\n - `im.list_thread_messages` payload={thread*} [read/low]\n - `im.remove_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.reply_message` payload={message_id*, text*} [send/high]\n - `im.search_chats` payload={query*, page_size} [read/low]\n - `im.search_messages` payload={query*} [read/medium]\n - `im.send_message` payload={chat_id*, text*, thread_id} [send/high]\n - `im.update_card` payload={token*, card*} [write/medium]\n - `im.update_message` payload={message_id*, text*} [write/medium]\n - `mail.search_unread` payload={query, limit} [read/high]\n - `sheets.append_row` payload={spreadsheet_token*, sheet_id*, values*} [write/medium]\n - `task.create` payload={title*, description, due_time} [write/medium]\n- `telegram`: Telegram (category=social; backend=adapter; platform_action actions=none registered)\n- `webhook`: Webhook (Generic) (category=webhook; backend=adapter; platform_action actions=none registered)\n\n## Learned Skills\nYou have access to the following learned skills. Use `skills_list` to browse or `skill_view` to read details:\n- api_designer: REST and GraphQL API design with OpenAPI spec generation and best practices [API, REST, GraphQL]\n- code_review: Structured code review for security, performance, and style [code, review, security]\n- data_analysis: CSV/JSON data analysis with statistical summaries, quality checks, and visualiza [data, analysis, statistics]\n- devops_helper: CI/CD pipeline configuration, Docker orchestration, and deployment strategy [devops, CI/CD, Docker]\n- document_writer: Structured document generation: technical docs, API references, reports, and REA [documentation, technical-writing, README]\n- git_workflow: Git operation orchestration: branching, commits, conflicts, and PR workflow [git, version-control, branching]\n- i18n_helper: Internationalization workflow: string extraction, translation, locale management [i18n, internationalization, localization]\n- project_analysis: Project structure analysis, dependency audit, and tech stack identification [project, analysis, dependencies]\n- security_audit: Security vulnerability scanning strategy, threat modeling, and fix recommendatio [security, vulnerability, audit]\n- shell_automation: Natural language to shell commands with safety review and step-by-step execution [shell, terminal, automation]\n- test_writer: Generate unit and integration tests from source code with edge-case coverage [testing, unit-test, integration-test]\n- web_research: Multi-step web search with cross-verification and structured synthesis [web, search, research]\n\n## Tool Usage\nTools are normally invoked through the native function-calling mechanism, not by writing JSON in your reply\ntext. Only if the provider signals that native function calling is unavailable for this turn, fall back to a\nsingle JSON code block: `{\"name\": \"tool_name\", \"arguments\": {\"key\": \"value\"}}` — use this fallback format\nonly, never both. Only call a tool whose exact name appears in the tool index above and is currently callable\n(or reachable via `capability_expand`). Never invent, rename, alias, or guess a tool name, platform ID, or\nplatform action from argument shape or wording — if the index does not list it, it does not exist.\n`platform_connect.action` (list/guide/preflight/connect/disconnect/remove/status/events_start/events_stop/\nevents_status) is the App Connector management namespace; `platform_action.action` only accepts exact\nregistered business actions such as `im.send_message` shown in the App Connector Capability Index — never mix\nthe two namespaces. If a tool call returns an unknown/unavailable result, use the returned suggestions or\navailable names for a single retry instead of trying further variations of the same guess.\n\n**Side-effect action rule** (`platform_action` with effect=send/write/execute):\n- Call each unique action+payload **exactly once**. Never include duplicates in the same turn.\n- Once the result returns `\"completed\": true`, that action is DONE for this task. Do NOT call it again\n in any subsequent turn — immediately summarize the result for the user instead.\n- The system enforces idempotency: duplicate calls are blocked and will not execute.\n- If the user explicitly requests sending/writing multiple times, use distinct payloads per call.\n\n**Resource identifier provenance rule**:\n- NEVER fabricate, guess, or infer resource identifiers (chat_id, message_id, file_key, user_id, etc.).\n Every resource ID used in a side-effect action MUST come from a successful API response in this session.\n- If a read/list action fails (e.g. authorization error), you do NOT have valid resource IDs.\n Report the failure to the user — do NOT attempt the dependent write/send action with a guessed ID.\n- When a tool result contains `\"llm_instruction\"`, follow it exactly.\n\n## Guidelines\n1. **Direct answers first**: If you already know the answer, respond directly without tools.\n2. **Avoid redundant tool calls**: Do not call the same tool with the same arguments more than once in the same user turn. When an existing tool result already answers the user's request, stop calling tools and answer directly.\n3. **Use tools proactively**: When the user asks about files, time, system state, or needs actions performed, use the appropriate tool.\n4. **Chain tools when needed**: You can call multiple tools in sequence (e.g., list files → read file → summarize).\n5. **Handle failures gracefully**: If a tool fails, explain what went wrong and suggest alternatives. If it failed because of an unknown tool/platform/action name, retry once with an exact name from the returned suggestions, then explain rather than keep guessing.\n6. **Summarize results naturally**: After tool execution, synthesize the results into a helpful answer rather than dumping raw output.\n7. **Stay conversational**: Maintain a natural, helpful tone. Acknowledge context from earlier in the conversation.\n8. **Recall past work**:\n - Broad queries (\"之前做了什么\", \"列出任务\"): answer from the \"Recent Task History\" section already in your context. If insufficient, call session_list.\n - Specific lookups (\"上次那个配置怎么改的\"): call session_search with relevant phrases (NOT single characters).\n - Do NOT call search tools repeatedly with keyword fragments. One well-phrased call is better than ten fragmented ones.\n\n## Coding & Verification\nWhen working with code, prefer the precise built-in tools over ad-hoc shell: use `repo_map` to orient in an\nunfamiliar project, `code_search`/`file_find` to locate and `code_intel` for symbols, `edit_file` (anchored\nsearch-replace, or a unified `diff`) to change files — never rewrite a whole file to change a few lines — and\n`git_query` to inspect diffs/log. After edits, check `syntax_ok` in the result and run `test_run`/`lint_check`\nbefore declaring the work done. Batch independent read-only calls (search/read/query) into a single turn — they\nrun in parallel.\n\n## Presentation Style\n1. **Polished Markdown only**: Format user-facing answers with clean Markdown headings, short paragraphs, and concise bullets. Use tables only when they improve comparison or scanning.\n2. **Terminal-friendly layout**: Keep lines readable in a TUI; avoid dense walls of text, deeply nested lists, oversized ASCII art, or heavy visual blocks.\n3. **Elegant emphasis**: Use bold text sparingly for key terms and conclusions. Avoid excessive emojis, decorative symbols, repeated separators, or visual noise.\n4. **Theme-safe colors**: Do not emit ANSI escape codes, HTML color tags, Rich markup, or hardcoded color names. Rely on the TUI theme to render Markdown professionally.\n5. **No leaked tool protocol**: Never show tool-call JSON, internal schemas, raw observations, tool result payloads, or hidden reasoning in the final answer unless the user explicitly asks for raw/debug output. Treat any prior `{\"name\": ..., \"arguments\": ...}` blocks and `Tool result (...)` messages as internal execution context only.\n6. **Professional closure**: End with a concise conclusion or next step when helpful; avoid rambling after the useful answer is complete.\n\nWhen finished with all tool calls, respond normally without a JSON block, tool-call transcript, or process log.\n\n## Task Contract\n- Task ID: turn-2\n- Original user request: Read the current value of the setpoint channel on bench_r8.\n- Workspace root: \n- Allowed roots: \n- Treat relative project paths as relative to the workspace root; never infer `.` as the project root when a workspace root is provided.\n- Workspace boundary is enforced by tools: do not read, search, edit, or run commands against paths outside the allowed roots unless the user explicitly requests an external path and the tool/approval policy permits it.\n- LeapFlow workspace config is optional at `/.leapflow/config.yaml`; runtime config is loaded from `~/.leapflow/config/user.yaml` and `~/.leapflow/profiles//config/*.yaml`.\n- Preserve this task contract across summarization, compression, tool loops, and memory retrieval.\n" + "content": "You are LeapFlow, an intelligent assistant that can both converse naturally and take real actions on the user's computer.\n\n## Capabilities\nThe tool index below lists **every** registered tool by name and a one-line summary — this is the complete\ncapability contract; nothing else exists. Only a subset is directly callable this turn (via native tool calling,\nnot a JSON block in your reply). If you need a tool from the index that is not yet callable, call\n`capability_expand` with its category name first — the matching tools become callable immediately after.\nWhen the user asks about LeapFlow **itself** — its identity/version/model, what tools, plugins, or capabilities\nit has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of\nself-awareness — answer from live runtime evidence via `self_describe` (facet=capabilities for \"what can you\ndo / do you support X\", identity for version/model, runtime for context/posture, or `all` for a full self-check),\nnever by reading LeapFlow's own source code, documentation, or memory. Report configuration-dependent or\nunavailable capabilities as limitations rather than inferring them.\n- **text_search**(text, pattern): Search for a regex pattern in text.\n- **text_replace**(text, old, new, count) [capability_expand category: write]: Replace occurrences of a substring in text.\n- **time_get**(): Get current date and time.\n- **env_info**(): Get system environment information (OS, Python version, cwd).\n- **skills_list**(query, category, source): List available learned skills. Use when user asks about capabilities or you need a specific skill.\n- **skill_view**(name): View the full content of a specific skill document.\n- **skill_list_files**(name) [capability_expand category: skills]: List all support files in a skill directory (configs, templates, examples). Use before skill_file_read to discover available files.\n- **skill_file_read**(name, file_path) [capability_expand category: skills]: Read an individual support file from a skill directory. Use skill_list_files first to discover available files.\n- **code_intel**(path, operation): Precise document symbols (outline) for a source file: classes, functions, and methods with line ranges. Python uses an exact AST parse; other languages use a keyword-prefix scan. Prefer over file_read mode=symbols for accurate navigation before editing. Read-only.\n- **repo_map**(path): Compact project orientation for a repository root: languages, detected test/lint commands, top-level structure, entry points, manifest, and VCS branch. Call this first when entering an unfamiliar codebase. Read-only.\n- **scm_sync**(action, cwd, remote, pull_ref, push_ref, timeout) [capability_expand category: scm]: Run a typed git SCM action. Use this instead of shell_run for git pull/push/status. For 'pull origin main then push', set action='pull_then_push', remote='origin', pull_ref='main', and omit push_ref so LeapFlow pushes the current local branch.\n- **git_query**(action, cwd, ref, path, staged, max_count, stat): Read-only structured git inspection: action=diff|log|status|branch|show. Prefer over shell_run for reading repo state — output is clipped, redacted, and log/branch are parsed into structured fields. Use scm_sync for pull/push.\n- **git_write**(action, cwd, message, stage_all, name, ref, create) [capability_expand category: scm]: Mutating git actions: action=commit (message, stage_all), branch (create+switch), checkout (switch; create=true for -b). Approval-gated. Use scm_sync for pull/push and git_query for reads.\n- **test_run**(command, cwd, timeout): Run the project's test suite and return structured results (framework, passed/failed counts, failing tests). Auto-detects the runner (pytest/npm/go/cargo) or uses a configured/explicit command; executes via the governed shell. ok=true means the runner executed — see 'success' for pass/fail.\n- **lint_check**(command, cwd, timeout): Run the project's linter and return a structured clean/issue result. Auto-detects the linter (ruff/eslint/go vet/clippy) or uses a configured/explicit command; executes via the governed shell. ok=true means the linter ran — see 'clean'.\n- **file_list**(path, pattern, depth): List files and directories at a given path. Use depth=1 or depth=2 to get a recursive tree in one call instead of listing each sub-directory separately.\n- **file_read**(path, max_lines, start_line, max_chars, mode): Read text file content with adaptive context governance. For large or unfamiliar files, prefer mode='outline' or mode='symbols' first, then use mode='raw' with start_line/max_lines for the specific range you actually need. For LeapFlow's own settings, use config_list / config_get / config_set — its config files are outside the workspace and not readable here.\n- **file_write**(path, content, mode) [capability_expand category: write]: Write content to a file (overwrite or append).\n- **code_search**(pattern, patterns, path, glob, ignore_case, multiline, max_results, context_lines): Search file CONTENTS by regex pattern across a directory tree (ripgrep-backed). Requires a regex pattern. NOT for listing or browsing directory contents — use file_list for that. Prefer this over shell_run grep: faster, skips VCS/dependency/build dirs, and returns structured path:line:column matches. Batch related lookups into ONE call via `patterns` (OR-combined, single pass) instead of issuing several separate searches. Use file_read for the surrounding context of a hit.\n- **file_find**(glob, path, max_results): Find files by a recursive glob pattern under a base path (e.g. '**/test_*.py' or '*.md'). Prefer this over shell_run find; skips VCS/dependency/build dirs.\n- **edit_file**(path, edits, dry_run, diff) [capability_expand category: write]: Apply targeted, anchored search-replace edits to an EXISTING text file (use file_write to create/overwrite). Each edit is {original_text, new_text, replace_all?}; original_text must match exactly and uniquely (or set replace_all) — a non-unique or missing anchor is rejected so files are never corrupted. Set dry_run to preview. Alternatively pass a unified 'diff' to apply its hunks as anchored edits. Far cheaper and safer than rewriting a whole file.\n- **shell_run**(command, cwd, timeout) [capability_expand category: shell]: Execute a one-shot shell command with timeout protection. Runs in the active workspace; paths resolving outside it are refused. Reach for a structured tool first when one fits — web_fetch for anything over HTTP(S), git_query/scm_sync for git, code_search/file_find/file_read for the repo, config_get/config_set for LeapFlow's own settings — because those report typed results, while a failed shell command can only be diagnosed from its exit code and stderr. Every shell run counts as an external side effect, so a failure stops the rest of the batch and is not retried automatically.\n- **terminal_open**(command, cwd, shell) [capability_expand category: terminal]: Open a PERSISTENT shell session (REPL/dev server/watch), returning a session_id for terminal_send/read/close. Disabled unless tools.terminal_session_enabled is set. For one-shot commands use shell_run instead.\n- **terminal_send**(session_id, input, wait) [capability_expand category: terminal]: Send a line of input to a persistent terminal session and return output captured shortly after.\n- **terminal_read**(session_id, wait): Drain buffered output from a persistent terminal session (optionally waiting briefly first).\n- **terminal_close**(session_id) [capability_expand category: terminal]: Terminate a persistent terminal session and release its process group.\n- **terminal_list**(): List active persistent terminal sessions.\n- **config_list**(category, limit): List LeapFlow's own writable settings (LLM model and endpoint, daemon, memory, perception, gateway, …) with current values. Use this to discover the exact key before changing anything. Optionally narrow by `category`. This is the only correct way to inspect LeapFlow configuration — never read config files from disk.\n- **config_get**(key): Read one LeapFlow setting by key (e.g. 'llm.model', 'llm.base_url', 'daemon.log_level'), returning its current value, type, scopes, and whether a change needs a daemon restart. There is no 'llm.provider' key: provider behavior is inferred from the OpenAI-compatible 'llm.base_url'. Never read LeapFlow config files from disk — use this.\n- **config_set**(key, value, scope) [capability_expand category: config]: Change one LeapFlow setting by key, e.g. switch an OpenAI-compatible LLM with key='llm.model' and key='llm.base_url'. There is no 'llm.provider' key: provider behavior is inferred from the endpoint. Values are validated and coerced; credentials are stored in the vault automatically. Call config_list or config_get first if unsure of the key. The result states whether a `leap daemon restart` is required. Never edit LeapFlow config files directly.\n- **web_fetch**(url, select, timeout, max_bytes): Read a URL over HTTP(S) and get back extracted, context-sized content: parsed JSON for API endpoints, readable text plus links for web pages. Use this for anything on the internet — prices, docs, releases, articles — instead of running curl through shell_run: it reports real HTTP status codes, retries rate limits on its own, and is a plain read so a retry is always safe. For JSON APIs pass `select` with a dotted path (e.g. 'chart.result.0.meta') to return just that part instead of the whole payload.\n- **memory_search**(query, limit): Search agent memory for relevant past experiences, observations, and facts.\n- **memory_add**(content, kind) [capability_expand category: write]: Store a new observation or insight in memory for future reference.\n- **research_note**(kind, text): Record a compact, structured note about the current task's state so it survives context compression on long / multi-step tasks. Use for durable findings, open questions still to resolve, decisions / excluded paths, and the immediate next step. One concise sentence per note.\n- **capability_expand**(category): Fetch the full callable schema for every tool in a capability category (e.g. 'hub', 'gateway', 'desktop', 'delegate', 'file', 'memory', 'skill'). The compact tool index always lists every registered tool by name and a one-line summary, but only a static low-risk subset is directly callable each turn. If you need a tool from the index that is not yet callable, call capability_expand with its category first; the matching tools become callable in this turn. Never invent a tool name — expand the category instead.\n- **delegate_task**(goal, context) [capability_expand category: delegate]: Delegate a complex sub-task to an isolated subagent. The subagent gets a fresh context and restricted tool access. Use when a task is self-contained and can be solved independently.\n- **schedule_reentry**(kind, reason, delay_seconds, event_match, max_reentries, deadline_seconds): Register a re-entry so this task can resume later from its current orientation (findings / open questions / next step). Use when work must pause and continue after a delay (kind=time) or when a matching platform event arrives (kind=event), instead of finishing now. The research-ledger state is carried over automatically.\n- **hub_push**(skill_name, visibility, version) [capability_expand category: hub]: Push a local skill to the ModelScope Hub for sharing or backup.\n- **hub_pull**(repo_id, version) [capability_expand category: hub]: Pull a skill from the ModelScope Hub to install locally.\n- **hub_search**(query) [capability_expand category: hub]: Search for skills on the Hub by keyword or description.\n- **hub_sync**(mode, dry_run) [capability_expand category: hub]: Preview or execute sync between local skills and Hub.\n- **platform_action**(platform, action, payload, backend_kind) [capability_expand category: gateway]: Execute an exact registered business action on an external platform through LeapFlow's App Connector layer. Actions must be copied from the App Connector Capability Index and are addressed as domain.operation, e.g. im.send_message or docs.create_markdown. All business fields (chat_id, text, query, etc.) MUST be placed inside `payload`, never at the top level. Example: {\"platform\":\"feishu\",\"action\":\"im.send_message\",\"payload\":{\"chat_id\":\"oc_xxx\",\"text\":\"hello\"}}. Do not invent action names, do not use management actions such as list/guide/connect/status here.\n- **platform_connect**(action, platform, credentials, options, checkpoint) [capability_expand category: gateway]: List, guide, connect, disconnect, remove, or check status for external platforms using the App Connector management namespace. Supports REST and CLI backends. Use this for management actions such as list/guide/preflight/connect/status; use platform_action only for exact registered business actions.\n- **gateway_send**(platform, chat_id, text, thread_id) [capability_expand category: gateway]: Send a message to a connected external platform (Feishu group, Telegram chat, DingTalk conversation, etc.). Requires the platform to be connected via gateway_connect first. Use gateway_connect with action='list' to see connected platforms and available chat IDs.\n- **gateway_connect**(action, platform, credentials, options) [capability_expand category: gateway]: Connect, configure, or manage external platform integrations (Feishu, DingTalk, Telegram, Slack, Discord, etc.). Conversational flow: 1) call 'guide' to get setup steps + required fields, 2) present the steps to the user and ask for ALL required credentials in a single message, 3) call 'connect' with the credentials. Goal: complete in 1–2 user turns. NEVER include credential values in your text response.\n- **plugin_list**(): List the live plugin registry across Tool/Gateway/LLM subsystems for plugin management: which plugins are loaded, their category and fiber lifecycle state. For questions about LeapFlow's own capabilities or whether it supports X, use self_describe instead.\n- **plugin_status**(plugin_id): Get detailed status of a specific plugin: its declared category, runtime dependencies, contributed tools, and fiber lifecycle state.\n- **plugin_versions**(plugin_id): List recorded source versions and active pointer for a profile-scoped plugin.\n- **plugin_propose**(requested_capability, plugin_id, proposed_tools, test_cases, risk_level, evidence): Create a side-effect-free PluginProposal from explicit capability-gap evidence. Use this before plugin_generate when a missing capability should be reviewed. Does not call an LLM, write files, or install anything.\n- **assess_compatibility**(manifest, source_path) [capability_expand category: plugin_management]: Assess whether a foreign plugin manifest or real DSH source bundle is compatible with LeapFlow. A source bundle assessment is static: runtime_ready stays false until restricted Node discovery during plugin_install. Returns component-level verdicts and limitations.\n- **plugin_generate**(plugin_id, description, proposal_id) [capability_expand category: system]: Generate a new ToolPlugin from a natural-language capability description. The LLM produces code that conforms to the ToolPlugin Protocol; it is then rigorously validated (syntax, structure, import, protocol conformance). The generated source is stored in CAS and receives explicit content approval. It DOES NOT install the plugin — installation requires a second, mutation-specific approval via plugin_install.\n- **plugin_install**(plugin_id, code, marketplace_name, source_path, proposal_id, version_label) [capability_expand category: system]: Install a plugin from exactly one source: validated Python code, the configured Python marketplace, or a real DSH source bundle. DSH bundles run restricted Node discovery before registration; only public host tools are installed and client UI limitations are reported. Writes profile-scoped state and mutates the process-global registry. REQUIRES APPROVAL.\n- **plugin_rollback**(plugin_id, version) [capability_expand category: system]: Rollback a profile-scoped plugin to a recorded source version and reload it. REQUIRES APPROVAL.\n- **plugin_reload**(plugin_id, version_label) [capability_expand category: system]: Hot-reload a plugin at runtime. Disposes the old plugin fiber, re-imports its module, and registers a fresh instance. Existing in-flight turns are unaffected (snapshot isolation). REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_disable**(plugin_id) [capability_expand category: system]: Disable a plugin by disposing its fiber, removing its tools from the runtime registry. Cannot disable self_management itself. REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_remove**(plugin_id, delete_source) [capability_expand category: system]: Terminally remove a plugin: dispose its fiber, unregister its tools, remove reload metadata, and optionally delete its profile-scoped source file. Cannot remove self_management itself. REQUIRES APPROVAL.\n- **plugin_unquarantine**(plugin_id) [capability_expand category: plugin_management]: Restore a quarantined plugin to probation status for re-evaluation. Unfreezes the trust ledger, transitions the proposal back to PROBATION, and reloads the plugin. REQUIRES APPROVAL.\n- **plugin_enable**(plugin_id) [capability_expand category: system]: Re-enable a previously disabled plugin by reloading its module and registering a fresh instance. REQUIRES APPROVAL.\n- **self_describe**(facet): Answer questions about LeapFlow ITSELF from live runtime evidence — its identity/version/model, what tools and plugins it has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of self-awareness. Prefer this over reading LeapFlow's own source code. Pick facet=capabilities for 'what can you do / do you support X', identity for version/model, runtime for context/posture, or all for a full self-check when explicitly requested.\n- **runtime_snapshot**(): Lightweight (~150 token) flat snapshot of current runtime state: model, context budget, posture, disclosure level, turn count, cache hit rate, uptime, tool count, and pending approvals.\n- **schedule_create**(trigger_expression, instruction, execution_mode, max_retries, delivery_target) [capability_expand category: scheduler]: Create a new scheduled task. Specify a trigger expression (e.g. '30m', 'every 2h', '0 9 * * *') and the instruction to execute on each trigger. Optionally provide an execution mode and a delivery target to receive notifications.\n- **schedule_list**() [capability_expand category: scheduler]: List all scheduled tasks with their current state, trigger, and next due time.\n- **schedule_status**(task_id) [capability_expand category: scheduler]: Get detailed status and recent execution history for a scheduled task.\n- **schedule_pause**(task_id) [capability_expand category: scheduler]: Pause a scheduled task so it stops firing without being cancelled.\n- **schedule_resume**(task_id) [capability_expand category: scheduler]: Resume a paused scheduled task — re-arms it and recalculates next due time.\n- **schedule_cancel**(task_id) [capability_expand category: scheduler]: Cancel a scheduled task permanently.\n- **tool_search**(query, max_results): Search all registered tools by keyword relevance. Returns a ranked list of matching tools with name, category, and summary. Use this when you need a tool but are unsure of its exact name or category.\n- **tool_describe**(tool_name): Get the full callable schema of a single tool by exact name. Returns the tool's description, parameters, and metadata. Use after tool_search to inspect a specific tool before calling it.\n- **hw_list**(): List connected hardware devices with their channel counts and measured quantities. Start here; it does not include operating limits.\n- **hw_describe**(device_id): Return the full reference for one device: every channel, its unit, its operating envelope, rate limit, reversibility, and required interlocks, plus any prior outcomes recorded for its writable channels. Required before commanding a device.\n- **hw_read**(device_id, channel_id): Read the current value of one device channel. Has no physical effect.\n- **hw_status**(device_id): Report connection health, halt capability, and recent notable events (threshold excursions, lost samples, stalled channels) for one device.\n- **hw_configure**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Set a configuration value or setpoint on a channel declaring effect=configure. Setpoints often have inertia: the value may need time to stabilise.\n- **hw_actuate**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Command motion or output on a channel declaring effect=actuate. This moves physical hardware; a repeat from an unknown state is not a safe retry.\n- **hw_dispense**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Consume an irreversible resource on a channel declaring effect=dispense. Running this twice dispenses twice; never repeat it after a failure without first verifying what already happened.\n- **hw_estop**(device_id) [capability_expand category: hardware]: Stop all motion and output on a device immediately. Never requires approval; use it whenever device behaviour is unexpected.\n- **hw_batch_actuate**(device_id, commands, dry_run, verify) [capability_expand category: hardware]: Write multiple actuator channels in one atomic transaction. Used for coordinated multi-joint control where all positions must be commanded simultaneously (e.g. robot arm joint-space control).\n- **hw_control_start**(device_id, mode, targets, frequency_hz, trajectory, policy, gains) [capability_expand category: hardware]: Start a real-time control loop on a robot device. Supports PID position tracking, impedance control, trajectory following, and neural policy chains.\n- **hw_control_stop**(device_id) [capability_expand category: hardware]: Stop the real-time control loop on a device. Gracefully halts the device and returns session statistics.\n- **hw_control_status**(device_id) [capability_expand category: hardware]: Get the status and statistics of a running control loop. Returns frequency, jitter, overruns, and policy state.\n- **hw_context_snapshot**(level) [capability_expand category: hardware]: Get a structured snapshot of all hardware devices and their state. Use this before planning physical manipulation tasks to understand what devices are available and what they can do.\n- **hw_health**(device_id) [capability_expand category: hardware]: Check hardware device health status. Without device_id, returns fleet-wide health summary. With device_id, returns health details for that specific device.\n- **hw_policy_infer**(device_id, policy, task, execute, chunk_size, verify, compute_budget) [capability_expand category: hardware]: Execute VLA policy inference on a robot device. Reads observation, runs policy, optionally writes action to actuators. Supports local (HuggingFace) and remote (PolicyServer) policies.\n- **hw_policy_episode**(action, device_id, policy, task) [capability_expand category: hardware]: Manage policy inference episode lifecycle. Start an episode to begin a task, stop to finalise and get stats.\n- **hw_policy_list**(source) [capability_expand category: hardware]: List available VLA policies and their capabilities.\n- **hw_orchestrate**(mode, operations, barrier_timeout_s, label) [capability_expand category: hardware]: Execute coordinated operations across multiple hardware devices. Supports sequential (one-at-a-time), parallel (concurrent), and barrier (synchronised) execution modes. Any failure in a parallel group triggers halt_all on every participating device for safety.\n- **session_search**(query, limit) [capability_expand category: unclassified]: Search past conversation sessions in the current workspace for relevant context.\n- **session_list**(limit): List recent conversation sessions in the current workspace with stable ids, titles, dates, and summaries. Use for browsing past tasks or when user asks to see history without specific search terms. No keywords needed — returns chronological list.\n- **session_detail**(session_id, limit, offset, include_inactive) [capability_expand category: unclassified]: Read a paginated persisted transcript for one past conversation session in the current workspace. Use after session_list or session_search returns a session_id.\n- **click**(element_index) [capability_expand category: desktop]: Click a UI element by its element_index (from the latest observe_ui snapshot)\n- **get_clipboard**() [capability_expand category: desktop]: Read current clipboard text content\n- **list_apps**(filter, running_only) [capability_expand category: desktop]: List available applications on this system. Use to discover correct bundle_id before switch_app.\n- **list_windows**() [capability_expand category: desktop]: List all top-level windows with pid, window_id, title, and per-window state (minimized, on-screen). Call this first to pick the pid and window_id that observe_ui and other window tools require.\n- **observe_ui**(pid, window_id, query) [capability_expand category: desktop]: Snapshot one window's actionable UI elements, each tagged with an element_index for click/right_click/read_text. Re-observe after actions — indices belong to one snapshot. Requires the window's pid and window_id from list_windows.\n- **open_url**(url, app_id) [capability_expand category: desktop]: Open a URL in the default or specified browser\n- **read_text**(element_index) [capability_expand category: desktop]: Read the text content of a specific UI element from the latest snapshot\n- **right_click**(element_index) [capability_expand category: desktop]: Right-click a UI element to open its context menu. Returns visible menu items.\n- **screenshot**(pid, window_id) [capability_expand category: desktop]: Capture a screenshot for visual verification. With pid + window_id captures that window (works across all displays); defaults to the last observed window, or the full desktop when no window has been observed.\n- **scroll**(element_index, direction, amount, pid, window_id) [capability_expand category: desktop]: Scroll a scrollable area of a window. Omit element_index to scroll the window's focused/page scroller; pass one to scroll an exact element from the latest snapshot.\n- **select_text**(element_index) [capability_expand category: desktop]: Select all text in a UI element (focus + select-all, for subsequent copy)\n- **set_clipboard**(text) [capability_expand category: desktop]: Write text to the clipboard\n- **shortcut**(keys) [capability_expand category: desktop]: Execute a keyboard shortcut\n- **switch_app**(app_id) [capability_expand category: desktop]: Switch to an app (launch if needed, activate, verify)\n- **type_text**(text) [capability_expand category: desktop]: Type text into the currently focused element\n- **wait**(seconds) [capability_expand category: desktop]: Wait for a specified duration before continuing\n- **wait_until**(condition, pid, window_id, timeout, poll_interval) [capability_expand category: desktop]: Wait until a UI condition is met (polls UI tree). Returns elements when found or on timeout.\n- **wait_until_stable**(timeout, poll_interval, pid, window_id) [capability_expand category: desktop]: Wait until the UI stops changing (element set stabilizes across polls).\n\n## App Connector Capability Index\nLeapFlow can onboard and manage external apps through `platform_connect` and execute exact registered business actions through `platform_action`.\nFor requests about connecting, setting up, configuring, enabling, or managing a supported app, use `platform_connect` first instead of generating SDK/Webhook sample code.\n`platform_connect.action` is the management namespace: list, guide, preflight, connect, disconnect, remove, status, events_start, events_stop, events_status.\n`platform_action.action` is only for exact registered platform business actions listed below, such as `im.send_message`; never use management actions like `list` or `guide` there.\nAll business fields MUST go inside `payload`; top-level keys are only `platform`, `action`, and `payload`.\nDo not invent platform IDs or platform action names. If the needed action is not listed, ask for discovery/clarification instead of guessing.\nUse `platform_connect` with `action='guide'` and the matching `platform` to start onboarding; use `action='list'` when the app is unclear.\nWhen a pending onboarding state is present, continue from that state with `platform_connect` instead of asking the user to restate the app.\nSupported apps:\n- `api_server`: API Server (OpenAI Compatible) (category=api; backend=adapter; platform_action actions=none registered)\n- `dingtalk`: 钉钉 (DingTalk) (category=im; backend=adapter; platform_action actions=none registered)\n- `feishu`: 飞书 (Feishu/Lark) (category=im; backend=cli)\n - `calendar.create_event` payload={summary*, start_time*, end_time*, attendees} [write/high]\n - `docs.create_markdown` payload={title*, markdown*, folder_token} [write/medium]\n - `drive.search` payload={query*, limit} [read/medium]\n - `im.add_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.download_resource` payload={message_id*, file_key*, type*} [read/low]\n - `im.get_messages` payload={message_ids*} [read/low]\n - `im.list_chats` payload={page_size} [read/low]\n - `im.list_messages` payload={chat_id*} [read/low]\n - `im.list_thread_messages` payload={thread*} [read/low]\n - `im.remove_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.reply_message` payload={message_id*, text*} [send/high]\n - `im.search_chats` payload={query*, page_size} [read/low]\n - `im.search_messages` payload={query*} [read/medium]\n - `im.send_message` payload={chat_id*, text*, thread_id} [send/high]\n - `im.update_card` payload={token*, card*} [write/medium]\n - `im.update_message` payload={message_id*, text*} [write/medium]\n - `mail.search_unread` payload={query, limit} [read/high]\n - `sheets.append_row` payload={spreadsheet_token*, sheet_id*, values*} [write/medium]\n - `task.create` payload={title*, description, due_time} [write/medium]\n- `telegram`: Telegram (category=social; backend=adapter; platform_action actions=none registered)\n- `webhook`: Webhook (Generic) (category=webhook; backend=adapter; platform_action actions=none registered)\n\n## Learned Skills\nYou have access to the following learned skills. Use `skills_list` to browse or `skill_view` to read details:\n- api_designer: REST and GraphQL API design with OpenAPI spec generation and best practices [API, REST, GraphQL]\n- code_review: Structured code review for security, performance, and style [code, review, security]\n- data_analysis: CSV/JSON data analysis with statistical summaries, quality checks, and visualiza [data, analysis, statistics]\n- devops_helper: CI/CD pipeline configuration, Docker orchestration, and deployment strategy [devops, CI/CD, Docker]\n- document_writer: Structured document generation: technical docs, API references, reports, and REA [documentation, technical-writing, README]\n- git_workflow: Git operation orchestration: branching, commits, conflicts, and PR workflow [git, version-control, branching]\n- i18n_helper: Internationalization workflow: string extraction, translation, locale management [i18n, internationalization, localization]\n- project_analysis: Project structure analysis, dependency audit, and tech stack identification [project, analysis, dependencies]\n- security_audit: Security vulnerability scanning strategy, threat modeling, and fix recommendatio [security, vulnerability, audit]\n- shell_automation: Natural language to shell commands with safety review and step-by-step execution [shell, terminal, automation]\n- test_writer: Generate unit and integration tests from source code with edge-case coverage [testing, unit-test, integration-test]\n- web_research: Multi-step web search with cross-verification and structured synthesis [web, search, research]\n\n## Tool Usage\nTools are normally invoked through the native function-calling mechanism, not by writing JSON in your reply\ntext. Only if the provider signals that native function calling is unavailable for this turn, fall back to a\nsingle JSON code block: `{\"name\": \"tool_name\", \"arguments\": {\"key\": \"value\"}}` — use this fallback format\nonly, never both. Only call a tool whose exact name appears in the tool index above and is currently callable\n(or reachable via `capability_expand`). Never invent, rename, alias, or guess a tool name, platform ID, or\nplatform action from argument shape or wording — if the index does not list it, it does not exist.\n`platform_connect.action` (list/guide/preflight/connect/disconnect/remove/status/events_start/events_stop/\nevents_status) is the App Connector management namespace; `platform_action.action` only accepts exact\nregistered business actions such as `im.send_message` shown in the App Connector Capability Index — never mix\nthe two namespaces. If a tool call returns an unknown/unavailable result, use the returned suggestions or\navailable names for a single retry instead of trying further variations of the same guess.\n\n**Side-effect action rule** (`platform_action` with effect=send/write/execute):\n- Call each unique action+payload **exactly once**. Never include duplicates in the same turn.\n- Once the result returns `\"completed\": true`, that action is DONE for this task. Do NOT call it again\n in any subsequent turn — immediately summarize the result for the user instead.\n- The system enforces idempotency: duplicate calls are blocked and will not execute.\n- If the user explicitly requests sending/writing multiple times, use distinct payloads per call.\n\n**Resource identifier provenance rule**:\n- NEVER fabricate, guess, or infer resource identifiers (chat_id, message_id, file_key, user_id, etc.).\n Every resource ID used in a side-effect action MUST come from a successful API response in this session.\n- If a read/list action fails (e.g. authorization error), you do NOT have valid resource IDs.\n Report the failure to the user — do NOT attempt the dependent write/send action with a guessed ID.\n- When a tool result contains `\"llm_instruction\"`, follow it exactly.\n\n## Guidelines\n1. **Direct answers first**: If you already know the answer, respond directly without tools.\n2. **Avoid redundant tool calls**: Do not call the same tool with the same arguments more than once in the same user turn. When an existing tool result already answers the user's request, stop calling tools and answer directly.\n3. **Use tools proactively**: When the user asks about files, time, system state, or needs actions performed, use the appropriate tool.\n4. **Chain tools when needed**: You can call multiple tools in sequence (e.g., list files → read file → summarize).\n5. **Handle failures gracefully**: If a tool fails, explain what went wrong and suggest alternatives. If it failed because of an unknown tool/platform/action name, retry once with an exact name from the returned suggestions, then explain rather than keep guessing.\n6. **Summarize results naturally**: After tool execution, synthesize the results into a helpful answer rather than dumping raw output.\n7. **Stay conversational**: Maintain a natural, helpful tone. Acknowledge context from earlier in the conversation.\n8. **Recall past work**:\n - Broad queries (\"之前做了什么\", \"列出任务\"): answer from the \"Recent Task History\" section already in your context. If insufficient, call session_list.\n - Specific lookups (\"上次那个配置怎么改的\"): call session_search with relevant phrases (NOT single characters).\n - Do NOT call search tools repeatedly with keyword fragments. One well-phrased call is better than ten fragmented ones.\n\n## Coding & Verification\nWhen working with code, prefer the precise built-in tools over ad-hoc shell: use `repo_map` to orient in an\nunfamiliar project, `code_search`/`file_find` to locate and `code_intel` for symbols, `edit_file` (anchored\nsearch-replace, or a unified `diff`) to change files — never rewrite a whole file to change a few lines — and\n`git_query` to inspect diffs/log. After edits, check `syntax_ok` in the result and run `test_run`/`lint_check`\nbefore declaring the work done. Batch independent read-only calls (search/read/query) into a single turn — they\nrun in parallel.\n\n## Presentation Style\n1. **Polished Markdown only**: Format user-facing answers with clean Markdown headings, short paragraphs, and concise bullets. Use tables only when they improve comparison or scanning.\n2. **Terminal-friendly layout**: Keep lines readable in a TUI; avoid dense walls of text, deeply nested lists, oversized ASCII art, or heavy visual blocks.\n3. **Elegant emphasis**: Use bold text sparingly for key terms and conclusions. Avoid excessive emojis, decorative symbols, repeated separators, or visual noise.\n4. **Theme-safe colors**: Do not emit ANSI escape codes, HTML color tags, Rich markup, or hardcoded color names. Rely on the TUI theme to render Markdown professionally.\n5. **No leaked tool protocol**: Never show tool-call JSON, internal schemas, raw observations, tool result payloads, or hidden reasoning in the final answer unless the user explicitly asks for raw/debug output. Treat any prior `{\"name\": ..., \"arguments\": ...}` blocks and `Tool result (...)` messages as internal execution context only.\n6. **Professional closure**: End with a concise conclusion or next step when helpful; avoid rambling after the useful answer is complete.\n\nWhen finished with all tool calls, respond normally without a JSON block, tool-call transcript, or process log.\n\n## Task Contract\n- Task ID: turn-2\n- Original user request: Read the current value of the setpoint channel on bench_r8.\n- Workspace root: \n- Allowed roots: \n- Treat relative project paths as relative to the workspace root; never infer `.` as the project root when a workspace root is provided.\n- Workspace boundary is enforced by tools: do not read, search, edit, or run commands against paths outside the allowed roots unless the user explicitly requests an external path and the tool/approval policy permits it.\n- LeapFlow workspace config is optional at `/.leapflow/config.yaml`; runtime config is loaded from `~/.leapflow/config/user.yaml` and `~/.leapflow/profiles//config/*.yaml`.\n- Preserve this task contract across summarization, compression, tool loops, and memory retrieval.\n" }, { "role": "user", @@ -48,12 +48,14 @@ "hw_actuate", "hw_batch_actuate", "hw_configure", + "hw_context_snapshot", "hw_control_start", "hw_control_status", "hw_control_stop", "hw_describe", "hw_dispense", "hw_estop", + "hw_health", "hw_list", "hw_orchestrate", "hw_policy_episode", diff --git a/tests/_fixtures/cassettes/r8_hardware/cassette-model-8a3bb4f6cbc14b09.cassette.json b/tests/_fixtures/cassettes/r8_hardware/cassette-model-f14f296b11267b20.cassette.json similarity index 59% rename from tests/_fixtures/cassettes/r8_hardware/cassette-model-8a3bb4f6cbc14b09.cassette.json rename to tests/_fixtures/cassettes/r8_hardware/cassette-model-f14f296b11267b20.cassette.json index 55614f7e..c0e89d07 100644 --- a/tests/_fixtures/cassettes/r8_hardware/cassette-model-8a3bb4f6cbc14b09.cassette.json +++ b/tests/_fixtures/cassettes/r8_hardware/cassette-model-f14f296b11267b20.cassette.json @@ -1,5 +1,5 @@ { - "fingerprint": "8a3bb4f6cbc14b0999a72f425b98bf5f69ea5f278e3abf5b629e2fb35ff00719", + "fingerprint": "f14f296b11267b20b59eb1ee0330e7255541cdc55d9e7f1592a9d6c8e94f923f", "note": "captured in seed mode", "request": { "model": "cassette-model", @@ -7,7 +7,7 @@ "messages": [ { "role": "system", - "content": "You are LeapFlow, an intelligent assistant that can both converse naturally and take real actions on the user's computer.\n\n## Capabilities\nThe tool index below lists **every** registered tool by name and a one-line summary — this is the complete\ncapability contract; nothing else exists. Only a subset is directly callable this turn (via native tool calling,\nnot a JSON block in your reply). If you need a tool from the index that is not yet callable, call\n`capability_expand` with its category name first — the matching tools become callable immediately after.\nWhen the user asks about LeapFlow **itself** — its identity/version/model, what tools, plugins, or capabilities\nit has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of\nself-awareness — answer from live runtime evidence via `self_describe` (facet=capabilities for \"what can you\ndo / do you support X\", identity for version/model, runtime for context/posture, or `all` for a full self-check),\nnever by reading LeapFlow's own source code, documentation, or memory. Report configuration-dependent or\nunavailable capabilities as limitations rather than inferring them.\n- **text_search**(text, pattern): Search for a regex pattern in text.\n- **text_replace**(text, old, new, count) [capability_expand category: write]: Replace occurrences of a substring in text.\n- **time_get**(): Get current date and time.\n- **env_info**(): Get system environment information (OS, Python version, cwd).\n- **skills_list**(query, category, source): List available learned skills. Use when user asks about capabilities or you need a specific skill.\n- **skill_view**(name): View the full content of a specific skill document.\n- **skill_list_files**(name) [capability_expand category: skills]: List all support files in a skill directory (configs, templates, examples). Use before skill_file_read to discover available files.\n- **skill_file_read**(name, file_path) [capability_expand category: skills]: Read an individual support file from a skill directory. Use skill_list_files first to discover available files.\n- **code_intel**(path, operation): Precise document symbols (outline) for a source file: classes, functions, and methods with line ranges. Python uses an exact AST parse; other languages use a keyword-prefix scan. Prefer over file_read mode=symbols for accurate navigation before editing. Read-only.\n- **repo_map**(path): Compact project orientation for a repository root: languages, detected test/lint commands, top-level structure, entry points, manifest, and VCS branch. Call this first when entering an unfamiliar codebase. Read-only.\n- **scm_sync**(action, cwd, remote, pull_ref, push_ref, timeout) [capability_expand category: scm]: Run a typed git SCM action. Use this instead of shell_run for git pull/push/status. For 'pull origin main then push', set action='pull_then_push', remote='origin', pull_ref='main', and omit push_ref so LeapFlow pushes the current local branch.\n- **git_query**(action, cwd, ref, path, staged, max_count, stat): Read-only structured git inspection: action=diff|log|status|branch|show. Prefer over shell_run for reading repo state — output is clipped, redacted, and log/branch are parsed into structured fields. Use scm_sync for pull/push.\n- **git_write**(action, cwd, message, stage_all, name, ref, create) [capability_expand category: scm]: Mutating git actions: action=commit (message, stage_all), branch (create+switch), checkout (switch; create=true for -b). Approval-gated. Use scm_sync for pull/push and git_query for reads.\n- **test_run**(command, cwd, timeout): Run the project's test suite and return structured results (framework, passed/failed counts, failing tests). Auto-detects the runner (pytest/npm/go/cargo) or uses a configured/explicit command; executes via the governed shell. ok=true means the runner executed — see 'success' for pass/fail.\n- **lint_check**(command, cwd, timeout): Run the project's linter and return a structured clean/issue result. Auto-detects the linter (ruff/eslint/go vet/clippy) or uses a configured/explicit command; executes via the governed shell. ok=true means the linter ran — see 'clean'.\n- **file_list**(path, pattern, depth): List files and directories at a given path. Use depth=1 or depth=2 to get a recursive tree in one call instead of listing each sub-directory separately.\n- **file_read**(path, max_lines, start_line, max_chars, mode): Read text file content with adaptive context governance. For large or unfamiliar files, prefer mode='outline' or mode='symbols' first, then use mode='raw' with start_line/max_lines for the specific range you actually need. For LeapFlow's own settings, use config_list / config_get / config_set — its config files are outside the workspace and not readable here.\n- **file_write**(path, content, mode) [capability_expand category: write]: Write content to a file (overwrite or append).\n- **code_search**(pattern, patterns, path, glob, ignore_case, multiline, max_results, context_lines): Search file CONTENTS by regex pattern across a directory tree (ripgrep-backed). Requires a regex pattern. NOT for listing or browsing directory contents — use file_list for that. Prefer this over shell_run grep: faster, skips VCS/dependency/build dirs, and returns structured path:line:column matches. Batch related lookups into ONE call via `patterns` (OR-combined, single pass) instead of issuing several separate searches. Use file_read for the surrounding context of a hit.\n- **file_find**(glob, path, max_results): Find files by a recursive glob pattern under a base path (e.g. '**/test_*.py' or '*.md'). Prefer this over shell_run find; skips VCS/dependency/build dirs.\n- **edit_file**(path, edits, dry_run, diff) [capability_expand category: write]: Apply targeted, anchored search-replace edits to an EXISTING text file (use file_write to create/overwrite). Each edit is {original_text, new_text, replace_all?}; original_text must match exactly and uniquely (or set replace_all) — a non-unique or missing anchor is rejected so files are never corrupted. Set dry_run to preview. Alternatively pass a unified 'diff' to apply its hunks as anchored edits. Far cheaper and safer than rewriting a whole file.\n- **shell_run**(command, cwd, timeout) [capability_expand category: shell]: Execute a one-shot shell command with timeout protection. Runs in the active workspace; paths resolving outside it are refused. Reach for a structured tool first when one fits — web_fetch for anything over HTTP(S), git_query/scm_sync for git, code_search/file_find/file_read for the repo, config_get/config_set for LeapFlow's own settings — because those report typed results, while a failed shell command can only be diagnosed from its exit code and stderr. Every shell run counts as an external side effect, so a failure stops the rest of the batch and is not retried automatically.\n- **terminal_open**(command, cwd, shell) [capability_expand category: terminal]: Open a PERSISTENT shell session (REPL/dev server/watch), returning a session_id for terminal_send/read/close. Disabled unless tools.terminal_session_enabled is set. For one-shot commands use shell_run instead.\n- **terminal_send**(session_id, input, wait) [capability_expand category: terminal]: Send a line of input to a persistent terminal session and return output captured shortly after.\n- **terminal_read**(session_id, wait): Drain buffered output from a persistent terminal session (optionally waiting briefly first).\n- **terminal_close**(session_id) [capability_expand category: terminal]: Terminate a persistent terminal session and release its process group.\n- **terminal_list**(): List active persistent terminal sessions.\n- **config_list**(category, limit): List LeapFlow's own writable settings (LLM model and endpoint, daemon, memory, perception, gateway, …) with current values. Use this to discover the exact key before changing anything. Optionally narrow by `category`. This is the only correct way to inspect LeapFlow configuration — never read config files from disk.\n- **config_get**(key): Read one LeapFlow setting by key (e.g. 'llm.model', 'llm.base_url', 'daemon.log_level'), returning its current value, type, scopes, and whether a change needs a daemon restart. There is no 'llm.provider' key: provider behavior is inferred from the OpenAI-compatible 'llm.base_url'. Never read LeapFlow config files from disk — use this.\n- **config_set**(key, value, scope) [capability_expand category: config]: Change one LeapFlow setting by key, e.g. switch an OpenAI-compatible LLM with key='llm.model' and key='llm.base_url'. There is no 'llm.provider' key: provider behavior is inferred from the endpoint. Values are validated and coerced; credentials are stored in the vault automatically. Call config_list or config_get first if unsure of the key. The result states whether a `leap daemon restart` is required. Never edit LeapFlow config files directly.\n- **web_fetch**(url, select, timeout, max_bytes): Read a URL over HTTP(S) and get back extracted, context-sized content: parsed JSON for API endpoints, readable text plus links for web pages. Use this for anything on the internet — prices, docs, releases, articles — instead of running curl through shell_run: it reports real HTTP status codes, retries rate limits on its own, and is a plain read so a retry is always safe. For JSON APIs pass `select` with a dotted path (e.g. 'chart.result.0.meta') to return just that part instead of the whole payload.\n- **memory_search**(query, limit): Search agent memory for relevant past experiences, observations, and facts.\n- **memory_add**(content, kind) [capability_expand category: write]: Store a new observation or insight in memory for future reference.\n- **research_note**(kind, text): Record a compact, structured note about the current task's state so it survives context compression on long / multi-step tasks. Use for durable findings, open questions still to resolve, decisions / excluded paths, and the immediate next step. One concise sentence per note.\n- **capability_expand**(category): Fetch the full callable schema for every tool in a capability category (e.g. 'hub', 'gateway', 'desktop', 'delegate', 'file', 'memory', 'skill'). The compact tool index always lists every registered tool by name and a one-line summary, but only a static low-risk subset is directly callable each turn. If you need a tool from the index that is not yet callable, call capability_expand with its category first; the matching tools become callable in this turn. Never invent a tool name — expand the category instead.\n- **delegate_task**(goal, context) [capability_expand category: delegate]: Delegate a complex sub-task to an isolated subagent. The subagent gets a fresh context and restricted tool access. Use when a task is self-contained and can be solved independently.\n- **schedule_reentry**(kind, reason, delay_seconds, event_match, max_reentries, deadline_seconds): Register a re-entry so this task can resume later from its current orientation (findings / open questions / next step). Use when work must pause and continue after a delay (kind=time) or when a matching platform event arrives (kind=event), instead of finishing now. The research-ledger state is carried over automatically.\n- **hub_push**(skill_name, visibility, version) [capability_expand category: hub]: Push a local skill to the ModelScope Hub for sharing or backup.\n- **hub_pull**(repo_id, version) [capability_expand category: hub]: Pull a skill from the ModelScope Hub to install locally.\n- **hub_search**(query) [capability_expand category: hub]: Search for skills on the Hub by keyword or description.\n- **hub_sync**(mode, dry_run) [capability_expand category: hub]: Preview or execute sync between local skills and Hub.\n- **platform_action**(platform, action, payload, backend_kind) [capability_expand category: gateway]: Execute an exact registered business action on an external platform through LeapFlow's App Connector layer. Actions must be copied from the App Connector Capability Index and are addressed as domain.operation, e.g. im.send_message or docs.create_markdown. All business fields (chat_id, text, query, etc.) MUST be placed inside `payload`, never at the top level. Example: {\"platform\":\"feishu\",\"action\":\"im.send_message\",\"payload\":{\"chat_id\":\"oc_xxx\",\"text\":\"hello\"}}. Do not invent action names, do not use management actions such as list/guide/connect/status here.\n- **platform_connect**(action, platform, credentials, options, checkpoint) [capability_expand category: gateway]: List, guide, connect, disconnect, remove, or check status for external platforms using the App Connector management namespace. Supports REST and CLI backends. Use this for management actions such as list/guide/preflight/connect/status; use platform_action only for exact registered business actions.\n- **gateway_send**(platform, chat_id, text, thread_id) [capability_expand category: gateway]: Send a message to a connected external platform (Feishu group, Telegram chat, DingTalk conversation, etc.). Requires the platform to be connected via gateway_connect first. Use gateway_connect with action='list' to see connected platforms and available chat IDs.\n- **gateway_connect**(action, platform, credentials, options) [capability_expand category: gateway]: Connect, configure, or manage external platform integrations (Feishu, DingTalk, Telegram, Slack, Discord, etc.). Conversational flow: 1) call 'guide' to get setup steps + required fields, 2) present the steps to the user and ask for ALL required credentials in a single message, 3) call 'connect' with the credentials. Goal: complete in 1–2 user turns. NEVER include credential values in your text response.\n- **plugin_list**(): List the live plugin registry across Tool/Gateway/LLM subsystems for plugin management: which plugins are loaded, their category and fiber lifecycle state. For questions about LeapFlow's own capabilities or whether it supports X, use self_describe instead.\n- **plugin_status**(plugin_id): Get detailed status of a specific plugin: its declared category, runtime dependencies, contributed tools, and fiber lifecycle state.\n- **plugin_versions**(plugin_id): List recorded source versions and active pointer for a profile-scoped plugin.\n- **plugin_propose**(requested_capability, plugin_id, proposed_tools, test_cases, risk_level, evidence): Create a side-effect-free PluginProposal from explicit capability-gap evidence. Use this before plugin_generate when a missing capability should be reviewed. Does not call an LLM, write files, or install anything.\n- **assess_compatibility**(manifest, source_path) [capability_expand category: plugin_management]: Assess whether a foreign plugin manifest or real DSH source bundle is compatible with LeapFlow. A source bundle assessment is static: runtime_ready stays false until restricted Node discovery during plugin_install. Returns component-level verdicts and limitations.\n- **plugin_generate**(plugin_id, description, proposal_id) [capability_expand category: system]: Generate a new ToolPlugin from a natural-language capability description. The LLM produces code that conforms to the ToolPlugin Protocol; it is then rigorously validated (syntax, structure, import, protocol conformance). The generated source is stored in CAS and receives explicit content approval. It DOES NOT install the plugin — installation requires a second, mutation-specific approval via plugin_install.\n- **plugin_install**(plugin_id, code, marketplace_name, source_path, proposal_id, version_label) [capability_expand category: system]: Install a plugin from exactly one source: validated Python code, the configured Python marketplace, or a real DSH source bundle. DSH bundles run restricted Node discovery before registration; only public host tools are installed and client UI limitations are reported. Writes profile-scoped state and mutates the process-global registry. REQUIRES APPROVAL.\n- **plugin_rollback**(plugin_id, version) [capability_expand category: system]: Rollback a profile-scoped plugin to a recorded source version and reload it. REQUIRES APPROVAL.\n- **plugin_reload**(plugin_id, version_label) [capability_expand category: system]: Hot-reload a plugin at runtime. Disposes the old plugin fiber, re-imports its module, and registers a fresh instance. Existing in-flight turns are unaffected (snapshot isolation). REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_disable**(plugin_id) [capability_expand category: system]: Disable a plugin by disposing its fiber, removing its tools from the runtime registry. Cannot disable self_management itself. REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_remove**(plugin_id, delete_source) [capability_expand category: system]: Terminally remove a plugin: dispose its fiber, unregister its tools, remove reload metadata, and optionally delete its profile-scoped source file. Cannot remove self_management itself. REQUIRES APPROVAL.\n- **plugin_unquarantine**(plugin_id) [capability_expand category: plugin_management]: Restore a quarantined plugin to probation status for re-evaluation. Unfreezes the trust ledger, transitions the proposal back to PROBATION, and reloads the plugin. REQUIRES APPROVAL.\n- **plugin_enable**(plugin_id) [capability_expand category: system]: Re-enable a previously disabled plugin by reloading its module and registering a fresh instance. REQUIRES APPROVAL.\n- **self_describe**(facet): Answer questions about LeapFlow ITSELF from live runtime evidence — its identity/version/model, what tools and plugins it has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of self-awareness. Prefer this over reading LeapFlow's own source code. Pick facet=capabilities for 'what can you do / do you support X', identity for version/model, runtime for context/posture, or all for a full self-check when explicitly requested.\n- **runtime_snapshot**(): Lightweight (~150 token) flat snapshot of current runtime state: model, context budget, posture, disclosure level, turn count, cache hit rate, uptime, tool count, and pending approvals.\n- **schedule_create**(trigger_expression, instruction, execution_mode, max_retries, delivery_target) [capability_expand category: scheduler]: Create a new scheduled task. Specify a trigger expression (e.g. '30m', 'every 2h', '0 9 * * *') and the instruction to execute on each trigger. Optionally provide an execution mode and a delivery target to receive notifications.\n- **schedule_list**() [capability_expand category: scheduler]: List all scheduled tasks with their current state, trigger, and next due time.\n- **schedule_status**(task_id) [capability_expand category: scheduler]: Get detailed status and recent execution history for a scheduled task.\n- **schedule_pause**(task_id) [capability_expand category: scheduler]: Pause a scheduled task so it stops firing without being cancelled.\n- **schedule_resume**(task_id) [capability_expand category: scheduler]: Resume a paused scheduled task — re-arms it and recalculates next due time.\n- **schedule_cancel**(task_id) [capability_expand category: scheduler]: Cancel a scheduled task permanently.\n- **tool_search**(query, max_results): Search all registered tools by keyword relevance. Returns a ranked list of matching tools with name, category, and summary. Use this when you need a tool but are unsure of its exact name or category.\n- **tool_describe**(tool_name): Get the full callable schema of a single tool by exact name. Returns the tool's description, parameters, and metadata. Use after tool_search to inspect a specific tool before calling it.\n- **hw_list**(): List connected hardware devices with their channel counts and measured quantities. Start here; it does not include operating limits.\n- **hw_describe**(device_id): Return the full reference for one device: every channel, its unit, its operating envelope, rate limit, reversibility, and required interlocks, plus any prior outcomes recorded for its writable channels. Required before commanding a device.\n- **hw_read**(device_id, channel_id): Read the current value of one device channel. Has no physical effect.\n- **hw_status**(device_id): Report connection health, halt capability, and recent notable events (threshold excursions, lost samples, stalled channels) for one device.\n- **hw_configure**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Set a configuration value or setpoint on a channel declaring effect=configure. Setpoints often have inertia: the value may need time to stabilise.\n- **hw_actuate**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Command motion or output on a channel declaring effect=actuate. This moves physical hardware; a repeat from an unknown state is not a safe retry.\n- **hw_dispense**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Consume an irreversible resource on a channel declaring effect=dispense. Running this twice dispenses twice; never repeat it after a failure without first verifying what already happened.\n- **hw_estop**(device_id) [capability_expand category: hardware]: Stop all motion and output on a device immediately. Never requires approval; use it whenever device behaviour is unexpected.\n- **hw_batch_actuate**(device_id, commands, dry_run, verify) [capability_expand category: hardware]: Write multiple actuator channels in one atomic transaction. Used for coordinated multi-joint control where all positions must be commanded simultaneously (e.g. robot arm joint-space control).\n- **hw_control_start**(device_id, mode, targets, frequency_hz, trajectory, policy, gains) [capability_expand category: hardware]: Start a real-time control loop on a robot device. Supports PID position tracking, impedance control, trajectory following, and neural policy chains.\n- **hw_control_stop**(device_id) [capability_expand category: hardware]: Stop the real-time control loop on a device. Gracefully halts the device and returns session statistics.\n- **hw_control_status**(device_id) [capability_expand category: hardware]: Get the status and statistics of a running control loop. Returns frequency, jitter, overruns, and policy state.\n- **hw_policy_infer**(device_id, policy, task, execute, chunk_size, verify, compute_budget) [capability_expand category: hardware]: Execute VLA policy inference on a robot device. Reads observation, runs policy, optionally writes action to actuators. Supports local (HuggingFace) and remote (PolicyServer) policies.\n- **hw_policy_episode**(action, device_id, policy, task) [capability_expand category: hardware]: Manage policy inference episode lifecycle. Start an episode to begin a task, stop to finalise and get stats.\n- **hw_policy_list**(source) [capability_expand category: hardware]: List available VLA policies and their capabilities.\n- **hw_orchestrate**(mode, operations, barrier_timeout_s, label) [capability_expand category: hardware]: Execute coordinated operations across multiple hardware devices. Supports sequential (one-at-a-time), parallel (concurrent), and barrier (synchronised) execution modes. Any failure in a parallel group triggers halt_all on every participating device for safety.\n- **session_search**(query, limit) [capability_expand category: unclassified]: Search past conversation sessions in the current workspace for relevant context.\n- **session_list**(limit): List recent conversation sessions in the current workspace with stable ids, titles, dates, and summaries. Use for browsing past tasks or when user asks to see history without specific search terms. No keywords needed — returns chronological list.\n- **session_detail**(session_id, limit, offset, include_inactive) [capability_expand category: unclassified]: Read a paginated persisted transcript for one past conversation session in the current workspace. Use after session_list or session_search returns a session_id.\n- **click**(element_index) [capability_expand category: desktop]: Click a UI element by its element_index (from the latest observe_ui snapshot)\n- **get_clipboard**() [capability_expand category: desktop]: Read current clipboard text content\n- **list_apps**(filter, running_only) [capability_expand category: desktop]: List available applications on this system. Use to discover correct bundle_id before switch_app.\n- **list_windows**() [capability_expand category: desktop]: List all top-level windows with pid, window_id, title, and per-window state (minimized, on-screen). Call this first to pick the pid and window_id that observe_ui and other window tools require.\n- **observe_ui**(pid, window_id, query) [capability_expand category: desktop]: Snapshot one window's actionable UI elements, each tagged with an element_index for click/right_click/read_text. Re-observe after actions — indices belong to one snapshot. Requires the window's pid and window_id from list_windows.\n- **open_url**(url, app_id) [capability_expand category: desktop]: Open a URL in the default or specified browser\n- **read_text**(element_index) [capability_expand category: desktop]: Read the text content of a specific UI element from the latest snapshot\n- **right_click**(element_index) [capability_expand category: desktop]: Right-click a UI element to open its context menu. Returns visible menu items.\n- **screenshot**(pid, window_id) [capability_expand category: desktop]: Capture a screenshot for visual verification. With pid + window_id captures that window (works across all displays); defaults to the last observed window, or the full desktop when no window has been observed.\n- **scroll**(element_index, direction, amount, pid, window_id) [capability_expand category: desktop]: Scroll a scrollable area of a window. Omit element_index to scroll the window's focused/page scroller; pass one to scroll an exact element from the latest snapshot.\n- **select_text**(element_index) [capability_expand category: desktop]: Select all text in a UI element (focus + select-all, for subsequent copy)\n- **set_clipboard**(text) [capability_expand category: desktop]: Write text to the clipboard\n- **shortcut**(keys) [capability_expand category: desktop]: Execute a keyboard shortcut\n- **switch_app**(app_id) [capability_expand category: desktop]: Switch to an app (launch if needed, activate, verify)\n- **type_text**(text) [capability_expand category: desktop]: Type text into the currently focused element\n- **wait**(seconds) [capability_expand category: desktop]: Wait for a specified duration before continuing\n- **wait_until**(condition, pid, window_id, timeout, poll_interval) [capability_expand category: desktop]: Wait until a UI condition is met (polls UI tree). Returns elements when found or on timeout.\n- **wait_until_stable**(timeout, poll_interval, pid, window_id) [capability_expand category: desktop]: Wait until the UI stops changing (element set stabilizes across polls).\n\n## App Connector Capability Index\nLeapFlow can onboard and manage external apps through `platform_connect` and execute exact registered business actions through `platform_action`.\nFor requests about connecting, setting up, configuring, enabling, or managing a supported app, use `platform_connect` first instead of generating SDK/Webhook sample code.\n`platform_connect.action` is the management namespace: list, guide, preflight, connect, disconnect, remove, status, events_start, events_stop, events_status.\n`platform_action.action` is only for exact registered platform business actions listed below, such as `im.send_message`; never use management actions like `list` or `guide` there.\nAll business fields MUST go inside `payload`; top-level keys are only `platform`, `action`, and `payload`.\nDo not invent platform IDs or platform action names. If the needed action is not listed, ask for discovery/clarification instead of guessing.\nUse `platform_connect` with `action='guide'` and the matching `platform` to start onboarding; use `action='list'` when the app is unclear.\nWhen a pending onboarding state is present, continue from that state with `platform_connect` instead of asking the user to restate the app.\nSupported apps:\n- `api_server`: API Server (OpenAI Compatible) (category=api; backend=adapter; platform_action actions=none registered)\n- `dingtalk`: 钉钉 (DingTalk) (category=im; backend=adapter; platform_action actions=none registered)\n- `feishu`: 飞书 (Feishu/Lark) (category=im; backend=cli)\n - `calendar.create_event` payload={summary*, start_time*, end_time*, attendees} [write/high]\n - `docs.create_markdown` payload={title*, markdown*, folder_token} [write/medium]\n - `drive.search` payload={query*, limit} [read/medium]\n - `im.add_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.download_resource` payload={message_id*, file_key*, type*} [read/low]\n - `im.get_messages` payload={message_ids*} [read/low]\n - `im.list_chats` payload={page_size} [read/low]\n - `im.list_messages` payload={chat_id*} [read/low]\n - `im.list_thread_messages` payload={thread*} [read/low]\n - `im.remove_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.reply_message` payload={message_id*, text*} [send/high]\n - `im.search_chats` payload={query*, page_size} [read/low]\n - `im.search_messages` payload={query*} [read/medium]\n - `im.send_message` payload={chat_id*, text*, thread_id} [send/high]\n - `im.update_card` payload={token*, card*} [write/medium]\n - `im.update_message` payload={message_id*, text*} [write/medium]\n - `mail.search_unread` payload={query, limit} [read/high]\n - `sheets.append_row` payload={spreadsheet_token*, sheet_id*, values*} [write/medium]\n - `task.create` payload={title*, description, due_time} [write/medium]\n- `telegram`: Telegram (category=social; backend=adapter; platform_action actions=none registered)\n- `webhook`: Webhook (Generic) (category=webhook; backend=adapter; platform_action actions=none registered)\n\n## Learned Skills\nYou have access to the following learned skills. Use `skills_list` to browse or `skill_view` to read details:\n- api_designer: REST and GraphQL API design with OpenAPI spec generation and best practices [API, REST, GraphQL]\n- code_review: Structured code review for security, performance, and style [code, review, security]\n- data_analysis: CSV/JSON data analysis with statistical summaries, quality checks, and visualiza [data, analysis, statistics]\n- devops_helper: CI/CD pipeline configuration, Docker orchestration, and deployment strategy [devops, CI/CD, Docker]\n- document_writer: Structured document generation: technical docs, API references, reports, and REA [documentation, technical-writing, README]\n- git_workflow: Git operation orchestration: branching, commits, conflicts, and PR workflow [git, version-control, branching]\n- i18n_helper: Internationalization workflow: string extraction, translation, locale management [i18n, internationalization, localization]\n- project_analysis: Project structure analysis, dependency audit, and tech stack identification [project, analysis, dependencies]\n- security_audit: Security vulnerability scanning strategy, threat modeling, and fix recommendatio [security, vulnerability, audit]\n- shell_automation: Natural language to shell commands with safety review and step-by-step execution [shell, terminal, automation]\n- test_writer: Generate unit and integration tests from source code with edge-case coverage [testing, unit-test, integration-test]\n- web_research: Multi-step web search with cross-verification and structured synthesis [web, search, research]\n\n## Tool Usage\nTools are normally invoked through the native function-calling mechanism, not by writing JSON in your reply\ntext. Only if the provider signals that native function calling is unavailable for this turn, fall back to a\nsingle JSON code block: `{\"name\": \"tool_name\", \"arguments\": {\"key\": \"value\"}}` — use this fallback format\nonly, never both. Only call a tool whose exact name appears in the tool index above and is currently callable\n(or reachable via `capability_expand`). Never invent, rename, alias, or guess a tool name, platform ID, or\nplatform action from argument shape or wording — if the index does not list it, it does not exist.\n`platform_connect.action` (list/guide/preflight/connect/disconnect/remove/status/events_start/events_stop/\nevents_status) is the App Connector management namespace; `platform_action.action` only accepts exact\nregistered business actions such as `im.send_message` shown in the App Connector Capability Index — never mix\nthe two namespaces. If a tool call returns an unknown/unavailable result, use the returned suggestions or\navailable names for a single retry instead of trying further variations of the same guess.\n\n**Side-effect action rule** (`platform_action` with effect=send/write/execute):\n- Call each unique action+payload **exactly once**. Never include duplicates in the same turn.\n- Once the result returns `\"completed\": true`, that action is DONE for this task. Do NOT call it again\n in any subsequent turn — immediately summarize the result for the user instead.\n- The system enforces idempotency: duplicate calls are blocked and will not execute.\n- If the user explicitly requests sending/writing multiple times, use distinct payloads per call.\n\n**Resource identifier provenance rule**:\n- NEVER fabricate, guess, or infer resource identifiers (chat_id, message_id, file_key, user_id, etc.).\n Every resource ID used in a side-effect action MUST come from a successful API response in this session.\n- If a read/list action fails (e.g. authorization error), you do NOT have valid resource IDs.\n Report the failure to the user — do NOT attempt the dependent write/send action with a guessed ID.\n- When a tool result contains `\"llm_instruction\"`, follow it exactly.\n\n## Guidelines\n1. **Direct answers first**: If you already know the answer, respond directly without tools.\n2. **Avoid redundant tool calls**: Do not call the same tool with the same arguments more than once in the same user turn. When an existing tool result already answers the user's request, stop calling tools and answer directly.\n3. **Use tools proactively**: When the user asks about files, time, system state, or needs actions performed, use the appropriate tool.\n4. **Chain tools when needed**: You can call multiple tools in sequence (e.g., list files → read file → summarize).\n5. **Handle failures gracefully**: If a tool fails, explain what went wrong and suggest alternatives. If it failed because of an unknown tool/platform/action name, retry once with an exact name from the returned suggestions, then explain rather than keep guessing.\n6. **Summarize results naturally**: After tool execution, synthesize the results into a helpful answer rather than dumping raw output.\n7. **Stay conversational**: Maintain a natural, helpful tone. Acknowledge context from earlier in the conversation.\n8. **Recall past work**:\n - Broad queries (\"之前做了什么\", \"列出任务\"): answer from the \"Recent Task History\" section already in your context. If insufficient, call session_list.\n - Specific lookups (\"上次那个配置怎么改的\"): call session_search with relevant phrases (NOT single characters).\n - Do NOT call search tools repeatedly with keyword fragments. One well-phrased call is better than ten fragmented ones.\n\n## Coding & Verification\nWhen working with code, prefer the precise built-in tools over ad-hoc shell: use `repo_map` to orient in an\nunfamiliar project, `code_search`/`file_find` to locate and `code_intel` for symbols, `edit_file` (anchored\nsearch-replace, or a unified `diff`) to change files — never rewrite a whole file to change a few lines — and\n`git_query` to inspect diffs/log. After edits, check `syntax_ok` in the result and run `test_run`/`lint_check`\nbefore declaring the work done. Batch independent read-only calls (search/read/query) into a single turn — they\nrun in parallel.\n\n## Presentation Style\n1. **Polished Markdown only**: Format user-facing answers with clean Markdown headings, short paragraphs, and concise bullets. Use tables only when they improve comparison or scanning.\n2. **Terminal-friendly layout**: Keep lines readable in a TUI; avoid dense walls of text, deeply nested lists, oversized ASCII art, or heavy visual blocks.\n3. **Elegant emphasis**: Use bold text sparingly for key terms and conclusions. Avoid excessive emojis, decorative symbols, repeated separators, or visual noise.\n4. **Theme-safe colors**: Do not emit ANSI escape codes, HTML color tags, Rich markup, or hardcoded color names. Rely on the TUI theme to render Markdown professionally.\n5. **No leaked tool protocol**: Never show tool-call JSON, internal schemas, raw observations, tool result payloads, or hidden reasoning in the final answer unless the user explicitly asks for raw/debug output. Treat any prior `{\"name\": ..., \"arguments\": ...}` blocks and `Tool result (...)` messages as internal execution context only.\n6. **Professional closure**: End with a concise conclusion or next step when helpful; avoid rambling after the useful answer is complete.\n\nWhen finished with all tool calls, respond normally without a JSON block, tool-call transcript, or process log.\n\n## Task Contract\n- Task ID: turn-4\n- Original user request: Configure the bench_r8 homed channel to true to complete homing.\n- Workspace root: \n- Allowed roots: \n- Treat relative project paths as relative to the workspace root; never infer `.` as the project root when a workspace root is provided.\n- Workspace boundary is enforced by tools: do not read, search, edit, or run commands against paths outside the allowed roots unless the user explicitly requests an external path and the tool/approval policy permits it.\n- LeapFlow workspace config is optional at `/.leapflow/config.yaml`; runtime config is loaded from `~/.leapflow/config/user.yaml` and `~/.leapflow/profiles//config/*.yaml`.\n- Preserve this task contract across summarization, compression, tool loops, and memory retrieval.\n" + "content": "You are LeapFlow, an intelligent assistant that can both converse naturally and take real actions on the user's computer.\n\n## Capabilities\nThe tool index below lists **every** registered tool by name and a one-line summary — this is the complete\ncapability contract; nothing else exists. Only a subset is directly callable this turn (via native tool calling,\nnot a JSON block in your reply). If you need a tool from the index that is not yet callable, call\n`capability_expand` with its category name first — the matching tools become callable immediately after.\nWhen the user asks about LeapFlow **itself** — its identity/version/model, what tools, plugins, or capabilities\nit has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of\nself-awareness — answer from live runtime evidence via `self_describe` (facet=capabilities for \"what can you\ndo / do you support X\", identity for version/model, runtime for context/posture, or `all` for a full self-check),\nnever by reading LeapFlow's own source code, documentation, or memory. Report configuration-dependent or\nunavailable capabilities as limitations rather than inferring them.\n- **text_search**(text, pattern): Search for a regex pattern in text.\n- **text_replace**(text, old, new, count) [capability_expand category: write]: Replace occurrences of a substring in text.\n- **time_get**(): Get current date and time.\n- **env_info**(): Get system environment information (OS, Python version, cwd).\n- **skills_list**(query, category, source): List available learned skills. Use when user asks about capabilities or you need a specific skill.\n- **skill_view**(name): View the full content of a specific skill document.\n- **skill_list_files**(name) [capability_expand category: skills]: List all support files in a skill directory (configs, templates, examples). Use before skill_file_read to discover available files.\n- **skill_file_read**(name, file_path) [capability_expand category: skills]: Read an individual support file from a skill directory. Use skill_list_files first to discover available files.\n- **code_intel**(path, operation): Precise document symbols (outline) for a source file: classes, functions, and methods with line ranges. Python uses an exact AST parse; other languages use a keyword-prefix scan. Prefer over file_read mode=symbols for accurate navigation before editing. Read-only.\n- **repo_map**(path): Compact project orientation for a repository root: languages, detected test/lint commands, top-level structure, entry points, manifest, and VCS branch. Call this first when entering an unfamiliar codebase. Read-only.\n- **scm_sync**(action, cwd, remote, pull_ref, push_ref, timeout) [capability_expand category: scm]: Run a typed git SCM action. Use this instead of shell_run for git pull/push/status. For 'pull origin main then push', set action='pull_then_push', remote='origin', pull_ref='main', and omit push_ref so LeapFlow pushes the current local branch.\n- **git_query**(action, cwd, ref, path, staged, max_count, stat): Read-only structured git inspection: action=diff|log|status|branch|show. Prefer over shell_run for reading repo state — output is clipped, redacted, and log/branch are parsed into structured fields. Use scm_sync for pull/push.\n- **git_write**(action, cwd, message, stage_all, name, ref, create) [capability_expand category: scm]: Mutating git actions: action=commit (message, stage_all), branch (create+switch), checkout (switch; create=true for -b). Approval-gated. Use scm_sync for pull/push and git_query for reads.\n- **test_run**(command, cwd, timeout): Run the project's test suite and return structured results (framework, passed/failed counts, failing tests). Auto-detects the runner (pytest/npm/go/cargo) or uses a configured/explicit command; executes via the governed shell. ok=true means the runner executed — see 'success' for pass/fail.\n- **lint_check**(command, cwd, timeout): Run the project's linter and return a structured clean/issue result. Auto-detects the linter (ruff/eslint/go vet/clippy) or uses a configured/explicit command; executes via the governed shell. ok=true means the linter ran — see 'clean'.\n- **file_list**(path, pattern, depth): List files and directories at a given path. Use depth=1 or depth=2 to get a recursive tree in one call instead of listing each sub-directory separately.\n- **file_read**(path, max_lines, start_line, max_chars, mode): Read text file content with adaptive context governance. For large or unfamiliar files, prefer mode='outline' or mode='symbols' first, then use mode='raw' with start_line/max_lines for the specific range you actually need. For LeapFlow's own settings, use config_list / config_get / config_set — its config files are outside the workspace and not readable here.\n- **file_write**(path, content, mode) [capability_expand category: write]: Write content to a file (overwrite or append).\n- **code_search**(pattern, patterns, path, glob, ignore_case, multiline, max_results, context_lines): Search file CONTENTS by regex pattern across a directory tree (ripgrep-backed). Requires a regex pattern. NOT for listing or browsing directory contents — use file_list for that. Prefer this over shell_run grep: faster, skips VCS/dependency/build dirs, and returns structured path:line:column matches. Batch related lookups into ONE call via `patterns` (OR-combined, single pass) instead of issuing several separate searches. Use file_read for the surrounding context of a hit.\n- **file_find**(glob, path, max_results): Find files by a recursive glob pattern under a base path (e.g. '**/test_*.py' or '*.md'). Prefer this over shell_run find; skips VCS/dependency/build dirs.\n- **edit_file**(path, edits, dry_run, diff) [capability_expand category: write]: Apply targeted, anchored search-replace edits to an EXISTING text file (use file_write to create/overwrite). Each edit is {original_text, new_text, replace_all?}; original_text must match exactly and uniquely (or set replace_all) — a non-unique or missing anchor is rejected so files are never corrupted. Set dry_run to preview. Alternatively pass a unified 'diff' to apply its hunks as anchored edits. Far cheaper and safer than rewriting a whole file.\n- **shell_run**(command, cwd, timeout) [capability_expand category: shell]: Execute a one-shot shell command with timeout protection. Runs in the active workspace; paths resolving outside it are refused. Reach for a structured tool first when one fits — web_fetch for anything over HTTP(S), git_query/scm_sync for git, code_search/file_find/file_read for the repo, config_get/config_set for LeapFlow's own settings — because those report typed results, while a failed shell command can only be diagnosed from its exit code and stderr. Every shell run counts as an external side effect, so a failure stops the rest of the batch and is not retried automatically.\n- **terminal_open**(command, cwd, shell) [capability_expand category: terminal]: Open a PERSISTENT shell session (REPL/dev server/watch), returning a session_id for terminal_send/read/close. Disabled unless tools.terminal_session_enabled is set. For one-shot commands use shell_run instead.\n- **terminal_send**(session_id, input, wait) [capability_expand category: terminal]: Send a line of input to a persistent terminal session and return output captured shortly after.\n- **terminal_read**(session_id, wait): Drain buffered output from a persistent terminal session (optionally waiting briefly first).\n- **terminal_close**(session_id) [capability_expand category: terminal]: Terminate a persistent terminal session and release its process group.\n- **terminal_list**(): List active persistent terminal sessions.\n- **config_list**(category, limit): List LeapFlow's own writable settings (LLM model and endpoint, daemon, memory, perception, gateway, …) with current values. Use this to discover the exact key before changing anything. Optionally narrow by `category`. This is the only correct way to inspect LeapFlow configuration — never read config files from disk.\n- **config_get**(key): Read one LeapFlow setting by key (e.g. 'llm.model', 'llm.base_url', 'daemon.log_level'), returning its current value, type, scopes, and whether a change needs a daemon restart. There is no 'llm.provider' key: provider behavior is inferred from the OpenAI-compatible 'llm.base_url'. Never read LeapFlow config files from disk — use this.\n- **config_set**(key, value, scope) [capability_expand category: config]: Change one LeapFlow setting by key, e.g. switch an OpenAI-compatible LLM with key='llm.model' and key='llm.base_url'. There is no 'llm.provider' key: provider behavior is inferred from the endpoint. Values are validated and coerced; credentials are stored in the vault automatically. Call config_list or config_get first if unsure of the key. The result states whether a `leap daemon restart` is required. Never edit LeapFlow config files directly.\n- **web_fetch**(url, select, timeout, max_bytes): Read a URL over HTTP(S) and get back extracted, context-sized content: parsed JSON for API endpoints, readable text plus links for web pages. Use this for anything on the internet — prices, docs, releases, articles — instead of running curl through shell_run: it reports real HTTP status codes, retries rate limits on its own, and is a plain read so a retry is always safe. For JSON APIs pass `select` with a dotted path (e.g. 'chart.result.0.meta') to return just that part instead of the whole payload.\n- **memory_search**(query, limit): Search agent memory for relevant past experiences, observations, and facts.\n- **memory_add**(content, kind) [capability_expand category: write]: Store a new observation or insight in memory for future reference.\n- **research_note**(kind, text): Record a compact, structured note about the current task's state so it survives context compression on long / multi-step tasks. Use for durable findings, open questions still to resolve, decisions / excluded paths, and the immediate next step. One concise sentence per note.\n- **capability_expand**(category): Fetch the full callable schema for every tool in a capability category (e.g. 'hub', 'gateway', 'desktop', 'delegate', 'file', 'memory', 'skill'). The compact tool index always lists every registered tool by name and a one-line summary, but only a static low-risk subset is directly callable each turn. If you need a tool from the index that is not yet callable, call capability_expand with its category first; the matching tools become callable in this turn. Never invent a tool name — expand the category instead.\n- **delegate_task**(goal, context) [capability_expand category: delegate]: Delegate a complex sub-task to an isolated subagent. The subagent gets a fresh context and restricted tool access. Use when a task is self-contained and can be solved independently.\n- **schedule_reentry**(kind, reason, delay_seconds, event_match, max_reentries, deadline_seconds): Register a re-entry so this task can resume later from its current orientation (findings / open questions / next step). Use when work must pause and continue after a delay (kind=time) or when a matching platform event arrives (kind=event), instead of finishing now. The research-ledger state is carried over automatically.\n- **hub_push**(skill_name, visibility, version) [capability_expand category: hub]: Push a local skill to the ModelScope Hub for sharing or backup.\n- **hub_pull**(repo_id, version) [capability_expand category: hub]: Pull a skill from the ModelScope Hub to install locally.\n- **hub_search**(query) [capability_expand category: hub]: Search for skills on the Hub by keyword or description.\n- **hub_sync**(mode, dry_run) [capability_expand category: hub]: Preview or execute sync between local skills and Hub.\n- **platform_action**(platform, action, payload, backend_kind) [capability_expand category: gateway]: Execute an exact registered business action on an external platform through LeapFlow's App Connector layer. Actions must be copied from the App Connector Capability Index and are addressed as domain.operation, e.g. im.send_message or docs.create_markdown. All business fields (chat_id, text, query, etc.) MUST be placed inside `payload`, never at the top level. Example: {\"platform\":\"feishu\",\"action\":\"im.send_message\",\"payload\":{\"chat_id\":\"oc_xxx\",\"text\":\"hello\"}}. Do not invent action names, do not use management actions such as list/guide/connect/status here.\n- **platform_connect**(action, platform, credentials, options, checkpoint) [capability_expand category: gateway]: List, guide, connect, disconnect, remove, or check status for external platforms using the App Connector management namespace. Supports REST and CLI backends. Use this for management actions such as list/guide/preflight/connect/status; use platform_action only for exact registered business actions.\n- **gateway_send**(platform, chat_id, text, thread_id) [capability_expand category: gateway]: Send a message to a connected external platform (Feishu group, Telegram chat, DingTalk conversation, etc.). Requires the platform to be connected via gateway_connect first. Use gateway_connect with action='list' to see connected platforms and available chat IDs.\n- **gateway_connect**(action, platform, credentials, options) [capability_expand category: gateway]: Connect, configure, or manage external platform integrations (Feishu, DingTalk, Telegram, Slack, Discord, etc.). Conversational flow: 1) call 'guide' to get setup steps + required fields, 2) present the steps to the user and ask for ALL required credentials in a single message, 3) call 'connect' with the credentials. Goal: complete in 1–2 user turns. NEVER include credential values in your text response.\n- **plugin_list**(): List the live plugin registry across Tool/Gateway/LLM subsystems for plugin management: which plugins are loaded, their category and fiber lifecycle state. For questions about LeapFlow's own capabilities or whether it supports X, use self_describe instead.\n- **plugin_status**(plugin_id): Get detailed status of a specific plugin: its declared category, runtime dependencies, contributed tools, and fiber lifecycle state.\n- **plugin_versions**(plugin_id): List recorded source versions and active pointer for a profile-scoped plugin.\n- **plugin_propose**(requested_capability, plugin_id, proposed_tools, test_cases, risk_level, evidence): Create a side-effect-free PluginProposal from explicit capability-gap evidence. Use this before plugin_generate when a missing capability should be reviewed. Does not call an LLM, write files, or install anything.\n- **assess_compatibility**(manifest, source_path) [capability_expand category: plugin_management]: Assess whether a foreign plugin manifest or real DSH source bundle is compatible with LeapFlow. A source bundle assessment is static: runtime_ready stays false until restricted Node discovery during plugin_install. Returns component-level verdicts and limitations.\n- **plugin_generate**(plugin_id, description, proposal_id) [capability_expand category: system]: Generate a new ToolPlugin from a natural-language capability description. The LLM produces code that conforms to the ToolPlugin Protocol; it is then rigorously validated (syntax, structure, import, protocol conformance). The generated source is stored in CAS and receives explicit content approval. It DOES NOT install the plugin — installation requires a second, mutation-specific approval via plugin_install.\n- **plugin_install**(plugin_id, code, marketplace_name, source_path, proposal_id, version_label) [capability_expand category: system]: Install a plugin from exactly one source: validated Python code, the configured Python marketplace, or a real DSH source bundle. DSH bundles run restricted Node discovery before registration; only public host tools are installed and client UI limitations are reported. Writes profile-scoped state and mutates the process-global registry. REQUIRES APPROVAL.\n- **plugin_rollback**(plugin_id, version) [capability_expand category: system]: Rollback a profile-scoped plugin to a recorded source version and reload it. REQUIRES APPROVAL.\n- **plugin_reload**(plugin_id, version_label) [capability_expand category: system]: Hot-reload a plugin at runtime. Disposes the old plugin fiber, re-imports its module, and registers a fresh instance. Existing in-flight turns are unaffected (snapshot isolation). REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_disable**(plugin_id) [capability_expand category: system]: Disable a plugin by disposing its fiber, removing its tools from the runtime registry. Cannot disable self_management itself. REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_remove**(plugin_id, delete_source) [capability_expand category: system]: Terminally remove a plugin: dispose its fiber, unregister its tools, remove reload metadata, and optionally delete its profile-scoped source file. Cannot remove self_management itself. REQUIRES APPROVAL.\n- **plugin_unquarantine**(plugin_id) [capability_expand category: plugin_management]: Restore a quarantined plugin to probation status for re-evaluation. Unfreezes the trust ledger, transitions the proposal back to PROBATION, and reloads the plugin. REQUIRES APPROVAL.\n- **plugin_enable**(plugin_id) [capability_expand category: system]: Re-enable a previously disabled plugin by reloading its module and registering a fresh instance. REQUIRES APPROVAL.\n- **self_describe**(facet): Answer questions about LeapFlow ITSELF from live runtime evidence — its identity/version/model, what tools and plugins it has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of self-awareness. Prefer this over reading LeapFlow's own source code. Pick facet=capabilities for 'what can you do / do you support X', identity for version/model, runtime for context/posture, or all for a full self-check when explicitly requested.\n- **runtime_snapshot**(): Lightweight (~150 token) flat snapshot of current runtime state: model, context budget, posture, disclosure level, turn count, cache hit rate, uptime, tool count, and pending approvals.\n- **schedule_create**(trigger_expression, instruction, execution_mode, max_retries, delivery_target) [capability_expand category: scheduler]: Create a new scheduled task. Specify a trigger expression (e.g. '30m', 'every 2h', '0 9 * * *') and the instruction to execute on each trigger. Optionally provide an execution mode and a delivery target to receive notifications.\n- **schedule_list**() [capability_expand category: scheduler]: List all scheduled tasks with their current state, trigger, and next due time.\n- **schedule_status**(task_id) [capability_expand category: scheduler]: Get detailed status and recent execution history for a scheduled task.\n- **schedule_pause**(task_id) [capability_expand category: scheduler]: Pause a scheduled task so it stops firing without being cancelled.\n- **schedule_resume**(task_id) [capability_expand category: scheduler]: Resume a paused scheduled task — re-arms it and recalculates next due time.\n- **schedule_cancel**(task_id) [capability_expand category: scheduler]: Cancel a scheduled task permanently.\n- **tool_search**(query, max_results): Search all registered tools by keyword relevance. Returns a ranked list of matching tools with name, category, and summary. Use this when you need a tool but are unsure of its exact name or category.\n- **tool_describe**(tool_name): Get the full callable schema of a single tool by exact name. Returns the tool's description, parameters, and metadata. Use after tool_search to inspect a specific tool before calling it.\n- **hw_list**(): List connected hardware devices with their channel counts and measured quantities. Start here; it does not include operating limits.\n- **hw_describe**(device_id): Return the full reference for one device: every channel, its unit, its operating envelope, rate limit, reversibility, and required interlocks, plus any prior outcomes recorded for its writable channels. Required before commanding a device.\n- **hw_read**(device_id, channel_id): Read the current value of one device channel. Has no physical effect.\n- **hw_status**(device_id): Report connection health, halt capability, and recent notable events (threshold excursions, lost samples, stalled channels) for one device.\n- **hw_configure**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Set a configuration value or setpoint on a channel declaring effect=configure. Setpoints often have inertia: the value may need time to stabilise.\n- **hw_actuate**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Command motion or output on a channel declaring effect=actuate. This moves physical hardware; a repeat from an unknown state is not a safe retry.\n- **hw_dispense**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Consume an irreversible resource on a channel declaring effect=dispense. Running this twice dispenses twice; never repeat it after a failure without first verifying what already happened.\n- **hw_estop**(device_id) [capability_expand category: hardware]: Stop all motion and output on a device immediately. Never requires approval; use it whenever device behaviour is unexpected.\n- **hw_batch_actuate**(device_id, commands, dry_run, verify) [capability_expand category: hardware]: Write multiple actuator channels in one atomic transaction. Used for coordinated multi-joint control where all positions must be commanded simultaneously (e.g. robot arm joint-space control).\n- **hw_control_start**(device_id, mode, targets, frequency_hz, trajectory, policy, gains) [capability_expand category: hardware]: Start a real-time control loop on a robot device. Supports PID position tracking, impedance control, trajectory following, and neural policy chains.\n- **hw_control_stop**(device_id) [capability_expand category: hardware]: Stop the real-time control loop on a device. Gracefully halts the device and returns session statistics.\n- **hw_control_status**(device_id) [capability_expand category: hardware]: Get the status and statistics of a running control loop. Returns frequency, jitter, overruns, and policy state.\n- **hw_context_snapshot**(level) [capability_expand category: hardware]: Get a structured snapshot of all hardware devices and their state. Use this before planning physical manipulation tasks to understand what devices are available and what they can do.\n- **hw_health**(device_id) [capability_expand category: hardware]: Check hardware device health status. Without device_id, returns fleet-wide health summary. With device_id, returns health details for that specific device.\n- **hw_policy_infer**(device_id, policy, task, execute, chunk_size, verify, compute_budget) [capability_expand category: hardware]: Execute VLA policy inference on a robot device. Reads observation, runs policy, optionally writes action to actuators. Supports local (HuggingFace) and remote (PolicyServer) policies.\n- **hw_policy_episode**(action, device_id, policy, task) [capability_expand category: hardware]: Manage policy inference episode lifecycle. Start an episode to begin a task, stop to finalise and get stats.\n- **hw_policy_list**(source) [capability_expand category: hardware]: List available VLA policies and their capabilities.\n- **hw_orchestrate**(mode, operations, barrier_timeout_s, label) [capability_expand category: hardware]: Execute coordinated operations across multiple hardware devices. Supports sequential (one-at-a-time), parallel (concurrent), and barrier (synchronised) execution modes. Any failure in a parallel group triggers halt_all on every participating device for safety.\n- **session_search**(query, limit) [capability_expand category: unclassified]: Search past conversation sessions in the current workspace for relevant context.\n- **session_list**(limit): List recent conversation sessions in the current workspace with stable ids, titles, dates, and summaries. Use for browsing past tasks or when user asks to see history without specific search terms. No keywords needed — returns chronological list.\n- **session_detail**(session_id, limit, offset, include_inactive) [capability_expand category: unclassified]: Read a paginated persisted transcript for one past conversation session in the current workspace. Use after session_list or session_search returns a session_id.\n- **click**(element_index) [capability_expand category: desktop]: Click a UI element by its element_index (from the latest observe_ui snapshot)\n- **get_clipboard**() [capability_expand category: desktop]: Read current clipboard text content\n- **list_apps**(filter, running_only) [capability_expand category: desktop]: List available applications on this system. Use to discover correct bundle_id before switch_app.\n- **list_windows**() [capability_expand category: desktop]: List all top-level windows with pid, window_id, title, and per-window state (minimized, on-screen). Call this first to pick the pid and window_id that observe_ui and other window tools require.\n- **observe_ui**(pid, window_id, query) [capability_expand category: desktop]: Snapshot one window's actionable UI elements, each tagged with an element_index for click/right_click/read_text. Re-observe after actions — indices belong to one snapshot. Requires the window's pid and window_id from list_windows.\n- **open_url**(url, app_id) [capability_expand category: desktop]: Open a URL in the default or specified browser\n- **read_text**(element_index) [capability_expand category: desktop]: Read the text content of a specific UI element from the latest snapshot\n- **right_click**(element_index) [capability_expand category: desktop]: Right-click a UI element to open its context menu. Returns visible menu items.\n- **screenshot**(pid, window_id) [capability_expand category: desktop]: Capture a screenshot for visual verification. With pid + window_id captures that window (works across all displays); defaults to the last observed window, or the full desktop when no window has been observed.\n- **scroll**(element_index, direction, amount, pid, window_id) [capability_expand category: desktop]: Scroll a scrollable area of a window. Omit element_index to scroll the window's focused/page scroller; pass one to scroll an exact element from the latest snapshot.\n- **select_text**(element_index) [capability_expand category: desktop]: Select all text in a UI element (focus + select-all, for subsequent copy)\n- **set_clipboard**(text) [capability_expand category: desktop]: Write text to the clipboard\n- **shortcut**(keys) [capability_expand category: desktop]: Execute a keyboard shortcut\n- **switch_app**(app_id) [capability_expand category: desktop]: Switch to an app (launch if needed, activate, verify)\n- **type_text**(text) [capability_expand category: desktop]: Type text into the currently focused element\n- **wait**(seconds) [capability_expand category: desktop]: Wait for a specified duration before continuing\n- **wait_until**(condition, pid, window_id, timeout, poll_interval) [capability_expand category: desktop]: Wait until a UI condition is met (polls UI tree). Returns elements when found or on timeout.\n- **wait_until_stable**(timeout, poll_interval, pid, window_id) [capability_expand category: desktop]: Wait until the UI stops changing (element set stabilizes across polls).\n\n## App Connector Capability Index\nLeapFlow can onboard and manage external apps through `platform_connect` and execute exact registered business actions through `platform_action`.\nFor requests about connecting, setting up, configuring, enabling, or managing a supported app, use `platform_connect` first instead of generating SDK/Webhook sample code.\n`platform_connect.action` is the management namespace: list, guide, preflight, connect, disconnect, remove, status, events_start, events_stop, events_status.\n`platform_action.action` is only for exact registered platform business actions listed below, such as `im.send_message`; never use management actions like `list` or `guide` there.\nAll business fields MUST go inside `payload`; top-level keys are only `platform`, `action`, and `payload`.\nDo not invent platform IDs or platform action names. If the needed action is not listed, ask for discovery/clarification instead of guessing.\nUse `platform_connect` with `action='guide'` and the matching `platform` to start onboarding; use `action='list'` when the app is unclear.\nWhen a pending onboarding state is present, continue from that state with `platform_connect` instead of asking the user to restate the app.\nSupported apps:\n- `api_server`: API Server (OpenAI Compatible) (category=api; backend=adapter; platform_action actions=none registered)\n- `dingtalk`: 钉钉 (DingTalk) (category=im; backend=adapter; platform_action actions=none registered)\n- `feishu`: 飞书 (Feishu/Lark) (category=im; backend=cli)\n - `calendar.create_event` payload={summary*, start_time*, end_time*, attendees} [write/high]\n - `docs.create_markdown` payload={title*, markdown*, folder_token} [write/medium]\n - `drive.search` payload={query*, limit} [read/medium]\n - `im.add_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.download_resource` payload={message_id*, file_key*, type*} [read/low]\n - `im.get_messages` payload={message_ids*} [read/low]\n - `im.list_chats` payload={page_size} [read/low]\n - `im.list_messages` payload={chat_id*} [read/low]\n - `im.list_thread_messages` payload={thread*} [read/low]\n - `im.remove_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.reply_message` payload={message_id*, text*} [send/high]\n - `im.search_chats` payload={query*, page_size} [read/low]\n - `im.search_messages` payload={query*} [read/medium]\n - `im.send_message` payload={chat_id*, text*, thread_id} [send/high]\n - `im.update_card` payload={token*, card*} [write/medium]\n - `im.update_message` payload={message_id*, text*} [write/medium]\n - `mail.search_unread` payload={query, limit} [read/high]\n - `sheets.append_row` payload={spreadsheet_token*, sheet_id*, values*} [write/medium]\n - `task.create` payload={title*, description, due_time} [write/medium]\n- `telegram`: Telegram (category=social; backend=adapter; platform_action actions=none registered)\n- `webhook`: Webhook (Generic) (category=webhook; backend=adapter; platform_action actions=none registered)\n\n## Learned Skills\nYou have access to the following learned skills. Use `skills_list` to browse or `skill_view` to read details:\n- api_designer: REST and GraphQL API design with OpenAPI spec generation and best practices [API, REST, GraphQL]\n- code_review: Structured code review for security, performance, and style [code, review, security]\n- data_analysis: CSV/JSON data analysis with statistical summaries, quality checks, and visualiza [data, analysis, statistics]\n- devops_helper: CI/CD pipeline configuration, Docker orchestration, and deployment strategy [devops, CI/CD, Docker]\n- document_writer: Structured document generation: technical docs, API references, reports, and REA [documentation, technical-writing, README]\n- git_workflow: Git operation orchestration: branching, commits, conflicts, and PR workflow [git, version-control, branching]\n- i18n_helper: Internationalization workflow: string extraction, translation, locale management [i18n, internationalization, localization]\n- project_analysis: Project structure analysis, dependency audit, and tech stack identification [project, analysis, dependencies]\n- security_audit: Security vulnerability scanning strategy, threat modeling, and fix recommendatio [security, vulnerability, audit]\n- shell_automation: Natural language to shell commands with safety review and step-by-step execution [shell, terminal, automation]\n- test_writer: Generate unit and integration tests from source code with edge-case coverage [testing, unit-test, integration-test]\n- web_research: Multi-step web search with cross-verification and structured synthesis [web, search, research]\n\n## Tool Usage\nTools are normally invoked through the native function-calling mechanism, not by writing JSON in your reply\ntext. Only if the provider signals that native function calling is unavailable for this turn, fall back to a\nsingle JSON code block: `{\"name\": \"tool_name\", \"arguments\": {\"key\": \"value\"}}` — use this fallback format\nonly, never both. Only call a tool whose exact name appears in the tool index above and is currently callable\n(or reachable via `capability_expand`). Never invent, rename, alias, or guess a tool name, platform ID, or\nplatform action from argument shape or wording — if the index does not list it, it does not exist.\n`platform_connect.action` (list/guide/preflight/connect/disconnect/remove/status/events_start/events_stop/\nevents_status) is the App Connector management namespace; `platform_action.action` only accepts exact\nregistered business actions such as `im.send_message` shown in the App Connector Capability Index — never mix\nthe two namespaces. If a tool call returns an unknown/unavailable result, use the returned suggestions or\navailable names for a single retry instead of trying further variations of the same guess.\n\n**Side-effect action rule** (`platform_action` with effect=send/write/execute):\n- Call each unique action+payload **exactly once**. Never include duplicates in the same turn.\n- Once the result returns `\"completed\": true`, that action is DONE for this task. Do NOT call it again\n in any subsequent turn — immediately summarize the result for the user instead.\n- The system enforces idempotency: duplicate calls are blocked and will not execute.\n- If the user explicitly requests sending/writing multiple times, use distinct payloads per call.\n\n**Resource identifier provenance rule**:\n- NEVER fabricate, guess, or infer resource identifiers (chat_id, message_id, file_key, user_id, etc.).\n Every resource ID used in a side-effect action MUST come from a successful API response in this session.\n- If a read/list action fails (e.g. authorization error), you do NOT have valid resource IDs.\n Report the failure to the user — do NOT attempt the dependent write/send action with a guessed ID.\n- When a tool result contains `\"llm_instruction\"`, follow it exactly.\n\n## Guidelines\n1. **Direct answers first**: If you already know the answer, respond directly without tools.\n2. **Avoid redundant tool calls**: Do not call the same tool with the same arguments more than once in the same user turn. When an existing tool result already answers the user's request, stop calling tools and answer directly.\n3. **Use tools proactively**: When the user asks about files, time, system state, or needs actions performed, use the appropriate tool.\n4. **Chain tools when needed**: You can call multiple tools in sequence (e.g., list files → read file → summarize).\n5. **Handle failures gracefully**: If a tool fails, explain what went wrong and suggest alternatives. If it failed because of an unknown tool/platform/action name, retry once with an exact name from the returned suggestions, then explain rather than keep guessing.\n6. **Summarize results naturally**: After tool execution, synthesize the results into a helpful answer rather than dumping raw output.\n7. **Stay conversational**: Maintain a natural, helpful tone. Acknowledge context from earlier in the conversation.\n8. **Recall past work**:\n - Broad queries (\"之前做了什么\", \"列出任务\"): answer from the \"Recent Task History\" section already in your context. If insufficient, call session_list.\n - Specific lookups (\"上次那个配置怎么改的\"): call session_search with relevant phrases (NOT single characters).\n - Do NOT call search tools repeatedly with keyword fragments. One well-phrased call is better than ten fragmented ones.\n\n## Coding & Verification\nWhen working with code, prefer the precise built-in tools over ad-hoc shell: use `repo_map` to orient in an\nunfamiliar project, `code_search`/`file_find` to locate and `code_intel` for symbols, `edit_file` (anchored\nsearch-replace, or a unified `diff`) to change files — never rewrite a whole file to change a few lines — and\n`git_query` to inspect diffs/log. After edits, check `syntax_ok` in the result and run `test_run`/`lint_check`\nbefore declaring the work done. Batch independent read-only calls (search/read/query) into a single turn — they\nrun in parallel.\n\n## Presentation Style\n1. **Polished Markdown only**: Format user-facing answers with clean Markdown headings, short paragraphs, and concise bullets. Use tables only when they improve comparison or scanning.\n2. **Terminal-friendly layout**: Keep lines readable in a TUI; avoid dense walls of text, deeply nested lists, oversized ASCII art, or heavy visual blocks.\n3. **Elegant emphasis**: Use bold text sparingly for key terms and conclusions. Avoid excessive emojis, decorative symbols, repeated separators, or visual noise.\n4. **Theme-safe colors**: Do not emit ANSI escape codes, HTML color tags, Rich markup, or hardcoded color names. Rely on the TUI theme to render Markdown professionally.\n5. **No leaked tool protocol**: Never show tool-call JSON, internal schemas, raw observations, tool result payloads, or hidden reasoning in the final answer unless the user explicitly asks for raw/debug output. Treat any prior `{\"name\": ..., \"arguments\": ...}` blocks and `Tool result (...)` messages as internal execution context only.\n6. **Professional closure**: End with a concise conclusion or next step when helpful; avoid rambling after the useful answer is complete.\n\nWhen finished with all tool calls, respond normally without a JSON block, tool-call transcript, or process log.\n\n## Task Contract\n- Task ID: turn-4\n- Original user request: Configure the bench_r8 homed channel to true to complete homing.\n- Workspace root: \n- Allowed roots: \n- Treat relative project paths as relative to the workspace root; never infer `.` as the project root when a workspace root is provided.\n- Workspace boundary is enforced by tools: do not read, search, edit, or run commands against paths outside the allowed roots unless the user explicitly requests an external path and the tool/approval policy permits it.\n- LeapFlow workspace config is optional at `/.leapflow/config.yaml`; runtime config is loaded from `~/.leapflow/config/user.yaml` and `~/.leapflow/profiles//config/*.yaml`.\n- Preserve this task contract across summarization, compression, tool loops, and memory retrieval.\n" }, { "role": "assistant", @@ -40,12 +40,14 @@ "hw_actuate", "hw_batch_actuate", "hw_configure", + "hw_context_snapshot", "hw_control_start", "hw_control_status", "hw_control_stop", "hw_describe", "hw_dispense", "hw_estop", + "hw_health", "hw_list", "hw_orchestrate", "hw_policy_episode", diff --git a/tests/_fixtures/cassettes/r8_hardware/cassette-model-f4eba7c0d916788b.cassette.json b/tests/_fixtures/cassettes/r8_hardware/cassette-model-f4eba7c0d916788b.cassette.json deleted file mode 100644 index 3b17036d..00000000 --- a/tests/_fixtures/cassettes/r8_hardware/cassette-model-f4eba7c0d916788b.cassette.json +++ /dev/null @@ -1,80 +0,0 @@ -{ - "fingerprint": "f4eba7c0d916788b8932d0a227fd114d975250697139c79435189b78f5b8cc17", - "note": "captured in seed mode", - "request": { - "model": "cassette-model", - "stream": false, - "messages": [ - { - "role": "system", - "content": "You are LeapFlow, an intelligent assistant that can both converse naturally and take real actions on the user's computer.\n\n## Capabilities\nThe tool index below lists **every** registered tool by name and a one-line summary — this is the complete\ncapability contract; nothing else exists. Only a subset is directly callable this turn (via native tool calling,\nnot a JSON block in your reply). If you need a tool from the index that is not yet callable, call\n`capability_expand` with its category name first — the matching tools become callable immediately after.\nWhen the user asks about LeapFlow **itself** — its identity/version/model, what tools, plugins, or capabilities\nit has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of\nself-awareness — answer from live runtime evidence via `self_describe` (facet=capabilities for \"what can you\ndo / do you support X\", identity for version/model, runtime for context/posture, or `all` for a full self-check),\nnever by reading LeapFlow's own source code, documentation, or memory. Report configuration-dependent or\nunavailable capabilities as limitations rather than inferring them.\n- **text_search**(text, pattern): Search for a regex pattern in text.\n- **text_replace**(text, old, new, count) [capability_expand category: write]: Replace occurrences of a substring in text.\n- **time_get**(): Get current date and time.\n- **env_info**(): Get system environment information (OS, Python version, cwd).\n- **skills_list**(query, category, source): List available learned skills. Use when user asks about capabilities or you need a specific skill.\n- **skill_view**(name): View the full content of a specific skill document.\n- **skill_list_files**(name) [capability_expand category: skills]: List all support files in a skill directory (configs, templates, examples). Use before skill_file_read to discover available files.\n- **skill_file_read**(name, file_path) [capability_expand category: skills]: Read an individual support file from a skill directory. Use skill_list_files first to discover available files.\n- **code_intel**(path, operation): Precise document symbols (outline) for a source file: classes, functions, and methods with line ranges. Python uses an exact AST parse; other languages use a keyword-prefix scan. Prefer over file_read mode=symbols for accurate navigation before editing. Read-only.\n- **repo_map**(path): Compact project orientation for a repository root: languages, detected test/lint commands, top-level structure, entry points, manifest, and VCS branch. Call this first when entering an unfamiliar codebase. Read-only.\n- **scm_sync**(action, cwd, remote, pull_ref, push_ref, timeout) [capability_expand category: scm]: Run a typed git SCM action. Use this instead of shell_run for git pull/push/status. For 'pull origin main then push', set action='pull_then_push', remote='origin', pull_ref='main', and omit push_ref so LeapFlow pushes the current local branch.\n- **git_query**(action, cwd, ref, path, staged, max_count, stat): Read-only structured git inspection: action=diff|log|status|branch|show. Prefer over shell_run for reading repo state — output is clipped, redacted, and log/branch are parsed into structured fields. Use scm_sync for pull/push.\n- **git_write**(action, cwd, message, stage_all, name, ref, create) [capability_expand category: scm]: Mutating git actions: action=commit (message, stage_all), branch (create+switch), checkout (switch; create=true for -b). Approval-gated. Use scm_sync for pull/push and git_query for reads.\n- **test_run**(command, cwd, timeout): Run the project's test suite and return structured results (framework, passed/failed counts, failing tests). Auto-detects the runner (pytest/npm/go/cargo) or uses a configured/explicit command; executes via the governed shell. ok=true means the runner executed — see 'success' for pass/fail.\n- **lint_check**(command, cwd, timeout): Run the project's linter and return a structured clean/issue result. Auto-detects the linter (ruff/eslint/go vet/clippy) or uses a configured/explicit command; executes via the governed shell. ok=true means the linter ran — see 'clean'.\n- **file_list**(path, pattern, depth): List files and directories at a given path. Use depth=1 or depth=2 to get a recursive tree in one call instead of listing each sub-directory separately.\n- **file_read**(path, max_lines, start_line, max_chars, mode): Read text file content with adaptive context governance. For large or unfamiliar files, prefer mode='outline' or mode='symbols' first, then use mode='raw' with start_line/max_lines for the specific range you actually need. For LeapFlow's own settings, use config_list / config_get / config_set — its config files are outside the workspace and not readable here.\n- **file_write**(path, content, mode) [capability_expand category: write]: Write content to a file (overwrite or append).\n- **code_search**(pattern, patterns, path, glob, ignore_case, multiline, max_results, context_lines): Search file CONTENTS by regex pattern across a directory tree (ripgrep-backed). Requires a regex pattern. NOT for listing or browsing directory contents — use file_list for that. Prefer this over shell_run grep: faster, skips VCS/dependency/build dirs, and returns structured path:line:column matches. Batch related lookups into ONE call via `patterns` (OR-combined, single pass) instead of issuing several separate searches. Use file_read for the surrounding context of a hit.\n- **file_find**(glob, path, max_results): Find files by a recursive glob pattern under a base path (e.g. '**/test_*.py' or '*.md'). Prefer this over shell_run find; skips VCS/dependency/build dirs.\n- **edit_file**(path, edits, dry_run, diff) [capability_expand category: write]: Apply targeted, anchored search-replace edits to an EXISTING text file (use file_write to create/overwrite). Each edit is {original_text, new_text, replace_all?}; original_text must match exactly and uniquely (or set replace_all) — a non-unique or missing anchor is rejected so files are never corrupted. Set dry_run to preview. Alternatively pass a unified 'diff' to apply its hunks as anchored edits. Far cheaper and safer than rewriting a whole file.\n- **shell_run**(command, cwd, timeout) [capability_expand category: shell]: Execute a one-shot shell command with timeout protection. Runs in the active workspace; paths resolving outside it are refused. Reach for a structured tool first when one fits — web_fetch for anything over HTTP(S), git_query/scm_sync for git, code_search/file_find/file_read for the repo, config_get/config_set for LeapFlow's own settings — because those report typed results, while a failed shell command can only be diagnosed from its exit code and stderr. Every shell run counts as an external side effect, so a failure stops the rest of the batch and is not retried automatically.\n- **terminal_open**(command, cwd, shell) [capability_expand category: terminal]: Open a PERSISTENT shell session (REPL/dev server/watch), returning a session_id for terminal_send/read/close. Disabled unless tools.terminal_session_enabled is set. For one-shot commands use shell_run instead.\n- **terminal_send**(session_id, input, wait) [capability_expand category: terminal]: Send a line of input to a persistent terminal session and return output captured shortly after.\n- **terminal_read**(session_id, wait): Drain buffered output from a persistent terminal session (optionally waiting briefly first).\n- **terminal_close**(session_id) [capability_expand category: terminal]: Terminate a persistent terminal session and release its process group.\n- **terminal_list**(): List active persistent terminal sessions.\n- **config_list**(category, limit): List LeapFlow's own writable settings (LLM model and endpoint, daemon, memory, perception, gateway, …) with current values. Use this to discover the exact key before changing anything. Optionally narrow by `category`. This is the only correct way to inspect LeapFlow configuration — never read config files from disk.\n- **config_get**(key): Read one LeapFlow setting by key (e.g. 'llm.model', 'llm.base_url', 'daemon.log_level'), returning its current value, type, scopes, and whether a change needs a daemon restart. There is no 'llm.provider' key: provider behavior is inferred from the OpenAI-compatible 'llm.base_url'. Never read LeapFlow config files from disk — use this.\n- **config_set**(key, value, scope) [capability_expand category: config]: Change one LeapFlow setting by key, e.g. switch an OpenAI-compatible LLM with key='llm.model' and key='llm.base_url'. There is no 'llm.provider' key: provider behavior is inferred from the endpoint. Values are validated and coerced; credentials are stored in the vault automatically. Call config_list or config_get first if unsure of the key. The result states whether a `leap daemon restart` is required. Never edit LeapFlow config files directly.\n- **web_fetch**(url, select, timeout, max_bytes): Read a URL over HTTP(S) and get back extracted, context-sized content: parsed JSON for API endpoints, readable text plus links for web pages. Use this for anything on the internet — prices, docs, releases, articles — instead of running curl through shell_run: it reports real HTTP status codes, retries rate limits on its own, and is a plain read so a retry is always safe. For JSON APIs pass `select` with a dotted path (e.g. 'chart.result.0.meta') to return just that part instead of the whole payload.\n- **memory_search**(query, limit): Search agent memory for relevant past experiences, observations, and facts.\n- **memory_add**(content, kind) [capability_expand category: write]: Store a new observation or insight in memory for future reference.\n- **research_note**(kind, text): Record a compact, structured note about the current task's state so it survives context compression on long / multi-step tasks. Use for durable findings, open questions still to resolve, decisions / excluded paths, and the immediate next step. One concise sentence per note.\n- **capability_expand**(category): Fetch the full callable schema for every tool in a capability category (e.g. 'hub', 'gateway', 'desktop', 'delegate', 'file', 'memory', 'skill'). The compact tool index always lists every registered tool by name and a one-line summary, but only a static low-risk subset is directly callable each turn. If you need a tool from the index that is not yet callable, call capability_expand with its category first; the matching tools become callable in this turn. Never invent a tool name — expand the category instead.\n- **delegate_task**(goal, context) [capability_expand category: delegate]: Delegate a complex sub-task to an isolated subagent. The subagent gets a fresh context and restricted tool access. Use when a task is self-contained and can be solved independently.\n- **schedule_reentry**(kind, reason, delay_seconds, event_match, max_reentries, deadline_seconds): Register a re-entry so this task can resume later from its current orientation (findings / open questions / next step). Use when work must pause and continue after a delay (kind=time) or when a matching platform event arrives (kind=event), instead of finishing now. The research-ledger state is carried over automatically.\n- **hub_push**(skill_name, visibility, version) [capability_expand category: hub]: Push a local skill to the ModelScope Hub for sharing or backup.\n- **hub_pull**(repo_id, version) [capability_expand category: hub]: Pull a skill from the ModelScope Hub to install locally.\n- **hub_search**(query) [capability_expand category: hub]: Search for skills on the Hub by keyword or description.\n- **hub_sync**(mode, dry_run) [capability_expand category: hub]: Preview or execute sync between local skills and Hub.\n- **platform_action**(platform, action, payload, backend_kind) [capability_expand category: gateway]: Execute an exact registered business action on an external platform through LeapFlow's App Connector layer. Actions must be copied from the App Connector Capability Index and are addressed as domain.operation, e.g. im.send_message or docs.create_markdown. All business fields (chat_id, text, query, etc.) MUST be placed inside `payload`, never at the top level. Example: {\"platform\":\"feishu\",\"action\":\"im.send_message\",\"payload\":{\"chat_id\":\"oc_xxx\",\"text\":\"hello\"}}. Do not invent action names, do not use management actions such as list/guide/connect/status here.\n- **platform_connect**(action, platform, credentials, options, checkpoint) [capability_expand category: gateway]: List, guide, connect, disconnect, remove, or check status for external platforms using the App Connector management namespace. Supports REST and CLI backends. Use this for management actions such as list/guide/preflight/connect/status; use platform_action only for exact registered business actions.\n- **gateway_send**(platform, chat_id, text, thread_id) [capability_expand category: gateway]: Send a message to a connected external platform (Feishu group, Telegram chat, DingTalk conversation, etc.). Requires the platform to be connected via gateway_connect first. Use gateway_connect with action='list' to see connected platforms and available chat IDs.\n- **gateway_connect**(action, platform, credentials, options) [capability_expand category: gateway]: Connect, configure, or manage external platform integrations (Feishu, DingTalk, Telegram, Slack, Discord, etc.). Conversational flow: 1) call 'guide' to get setup steps + required fields, 2) present the steps to the user and ask for ALL required credentials in a single message, 3) call 'connect' with the credentials. Goal: complete in 1–2 user turns. NEVER include credential values in your text response.\n- **plugin_list**(): List the live plugin registry across Tool/Gateway/LLM subsystems for plugin management: which plugins are loaded, their category and fiber lifecycle state. For questions about LeapFlow's own capabilities or whether it supports X, use self_describe instead.\n- **plugin_status**(plugin_id): Get detailed status of a specific plugin: its declared category, runtime dependencies, contributed tools, and fiber lifecycle state.\n- **plugin_versions**(plugin_id): List recorded source versions and active pointer for a profile-scoped plugin.\n- **plugin_propose**(requested_capability, plugin_id, proposed_tools, test_cases, risk_level, evidence): Create a side-effect-free PluginProposal from explicit capability-gap evidence. Use this before plugin_generate when a missing capability should be reviewed. Does not call an LLM, write files, or install anything.\n- **assess_compatibility**(manifest, source_path) [capability_expand category: plugin_management]: Assess whether a foreign plugin manifest or real DSH source bundle is compatible with LeapFlow. A source bundle assessment is static: runtime_ready stays false until restricted Node discovery during plugin_install. Returns component-level verdicts and limitations.\n- **plugin_generate**(plugin_id, description, proposal_id) [capability_expand category: system]: Generate a new ToolPlugin from a natural-language capability description. The LLM produces code that conforms to the ToolPlugin Protocol; it is then rigorously validated (syntax, structure, import, protocol conformance). The generated source is stored in CAS and receives explicit content approval. It DOES NOT install the plugin — installation requires a second, mutation-specific approval via plugin_install.\n- **plugin_install**(plugin_id, code, marketplace_name, source_path, proposal_id, version_label) [capability_expand category: system]: Install a plugin from exactly one source: validated Python code, the configured Python marketplace, or a real DSH source bundle. DSH bundles run restricted Node discovery before registration; only public host tools are installed and client UI limitations are reported. Writes profile-scoped state and mutates the process-global registry. REQUIRES APPROVAL.\n- **plugin_rollback**(plugin_id, version) [capability_expand category: system]: Rollback a profile-scoped plugin to a recorded source version and reload it. REQUIRES APPROVAL.\n- **plugin_reload**(plugin_id, version_label) [capability_expand category: system]: Hot-reload a plugin at runtime. Disposes the old plugin fiber, re-imports its module, and registers a fresh instance. Existing in-flight turns are unaffected (snapshot isolation). REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_disable**(plugin_id) [capability_expand category: system]: Disable a plugin by disposing its fiber, removing its tools from the runtime registry. Cannot disable self_management itself. REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_remove**(plugin_id, delete_source) [capability_expand category: system]: Terminally remove a plugin: dispose its fiber, unregister its tools, remove reload metadata, and optionally delete its profile-scoped source file. Cannot remove self_management itself. REQUIRES APPROVAL.\n- **plugin_unquarantine**(plugin_id) [capability_expand category: plugin_management]: Restore a quarantined plugin to probation status for re-evaluation. Unfreezes the trust ledger, transitions the proposal back to PROBATION, and reloads the plugin. REQUIRES APPROVAL.\n- **plugin_enable**(plugin_id) [capability_expand category: system]: Re-enable a previously disabled plugin by reloading its module and registering a fresh instance. REQUIRES APPROVAL.\n- **self_describe**(facet): Answer questions about LeapFlow ITSELF from live runtime evidence — its identity/version/model, what tools and plugins it has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of self-awareness. Prefer this over reading LeapFlow's own source code. Pick facet=capabilities for 'what can you do / do you support X', identity for version/model, runtime for context/posture, or all for a full self-check when explicitly requested.\n- **runtime_snapshot**(): Lightweight (~150 token) flat snapshot of current runtime state: model, context budget, posture, disclosure level, turn count, cache hit rate, uptime, tool count, and pending approvals.\n- **schedule_create**(trigger_expression, instruction, execution_mode, max_retries, delivery_target) [capability_expand category: scheduler]: Create a new scheduled task. Specify a trigger expression (e.g. '30m', 'every 2h', '0 9 * * *') and the instruction to execute on each trigger. Optionally provide an execution mode and a delivery target to receive notifications.\n- **schedule_list**() [capability_expand category: scheduler]: List all scheduled tasks with their current state, trigger, and next due time.\n- **schedule_status**(task_id) [capability_expand category: scheduler]: Get detailed status and recent execution history for a scheduled task.\n- **schedule_pause**(task_id) [capability_expand category: scheduler]: Pause a scheduled task so it stops firing without being cancelled.\n- **schedule_resume**(task_id) [capability_expand category: scheduler]: Resume a paused scheduled task — re-arms it and recalculates next due time.\n- **schedule_cancel**(task_id) [capability_expand category: scheduler]: Cancel a scheduled task permanently.\n- **tool_search**(query, max_results): Search all registered tools by keyword relevance. Returns a ranked list of matching tools with name, category, and summary. Use this when you need a tool but are unsure of its exact name or category.\n- **tool_describe**(tool_name): Get the full callable schema of a single tool by exact name. Returns the tool's description, parameters, and metadata. Use after tool_search to inspect a specific tool before calling it.\n- **hw_list**(): List connected hardware devices with their channel counts and measured quantities. Start here; it does not include operating limits.\n- **hw_describe**(device_id): Return the full reference for one device: every channel, its unit, its operating envelope, rate limit, reversibility, and required interlocks, plus any prior outcomes recorded for its writable channels. Required before commanding a device.\n- **hw_read**(device_id, channel_id): Read the current value of one device channel. Has no physical effect.\n- **hw_status**(device_id): Report connection health, halt capability, and recent notable events (threshold excursions, lost samples, stalled channels) for one device.\n- **hw_configure**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Set a configuration value or setpoint on a channel declaring effect=configure. Setpoints often have inertia: the value may need time to stabilise.\n- **hw_actuate**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Command motion or output on a channel declaring effect=actuate. This moves physical hardware; a repeat from an unknown state is not a safe retry.\n- **hw_dispense**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Consume an irreversible resource on a channel declaring effect=dispense. Running this twice dispenses twice; never repeat it after a failure without first verifying what already happened.\n- **hw_estop**(device_id) [capability_expand category: hardware]: Stop all motion and output on a device immediately. Never requires approval; use it whenever device behaviour is unexpected.\n- **hw_batch_actuate**(device_id, commands, dry_run, verify) [capability_expand category: hardware]: Write multiple actuator channels in one atomic transaction. Used for coordinated multi-joint control where all positions must be commanded simultaneously (e.g. robot arm joint-space control).\n- **session_search**(query, limit) [capability_expand category: unclassified]: Search past conversation sessions in the current workspace for relevant context.\n- **session_list**(limit): List recent conversation sessions in the current workspace with stable ids, titles, dates, and summaries. Use for browsing past tasks or when user asks to see history without specific search terms. No keywords needed — returns chronological list.\n- **session_detail**(session_id, limit, offset, include_inactive) [capability_expand category: unclassified]: Read a paginated persisted transcript for one past conversation session in the current workspace. Use after session_list or session_search returns a session_id.\n- **click**(element_index) [capability_expand category: desktop]: Click a UI element by its element_index (from the latest observe_ui snapshot)\n- **get_clipboard**() [capability_expand category: desktop]: Read current clipboard text content\n- **list_apps**(filter, running_only) [capability_expand category: desktop]: List available applications on this system. Use to discover correct bundle_id before switch_app.\n- **list_windows**() [capability_expand category: desktop]: List all top-level windows with pid, window_id, title, and per-window state (minimized, on-screen). Call this first to pick the pid and window_id that observe_ui and other window tools require.\n- **observe_ui**(pid, window_id, query) [capability_expand category: desktop]: Snapshot one window's actionable UI elements, each tagged with an element_index for click/right_click/read_text. Re-observe after actions — indices belong to one snapshot. Requires the window's pid and window_id from list_windows.\n- **open_url**(url, app_id) [capability_expand category: desktop]: Open a URL in the default or specified browser\n- **read_text**(element_index) [capability_expand category: desktop]: Read the text content of a specific UI element from the latest snapshot\n- **right_click**(element_index) [capability_expand category: desktop]: Right-click a UI element to open its context menu. Returns visible menu items.\n- **screenshot**(pid, window_id) [capability_expand category: desktop]: Capture a screenshot for visual verification. With pid + window_id captures that window (works across all displays); defaults to the last observed window, or the full desktop when no window has been observed.\n- **scroll**(element_index, direction, amount, pid, window_id) [capability_expand category: desktop]: Scroll a scrollable area of a window. Omit element_index to scroll the window's focused/page scroller; pass one to scroll an exact element from the latest snapshot.\n- **select_text**(element_index) [capability_expand category: desktop]: Select all text in a UI element (focus + select-all, for subsequent copy)\n- **set_clipboard**(text) [capability_expand category: desktop]: Write text to the clipboard\n- **shortcut**(keys) [capability_expand category: desktop]: Execute a keyboard shortcut\n- **switch_app**(app_id) [capability_expand category: desktop]: Switch to an app (launch if needed, activate, verify)\n- **type_text**(text) [capability_expand category: desktop]: Type text into the currently focused element\n- **wait**(seconds) [capability_expand category: desktop]: Wait for a specified duration before continuing\n- **wait_until**(condition, pid, window_id, timeout, poll_interval) [capability_expand category: desktop]: Wait until a UI condition is met (polls UI tree). Returns elements when found or on timeout.\n- **wait_until_stable**(timeout, poll_interval, pid, window_id) [capability_expand category: desktop]: Wait until the UI stops changing (element set stabilizes across polls).\n\n## App Connector Capability Index\nLeapFlow can onboard and manage external apps through `platform_connect` and execute exact registered business actions through `platform_action`.\nFor requests about connecting, setting up, configuring, enabling, or managing a supported app, use `platform_connect` first instead of generating SDK/Webhook sample code.\n`platform_connect.action` is the management namespace: list, guide, preflight, connect, disconnect, remove, status, events_start, events_stop, events_status.\n`platform_action.action` is only for exact registered platform business actions listed below, such as `im.send_message`; never use management actions like `list` or `guide` there.\nAll business fields MUST go inside `payload`; top-level keys are only `platform`, `action`, and `payload`.\nDo not invent platform IDs or platform action names. If the needed action is not listed, ask for discovery/clarification instead of guessing.\nUse `platform_connect` with `action='guide'` and the matching `platform` to start onboarding; use `action='list'` when the app is unclear.\nWhen a pending onboarding state is present, continue from that state with `platform_connect` instead of asking the user to restate the app.\nSupported apps:\n- `api_server`: API Server (OpenAI Compatible) (category=api; backend=adapter; platform_action actions=none registered)\n- `dingtalk`: 钉钉 (DingTalk) (category=im; backend=adapter; platform_action actions=none registered)\n- `feishu`: 飞书 (Feishu/Lark) (category=im; backend=cli)\n - `calendar.create_event` payload={summary*, start_time*, end_time*, attendees} [write/high]\n - `docs.create_markdown` payload={title*, markdown*, folder_token} [write/medium]\n - `drive.search` payload={query*, limit} [read/medium]\n - `im.add_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.download_resource` payload={message_id*, file_key*, type*} [read/low]\n - `im.get_messages` payload={message_ids*} [read/low]\n - `im.list_chats` payload={page_size} [read/low]\n - `im.list_messages` payload={chat_id*} [read/low]\n - `im.list_thread_messages` payload={thread*} [read/low]\n - `im.remove_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.reply_message` payload={message_id*, text*} [send/high]\n - `im.search_chats` payload={query*, page_size} [read/low]\n - `im.search_messages` payload={query*} [read/medium]\n - `im.send_message` payload={chat_id*, text*, thread_id} [send/high]\n - `im.update_card` payload={token*, card*} [write/medium]\n - `im.update_message` payload={message_id*, text*} [write/medium]\n - `mail.search_unread` payload={query, limit} [read/high]\n - `sheets.append_row` payload={spreadsheet_token*, sheet_id*, values*} [write/medium]\n - `task.create` payload={title*, description, due_time} [write/medium]\n- `telegram`: Telegram (category=social; backend=adapter; platform_action actions=none registered)\n- `webhook`: Webhook (Generic) (category=webhook; backend=adapter; platform_action actions=none registered)\n\n## Learned Skills\nYou have access to the following learned skills. Use `skills_list` to browse or `skill_view` to read details:\n- api_designer: REST and GraphQL API design with OpenAPI spec generation and best practices [API, REST, GraphQL]\n- code_review: Structured code review for security, performance, and style [code, review, security]\n- data_analysis: CSV/JSON data analysis with statistical summaries, quality checks, and visualiza [data, analysis, statistics]\n- devops_helper: CI/CD pipeline configuration, Docker orchestration, and deployment strategy [devops, CI/CD, Docker]\n- document_writer: Structured document generation: technical docs, API references, reports, and REA [documentation, technical-writing, README]\n- git_workflow: Git operation orchestration: branching, commits, conflicts, and PR workflow [git, version-control, branching]\n- i18n_helper: Internationalization workflow: string extraction, translation, locale management [i18n, internationalization, localization]\n- project_analysis: Project structure analysis, dependency audit, and tech stack identification [project, analysis, dependencies]\n- security_audit: Security vulnerability scanning strategy, threat modeling, and fix recommendatio [security, vulnerability, audit]\n- shell_automation: Natural language to shell commands with safety review and step-by-step execution [shell, terminal, automation]\n- test_writer: Generate unit and integration tests from source code with edge-case coverage [testing, unit-test, integration-test]\n- web_research: Multi-step web search with cross-verification and structured synthesis [web, search, research]\n\n## Tool Usage\nTools are normally invoked through the native function-calling mechanism, not by writing JSON in your reply\ntext. Only if the provider signals that native function calling is unavailable for this turn, fall back to a\nsingle JSON code block: `{\"name\": \"tool_name\", \"arguments\": {\"key\": \"value\"}}` — use this fallback format\nonly, never both. Only call a tool whose exact name appears in the tool index above and is currently callable\n(or reachable via `capability_expand`). Never invent, rename, alias, or guess a tool name, platform ID, or\nplatform action from argument shape or wording — if the index does not list it, it does not exist.\n`platform_connect.action` (list/guide/preflight/connect/disconnect/remove/status/events_start/events_stop/\nevents_status) is the App Connector management namespace; `platform_action.action` only accepts exact\nregistered business actions such as `im.send_message` shown in the App Connector Capability Index — never mix\nthe two namespaces. If a tool call returns an unknown/unavailable result, use the returned suggestions or\navailable names for a single retry instead of trying further variations of the same guess.\n\n**Side-effect action rule** (`platform_action` with effect=send/write/execute):\n- Call each unique action+payload **exactly once**. Never include duplicates in the same turn.\n- Once the result returns `\"completed\": true`, that action is DONE for this task. Do NOT call it again\n in any subsequent turn — immediately summarize the result for the user instead.\n- The system enforces idempotency: duplicate calls are blocked and will not execute.\n- If the user explicitly requests sending/writing multiple times, use distinct payloads per call.\n\n**Resource identifier provenance rule**:\n- NEVER fabricate, guess, or infer resource identifiers (chat_id, message_id, file_key, user_id, etc.).\n Every resource ID used in a side-effect action MUST come from a successful API response in this session.\n- If a read/list action fails (e.g. authorization error), you do NOT have valid resource IDs.\n Report the failure to the user — do NOT attempt the dependent write/send action with a guessed ID.\n- When a tool result contains `\"llm_instruction\"`, follow it exactly.\n\n## Guidelines\n1. **Direct answers first**: If you already know the answer, respond directly without tools.\n2. **Avoid redundant tool calls**: Do not call the same tool with the same arguments more than once in the same user turn. When an existing tool result already answers the user's request, stop calling tools and answer directly.\n3. **Use tools proactively**: When the user asks about files, time, system state, or needs actions performed, use the appropriate tool.\n4. **Chain tools when needed**: You can call multiple tools in sequence (e.g., list files → read file → summarize).\n5. **Handle failures gracefully**: If a tool fails, explain what went wrong and suggest alternatives. If it failed because of an unknown tool/platform/action name, retry once with an exact name from the returned suggestions, then explain rather than keep guessing.\n6. **Summarize results naturally**: After tool execution, synthesize the results into a helpful answer rather than dumping raw output.\n7. **Stay conversational**: Maintain a natural, helpful tone. Acknowledge context from earlier in the conversation.\n8. **Recall past work**:\n - Broad queries (\"之前做了什么\", \"列出任务\"): answer from the \"Recent Task History\" section already in your context. If insufficient, call session_list.\n - Specific lookups (\"上次那个配置怎么改的\"): call session_search with relevant phrases (NOT single characters).\n - Do NOT call search tools repeatedly with keyword fragments. One well-phrased call is better than ten fragmented ones.\n\n## Coding & Verification\nWhen working with code, prefer the precise built-in tools over ad-hoc shell: use `repo_map` to orient in an\nunfamiliar project, `code_search`/`file_find` to locate and `code_intel` for symbols, `edit_file` (anchored\nsearch-replace, or a unified `diff`) to change files — never rewrite a whole file to change a few lines — and\n`git_query` to inspect diffs/log. After edits, check `syntax_ok` in the result and run `test_run`/`lint_check`\nbefore declaring the work done. Batch independent read-only calls (search/read/query) into a single turn — they\nrun in parallel.\n\n## Presentation Style\n1. **Polished Markdown only**: Format user-facing answers with clean Markdown headings, short paragraphs, and concise bullets. Use tables only when they improve comparison or scanning.\n2. **Terminal-friendly layout**: Keep lines readable in a TUI; avoid dense walls of text, deeply nested lists, oversized ASCII art, or heavy visual blocks.\n3. **Elegant emphasis**: Use bold text sparingly for key terms and conclusions. Avoid excessive emojis, decorative symbols, repeated separators, or visual noise.\n4. **Theme-safe colors**: Do not emit ANSI escape codes, HTML color tags, Rich markup, or hardcoded color names. Rely on the TUI theme to render Markdown professionally.\n5. **No leaked tool protocol**: Never show tool-call JSON, internal schemas, raw observations, tool result payloads, or hidden reasoning in the final answer unless the user explicitly asks for raw/debug output. Treat any prior `{\"name\": ..., \"arguments\": ...}` blocks and `Tool result (...)` messages as internal execution context only.\n6. **Professional closure**: End with a concise conclusion or next step when helpful; avoid rambling after the useful answer is complete.\n\nWhen finished with all tool calls, respond normally without a JSON block, tool-call transcript, or process log.\n\n## Task Contract\n- Task ID: turn-5\n- Original user request: Preview an actuate of bench_r8 setpoint to 55, dry run only.\n- Workspace root: \n- Allowed roots: \n- Treat relative project paths as relative to the workspace root; never infer `.` as the project root when a workspace root is provided.\n- Workspace boundary is enforced by tools: do not read, search, edit, or run commands against paths outside the allowed roots unless the user explicitly requests an external path and the tool/approval policy permits it.\n- LeapFlow workspace config is optional at `/.leapflow/config.yaml`; runtime config is loaded from `~/.leapflow/config/user.yaml` and `~/.leapflow/profiles//config/*.yaml`.\n- Preserve this task contract across summarization, compression, tool loops, and memory retrieval.\n" - }, - { - "role": "assistant", - "content": "Read the setpoint channel." - }, - { - "role": "user", - "content": "Actuate the bench_r8 setpoint to 60.\nConfigure the bench_r8 homed channel to true to complete homing." - }, - { - "role": "assistant", - "content": "[Called: hw_configure]\nHomed the device; the readiness gate is now satisfied." - }, - { - "role": "user", - "content": "Preview an actuate of bench_r8 setpoint to 55, dry run only.\nPreview an actuate of bench_r8 setpoint to 55, dry run only." - } - ], - "tools": [ - "capability_expand", - "code_intel", - "code_search", - "config_get", - "config_list", - "env_info", - "file_find", - "file_list", - "file_read", - "git_query", - "hw_actuate", - "hw_batch_actuate", - "hw_configure", - "hw_describe", - "hw_dispense", - "hw_estop", - "hw_list", - "hw_read", - "hw_status", - "lint_check", - "memory_search", - "plugin_list", - "plugin_propose", - "plugin_status", - "plugin_versions", - "repo_map", - "research_note", - "runtime_snapshot", - "schedule_reentry", - "self_describe", - "session_list", - "skill_view", - "skills_list", - "terminal_list", - "terminal_read", - "test_run", - "text_search", - "time_get", - "tool_describe", - "tool_search", - "web_fetch" - ] - }, - "responses": [ - { - "status": 200, - "content_type": "application/json", - "body": "{\"id\": \"chatcmpl-cassette\", \"object\": \"chat.completion\", \"model\": \"cassette-model\", \"choices\": [{\"index\": 0, \"message\": {\"role\": \"assistant\", \"content\": \"\", \"tool_calls\": [{\"id\": \"call_1\", \"type\": \"function\", \"function\": {\"name\": \"hw_actuate\", \"arguments\": \"{\\\"device_id\\\": \\\"bench_r8\\\", \\\"channel_id\\\": \\\"setpoint\\\", \\\"value\\\": 55.0, \\\"dry_run\\\": true}\"}}]}, \"finish_reason\": \"tool_calls\"}], \"usage\": {\"prompt_tokens\": 64, \"completion_tokens\": 16, \"total_tokens\": 80}}" - } - ] -} diff --git a/tests/_fixtures/cassettes/r8_hardware/cassette-model-ff9056562a13326a.cassette.json b/tests/_fixtures/cassettes/r8_hardware/cassette-model-ff9056562a13326a.cassette.json deleted file mode 100644 index a063f3f9..00000000 --- a/tests/_fixtures/cassettes/r8_hardware/cassette-model-ff9056562a13326a.cassette.json +++ /dev/null @@ -1,80 +0,0 @@ -{ - "fingerprint": "ff9056562a13326a54b0ec73b68422054089499292e1c93864a6d0baf7798030", - "note": "captured in seed mode", - "request": { - "model": "cassette-model", - "stream": false, - "messages": [ - { - "role": "system", - "content": "You are LeapFlow, an intelligent assistant that can both converse naturally and take real actions on the user's computer.\n\n## Capabilities\nThe tool index below lists **every** registered tool by name and a one-line summary — this is the complete\ncapability contract; nothing else exists. Only a subset is directly callable this turn (via native tool calling,\nnot a JSON block in your reply). If you need a tool from the index that is not yet callable, call\n`capability_expand` with its category name first — the matching tools become callable immediately after.\nWhen the user asks about LeapFlow **itself** — its identity/version/model, what tools, plugins, or capabilities\nit has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of\nself-awareness — answer from live runtime evidence via `self_describe` (facet=capabilities for \"what can you\ndo / do you support X\", identity for version/model, runtime for context/posture, or `all` for a full self-check),\nnever by reading LeapFlow's own source code, documentation, or memory. Report configuration-dependent or\nunavailable capabilities as limitations rather than inferring them.\n- **text_search**(text, pattern): Search for a regex pattern in text.\n- **text_replace**(text, old, new, count) [capability_expand category: write]: Replace occurrences of a substring in text.\n- **time_get**(): Get current date and time.\n- **env_info**(): Get system environment information (OS, Python version, cwd).\n- **skills_list**(query, category, source): List available learned skills. Use when user asks about capabilities or you need a specific skill.\n- **skill_view**(name): View the full content of a specific skill document.\n- **skill_list_files**(name) [capability_expand category: skills]: List all support files in a skill directory (configs, templates, examples). Use before skill_file_read to discover available files.\n- **skill_file_read**(name, file_path) [capability_expand category: skills]: Read an individual support file from a skill directory. Use skill_list_files first to discover available files.\n- **code_intel**(path, operation): Precise document symbols (outline) for a source file: classes, functions, and methods with line ranges. Python uses an exact AST parse; other languages use a keyword-prefix scan. Prefer over file_read mode=symbols for accurate navigation before editing. Read-only.\n- **repo_map**(path): Compact project orientation for a repository root: languages, detected test/lint commands, top-level structure, entry points, manifest, and VCS branch. Call this first when entering an unfamiliar codebase. Read-only.\n- **scm_sync**(action, cwd, remote, pull_ref, push_ref, timeout) [capability_expand category: scm]: Run a typed git SCM action. Use this instead of shell_run for git pull/push/status. For 'pull origin main then push', set action='pull_then_push', remote='origin', pull_ref='main', and omit push_ref so LeapFlow pushes the current local branch.\n- **git_query**(action, cwd, ref, path, staged, max_count, stat): Read-only structured git inspection: action=diff|log|status|branch|show. Prefer over shell_run for reading repo state — output is clipped, redacted, and log/branch are parsed into structured fields. Use scm_sync for pull/push.\n- **git_write**(action, cwd, message, stage_all, name, ref, create) [capability_expand category: scm]: Mutating git actions: action=commit (message, stage_all), branch (create+switch), checkout (switch; create=true for -b). Approval-gated. Use scm_sync for pull/push and git_query for reads.\n- **test_run**(command, cwd, timeout): Run the project's test suite and return structured results (framework, passed/failed counts, failing tests). Auto-detects the runner (pytest/npm/go/cargo) or uses a configured/explicit command; executes via the governed shell. ok=true means the runner executed — see 'success' for pass/fail.\n- **lint_check**(command, cwd, timeout): Run the project's linter and return a structured clean/issue result. Auto-detects the linter (ruff/eslint/go vet/clippy) or uses a configured/explicit command; executes via the governed shell. ok=true means the linter ran — see 'clean'.\n- **file_list**(path, pattern, depth): List files and directories at a given path. Use depth=1 or depth=2 to get a recursive tree in one call instead of listing each sub-directory separately.\n- **file_read**(path, max_lines, start_line, max_chars, mode): Read text file content with adaptive context governance. For large or unfamiliar files, prefer mode='outline' or mode='symbols' first, then use mode='raw' with start_line/max_lines for the specific range you actually need. For LeapFlow's own settings, use config_list / config_get / config_set — its config files are outside the workspace and not readable here.\n- **file_write**(path, content, mode) [capability_expand category: write]: Write content to a file (overwrite or append).\n- **code_search**(pattern, patterns, path, glob, ignore_case, multiline, max_results, context_lines): Search file CONTENTS by regex pattern across a directory tree (ripgrep-backed). Requires a regex pattern. NOT for listing or browsing directory contents — use file_list for that. Prefer this over shell_run grep: faster, skips VCS/dependency/build dirs, and returns structured path:line:column matches. Batch related lookups into ONE call via `patterns` (OR-combined, single pass) instead of issuing several separate searches. Use file_read for the surrounding context of a hit.\n- **file_find**(glob, path, max_results): Find files by a recursive glob pattern under a base path (e.g. '**/test_*.py' or '*.md'). Prefer this over shell_run find; skips VCS/dependency/build dirs.\n- **edit_file**(path, edits, dry_run, diff) [capability_expand category: write]: Apply targeted, anchored search-replace edits to an EXISTING text file (use file_write to create/overwrite). Each edit is {original_text, new_text, replace_all?}; original_text must match exactly and uniquely (or set replace_all) — a non-unique or missing anchor is rejected so files are never corrupted. Set dry_run to preview. Alternatively pass a unified 'diff' to apply its hunks as anchored edits. Far cheaper and safer than rewriting a whole file.\n- **shell_run**(command, cwd, timeout) [capability_expand category: shell]: Execute a one-shot shell command with timeout protection. Runs in the active workspace; paths resolving outside it are refused. Reach for a structured tool first when one fits — web_fetch for anything over HTTP(S), git_query/scm_sync for git, code_search/file_find/file_read for the repo, config_get/config_set for LeapFlow's own settings — because those report typed results, while a failed shell command can only be diagnosed from its exit code and stderr. Every shell run counts as an external side effect, so a failure stops the rest of the batch and is not retried automatically.\n- **terminal_open**(command, cwd, shell) [capability_expand category: terminal]: Open a PERSISTENT shell session (REPL/dev server/watch), returning a session_id for terminal_send/read/close. Disabled unless tools.terminal_session_enabled is set. For one-shot commands use shell_run instead.\n- **terminal_send**(session_id, input, wait) [capability_expand category: terminal]: Send a line of input to a persistent terminal session and return output captured shortly after.\n- **terminal_read**(session_id, wait): Drain buffered output from a persistent terminal session (optionally waiting briefly first).\n- **terminal_close**(session_id) [capability_expand category: terminal]: Terminate a persistent terminal session and release its process group.\n- **terminal_list**(): List active persistent terminal sessions.\n- **config_list**(category, limit): List LeapFlow's own writable settings (LLM model and endpoint, daemon, memory, perception, gateway, …) with current values. Use this to discover the exact key before changing anything. Optionally narrow by `category`. This is the only correct way to inspect LeapFlow configuration — never read config files from disk.\n- **config_get**(key): Read one LeapFlow setting by key (e.g. 'llm.model', 'llm.base_url', 'daemon.log_level'), returning its current value, type, scopes, and whether a change needs a daemon restart. There is no 'llm.provider' key: provider behavior is inferred from the OpenAI-compatible 'llm.base_url'. Never read LeapFlow config files from disk — use this.\n- **config_set**(key, value, scope) [capability_expand category: config]: Change one LeapFlow setting by key, e.g. switch an OpenAI-compatible LLM with key='llm.model' and key='llm.base_url'. There is no 'llm.provider' key: provider behavior is inferred from the endpoint. Values are validated and coerced; credentials are stored in the vault automatically. Call config_list or config_get first if unsure of the key. The result states whether a `leap daemon restart` is required. Never edit LeapFlow config files directly.\n- **web_fetch**(url, select, timeout, max_bytes): Read a URL over HTTP(S) and get back extracted, context-sized content: parsed JSON for API endpoints, readable text plus links for web pages. Use this for anything on the internet — prices, docs, releases, articles — instead of running curl through shell_run: it reports real HTTP status codes, retries rate limits on its own, and is a plain read so a retry is always safe. For JSON APIs pass `select` with a dotted path (e.g. 'chart.result.0.meta') to return just that part instead of the whole payload.\n- **memory_search**(query, limit): Search agent memory for relevant past experiences, observations, and facts.\n- **memory_add**(content, kind) [capability_expand category: write]: Store a new observation or insight in memory for future reference.\n- **research_note**(kind, text): Record a compact, structured note about the current task's state so it survives context compression on long / multi-step tasks. Use for durable findings, open questions still to resolve, decisions / excluded paths, and the immediate next step. One concise sentence per note.\n- **capability_expand**(category): Fetch the full callable schema for every tool in a capability category (e.g. 'hub', 'gateway', 'desktop', 'delegate', 'file', 'memory', 'skill'). The compact tool index always lists every registered tool by name and a one-line summary, but only a static low-risk subset is directly callable each turn. If you need a tool from the index that is not yet callable, call capability_expand with its category first; the matching tools become callable in this turn. Never invent a tool name — expand the category instead.\n- **delegate_task**(goal, context) [capability_expand category: delegate]: Delegate a complex sub-task to an isolated subagent. The subagent gets a fresh context and restricted tool access. Use when a task is self-contained and can be solved independently.\n- **schedule_reentry**(kind, reason, delay_seconds, event_match, max_reentries, deadline_seconds): Register a re-entry so this task can resume later from its current orientation (findings / open questions / next step). Use when work must pause and continue after a delay (kind=time) or when a matching platform event arrives (kind=event), instead of finishing now. The research-ledger state is carried over automatically.\n- **hub_push**(skill_name, visibility, version) [capability_expand category: hub]: Push a local skill to the ModelScope Hub for sharing or backup.\n- **hub_pull**(repo_id, version) [capability_expand category: hub]: Pull a skill from the ModelScope Hub to install locally.\n- **hub_search**(query) [capability_expand category: hub]: Search for skills on the Hub by keyword or description.\n- **hub_sync**(mode, dry_run) [capability_expand category: hub]: Preview or execute sync between local skills and Hub.\n- **platform_action**(platform, action, payload, backend_kind) [capability_expand category: gateway]: Execute an exact registered business action on an external platform through LeapFlow's App Connector layer. Actions must be copied from the App Connector Capability Index and are addressed as domain.operation, e.g. im.send_message or docs.create_markdown. All business fields (chat_id, text, query, etc.) MUST be placed inside `payload`, never at the top level. Example: {\"platform\":\"feishu\",\"action\":\"im.send_message\",\"payload\":{\"chat_id\":\"oc_xxx\",\"text\":\"hello\"}}. Do not invent action names, do not use management actions such as list/guide/connect/status here.\n- **platform_connect**(action, platform, credentials, options, checkpoint) [capability_expand category: gateway]: List, guide, connect, disconnect, remove, or check status for external platforms using the App Connector management namespace. Supports REST and CLI backends. Use this for management actions such as list/guide/preflight/connect/status; use platform_action only for exact registered business actions.\n- **gateway_send**(platform, chat_id, text, thread_id) [capability_expand category: gateway]: Send a message to a connected external platform (Feishu group, Telegram chat, DingTalk conversation, etc.). Requires the platform to be connected via gateway_connect first. Use gateway_connect with action='list' to see connected platforms and available chat IDs.\n- **gateway_connect**(action, platform, credentials, options) [capability_expand category: gateway]: Connect, configure, or manage external platform integrations (Feishu, DingTalk, Telegram, Slack, Discord, etc.). Conversational flow: 1) call 'guide' to get setup steps + required fields, 2) present the steps to the user and ask for ALL required credentials in a single message, 3) call 'connect' with the credentials. Goal: complete in 1–2 user turns. NEVER include credential values in your text response.\n- **plugin_list**(): List the live plugin registry across Tool/Gateway/LLM subsystems for plugin management: which plugins are loaded, their category and fiber lifecycle state. For questions about LeapFlow's own capabilities or whether it supports X, use self_describe instead.\n- **plugin_status**(plugin_id): Get detailed status of a specific plugin: its declared category, runtime dependencies, contributed tools, and fiber lifecycle state.\n- **plugin_versions**(plugin_id): List recorded source versions and active pointer for a profile-scoped plugin.\n- **plugin_propose**(requested_capability, plugin_id, proposed_tools, test_cases, risk_level, evidence): Create a side-effect-free PluginProposal from explicit capability-gap evidence. Use this before plugin_generate when a missing capability should be reviewed. Does not call an LLM, write files, or install anything.\n- **assess_compatibility**(manifest, source_path) [capability_expand category: plugin_management]: Assess whether a foreign plugin manifest or real DSH source bundle is compatible with LeapFlow. A source bundle assessment is static: runtime_ready stays false until restricted Node discovery during plugin_install. Returns component-level verdicts and limitations.\n- **plugin_generate**(plugin_id, description, proposal_id) [capability_expand category: system]: Generate a new ToolPlugin from a natural-language capability description. The LLM produces code that conforms to the ToolPlugin Protocol; it is then rigorously validated (syntax, structure, import, protocol conformance). The generated source is stored in CAS and receives explicit content approval. It DOES NOT install the plugin — installation requires a second, mutation-specific approval via plugin_install.\n- **plugin_install**(plugin_id, code, marketplace_name, source_path, proposal_id, version_label) [capability_expand category: system]: Install a plugin from exactly one source: validated Python code, the configured Python marketplace, or a real DSH source bundle. DSH bundles run restricted Node discovery before registration; only public host tools are installed and client UI limitations are reported. Writes profile-scoped state and mutates the process-global registry. REQUIRES APPROVAL.\n- **plugin_rollback**(plugin_id, version) [capability_expand category: system]: Rollback a profile-scoped plugin to a recorded source version and reload it. REQUIRES APPROVAL.\n- **plugin_reload**(plugin_id, version_label) [capability_expand category: system]: Hot-reload a plugin at runtime. Disposes the old plugin fiber, re-imports its module, and registers a fresh instance. Existing in-flight turns are unaffected (snapshot isolation). REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_disable**(plugin_id) [capability_expand category: system]: Disable a plugin by disposing its fiber, removing its tools from the runtime registry. Cannot disable self_management itself. REQUIRES APPROVAL — this is a self-modification action.\n- **plugin_remove**(plugin_id, delete_source) [capability_expand category: system]: Terminally remove a plugin: dispose its fiber, unregister its tools, remove reload metadata, and optionally delete its profile-scoped source file. Cannot remove self_management itself. REQUIRES APPROVAL.\n- **plugin_unquarantine**(plugin_id) [capability_expand category: plugin_management]: Restore a quarantined plugin to probation status for re-evaluation. Unfreezes the trust ledger, transitions the proposal back to PROBATION, and reloads the plugin. REQUIRES APPROVAL.\n- **plugin_enable**(plugin_id) [capability_expand category: system]: Re-enable a previously disabled plugin by reloading its module and registering a fresh instance. REQUIRES APPROVAL.\n- **self_describe**(facet): Answer questions about LeapFlow ITSELF from live runtime evidence — its identity/version/model, what tools and plugins it has, whether it supports plugins/self-evolution/hot-reload, its runtime state, or whether it has any form of self-awareness. Prefer this over reading LeapFlow's own source code. Pick facet=capabilities for 'what can you do / do you support X', identity for version/model, runtime for context/posture, or all for a full self-check when explicitly requested.\n- **runtime_snapshot**(): Lightweight (~150 token) flat snapshot of current runtime state: model, context budget, posture, disclosure level, turn count, cache hit rate, uptime, tool count, and pending approvals.\n- **schedule_create**(trigger_expression, instruction, execution_mode, max_retries, delivery_target) [capability_expand category: scheduler]: Create a new scheduled task. Specify a trigger expression (e.g. '30m', 'every 2h', '0 9 * * *') and the instruction to execute on each trigger. Optionally provide an execution mode and a delivery target to receive notifications.\n- **schedule_list**() [capability_expand category: scheduler]: List all scheduled tasks with their current state, trigger, and next due time.\n- **schedule_status**(task_id) [capability_expand category: scheduler]: Get detailed status and recent execution history for a scheduled task.\n- **schedule_pause**(task_id) [capability_expand category: scheduler]: Pause a scheduled task so it stops firing without being cancelled.\n- **schedule_resume**(task_id) [capability_expand category: scheduler]: Resume a paused scheduled task — re-arms it and recalculates next due time.\n- **schedule_cancel**(task_id) [capability_expand category: scheduler]: Cancel a scheduled task permanently.\n- **tool_search**(query, max_results): Search all registered tools by keyword relevance. Returns a ranked list of matching tools with name, category, and summary. Use this when you need a tool but are unsure of its exact name or category.\n- **tool_describe**(tool_name): Get the full callable schema of a single tool by exact name. Returns the tool's description, parameters, and metadata. Use after tool_search to inspect a specific tool before calling it.\n- **hw_list**(): List connected hardware devices with their channel counts and measured quantities. Start here; it does not include operating limits.\n- **hw_describe**(device_id): Return the full reference for one device: every channel, its unit, its operating envelope, rate limit, reversibility, and required interlocks, plus any prior outcomes recorded for its writable channels. Required before commanding a device.\n- **hw_read**(device_id, channel_id): Read the current value of one device channel. Has no physical effect.\n- **hw_status**(device_id): Report connection health, halt capability, and recent notable events (threshold excursions, lost samples, stalled channels) for one device.\n- **hw_configure**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Set a configuration value or setpoint on a channel declaring effect=configure. Setpoints often have inertia: the value may need time to stabilise.\n- **hw_actuate**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Command motion or output on a channel declaring effect=actuate. This moves physical hardware; a repeat from an unknown state is not a safe retry.\n- **hw_dispense**(device_id, channel_id, value, conditions, dry_run) [capability_expand category: hardware]: Consume an irreversible resource on a channel declaring effect=dispense. Running this twice dispenses twice; never repeat it after a failure without first verifying what already happened.\n- **hw_estop**(device_id) [capability_expand category: hardware]: Stop all motion and output on a device immediately. Never requires approval; use it whenever device behaviour is unexpected.\n- **hw_batch_actuate**(device_id, commands, dry_run, verify) [capability_expand category: hardware]: Write multiple actuator channels in one atomic transaction. Used for coordinated multi-joint control where all positions must be commanded simultaneously (e.g. robot arm joint-space control).\n- **session_search**(query, limit) [capability_expand category: unclassified]: Search past conversation sessions in the current workspace for relevant context.\n- **session_list**(limit): List recent conversation sessions in the current workspace with stable ids, titles, dates, and summaries. Use for browsing past tasks or when user asks to see history without specific search terms. No keywords needed — returns chronological list.\n- **session_detail**(session_id, limit, offset, include_inactive) [capability_expand category: unclassified]: Read a paginated persisted transcript for one past conversation session in the current workspace. Use after session_list or session_search returns a session_id.\n- **click**(element_index) [capability_expand category: desktop]: Click a UI element by its element_index (from the latest observe_ui snapshot)\n- **get_clipboard**() [capability_expand category: desktop]: Read current clipboard text content\n- **list_apps**(filter, running_only) [capability_expand category: desktop]: List available applications on this system. Use to discover correct bundle_id before switch_app.\n- **list_windows**() [capability_expand category: desktop]: List all top-level windows with pid, window_id, title, and per-window state (minimized, on-screen). Call this first to pick the pid and window_id that observe_ui and other window tools require.\n- **observe_ui**(pid, window_id, query) [capability_expand category: desktop]: Snapshot one window's actionable UI elements, each tagged with an element_index for click/right_click/read_text. Re-observe after actions — indices belong to one snapshot. Requires the window's pid and window_id from list_windows.\n- **open_url**(url, app_id) [capability_expand category: desktop]: Open a URL in the default or specified browser\n- **read_text**(element_index) [capability_expand category: desktop]: Read the text content of a specific UI element from the latest snapshot\n- **right_click**(element_index) [capability_expand category: desktop]: Right-click a UI element to open its context menu. Returns visible menu items.\n- **screenshot**(pid, window_id) [capability_expand category: desktop]: Capture a screenshot for visual verification. With pid + window_id captures that window (works across all displays); defaults to the last observed window, or the full desktop when no window has been observed.\n- **scroll**(element_index, direction, amount, pid, window_id) [capability_expand category: desktop]: Scroll a scrollable area of a window. Omit element_index to scroll the window's focused/page scroller; pass one to scroll an exact element from the latest snapshot.\n- **select_text**(element_index) [capability_expand category: desktop]: Select all text in a UI element (focus + select-all, for subsequent copy)\n- **set_clipboard**(text) [capability_expand category: desktop]: Write text to the clipboard\n- **shortcut**(keys) [capability_expand category: desktop]: Execute a keyboard shortcut\n- **switch_app**(app_id) [capability_expand category: desktop]: Switch to an app (launch if needed, activate, verify)\n- **type_text**(text) [capability_expand category: desktop]: Type text into the currently focused element\n- **wait**(seconds) [capability_expand category: desktop]: Wait for a specified duration before continuing\n- **wait_until**(condition, pid, window_id, timeout, poll_interval) [capability_expand category: desktop]: Wait until a UI condition is met (polls UI tree). Returns elements when found or on timeout.\n- **wait_until_stable**(timeout, poll_interval, pid, window_id) [capability_expand category: desktop]: Wait until the UI stops changing (element set stabilizes across polls).\n\n## App Connector Capability Index\nLeapFlow can onboard and manage external apps through `platform_connect` and execute exact registered business actions through `platform_action`.\nFor requests about connecting, setting up, configuring, enabling, or managing a supported app, use `platform_connect` first instead of generating SDK/Webhook sample code.\n`platform_connect.action` is the management namespace: list, guide, preflight, connect, disconnect, remove, status, events_start, events_stop, events_status.\n`platform_action.action` is only for exact registered platform business actions listed below, such as `im.send_message`; never use management actions like `list` or `guide` there.\nAll business fields MUST go inside `payload`; top-level keys are only `platform`, `action`, and `payload`.\nDo not invent platform IDs or platform action names. If the needed action is not listed, ask for discovery/clarification instead of guessing.\nUse `platform_connect` with `action='guide'` and the matching `platform` to start onboarding; use `action='list'` when the app is unclear.\nWhen a pending onboarding state is present, continue from that state with `platform_connect` instead of asking the user to restate the app.\nSupported apps:\n- `api_server`: API Server (OpenAI Compatible) (category=api; backend=adapter; platform_action actions=none registered)\n- `dingtalk`: 钉钉 (DingTalk) (category=im; backend=adapter; platform_action actions=none registered)\n- `feishu`: 飞书 (Feishu/Lark) (category=im; backend=cli)\n - `calendar.create_event` payload={summary*, start_time*, end_time*, attendees} [write/high]\n - `docs.create_markdown` payload={title*, markdown*, folder_token} [write/medium]\n - `drive.search` payload={query*, limit} [read/medium]\n - `im.add_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.download_resource` payload={message_id*, file_key*, type*} [read/low]\n - `im.get_messages` payload={message_ids*} [read/low]\n - `im.list_chats` payload={page_size} [read/low]\n - `im.list_messages` payload={chat_id*} [read/low]\n - `im.list_thread_messages` payload={thread*} [read/low]\n - `im.remove_reaction` payload={message_id*, emoji_type*} [write/low]\n - `im.reply_message` payload={message_id*, text*} [send/high]\n - `im.search_chats` payload={query*, page_size} [read/low]\n - `im.search_messages` payload={query*} [read/medium]\n - `im.send_message` payload={chat_id*, text*, thread_id} [send/high]\n - `im.update_card` payload={token*, card*} [write/medium]\n - `im.update_message` payload={message_id*, text*} [write/medium]\n - `mail.search_unread` payload={query, limit} [read/high]\n - `sheets.append_row` payload={spreadsheet_token*, sheet_id*, values*} [write/medium]\n - `task.create` payload={title*, description, due_time} [write/medium]\n- `telegram`: Telegram (category=social; backend=adapter; platform_action actions=none registered)\n- `webhook`: Webhook (Generic) (category=webhook; backend=adapter; platform_action actions=none registered)\n\n## Learned Skills\nYou have access to the following learned skills. Use `skills_list` to browse or `skill_view` to read details:\n- api_designer: REST and GraphQL API design with OpenAPI spec generation and best practices [API, REST, GraphQL]\n- code_review: Structured code review for security, performance, and style [code, review, security]\n- data_analysis: CSV/JSON data analysis with statistical summaries, quality checks, and visualiza [data, analysis, statistics]\n- devops_helper: CI/CD pipeline configuration, Docker orchestration, and deployment strategy [devops, CI/CD, Docker]\n- document_writer: Structured document generation: technical docs, API references, reports, and REA [documentation, technical-writing, README]\n- git_workflow: Git operation orchestration: branching, commits, conflicts, and PR workflow [git, version-control, branching]\n- i18n_helper: Internationalization workflow: string extraction, translation, locale management [i18n, internationalization, localization]\n- project_analysis: Project structure analysis, dependency audit, and tech stack identification [project, analysis, dependencies]\n- security_audit: Security vulnerability scanning strategy, threat modeling, and fix recommendatio [security, vulnerability, audit]\n- shell_automation: Natural language to shell commands with safety review and step-by-step execution [shell, terminal, automation]\n- test_writer: Generate unit and integration tests from source code with edge-case coverage [testing, unit-test, integration-test]\n- web_research: Multi-step web search with cross-verification and structured synthesis [web, search, research]\n\n## Tool Usage\nTools are normally invoked through the native function-calling mechanism, not by writing JSON in your reply\ntext. Only if the provider signals that native function calling is unavailable for this turn, fall back to a\nsingle JSON code block: `{\"name\": \"tool_name\", \"arguments\": {\"key\": \"value\"}}` — use this fallback format\nonly, never both. Only call a tool whose exact name appears in the tool index above and is currently callable\n(or reachable via `capability_expand`). Never invent, rename, alias, or guess a tool name, platform ID, or\nplatform action from argument shape or wording — if the index does not list it, it does not exist.\n`platform_connect.action` (list/guide/preflight/connect/disconnect/remove/status/events_start/events_stop/\nevents_status) is the App Connector management namespace; `platform_action.action` only accepts exact\nregistered business actions such as `im.send_message` shown in the App Connector Capability Index — never mix\nthe two namespaces. If a tool call returns an unknown/unavailable result, use the returned suggestions or\navailable names for a single retry instead of trying further variations of the same guess.\n\n**Side-effect action rule** (`platform_action` with effect=send/write/execute):\n- Call each unique action+payload **exactly once**. Never include duplicates in the same turn.\n- Once the result returns `\"completed\": true`, that action is DONE for this task. Do NOT call it again\n in any subsequent turn — immediately summarize the result for the user instead.\n- The system enforces idempotency: duplicate calls are blocked and will not execute.\n- If the user explicitly requests sending/writing multiple times, use distinct payloads per call.\n\n**Resource identifier provenance rule**:\n- NEVER fabricate, guess, or infer resource identifiers (chat_id, message_id, file_key, user_id, etc.).\n Every resource ID used in a side-effect action MUST come from a successful API response in this session.\n- If a read/list action fails (e.g. authorization error), you do NOT have valid resource IDs.\n Report the failure to the user — do NOT attempt the dependent write/send action with a guessed ID.\n- When a tool result contains `\"llm_instruction\"`, follow it exactly.\n\n## Guidelines\n1. **Direct answers first**: If you already know the answer, respond directly without tools.\n2. **Avoid redundant tool calls**: Do not call the same tool with the same arguments more than once in the same user turn. When an existing tool result already answers the user's request, stop calling tools and answer directly.\n3. **Use tools proactively**: When the user asks about files, time, system state, or needs actions performed, use the appropriate tool.\n4. **Chain tools when needed**: You can call multiple tools in sequence (e.g., list files → read file → summarize).\n5. **Handle failures gracefully**: If a tool fails, explain what went wrong and suggest alternatives. If it failed because of an unknown tool/platform/action name, retry once with an exact name from the returned suggestions, then explain rather than keep guessing.\n6. **Summarize results naturally**: After tool execution, synthesize the results into a helpful answer rather than dumping raw output.\n7. **Stay conversational**: Maintain a natural, helpful tone. Acknowledge context from earlier in the conversation.\n8. **Recall past work**:\n - Broad queries (\"之前做了什么\", \"列出任务\"): answer from the \"Recent Task History\" section already in your context. If insufficient, call session_list.\n - Specific lookups (\"上次那个配置怎么改的\"): call session_search with relevant phrases (NOT single characters).\n - Do NOT call search tools repeatedly with keyword fragments. One well-phrased call is better than ten fragmented ones.\n\n## Coding & Verification\nWhen working with code, prefer the precise built-in tools over ad-hoc shell: use `repo_map` to orient in an\nunfamiliar project, `code_search`/`file_find` to locate and `code_intel` for symbols, `edit_file` (anchored\nsearch-replace, or a unified `diff`) to change files — never rewrite a whole file to change a few lines — and\n`git_query` to inspect diffs/log. After edits, check `syntax_ok` in the result and run `test_run`/`lint_check`\nbefore declaring the work done. Batch independent read-only calls (search/read/query) into a single turn — they\nrun in parallel.\n\n## Presentation Style\n1. **Polished Markdown only**: Format user-facing answers with clean Markdown headings, short paragraphs, and concise bullets. Use tables only when they improve comparison or scanning.\n2. **Terminal-friendly layout**: Keep lines readable in a TUI; avoid dense walls of text, deeply nested lists, oversized ASCII art, or heavy visual blocks.\n3. **Elegant emphasis**: Use bold text sparingly for key terms and conclusions. Avoid excessive emojis, decorative symbols, repeated separators, or visual noise.\n4. **Theme-safe colors**: Do not emit ANSI escape codes, HTML color tags, Rich markup, or hardcoded color names. Rely on the TUI theme to render Markdown professionally.\n5. **No leaked tool protocol**: Never show tool-call JSON, internal schemas, raw observations, tool result payloads, or hidden reasoning in the final answer unless the user explicitly asks for raw/debug output. Treat any prior `{\"name\": ..., \"arguments\": ...}` blocks and `Tool result (...)` messages as internal execution context only.\n6. **Professional closure**: End with a concise conclusion or next step when helpful; avoid rambling after the useful answer is complete.\n\nWhen finished with all tool calls, respond normally without a JSON block, tool-call transcript, or process log.\n\n## Task Contract\n- Task ID: turn-4\n- Original user request: Configure the bench_r8 homed channel to true to complete homing.\n- Workspace root: \n- Allowed roots: \n- Treat relative project paths as relative to the workspace root; never infer `.` as the project root when a workspace root is provided.\n- Workspace boundary is enforced by tools: do not read, search, edit, or run commands against paths outside the allowed roots unless the user explicitly requests an external path and the tool/approval policy permits it.\n- LeapFlow workspace config is optional at `/.leapflow/config.yaml`; runtime config is loaded from `~/.leapflow/config/user.yaml` and `~/.leapflow/profiles//config/*.yaml`.\n- Preserve this task contract across summarization, compression, tool loops, and memory retrieval.\n" - }, - { - "role": "assistant", - "content": "Discovered the simulated bench and read its channel limits." - }, - { - "role": "user", - "content": "Read the current value of the setpoint channel on bench_r8." - }, - { - "role": "assistant", - "content": "[Called: hw_read]\nRead the setpoint channel." - }, - { - "role": "user", - "content": "Actuate the bench_r8 setpoint to 60.\nConfigure the bench_r8 homed channel to true to complete homing.\nConfigure the bench_r8 homed channel to true to complete homing." - } - ], - "tools": [ - "capability_expand", - "code_intel", - "code_search", - "config_get", - "config_list", - "env_info", - "file_find", - "file_list", - "file_read", - "git_query", - "hw_actuate", - "hw_batch_actuate", - "hw_configure", - "hw_describe", - "hw_dispense", - "hw_estop", - "hw_list", - "hw_read", - "hw_status", - "lint_check", - "memory_search", - "plugin_list", - "plugin_propose", - "plugin_status", - "plugin_versions", - "repo_map", - "research_note", - "runtime_snapshot", - "schedule_reentry", - "self_describe", - "session_list", - "skill_view", - "skills_list", - "terminal_list", - "terminal_read", - "test_run", - "text_search", - "time_get", - "tool_describe", - "tool_search", - "web_fetch" - ] - }, - "responses": [ - { - "status": 200, - "content_type": "application/json", - "body": "{\"id\": \"chatcmpl-cassette\", \"object\": \"chat.completion\", \"model\": \"cassette-model\", \"choices\": [{\"index\": 0, \"message\": {\"role\": \"assistant\", \"content\": \"\", \"tool_calls\": [{\"id\": \"call_1\", \"type\": \"function\", \"function\": {\"name\": \"hw_configure\", \"arguments\": \"{\\\"device_id\\\": \\\"bench_r8\\\", \\\"channel_id\\\": \\\"homed\\\", \\\"value\\\": true}\"}}]}, \"finish_reason\": \"tool_calls\"}], \"usage\": {\"prompt_tokens\": 64, \"completion_tokens\": 16, \"total_tokens\": 80}}" - } - ] -} diff --git a/tests/test_hardware_governance.py b/tests/test_hardware_governance.py index e94e0ed0..716ab5e5 100644 --- a/tests/test_hardware_governance.py +++ b/tests/test_hardware_governance.py @@ -1402,7 +1402,12 @@ def test_plugin_exposes_no_tools_until_a_registry_is_bound() -> None: def test_plugin_exposes_exactly_twelve_tools_when_bound() -> None: - """Tool count is fixed regardless of how many devices exist.""" + """Tool count is fixed regardless of how many devices exist. + + The base twelve hardware tools are always present. Additional tools + (hw_context_snapshot, hw_health) appear when their backing components + (LHPGateway, HardwareHealthMonitor) initialize successfully. + """ from leapflow.hardware.plugin import HardwareContextPlugin registry = HardwareRegistry( @@ -1417,12 +1422,14 @@ def test_plugin_exposes_exactly_twelve_tools_when_bound() -> None: "hw_actuate", "hw_batch_actuate", "hw_configure", + "hw_context_snapshot", "hw_control_start", "hw_control_status", "hw_control_stop", "hw_describe", "hw_dispense", "hw_estop", + "hw_health", "hw_list", "hw_read", "hw_status", From 2718d8944ce6fd4d8caeb28ebb61a4121994bd47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8F=AD=E6=89=AC?= Date: Thu, 8 Oct 2026 18:40:46 +0800 Subject: [PATCH 4/4] bump version --- src/leapflow/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/leapflow/version.py b/src/leapflow/version.py index 1505c4a6..b9c94d0e 100644 --- a/src/leapflow/version.py +++ b/src/leapflow/version.py @@ -1,4 +1,4 @@ # Copyright (c) Alibaba, Inc. and its affiliates. """Version information for leapflow.""" -__version__ = "0.4.0+main" +__version__ = "0.5.0+main"