machinaos 0.0.76 → 0.0.78
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +143 -107
- package/client/dist/assets/ActionBar-Du2MSFSz.js +1 -0
- package/client/dist/assets/ApiKeyInput-k2LBmBjb.js +1 -0
- package/client/dist/assets/ApiKeyPanel-C_bV9U0X.js +1 -0
- package/client/dist/assets/ApiUsageSection-CmVfwZzL.js +1 -0
- package/client/dist/assets/EmailPanel-CeKIMGu-.js +1 -0
- package/client/dist/assets/OAuthPanel-KA3t3Q2K.js +1 -0
- package/client/dist/assets/QrPairingPanel-NgNpJNuk.js +1 -0
- package/client/dist/assets/RateLimitSection-Du5YNVIA.js +1 -0
- package/client/dist/assets/StatusCard-DNLyayXc.js +1 -0
- package/client/dist/assets/index-DQ0nwhec.js +257 -0
- package/client/dist/assets/index-DxmbVskS.css +1 -0
- package/client/dist/assets/vendor-flow-CZmBvHRo.js +1 -0
- package/client/dist/assets/vendor-icons-CVrPjN2Q.js +22 -0
- package/client/dist/assets/vendor-markdown-CRou3yQ5.js +62 -0
- package/client/dist/assets/vendor-misc-C4VxKHs5.js +1 -0
- package/client/dist/assets/vendor-query-SzWcOU0G.js +1 -0
- package/client/dist/assets/vendor-radix-Dnos29jG.js +56 -0
- package/client/dist/assets/vendor-react-DvWIbVx0.js +1 -0
- package/client/dist/index.html +37 -3
- package/client/index.html +28 -1
- package/client/package.json +44 -40
- package/client/src/App.tsx +2 -0
- package/client/src/Dashboard.tsx +157 -45
- package/client/src/ParameterPanel.tsx +3 -5
- package/client/src/adapters/nodeSpecToDescription.ts +1 -0
- package/client/src/assets/icons/NodeIcon.tsx +32 -0
- package/client/src/assets/icons/index.ts +4 -0
- package/client/src/assets/icons/stripe.svg +1 -0
- package/client/src/assets/icons/themedGlyphs.ts +404 -0
- package/client/src/components/AIAgentNode.tsx +77 -53
- package/client/src/components/GenericNode.tsx +34 -52
- package/client/src/components/OutputPanel.tsx +64 -147
- package/client/src/components/ParameterRenderer.tsx +5 -3
- package/client/src/components/SkillEditorModal.tsx +9 -18
- package/client/src/components/SquareNode.tsx +97 -115
- package/client/src/components/StartNode.tsx +32 -42
- package/client/src/components/SvgFilterDefs.tsx +54 -0
- package/client/src/components/TeamMonitorNode.tsx +12 -14
- package/client/src/components/ToolkitNode.tsx +35 -60
- package/client/src/components/TriggerNode.tsx +43 -77
- package/client/src/components/__tests__/CredentialsModal.test.tsx +49 -45
- package/client/src/components/credentials/CredentialsModal.tsx +98 -30
- package/client/src/components/credentials/CredentialsPalette.tsx +73 -5
- package/client/src/components/credentials/catalogueAdapter.ts +17 -1
- package/client/src/components/credentials/panels/ApiKeyPanel.tsx +102 -37
- package/client/src/components/credentials/panels/EmailPanel.tsx +7 -19
- package/client/src/components/credentials/panels/OAuthPanel.tsx +5 -1
- package/client/src/components/credentials/panels/QrPairingPanel.tsx +1 -3
- package/client/src/components/credentials/primitives/ActionBar.tsx +7 -11
- package/client/src/components/credentials/primitives/OAuthConnect.tsx +19 -28
- package/client/src/components/credentials/sections/ProviderDefaultsSection.tsx +24 -3
- package/client/src/components/credentials/types.ts +12 -2
- package/client/src/components/credentials/useCredentialPanel.ts +43 -19
- package/client/src/components/icons/AIProviderIcons.tsx +16 -0
- package/client/src/components/onboarding/OnboardingWizard.tsx +23 -63
- package/client/src/components/onboarding/nodeRoleClasses.ts +23 -0
- package/client/src/components/onboarding/steps/CanvasStep.tsx +15 -21
- package/client/src/components/onboarding/steps/ConceptsStep.tsx +2 -11
- package/client/src/components/onboarding/steps/GetStartedStep.tsx +2 -10
- package/client/src/components/parameterPanel/InputSection.tsx +9 -7
- package/client/src/components/parameterPanel/MasterSkillEditor.tsx +84 -198
- package/client/src/components/parameterPanel/MiddleSection.tsx +57 -80
- package/client/src/components/parameterPanel/ToolSchemaEditor.tsx +31 -25
- package/client/src/components/parameterPanel/__tests__/InputSection.test.tsx +7 -2
- package/client/src/components/ui/AIResultModal.tsx +1 -1
- package/client/src/components/ui/CollapsibleSection.tsx +9 -5
- package/client/src/components/ui/CommandPalette.tsx +147 -0
- package/client/src/components/ui/CommandPaletteHost.tsx +189 -0
- package/client/src/components/ui/ComponentItem.tsx +13 -7
- package/client/src/components/ui/ComponentPalette.tsx +24 -13
- package/client/src/components/ui/ConsolePanel.tsx +19 -11
- package/client/src/components/ui/DropCap.tsx +28 -0
- package/client/src/components/ui/EditableNodeLabel.tsx +10 -2
- package/client/src/components/ui/InputNodesPanel.tsx +1 -1
- package/client/src/components/ui/Modal.tsx +38 -6
- package/client/src/components/ui/OutputDisplayPanel.tsx +1 -1
- package/client/src/components/ui/SettingsPanel.tsx +42 -13
- package/client/src/components/ui/StatusBar.tsx +108 -0
- package/client/src/components/ui/ThemeSwitcher.tsx +109 -0
- package/client/src/components/ui/TopToolbar.tsx +42 -25
- package/client/src/components/ui/WorkflowSidebar.tsx +32 -16
- package/client/src/components/ui/action-button.tsx +40 -15
- package/client/src/components/ui/button.tsx +24 -1
- package/client/src/components/ui/dropdown-menu.tsx +24 -2
- package/client/src/components/ui/input.tsx +19 -2
- package/client/src/components/ui/select.tsx +15 -0
- package/client/src/components/ui/textarea.tsx +15 -2
- package/client/src/contexts/AuthContext.tsx +148 -109
- package/client/src/contexts/ThemeContext.tsx +93 -17
- package/client/src/contexts/WebSocketContext.tsx +373 -206
- package/client/src/contexts/__tests__/AuthContext.test.tsx +221 -0
- package/client/src/hooks/__tests__/useDragVariable.test.ts +7 -1
- package/client/src/hooks/__tests__/useWorkflowOpsListener.test.ts +142 -0
- package/client/src/hooks/useAppTheme.ts +209 -7
- package/client/src/hooks/useAutoSkillEdges.ts +7 -2
- package/client/src/hooks/useCatalogueQuery.ts +67 -1
- package/client/src/hooks/useDragVariable.ts +1 -1
- package/client/src/hooks/useNodeAllowlist.ts +115 -8
- package/client/src/hooks/useOnboarding.ts +20 -8
- package/client/src/hooks/useParameterPanel.ts +2 -1
- package/client/src/hooks/useReactFlowNodes.ts +2 -1
- package/client/src/hooks/useSound.ts +185 -0
- package/client/src/hooks/useWorkflowManagement.ts +6 -8
- package/client/src/hooks/useWorkflowOpsListener.ts +90 -0
- package/client/src/index.css +65 -3
- package/client/src/lib/__tests__/connectionConfig.test.ts +91 -0
- package/client/src/lib/aiModelProviders.ts +8 -0
- package/client/src/lib/connectionConfig.ts +107 -0
- package/client/src/lib/queryPersist.ts +13 -5
- package/client/src/lib/sound.ts +393 -0
- package/client/src/main.tsx +20 -0
- package/client/src/store/useAppStore.ts +26 -0
- package/client/src/styles/canvasAnimations.ts +37 -36
- package/client/src/styles/theme.ts +36 -20
- package/client/src/test/setup.ts +1 -0
- package/client/src/themes/atomic.css +253 -0
- package/client/src/themes/base.css +373 -0
- package/client/src/themes/cyber.css +890 -0
- package/client/src/themes/dark.css +70 -0
- package/client/src/themes/edo.css +246 -0
- package/client/src/themes/greek.css +293 -0
- package/client/src/themes/light.css +78 -0
- package/client/src/themes/plague.css +253 -0
- package/client/src/themes/renaissance.css +727 -0
- package/client/src/themes/rot.css +249 -0
- package/client/src/themes/steampunk.css +272 -0
- package/client/src/themes/surveillance.css +289 -0
- package/client/src/themes/wasteland.css +250 -0
- package/client/src/types/INodeProperties.ts +5 -0
- package/client/src/types/NodeTypes.ts +11 -1
- package/client/src/types/__tests__/cloudEvents.test.ts +99 -0
- package/client/src/types/cloudEvents.ts +78 -0
- package/client/src/vite-env.d.ts +7 -0
- package/client/tsconfig.json +1 -1
- package/client/vite.config.js +62 -2
- package/install.ps1 +1 -1
- package/install.sh +1 -1
- package/machina/commands/build.py +51 -7
- package/machina/pyproject.toml +4 -0
- package/machina/supervisor.py +12 -2
- package/machina/tree.py +71 -21
- package/package.json +4 -4
- package/scripts/install.js +16 -1
- package/server/config/ai_cli_providers.json +54 -0
- package/server/config/credential_providers.json +109 -2
- package/server/config/llm_defaults.json +24 -0
- package/server/config/model_registry.json +338 -499
- package/server/config/node_allowlist.json +16 -1
- package/server/config/pricing.json +8 -0
- package/server/constants.py +38 -15
- package/server/core/container.py +2 -2
- package/server/core/credentials_database.py +35 -2
- package/server/core/logging.py +4 -3
- package/server/main.py +99 -13
- package/server/models/node_metadata.py +1 -0
- package/server/nodejs/package.json +8 -6
- package/server/nodejs/src/index.ts +22 -5
- package/server/nodes/README.md +31 -4
- package/server/nodes/agent/_inline.py +2 -0
- package/server/nodes/agent/_specialized.py +6 -3
- package/server/nodes/agent/ai_agent.py +13 -3
- package/server/nodes/agent/chat_agent.py +6 -3
- package/server/nodes/agent/claude_code_agent.py +287 -75
- package/server/nodes/agent/codex_agent.py +239 -0
- package/server/nodes/agent/deep_agent.py +3 -3
- package/server/nodes/agent/rlm_agent.py +3 -3
- package/server/nodes/android/__init__.py +31 -1
- package/server/nodes/android/_base.py +9 -5
- package/server/{services/android_service.py → nodes/android/_dispatcher.py} +2 -2
- package/server/nodes/android/_handlers.py +154 -0
- package/server/nodes/android/_option_loaders.py +44 -0
- package/server/nodes/android/_refresh.py +127 -0
- package/server/{services/android → nodes/android/_relay}/client.py +4 -4
- package/server/{routers/android.py → nodes/android/_router.py} +27 -8
- package/server/nodes/browser/browser.py +2 -2
- package/server/nodes/code/_base.py +6 -2
- package/server/nodes/code/_claude_code.py +134 -0
- package/server/nodes/document/embedding_generator.py +3 -3
- package/server/nodes/document/http_scraper.py +3 -3
- package/server/nodes/document/vector_store.py +5 -5
- package/server/nodes/email/__init__.py +11 -1
- package/server/nodes/email/_filters.py +21 -0
- package/server/{services/himalaya_service.py → nodes/email/_himalaya.py} +6 -10
- package/server/{services/email_service.py → nodes/email/_service.py} +9 -13
- package/server/nodes/email/email_read.py +1 -1
- package/server/nodes/email/email_receive.py +54 -5
- package/server/nodes/email/email_send.py +1 -1
- package/server/nodes/filesystem/shell.py +24 -1
- package/server/nodes/google/__init__.py +55 -1
- package/server/{services/handlers/google_auth.py → nodes/google/_auth_helper.py} +8 -5
- package/server/nodes/google/_base.py +2 -2
- package/server/nodes/google/_credentials.py +5 -5
- package/server/nodes/google/_filters.py +25 -0
- package/server/nodes/google/_handlers.py +57 -0
- package/server/{services/google_oauth.py → nodes/google/_oauth.py} +195 -162
- package/server/nodes/google/_option_loaders.py +107 -0
- package/server/nodes/google/_refresh.py +66 -0
- package/server/nodes/google/_router.py +131 -0
- package/server/nodes/google/gmail_receive.py +41 -4
- package/server/nodes/groups.py +1 -0
- package/server/nodes/location/_credentials.py +45 -1
- package/server/{services/maps.py → nodes/location/_service.py} +18 -3
- package/server/nodes/location/gmaps_create.py +4 -4
- package/server/nodes/location/gmaps_locations.py +4 -4
- package/server/nodes/location/gmaps_nearby_places.py +4 -4
- package/server/nodes/model/_base.py +8 -3
- package/server/nodes/model/_credentials.py +96 -8
- package/server/nodes/model/_local_validator.py +345 -0
- package/server/nodes/model/lmstudio_chat_model.py +23 -0
- package/server/nodes/model/ollama_chat_model.py +25 -0
- package/server/nodes/proxy/_usage.py +2 -2
- package/server/nodes/proxy/proxy_config.py +14 -14
- package/server/nodes/proxy/proxy_request.py +4 -4
- package/server/nodes/scraper/_credentials.py +29 -1
- package/server/nodes/scraper/apify_actor.py +9 -9
- package/server/nodes/scraper/crawlee_scraper.py +5 -5
- package/server/nodes/search/brave_search.py +4 -0
- package/server/nodes/search/perplexity_search.py +9 -0
- package/server/nodes/search/serper_search.py +3 -0
- package/server/nodes/skill/simple_memory.py +12 -0
- package/server/nodes/social/_base.py +2 -2
- package/server/nodes/stripe/__init__.py +46 -0
- package/server/nodes/stripe/_credentials.py +33 -0
- package/server/nodes/stripe/_handlers.py +270 -0
- package/server/nodes/stripe/_install.py +127 -0
- package/server/nodes/stripe/_source.py +174 -0
- package/server/nodes/stripe/stripe_action.py +81 -0
- package/server/nodes/stripe/stripe_receive.py +92 -0
- package/server/nodes/telegram/_credentials.py +52 -1
- package/server/nodes/telegram/_handlers.py +19 -18
- package/server/nodes/telegram/_service.py +134 -32
- package/server/nodes/telegram/telegram_send.py +5 -6
- package/server/nodes/text/file_handler.py +2 -2
- package/server/nodes/text/text_generator.py +2 -2
- package/server/nodes/tool/agent_builder.py +630 -0
- package/server/nodes/tool/task_manager.py +144 -2
- package/server/nodes/twitter/__init__.py +38 -1
- package/server/nodes/twitter/_base.py +7 -7
- package/server/nodes/twitter/_credentials.py +1 -1
- package/server/nodes/twitter/_filters.py +37 -0
- package/server/nodes/twitter/_handlers.py +77 -0
- package/server/nodes/twitter/_oauth.py +124 -0
- package/server/nodes/twitter/_refresh.py +78 -0
- package/server/nodes/twitter/_router.py +29 -0
- package/server/nodes/twitter/twitter_receive.py +4 -0
- package/server/nodes/visuals.json +64 -19
- package/server/nodes/whatsapp/__init__.py +45 -5
- package/server/nodes/whatsapp/_base.py +3 -3
- package/server/nodes/whatsapp/_filters.py +137 -0
- package/server/nodes/whatsapp/_handlers.py +167 -0
- package/server/nodes/whatsapp/_option_loaders.py +68 -0
- package/server/nodes/whatsapp/_refresh.py +62 -0
- package/server/nodes/whatsapp/_runtime.py +1 -1
- package/server/pyproject.toml +29 -7
- package/server/routers/schemas.py +2 -2
- package/server/routers/webhook.py +26 -9
- package/server/routers/websocket.py +149 -810
- package/server/services/ai.py +89 -8
- package/server/services/auth.py +220 -43
- package/server/services/claude_oauth.py +126 -100
- package/server/services/cli_agent/__init__.py +78 -0
- package/server/services/cli_agent/_handlers.py +237 -0
- package/server/services/cli_agent/config.py +112 -0
- package/server/services/cli_agent/factory.py +48 -0
- package/server/services/cli_agent/lockfile.py +141 -0
- package/server/services/cli_agent/mcp_server.py +482 -0
- package/server/services/cli_agent/protocol.py +173 -0
- package/server/services/cli_agent/providers/__init__.py +9 -0
- package/server/services/cli_agent/providers/anthropic_claude.py +419 -0
- package/server/services/cli_agent/providers/google_gemini.py +80 -0
- package/server/services/cli_agent/providers/openai_codex.py +310 -0
- package/server/services/cli_agent/service.py +607 -0
- package/server/services/cli_agent/session.py +618 -0
- package/server/services/cli_agent/types.py +227 -0
- package/server/services/cli_agent/workflow_tools.py +233 -0
- package/server/services/credential_registry.py +26 -1
- package/server/services/deployment/manager.py +26 -145
- package/server/services/deployment/poll_registry.py +59 -0
- package/server/services/event_waiter.py +76 -246
- package/server/services/events/__init__.py +54 -0
- package/server/services/events/cli.py +78 -0
- package/server/services/events/daemon.py +163 -0
- package/server/services/events/envelope.py +281 -0
- package/server/services/events/lifecycle.py +99 -0
- package/server/services/events/oauth_lifecycle.py +534 -0
- package/server/services/events/polling.py +60 -0
- package/server/services/events/push.py +36 -0
- package/server/services/events/source.py +63 -0
- package/server/services/events/triggers.py +118 -0
- package/server/services/events/verifiers/__init__.py +25 -0
- package/server/services/events/verifiers/base.py +28 -0
- package/server/services/events/verifiers/github.py +25 -0
- package/server/services/events/verifiers/hmac_basic.py +32 -0
- package/server/services/events/verifiers/standard_webhooks.py +47 -0
- package/server/services/events/verifiers/stripe.py +42 -0
- package/server/services/events/webhook.py +105 -0
- package/server/services/handlers/tools.py +28 -186
- package/server/services/llm/config.py +7 -0
- package/server/services/llm/factory.py +8 -2
- package/server/services/memory/__init__.py +52 -0
- package/server/services/memory/jsonl.py +80 -0
- package/server/services/memory/markdown.py +65 -0
- package/server/services/memory/state.py +112 -0
- package/server/services/memory/vector_store.py +40 -0
- package/server/services/model_registry.py +76 -0
- package/server/services/node_allowlist.py +71 -15
- package/server/services/node_executor.py +2 -2
- package/server/services/node_output_schemas.py +21 -10
- package/server/services/node_spec.py +1 -1
- package/server/services/oauth_utils.py +1 -1
- package/server/services/plugin/__init__.py +2 -0
- package/server/services/plugin/base.py +44 -2
- package/server/services/plugin/credential.py +288 -1
- package/server/services/plugin/deps.py +105 -0
- package/server/services/plugin/edge_walker.py +12 -4
- package/server/services/plugin/oauth.py +381 -0
- package/server/services/plugin/polling.py +247 -0
- package/server/services/plugin/registry.py +145 -0
- package/server/services/plugin/singleton.py +65 -0
- package/server/services/plugin/ws.py +81 -0
- package/server/services/process_service.py +31 -2
- package/server/services/status_broadcaster.py +155 -238
- package/server/services/temporal/workflow.py +7 -7
- package/server/services/workflow.py +21 -3
- package/server/services/ws_handler_registry.py +111 -28
- package/server/skills/GUIDE.md +16 -1
- package/server/skills/assistant/agent-builder-skill/SKILL.md +166 -0
- package/server/skills/payments_agent/stripe-skill/SKILL.md +306 -0
- package/server/tests/credentials/test_auth_service.py +16 -9
- package/server/tests/credentials/test_credential_broadcasts.py +219 -0
- package/server/tests/credentials/test_google_oauth.py +6 -6
- package/server/tests/credentials/test_oauth_utils.py +1 -1
- package/server/tests/credentials/test_twitter_oauth.py +2 -2
- package/server/tests/credentials/test_websocket_handlers.py +44 -20
- package/server/tests/llm/test_factory.py +1 -0
- package/server/tests/llm/test_wiring.py +5 -1
- package/server/tests/nodes/_compat.py +24 -24
- package/server/tests/nodes/test_agent_builder.py +439 -0
- package/server/tests/nodes/test_ai_tools.py +18 -14
- package/server/tests/nodes/test_code_fs_process.py +17 -8
- package/server/tests/nodes/test_email.py +10 -9
- package/server/tests/nodes/test_google_workspace.py +2 -2
- package/server/tests/nodes/test_specialized_agents.py +100 -53
- package/server/tests/nodes/test_stripe_plugin.py +293 -0
- package/server/tests/nodes/test_telegram_social.py +4 -4
- package/server/tests/nodes/test_twitter.py +1 -1
- package/server/tests/nodes/test_web_automation.py +2 -2
- package/server/tests/nodes/test_whatsapp.py +9 -9
- package/server/tests/services/cli_agent/__init__.py +0 -0
- package/server/tests/services/cli_agent/test_mcp_server.py +432 -0
- package/server/tests/services/cli_agent/test_providers.py +358 -0
- package/server/tests/services/cli_agent/test_service.py +298 -0
- package/server/tests/services/memory/__init__.py +0 -0
- package/server/tests/services/memory/test_jsonl.py +188 -0
- package/server/tests/services/test_events.py +333 -0
- package/server/tests/test_node_spec.py +56 -16
- package/server/tests/test_plugin_helpers.py +116 -0
- package/server/tests/test_plugin_self_containment.py +486 -0
- package/server/tests/test_status_broadcasts.py +425 -0
- package/workflows/{AI Assistant_workflow-1777421105154-0m4snkzjf.json → AI Assistant_workflow-1778504793388-ou1m1tz2x.json } +70 -266
- package/workflows/{AI Employee_workflow-1777720598005-u4cm858dv.json → AI Employee_example_workflow-1777720598005-u4cm858dv.json } +112 -112
- package/workflows/Claude Assistant_workflow-1778380124051-mdibn807c.json +709 -0
- package/client/dist/assets/ActionBar-vzPpSR77.js +0 -1
- package/client/dist/assets/ApiKeyInput-Ds7AKFe8.js +0 -1
- package/client/dist/assets/ApiKeyPanel-gfblELep.js +0 -1
- package/client/dist/assets/ApiUsageSection-BMNWTe2r.js +0 -1
- package/client/dist/assets/EmailPanel-B1Om64p5.js +0 -1
- package/client/dist/assets/OAuthPanel-CXyQYGBz.js +0 -1
- package/client/dist/assets/QrPairingPanel-BgNuI1we.js +0 -1
- package/client/dist/assets/RateLimitSection-YYK8sx1T.js +0 -1
- package/client/dist/assets/StatusCard-DuYA5hJR.js +0 -1
- package/client/dist/assets/index-D9tZfgvi.js +0 -363
- package/client/dist/assets/index-al7snTkG.css +0 -1
- package/client/src/components/credentials/providers.tsx +0 -177
- package/server/routers/google.py +0 -277
- package/server/routers/maps.py +0 -142
- package/server/routers/twitter.py +0 -365
- package/server/services/claude_code_service.py +0 -106
- package/server/services/memory.py +0 -159
- package/server/services/node_option_loaders/__init__.py +0 -77
- package/server/services/node_option_loaders/android_loaders.py +0 -55
- package/server/services/node_option_loaders/google_loaders.py +0 -97
- package/server/services/node_option_loaders/whatsapp_loaders.py +0 -69
- package/server/services/twitter_oauth.py +0 -411
- package/server/services/websocket_client.py +0 -29
- /package/server/{services/android → nodes/android/_relay}/__init__.py +0 -0
- /package/server/{services/android → nodes/android/_relay}/broadcaster.py +0 -0
- /package/server/{services/android → nodes/android/_relay}/manager.py +0 -0
- /package/server/{services/android → nodes/android/_relay}/protocol.py +0 -0
- /package/server/{services/browser_service.py → nodes/browser/_service.py} +0 -0
- /package/server/{services/whatsapp_service.py → nodes/whatsapp/_service.py} +0 -0
- /package/server/skills/{task_agent → assistant}/write-todos-skill/SKILL.md +0 -0
|
@@ -0,0 +1,618 @@
|
|
|
1
|
+
"""One CLI session per task — `BaseProcessSupervisor` subclass.
|
|
2
|
+
|
|
3
|
+
Each session is bound to:
|
|
4
|
+
- one provider (Claude or Codex)
|
|
5
|
+
- one task spec (`ClaudeTaskSpec` / `CodexTaskSpec`)
|
|
6
|
+
- one git worktree (created in `_pre_spawn`, removed in `cleanup`)
|
|
7
|
+
- one IDE lockfile (written in `_pre_spawn` when the provider supports it)
|
|
8
|
+
|
|
9
|
+
Inherits `BaseProcessSupervisor`'s locked idempotent start/stop, recursive
|
|
10
|
+
`kill_tree`, `terminate_then_kill(5s)` grace, and Windows
|
|
11
|
+
`CTRL_BREAK_EVENT` path. We override `_do_start` to wire NDJSON consumers
|
|
12
|
+
instead of the parent's generic `drain_stream(logger.info)`.
|
|
13
|
+
|
|
14
|
+
Sessions are NOT registered in the global supervisor registry — they're
|
|
15
|
+
owned by `AICliService.run_batch()` for the lifetime of one batch.
|
|
16
|
+
|
|
17
|
+
Liveness: relies on `wait_for_completion(timeout_seconds)` as the watchdog
|
|
18
|
+
and the per-broadcast Temporal heartbeat fired by every
|
|
19
|
+
`update_node_status()` call. We do not run our own per-second heartbeat
|
|
20
|
+
loop or write diagnostic dump files.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import asyncio
|
|
26
|
+
import json
|
|
27
|
+
import os
|
|
28
|
+
import shutil
|
|
29
|
+
import subprocess
|
|
30
|
+
import sys
|
|
31
|
+
import uuid
|
|
32
|
+
from pathlib import Path
|
|
33
|
+
from typing import Any, Dict, List, Optional
|
|
34
|
+
|
|
35
|
+
import anyio
|
|
36
|
+
import yaml
|
|
37
|
+
|
|
38
|
+
from core.logging import get_logger
|
|
39
|
+
from services._supervisor.process import BaseProcessSupervisor
|
|
40
|
+
from services.cli_agent.lockfile import remove_ide_lockfile, write_ide_lockfile
|
|
41
|
+
from services.cli_agent.protocol import AICliProvider, CanonicalUsage, SessionResult
|
|
42
|
+
from services.cli_agent.types import BaseAICliTaskSpec
|
|
43
|
+
|
|
44
|
+
logger = get_logger(__name__)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class AICliSession(BaseProcessSupervisor):
|
|
48
|
+
"""One CLI subprocess for one task."""
|
|
49
|
+
|
|
50
|
+
pipe_streams = True
|
|
51
|
+
terminate_grace_seconds = 5.0
|
|
52
|
+
graceful_shutdown = sys.platform == "win32"
|
|
53
|
+
|
|
54
|
+
def __init__(
|
|
55
|
+
self,
|
|
56
|
+
*,
|
|
57
|
+
provider: AICliProvider,
|
|
58
|
+
task: BaseAICliTaskSpec,
|
|
59
|
+
repo_root: Path,
|
|
60
|
+
workspace_dir: Path,
|
|
61
|
+
node_id: str,
|
|
62
|
+
workflow_id: str,
|
|
63
|
+
broadcaster: Any,
|
|
64
|
+
defaults: Dict[str, Any],
|
|
65
|
+
mcp_port: int,
|
|
66
|
+
batch_token: str,
|
|
67
|
+
connected_tool_names: Optional[List[str]] = None,
|
|
68
|
+
connected_skill_names: Optional[List[str]] = None,
|
|
69
|
+
memory_bound: bool = False,
|
|
70
|
+
) -> None:
|
|
71
|
+
super().__init__()
|
|
72
|
+
self._provider = provider
|
|
73
|
+
self._task = task
|
|
74
|
+
self._task_id = task.task_id or f"t_{uuid.uuid4().hex[:8]}"
|
|
75
|
+
self._repo_root = Path(repo_root).resolve()
|
|
76
|
+
self._worktree_dir = (
|
|
77
|
+
Path(workspace_dir).resolve() / node_id / f"wt_{self._task_id}"
|
|
78
|
+
)
|
|
79
|
+
self._branch = task.branch or f"machina/{self._task_id}"
|
|
80
|
+
self._broadcaster = broadcaster
|
|
81
|
+
self._defaults = defaults
|
|
82
|
+
self._mcp_port = mcp_port
|
|
83
|
+
self._batch_token = batch_token
|
|
84
|
+
self._node_id = node_id
|
|
85
|
+
self._workflow_id = workflow_id
|
|
86
|
+
# Names of `mcp__machinaos__*` tools to add to `--allowedTools`.
|
|
87
|
+
self._connected_tool_names: List[str] = list(connected_tool_names or [])
|
|
88
|
+
# Skills to materialise into `<worktree>/.claude/skills/<name>/SKILL.md`
|
|
89
|
+
# so claude auto-discovers them per the documented project-scope
|
|
90
|
+
# path (https://code.claude.com/docs/en/skills#where-skills-live).
|
|
91
|
+
self._connected_skill_names: List[str] = list(connected_skill_names or [])
|
|
92
|
+
# Memory-bound runs use ``cwd=repo_root`` so claude's project_key
|
|
93
|
+
# (derived from cwd via `[^a-zA-Z0-9.-] -> -`) stays stable
|
|
94
|
+
# across spawns. With a stable project_key, ``--resume <UUID>``
|
|
95
|
+
# finds the prior session JSONL claude wrote on its previous
|
|
96
|
+
# turn under ``<CLAUDE_CONFIG_DIR>/projects/<key>/<UUID>.jsonl``.
|
|
97
|
+
# The per-task git worktree (random `wt_t_<rand>` suffix) is
|
|
98
|
+
# incompatible with this — every spawn would land under a
|
|
99
|
+
# brand-new project_key with no prior JSONL.
|
|
100
|
+
self._memory_bound: bool = bool(memory_bound)
|
|
101
|
+
|
|
102
|
+
# Streaming state
|
|
103
|
+
self._events: List[Dict[str, Any]] = []
|
|
104
|
+
self._stderr_lines: List[str] = []
|
|
105
|
+
self._exit_code: Optional[int] = None
|
|
106
|
+
self._lockfile_path: Optional[Path] = None
|
|
107
|
+
|
|
108
|
+
# ------------------------------------------------------------------
|
|
109
|
+
# Identity
|
|
110
|
+
# ------------------------------------------------------------------
|
|
111
|
+
|
|
112
|
+
@property
|
|
113
|
+
def label(self) -> str:
|
|
114
|
+
return f"AICliSession_{self._provider.name}_{self._task_id}"
|
|
115
|
+
|
|
116
|
+
@property
|
|
117
|
+
def task_id(self) -> str:
|
|
118
|
+
return self._task_id
|
|
119
|
+
|
|
120
|
+
@property
|
|
121
|
+
def branch(self) -> str:
|
|
122
|
+
return self._branch
|
|
123
|
+
|
|
124
|
+
@property
|
|
125
|
+
def worktree_dir(self) -> Path:
|
|
126
|
+
return self._worktree_dir
|
|
127
|
+
|
|
128
|
+
# ------------------------------------------------------------------
|
|
129
|
+
# BaseProcessSupervisor surface
|
|
130
|
+
# ------------------------------------------------------------------
|
|
131
|
+
|
|
132
|
+
def binary_path(self) -> Path:
|
|
133
|
+
return self._provider.binary_path()
|
|
134
|
+
|
|
135
|
+
def argv(self) -> List[str]:
|
|
136
|
+
return self._provider.headless_argv(
|
|
137
|
+
self._task,
|
|
138
|
+
defaults=self._defaults,
|
|
139
|
+
mcp_endpoint_url=self._mcp_endpoint_url(),
|
|
140
|
+
mcp_bearer_token=self._batch_token,
|
|
141
|
+
connected_tool_names=self._connected_tool_names,
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
def _mcp_endpoint_url(self) -> str:
|
|
145
|
+
"""Absolute URL of MachinaOs's FastMCP JSON-RPC endpoint.
|
|
146
|
+
|
|
147
|
+
Mirrors the ``url`` written into the IDE lockfile. FastMCP serves
|
|
148
|
+
at ``/mcp`` of the sub-app; ``main.py`` mounts it at ``/mcp/ide``;
|
|
149
|
+
the JSON-RPC URL is therefore ``/mcp/ide/mcp``."""
|
|
150
|
+
return f"http://127.0.0.1:{self._mcp_port}/mcp/ide/mcp"
|
|
151
|
+
|
|
152
|
+
def cwd(self) -> Optional[Path]:
|
|
153
|
+
# Memory-bound runs spawn directly under repo_root so claude's
|
|
154
|
+
# project_key stays constant and `--resume` finds the prior
|
|
155
|
+
# JSONL across runs. Non-memory runs use the per-task worktree.
|
|
156
|
+
if self._memory_bound:
|
|
157
|
+
return self._repo_root
|
|
158
|
+
return self._worktree_dir
|
|
159
|
+
|
|
160
|
+
def env(self) -> Dict[str, str]:
|
|
161
|
+
# Inherit parent env, then redirect provider config to MachinaOs's
|
|
162
|
+
# project-local isolation dir for providers that support it (Claude
|
|
163
|
+
# via CLAUDE_CONFIG_DIR; Codex/Gemini still use their native auth
|
|
164
|
+
# paths until isolation is wired). Without this, the agent's
|
|
165
|
+
# spawned CLI reads the user's personal `~/.claude/` credentials
|
|
166
|
+
# instead of the ones the credentials modal's Login button wrote.
|
|
167
|
+
e: Dict[str, str] = {**os.environ, "PYTHONUNBUFFERED": "1"}
|
|
168
|
+
if self._provider.name == "claude":
|
|
169
|
+
from services.claude_oauth import MACHINA_CLAUDE_DIR
|
|
170
|
+
e["CLAUDE_CONFIG_DIR"] = str(MACHINA_CLAUDE_DIR)
|
|
171
|
+
if self._lockfile_path and self._provider.ide_lock_env_var:
|
|
172
|
+
e[self._provider.ide_lock_env_var] = str(self._lockfile_path)
|
|
173
|
+
# Composio-style parent-run-ID for MCP correlation
|
|
174
|
+
e["MACHINA_PARENT_RUN_ID"] = (
|
|
175
|
+
f"{self._workflow_id}:{self._node_id}:{self._batch_token[:8]}"
|
|
176
|
+
)
|
|
177
|
+
return e
|
|
178
|
+
|
|
179
|
+
async def _pre_spawn(self) -> None:
|
|
180
|
+
"""Create the per-task git worktree (non-memory-bound runs only)
|
|
181
|
+
and (if supported) write the IDE lockfile. Failures abort
|
|
182
|
+
`_do_start` cleanly via RuntimeError."""
|
|
183
|
+
# 1. Per-task git worktree — skipped for memory-bound runs
|
|
184
|
+
# which use cwd=repo_root to keep claude's project_key stable.
|
|
185
|
+
if not self._memory_bound:
|
|
186
|
+
self._worktree_dir.parent.mkdir(parents=True, exist_ok=True)
|
|
187
|
+
wt_proc = await anyio.run_process(
|
|
188
|
+
[
|
|
189
|
+
"git", "-C", str(self._repo_root),
|
|
190
|
+
"worktree", "add",
|
|
191
|
+
str(self._worktree_dir),
|
|
192
|
+
"-b", self._branch,
|
|
193
|
+
],
|
|
194
|
+
check=False,
|
|
195
|
+
)
|
|
196
|
+
if wt_proc.returncode != 0:
|
|
197
|
+
err = (wt_proc.stderr or b"").decode(
|
|
198
|
+
"utf-8", errors="replace",
|
|
199
|
+
).strip()
|
|
200
|
+
raise RuntimeError(f"git worktree add failed: {err}")
|
|
201
|
+
else:
|
|
202
|
+
logger.info(
|
|
203
|
+
"[%s] memory-bound: skipping worktree, using cwd=%s",
|
|
204
|
+
self.label, self._repo_root,
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
# 2. IDE lockfile (VSCode pattern) — providers that support it.
|
|
208
|
+
# `workspace_dir` in the lockfile points at whatever cwd will
|
|
209
|
+
# actually be — repo_root for memory-bound runs, else worktree.
|
|
210
|
+
lockfile_workspace = (
|
|
211
|
+
self._repo_root if self._memory_bound else self._worktree_dir
|
|
212
|
+
)
|
|
213
|
+
if self._provider.supports("ide_lockfile") and self._provider.ide_lockfile_dir:
|
|
214
|
+
try:
|
|
215
|
+
self._lockfile_path = write_ide_lockfile(
|
|
216
|
+
ide_lockfile_dir=self._provider.ide_lockfile_dir,
|
|
217
|
+
pid=os.getpid(),
|
|
218
|
+
port=self._mcp_port,
|
|
219
|
+
token=self._batch_token,
|
|
220
|
+
workspace_dir=lockfile_workspace,
|
|
221
|
+
ide_name=self._provider.name,
|
|
222
|
+
)
|
|
223
|
+
except OSError as exc:
|
|
224
|
+
logger.warning(
|
|
225
|
+
"[%s] IDE lockfile write failed (%s) — continuing without MCP tools",
|
|
226
|
+
self.label, exc,
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
# 3. Materialise connected skills under cwd's `.claude/skills/`.
|
|
230
|
+
# cwd is repo_root for memory-bound, worktree otherwise — both
|
|
231
|
+
# are project-scope per the spec.
|
|
232
|
+
if self._connected_skill_names:
|
|
233
|
+
await self._materialise_skills()
|
|
234
|
+
|
|
235
|
+
async def _materialise_skills(self) -> None:
|
|
236
|
+
"""Write `<worktree>/.claude/skills/<name>/SKILL.md` for each
|
|
237
|
+
connected skill so the spawned claude can invoke them via the
|
|
238
|
+
built-in `Skill` tool. Filesystem skills are copied wholesale
|
|
239
|
+
(preserves `scripts/` + `references/`); DB skills are
|
|
240
|
+
reconstructed from frontmatter."""
|
|
241
|
+
from services.skill_loader import get_skill_loader
|
|
242
|
+
|
|
243
|
+
loader = get_skill_loader()
|
|
244
|
+
# cwd-relative — repo_root for memory-bound runs, worktree otherwise.
|
|
245
|
+
skills_dir = self.cwd() / ".claude" / "skills"
|
|
246
|
+
try:
|
|
247
|
+
skills_dir.mkdir(parents=True, exist_ok=True)
|
|
248
|
+
except OSError as exc:
|
|
249
|
+
logger.warning(
|
|
250
|
+
"[%s] cannot create skills dir %s: %s — skipping",
|
|
251
|
+
self.label, skills_dir, exc,
|
|
252
|
+
)
|
|
253
|
+
return
|
|
254
|
+
|
|
255
|
+
for name in self._connected_skill_names:
|
|
256
|
+
try:
|
|
257
|
+
skill = await loader.load_skill_async(name)
|
|
258
|
+
except Exception as exc:
|
|
259
|
+
logger.warning(
|
|
260
|
+
"[%s] load_skill_async(%r) failed: %s",
|
|
261
|
+
self.label, name, exc,
|
|
262
|
+
)
|
|
263
|
+
continue
|
|
264
|
+
if skill is None:
|
|
265
|
+
logger.warning(
|
|
266
|
+
"[%s] skill %r not found — skipping materialisation",
|
|
267
|
+
self.label, name,
|
|
268
|
+
)
|
|
269
|
+
continue
|
|
270
|
+
|
|
271
|
+
dest = skills_dir / name
|
|
272
|
+
try:
|
|
273
|
+
if skill.metadata.path is not None:
|
|
274
|
+
# Filesystem skill: copy whole directory tree.
|
|
275
|
+
shutil.copytree(
|
|
276
|
+
skill.metadata.path, dest, dirs_exist_ok=True,
|
|
277
|
+
)
|
|
278
|
+
else:
|
|
279
|
+
# DB skill: reconstruct frontmatter + body.
|
|
280
|
+
dest.mkdir(parents=True, exist_ok=True)
|
|
281
|
+
frontmatter = {
|
|
282
|
+
"name": skill.metadata.name,
|
|
283
|
+
"description": skill.metadata.description,
|
|
284
|
+
"allowed-tools": " ".join(skill.metadata.allowed_tools),
|
|
285
|
+
"metadata": skill.metadata.metadata,
|
|
286
|
+
}
|
|
287
|
+
body = (
|
|
288
|
+
f"---\n"
|
|
289
|
+
f"{yaml.safe_dump(frontmatter, sort_keys=False)}"
|
|
290
|
+
f"---\n\n"
|
|
291
|
+
f"{skill.instructions}"
|
|
292
|
+
)
|
|
293
|
+
(dest / "SKILL.md").write_text(body, encoding="utf-8")
|
|
294
|
+
logger.info(
|
|
295
|
+
"[CC-Agent _pre_spawn] materialised skill %r -> %s",
|
|
296
|
+
name, dest,
|
|
297
|
+
)
|
|
298
|
+
except OSError as exc:
|
|
299
|
+
logger.warning(
|
|
300
|
+
"[%s] failed to materialise skill %r at %s: %s",
|
|
301
|
+
self.label, name, dest, exc,
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
# ------------------------------------------------------------------
|
|
305
|
+
# _do_start: replace parent's drain tasks with NDJSON consumers
|
|
306
|
+
# ------------------------------------------------------------------
|
|
307
|
+
|
|
308
|
+
async def _do_start(self) -> None:
|
|
309
|
+
binary = self.binary_path()
|
|
310
|
+
if not binary.exists():
|
|
311
|
+
raise FileNotFoundError(f"{self.label} binary not found at {binary}")
|
|
312
|
+
|
|
313
|
+
await self._pre_spawn()
|
|
314
|
+
|
|
315
|
+
kwargs: Dict[str, Any] = {
|
|
316
|
+
"cwd": str(self.cwd()),
|
|
317
|
+
"env": self.env(),
|
|
318
|
+
# `claude -p` opens stdin to read piped input; we never pipe
|
|
319
|
+
# anything, so without DEVNULL the CLI waits 3s and prints
|
|
320
|
+
# "Warning: no stdin data received in 3s, proceeding without
|
|
321
|
+
# it." Tools come over MCP, not stdin — close the handle
|
|
322
|
+
# explicitly to skip the warning + the 3s stall.
|
|
323
|
+
"stdin": subprocess.DEVNULL,
|
|
324
|
+
"stdout": subprocess.PIPE,
|
|
325
|
+
"stderr": subprocess.PIPE,
|
|
326
|
+
}
|
|
327
|
+
if sys.platform == "win32" and self.graceful_shutdown:
|
|
328
|
+
kwargs["creationflags"] = subprocess.CREATE_NEW_PROCESS_GROUP # type: ignore[attr-defined]
|
|
329
|
+
|
|
330
|
+
self._proc = await anyio.open_process(self.argv(), **kwargs)
|
|
331
|
+
self._logger.info(
|
|
332
|
+
"[%s] spawned pid=%s task=%s branch=%s",
|
|
333
|
+
self.label, self._proc.pid, self._task_id, self._branch,
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
# NDJSON consumers replace the parent's `drain_stream(logger.info)`.
|
|
337
|
+
# We populate `self._drain_tasks` so the parent's `_do_stop`
|
|
338
|
+
# cancels them on stop.
|
|
339
|
+
self._drain_tasks = [
|
|
340
|
+
asyncio.create_task(self._consume_stdout(self._proc.stdout)),
|
|
341
|
+
asyncio.create_task(self._consume_stderr(self._proc.stderr)),
|
|
342
|
+
]
|
|
343
|
+
|
|
344
|
+
# ------------------------------------------------------------------
|
|
345
|
+
# Stream consumers
|
|
346
|
+
# ------------------------------------------------------------------
|
|
347
|
+
|
|
348
|
+
async def _consume_stdout(self, stream: Optional[anyio.abc.ByteReceiveStream]) -> None:
|
|
349
|
+
if stream is None:
|
|
350
|
+
return
|
|
351
|
+
buf = b""
|
|
352
|
+
try:
|
|
353
|
+
async for chunk in stream:
|
|
354
|
+
buf += chunk
|
|
355
|
+
while b"\n" in buf:
|
|
356
|
+
raw, buf = buf.split(b"\n", 1)
|
|
357
|
+
text = raw.decode("utf-8", errors="replace").strip()
|
|
358
|
+
if not text:
|
|
359
|
+
continue
|
|
360
|
+
event = self._provider.parse_event(text)
|
|
361
|
+
if event is None:
|
|
362
|
+
continue
|
|
363
|
+
self._events.append(event)
|
|
364
|
+
await self._on_event(event)
|
|
365
|
+
if buf:
|
|
366
|
+
text = buf.decode("utf-8", errors="replace").strip()
|
|
367
|
+
if text:
|
|
368
|
+
event = self._provider.parse_event(text)
|
|
369
|
+
if event is not None:
|
|
370
|
+
self._events.append(event)
|
|
371
|
+
await self._on_event(event)
|
|
372
|
+
except (anyio.ClosedResourceError, anyio.EndOfStream, asyncio.CancelledError):
|
|
373
|
+
pass
|
|
374
|
+
except Exception as exc: # pragma: no cover — defensive
|
|
375
|
+
self._logger.debug("[%s] stdout consumer ended: %s", self.label, exc)
|
|
376
|
+
|
|
377
|
+
async def _consume_stderr(self, stream: Optional[anyio.abc.ByteReceiveStream]) -> None:
|
|
378
|
+
if stream is None:
|
|
379
|
+
return
|
|
380
|
+
buf = b""
|
|
381
|
+
try:
|
|
382
|
+
async for chunk in stream:
|
|
383
|
+
buf += chunk
|
|
384
|
+
while b"\n" in buf:
|
|
385
|
+
raw, buf = buf.split(b"\n", 1)
|
|
386
|
+
text = raw.decode("utf-8", errors="replace").rstrip()
|
|
387
|
+
if not text:
|
|
388
|
+
continue
|
|
389
|
+
self._stderr_lines.append(text)
|
|
390
|
+
# Mirror to backend log too — without this the spawned
|
|
391
|
+
# CLI's MCP-discovery / auth-failure / debug output
|
|
392
|
+
# only reaches the Terminal panel, leaving the
|
|
393
|
+
# operator log blind during runtime debugging.
|
|
394
|
+
self._logger.info("[CC-Agent stderr] %s", text)
|
|
395
|
+
await self._safe_terminal_log(text, level="error")
|
|
396
|
+
if buf:
|
|
397
|
+
text = buf.decode("utf-8", errors="replace").rstrip()
|
|
398
|
+
if text:
|
|
399
|
+
self._stderr_lines.append(text)
|
|
400
|
+
self._logger.info("[CC-Agent stderr] %s", text)
|
|
401
|
+
await self._safe_terminal_log(text, level="error")
|
|
402
|
+
except (anyio.ClosedResourceError, anyio.EndOfStream, asyncio.CancelledError):
|
|
403
|
+
pass
|
|
404
|
+
except Exception as exc: # pragma: no cover
|
|
405
|
+
self._logger.debug("[%s] stderr consumer ended: %s", self.label, exc)
|
|
406
|
+
|
|
407
|
+
# ------------------------------------------------------------------
|
|
408
|
+
# Event dispatch (UI broadcasts)
|
|
409
|
+
# ------------------------------------------------------------------
|
|
410
|
+
|
|
411
|
+
async def _on_event(self, event: Dict[str, Any]) -> None:
|
|
412
|
+
# Tag every interesting event in the backend log so the operator
|
|
413
|
+
# can see what claude is actually doing — tool calls, assistant
|
|
414
|
+
# text, hook events. Without this the stream is invisible from
|
|
415
|
+
# the backend (only the Terminal panel saw it).
|
|
416
|
+
self._log_event_summary(event)
|
|
417
|
+
|
|
418
|
+
if self._provider.is_final_event(event):
|
|
419
|
+
payload = {
|
|
420
|
+
"phase": "ai_cli_subtask",
|
|
421
|
+
"task_id": self._task_id,
|
|
422
|
+
"provider": self._provider.name,
|
|
423
|
+
"status": "finalising",
|
|
424
|
+
}
|
|
425
|
+
for k in ("total_cost_usd", "duration_ms", "num_turns", "session_id"):
|
|
426
|
+
v = event.get(k)
|
|
427
|
+
if v is not None:
|
|
428
|
+
payload[k] = v
|
|
429
|
+
await self._safe_node_status("executing", payload)
|
|
430
|
+
else:
|
|
431
|
+
msg = (
|
|
432
|
+
event.get("message")
|
|
433
|
+
or event.get("text")
|
|
434
|
+
or event.get("delta")
|
|
435
|
+
or json.dumps(event)
|
|
436
|
+
)
|
|
437
|
+
text = msg if isinstance(msg, str) else json.dumps(msg)
|
|
438
|
+
await self._safe_terminal_log(text[:500], level="info")
|
|
439
|
+
|
|
440
|
+
def _log_event_summary(self, event: Dict[str, Any]) -> None:
|
|
441
|
+
"""One-line summary per claude stream event. Picks out the event
|
|
442
|
+
types that matter for tool-call debugging."""
|
|
443
|
+
etype = event.get("type", "?")
|
|
444
|
+
try:
|
|
445
|
+
if etype == "system" and event.get("subtype") == "init":
|
|
446
|
+
tools = event.get("tools") or []
|
|
447
|
+
mcp_servers = event.get("mcp_servers") or []
|
|
448
|
+
self._logger.info(
|
|
449
|
+
"[CC-Agent stream] system.init tools=%d (sample=%s) "
|
|
450
|
+
"mcp_servers=%s",
|
|
451
|
+
len(tools), tools[:8],
|
|
452
|
+
[s.get("name") for s in mcp_servers if isinstance(s, dict)],
|
|
453
|
+
)
|
|
454
|
+
elif etype == "assistant":
|
|
455
|
+
msg = event.get("message") or {}
|
|
456
|
+
content = msg.get("content") or []
|
|
457
|
+
tool_uses = [
|
|
458
|
+
c for c in content
|
|
459
|
+
if isinstance(c, dict) and c.get("type") == "tool_use"
|
|
460
|
+
]
|
|
461
|
+
texts = [
|
|
462
|
+
c.get("text", "") for c in content
|
|
463
|
+
if isinstance(c, dict) and c.get("type") == "text"
|
|
464
|
+
]
|
|
465
|
+
if tool_uses:
|
|
466
|
+
for tu in tool_uses:
|
|
467
|
+
self._logger.info(
|
|
468
|
+
"[CC-Agent stream] assistant->tool_use name=%s "
|
|
469
|
+
"input_keys=%s",
|
|
470
|
+
tu.get("name"),
|
|
471
|
+
list((tu.get("input") or {}).keys()),
|
|
472
|
+
)
|
|
473
|
+
elif texts:
|
|
474
|
+
sample = " ".join(t for t in texts if isinstance(t, str))[:300]
|
|
475
|
+
self._logger.info(
|
|
476
|
+
"[CC-Agent stream] assistant.text: %r", sample,
|
|
477
|
+
)
|
|
478
|
+
elif etype == "tool_use":
|
|
479
|
+
self._logger.info(
|
|
480
|
+
"[CC-Agent stream] tool_use name=%s input_keys=%s",
|
|
481
|
+
event.get("name"),
|
|
482
|
+
list((event.get("input") or {}).keys()),
|
|
483
|
+
)
|
|
484
|
+
elif etype == "tool_result":
|
|
485
|
+
content = event.get("content") or ""
|
|
486
|
+
preview = content if isinstance(content, str) else json.dumps(content)
|
|
487
|
+
self._logger.info(
|
|
488
|
+
"[CC-Agent stream] tool_result is_error=%s content=%r",
|
|
489
|
+
event.get("is_error", False), preview[:300],
|
|
490
|
+
)
|
|
491
|
+
elif etype == "hook":
|
|
492
|
+
self._logger.info(
|
|
493
|
+
"[CC-Agent stream] hook %s",
|
|
494
|
+
event.get("hook_event_name") or event.get("subtype") or "?",
|
|
495
|
+
)
|
|
496
|
+
elif etype == "result":
|
|
497
|
+
self._logger.info(
|
|
498
|
+
"[CC-Agent stream] result is_error=%s subtype=%s "
|
|
499
|
+
"session_id=%s duration_ms=%s num_turns=%s cost=%s",
|
|
500
|
+
event.get("is_error", False), event.get("subtype"),
|
|
501
|
+
event.get("session_id"),
|
|
502
|
+
event.get("duration_ms"), event.get("num_turns"),
|
|
503
|
+
event.get("total_cost_usd"),
|
|
504
|
+
)
|
|
505
|
+
except Exception:
|
|
506
|
+
self._logger.debug("[CC-Agent stream] log-summary failed for event")
|
|
507
|
+
|
|
508
|
+
async def _safe_terminal_log(self, message: str, *, level: str) -> None:
|
|
509
|
+
if not self._broadcaster:
|
|
510
|
+
return
|
|
511
|
+
try:
|
|
512
|
+
await self._broadcaster.broadcast_terminal_log({
|
|
513
|
+
"source": f"{self._provider.name}:{self._task_id}",
|
|
514
|
+
"level": level,
|
|
515
|
+
"message": message,
|
|
516
|
+
})
|
|
517
|
+
except Exception:
|
|
518
|
+
pass
|
|
519
|
+
|
|
520
|
+
async def _safe_node_status(self, status: str, data: Dict[str, Any]) -> None:
|
|
521
|
+
if not self._broadcaster:
|
|
522
|
+
return
|
|
523
|
+
try:
|
|
524
|
+
await self._broadcaster.update_node_status(
|
|
525
|
+
self._node_id, status, data,
|
|
526
|
+
workflow_id=self._workflow_id,
|
|
527
|
+
)
|
|
528
|
+
except Exception:
|
|
529
|
+
pass
|
|
530
|
+
|
|
531
|
+
# ------------------------------------------------------------------
|
|
532
|
+
# Public lifecycle
|
|
533
|
+
# ------------------------------------------------------------------
|
|
534
|
+
|
|
535
|
+
async def wait_for_completion(self, timeout_seconds: int) -> SessionResult:
|
|
536
|
+
"""Wait for the CLI to exit, with hard timeout watchdog."""
|
|
537
|
+
if self._proc is None:
|
|
538
|
+
return self._build_result(
|
|
539
|
+
success=False, error="session never started",
|
|
540
|
+
)
|
|
541
|
+
|
|
542
|
+
try:
|
|
543
|
+
await asyncio.wait_for(self._proc.wait(), timeout=timeout_seconds)
|
|
544
|
+
self._exit_code = self._proc.returncode
|
|
545
|
+
return self._build_result(success=(self._exit_code == 0))
|
|
546
|
+
except asyncio.TimeoutError:
|
|
547
|
+
await self.stop()
|
|
548
|
+
return self._build_result(
|
|
549
|
+
success=False,
|
|
550
|
+
error=f"timeout after {timeout_seconds}s",
|
|
551
|
+
)
|
|
552
|
+
|
|
553
|
+
async def cleanup(self) -> None:
|
|
554
|
+
"""Stop the process and remove the worktree + lockfile."""
|
|
555
|
+
try:
|
|
556
|
+
await self.stop()
|
|
557
|
+
except Exception as exc:
|
|
558
|
+
self._logger.debug("[%s] stop during cleanup: %s", self.label, exc)
|
|
559
|
+
|
|
560
|
+
if self._lockfile_path:
|
|
561
|
+
remove_ide_lockfile(self._lockfile_path)
|
|
562
|
+
self._lockfile_path = None
|
|
563
|
+
|
|
564
|
+
# Remove the per-task worktree (only created for non-memory
|
|
565
|
+
# runs; memory-bound spawns ran directly under repo_root).
|
|
566
|
+
if not self._memory_bound:
|
|
567
|
+
try:
|
|
568
|
+
await anyio.run_process(
|
|
569
|
+
[
|
|
570
|
+
"git", "-C", str(self._repo_root),
|
|
571
|
+
"worktree", "remove", "--force",
|
|
572
|
+
str(self._worktree_dir),
|
|
573
|
+
],
|
|
574
|
+
check=False,
|
|
575
|
+
)
|
|
576
|
+
except Exception as exc:
|
|
577
|
+
self._logger.debug("[%s] worktree remove: %s", self.label, exc)
|
|
578
|
+
|
|
579
|
+
# ------------------------------------------------------------------
|
|
580
|
+
# Result construction
|
|
581
|
+
# ------------------------------------------------------------------
|
|
582
|
+
|
|
583
|
+
def _build_result(
|
|
584
|
+
self,
|
|
585
|
+
*,
|
|
586
|
+
success: bool,
|
|
587
|
+
error: Optional[str] = None,
|
|
588
|
+
) -> SessionResult:
|
|
589
|
+
provider_result = self._provider.event_to_session_result(
|
|
590
|
+
self._events,
|
|
591
|
+
"\n".join(self._stderr_lines),
|
|
592
|
+
self._exit_code if self._exit_code is not None else -1,
|
|
593
|
+
)
|
|
594
|
+
|
|
595
|
+
canonical = provider_result.get("canonical_usage")
|
|
596
|
+
if not isinstance(canonical, CanonicalUsage):
|
|
597
|
+
canonical = self._provider.canonical_usage(self._events)
|
|
598
|
+
|
|
599
|
+
final_success = success and provider_result.get("success", True)
|
|
600
|
+
final_error = error or provider_result.get("error")
|
|
601
|
+
|
|
602
|
+
return SessionResult(
|
|
603
|
+
task_id=self._task_id,
|
|
604
|
+
session_id=provider_result.get("session_id"),
|
|
605
|
+
provider=self._provider.name,
|
|
606
|
+
prompt=self._task.prompt,
|
|
607
|
+
branch=self._branch,
|
|
608
|
+
worktree_path=str(self._worktree_dir),
|
|
609
|
+
response=str(provider_result.get("response") or "")[:4000],
|
|
610
|
+
cost_usd=provider_result.get("cost_usd"),
|
|
611
|
+
duration_ms=provider_result.get("duration_ms"),
|
|
612
|
+
num_turns=provider_result.get("num_turns"),
|
|
613
|
+
tool_calls=int(provider_result.get("tool_calls", 0)),
|
|
614
|
+
canonical_usage=canonical,
|
|
615
|
+
provider_data=dict(provider_result.get("provider_data") or {}),
|
|
616
|
+
success=final_success,
|
|
617
|
+
error=final_error if not final_success else None,
|
|
618
|
+
)
|