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,607 @@
|
|
|
1
|
+
"""`AICliService.run_batch()` — top-level entry for `claude_code_agent` /
|
|
2
|
+
`codex_agent` plugins.
|
|
3
|
+
|
|
4
|
+
Runs N parallel `AICliSession`s under an `asyncio.Semaphore`, mirroring
|
|
5
|
+
the semaphore-+-gather pattern already used in
|
|
6
|
+
`nodes/document/file_downloader.py`. No separate pool class — the
|
|
7
|
+
machinery is small enough to live inline.
|
|
8
|
+
|
|
9
|
+
Per-batch lifecycle:
|
|
10
|
+
|
|
11
|
+
1. Verify `working_directory` is a git repo (uses `git rev-parse --show-toplevel`).
|
|
12
|
+
2. Allocate a bearer token, register a `BatchContext` in the MCP server.
|
|
13
|
+
3. `asyncio.gather` N sessions, each wrapped in `_run_session` with
|
|
14
|
+
try/finally cleanup.
|
|
15
|
+
4. Aggregate per-task `SessionResult`s into a `BatchResult`.
|
|
16
|
+
5. Deregister the bearer token in the `finally` so 401s flip on the
|
|
17
|
+
next MCP request after the batch settles.
|
|
18
|
+
|
|
19
|
+
Active sessions are tracked in `_active_sessions[(workflow_id, node_id)]`
|
|
20
|
+
so workflow cancel can target them.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import asyncio
|
|
26
|
+
import os
|
|
27
|
+
import time
|
|
28
|
+
from datetime import datetime, timezone
|
|
29
|
+
from pathlib import Path
|
|
30
|
+
from typing import Any, Dict, Iterable, List, Optional, Tuple
|
|
31
|
+
|
|
32
|
+
import anyio
|
|
33
|
+
|
|
34
|
+
from core.logging import get_logger
|
|
35
|
+
|
|
36
|
+
from services.cli_agent.config import get_provider_config
|
|
37
|
+
from services.cli_agent.factory import create_cli_provider
|
|
38
|
+
from services.cli_agent.mcp_server import (
|
|
39
|
+
BatchContext,
|
|
40
|
+
issue_token,
|
|
41
|
+
register_batch,
|
|
42
|
+
unregister_batch,
|
|
43
|
+
)
|
|
44
|
+
from services.cli_agent.protocol import BatchResult, SessionResult
|
|
45
|
+
from services.cli_agent.session import AICliSession
|
|
46
|
+
from services.cli_agent.types import BaseAICliTaskSpec
|
|
47
|
+
|
|
48
|
+
logger = get_logger(__name__)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
DEFAULT_MAX_PARALLEL = 5
|
|
52
|
+
|
|
53
|
+
BatchKey = Tuple[str, str] # (workflow_id, node_id)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class AICliService:
|
|
57
|
+
"""Singleton service. Use `get_ai_cli_service()` to access."""
|
|
58
|
+
|
|
59
|
+
def __init__(self) -> None:
|
|
60
|
+
# workflow_id+node_id -> live session list (for cancel targeting).
|
|
61
|
+
self._active_sessions: Dict[BatchKey, List[AICliSession]] = {}
|
|
62
|
+
self._lock = asyncio.Lock()
|
|
63
|
+
|
|
64
|
+
# ------------------------------------------------------------------
|
|
65
|
+
# Public API
|
|
66
|
+
# ------------------------------------------------------------------
|
|
67
|
+
|
|
68
|
+
async def run_batch(
|
|
69
|
+
self,
|
|
70
|
+
provider_name: str,
|
|
71
|
+
*,
|
|
72
|
+
tasks: Iterable[BaseAICliTaskSpec],
|
|
73
|
+
node_id: str,
|
|
74
|
+
workflow_id: str,
|
|
75
|
+
workspace_dir: Path,
|
|
76
|
+
broadcaster: Any,
|
|
77
|
+
repo_root: Optional[Path] = None,
|
|
78
|
+
connected_skill_names: Optional[List[str]] = None,
|
|
79
|
+
connected_tools: Optional[List[Dict[str, Any]]] = None,
|
|
80
|
+
connected_memory: Optional[Dict[str, Any]] = None,
|
|
81
|
+
allowed_credentials: Optional[List[str]] = None,
|
|
82
|
+
max_parallel: int = DEFAULT_MAX_PARALLEL,
|
|
83
|
+
mcp_port: Optional[int] = None,
|
|
84
|
+
) -> BatchResult:
|
|
85
|
+
"""Run a list of CLI tasks under one batch.
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
`BatchResult` aggregating per-task `SessionResult`s.
|
|
89
|
+
|
|
90
|
+
Raises:
|
|
91
|
+
ValueError / NotImplementedError: provider unknown / v2-deferred.
|
|
92
|
+
"""
|
|
93
|
+
provider = create_cli_provider(provider_name)
|
|
94
|
+
task_list: List[BaseAICliTaskSpec] = list(tasks)
|
|
95
|
+
tool_names = [t.get("node_type") for t in (connected_tools or [])]
|
|
96
|
+
memory_node = (
|
|
97
|
+
connected_memory.get("node_id") if connected_memory else None
|
|
98
|
+
)
|
|
99
|
+
logger.info(
|
|
100
|
+
"[CC-Agent run_batch] enter provider=%s node=%s wf=%s tasks=%d "
|
|
101
|
+
"skills=%s tools=%s creds=%s memory=%s workspace=%s",
|
|
102
|
+
provider_name, node_id, workflow_id, len(task_list),
|
|
103
|
+
connected_skill_names or [], tool_names,
|
|
104
|
+
allowed_credentials or [], memory_node, workspace_dir,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
# Verify the working directory is under a git repo.
|
|
108
|
+
resolved_repo_root = await self._resolve_repo_root(
|
|
109
|
+
workspace_dir=workspace_dir, override=repo_root,
|
|
110
|
+
)
|
|
111
|
+
if resolved_repo_root is None:
|
|
112
|
+
logger.warning(
|
|
113
|
+
"[CC-Agent run_batch] aborting: workspace=%s is not inside a git "
|
|
114
|
+
"repo (run `git init` there or set `working_directory` to "
|
|
115
|
+
"an existing repo).", workspace_dir,
|
|
116
|
+
)
|
|
117
|
+
return self._abort_not_git_repo(
|
|
118
|
+
provider_name=provider_name,
|
|
119
|
+
tasks=task_list,
|
|
120
|
+
)
|
|
121
|
+
logger.info(
|
|
122
|
+
"[CC-Agent run_batch] resolved repo_root=%s for workspace=%s",
|
|
123
|
+
resolved_repo_root, workspace_dir,
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
# Per-batch bearer token + MCP context
|
|
127
|
+
token = issue_token()
|
|
128
|
+
port = mcp_port or int(os.environ.get("MACHINA_BACKEND_PORT", "3010"))
|
|
129
|
+
ctx = BatchContext(
|
|
130
|
+
workflow_id=workflow_id,
|
|
131
|
+
node_id=node_id,
|
|
132
|
+
workspace_dir=Path(workspace_dir).resolve(),
|
|
133
|
+
connected_skill_names=set(connected_skill_names or []),
|
|
134
|
+
allowed_credentials=set(allowed_credentials or []),
|
|
135
|
+
connected_tools=list(connected_tools or []),
|
|
136
|
+
broadcaster=broadcaster,
|
|
137
|
+
)
|
|
138
|
+
register_batch(token, ctx)
|
|
139
|
+
|
|
140
|
+
cfg = get_provider_config(provider_name)
|
|
141
|
+
defaults = dict(cfg.defaults) if cfg else {}
|
|
142
|
+
|
|
143
|
+
key: BatchKey = (workflow_id, node_id)
|
|
144
|
+
async with self._lock:
|
|
145
|
+
if key in self._active_sessions:
|
|
146
|
+
logger.warning(
|
|
147
|
+
"[CC-Agent service] replacing stale session list for %s", key,
|
|
148
|
+
)
|
|
149
|
+
# Cancel anything previously left dangling.
|
|
150
|
+
for sess in self._active_sessions[key]:
|
|
151
|
+
try:
|
|
152
|
+
await sess.cleanup()
|
|
153
|
+
except Exception:
|
|
154
|
+
pass
|
|
155
|
+
self._active_sessions[key] = []
|
|
156
|
+
|
|
157
|
+
start = time.monotonic()
|
|
158
|
+
await self._broadcast_phase(broadcaster, node_id, workflow_id, "batch_started", {
|
|
159
|
+
"provider": provider_name,
|
|
160
|
+
"n_tasks": len(task_list),
|
|
161
|
+
"max_parallel": max_parallel,
|
|
162
|
+
"isolation": "worktree",
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
sem = asyncio.Semaphore(max(1, int(max_parallel)))
|
|
166
|
+
|
|
167
|
+
async def run_one(task: BaseAICliTaskSpec) -> SessionResult:
|
|
168
|
+
async with sem:
|
|
169
|
+
session = AICliSession(
|
|
170
|
+
provider=provider, task=task,
|
|
171
|
+
repo_root=resolved_repo_root, workspace_dir=workspace_dir,
|
|
172
|
+
node_id=node_id, workflow_id=workflow_id,
|
|
173
|
+
broadcaster=broadcaster, defaults=defaults,
|
|
174
|
+
mcp_port=port, batch_token=token,
|
|
175
|
+
connected_tool_names=[
|
|
176
|
+
t.get("node_type") for t in (connected_tools or [])
|
|
177
|
+
if t.get("node_type")
|
|
178
|
+
],
|
|
179
|
+
connected_skill_names=list(connected_skill_names or []),
|
|
180
|
+
memory_bound=bool(connected_memory),
|
|
181
|
+
)
|
|
182
|
+
async with self._lock:
|
|
183
|
+
self._active_sessions[key].append(session)
|
|
184
|
+
try:
|
|
185
|
+
try:
|
|
186
|
+
await session.start()
|
|
187
|
+
except FileNotFoundError as exc:
|
|
188
|
+
return self._fail_result(provider_name, task, session.task_id,
|
|
189
|
+
f"cli_not_installed: {exc}")
|
|
190
|
+
except RuntimeError as exc:
|
|
191
|
+
# `_pre_spawn` raises on git-worktree failure.
|
|
192
|
+
return self._fail_result(provider_name, task, session.task_id,
|
|
193
|
+
f"worktree_setup_failed: {exc}")
|
|
194
|
+
except Exception as exc:
|
|
195
|
+
logger.exception("[CC-Agent service] start failed")
|
|
196
|
+
return self._fail_result(provider_name, task, session.task_id,
|
|
197
|
+
f"start_failed: {exc}")
|
|
198
|
+
return await session.wait_for_completion(task.timeout_seconds)
|
|
199
|
+
finally:
|
|
200
|
+
try:
|
|
201
|
+
await session.cleanup()
|
|
202
|
+
except Exception as exc:
|
|
203
|
+
logger.debug("[CC-Agent service] cleanup: %s", exc)
|
|
204
|
+
async with self._lock:
|
|
205
|
+
try:
|
|
206
|
+
self._active_sessions[key].remove(session)
|
|
207
|
+
except (KeyError, ValueError):
|
|
208
|
+
pass
|
|
209
|
+
|
|
210
|
+
try:
|
|
211
|
+
results: List[SessionResult] = await asyncio.gather(
|
|
212
|
+
*(run_one(t) for t in task_list),
|
|
213
|
+
return_exceptions=False,
|
|
214
|
+
)
|
|
215
|
+
finally:
|
|
216
|
+
async with self._lock:
|
|
217
|
+
self._active_sessions.pop(key, None)
|
|
218
|
+
unregister_batch(token)
|
|
219
|
+
|
|
220
|
+
# Memory bridge: persist claude's session_id + append the
|
|
221
|
+
# rendered exchange to simpleMemory's markdown transcript so the
|
|
222
|
+
# next run can `--resume <UUID>` and the UI sees the
|
|
223
|
+
# conversation refresh live. Fire-and-forget — failure here
|
|
224
|
+
# doesn't fail the batch.
|
|
225
|
+
if connected_memory:
|
|
226
|
+
try:
|
|
227
|
+
await self._persist_memory(
|
|
228
|
+
connected_memory, results, broadcaster=broadcaster,
|
|
229
|
+
)
|
|
230
|
+
except Exception as exc: # pragma: no cover — best-effort
|
|
231
|
+
logger.warning(
|
|
232
|
+
"[CC-Agent run_batch] memory persistence failed: %s",
|
|
233
|
+
exc,
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
elapsed_ms = int((time.monotonic() - start) * 1000)
|
|
237
|
+
n_succeeded = sum(1 for r in results if r.success)
|
|
238
|
+
n_failed = len(results) - n_succeeded
|
|
239
|
+
|
|
240
|
+
# Cost roll-up: prefer the provider's reported cost (Claude exposes
|
|
241
|
+
# `total_cost_usd` natively); for providers that don't (Codex,
|
|
242
|
+
# Gemini v2), derive USD from `canonical_usage` via the existing
|
|
243
|
+
# PricingService — a single source of truth for all LLM cost in
|
|
244
|
+
# MachinaOs.
|
|
245
|
+
for r in results:
|
|
246
|
+
if r.cost_usd is None:
|
|
247
|
+
derived = self._derive_cost(r, task_list)
|
|
248
|
+
if derived is not None:
|
|
249
|
+
r.cost_usd = derived
|
|
250
|
+
|
|
251
|
+
costs = [r.cost_usd for r in results]
|
|
252
|
+
total_cost = (
|
|
253
|
+
None if any(c is None for c in costs) else round(sum(c or 0 for c in costs), 6)
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
result = BatchResult(
|
|
257
|
+
tasks=results,
|
|
258
|
+
n_tasks=len(results),
|
|
259
|
+
n_succeeded=n_succeeded,
|
|
260
|
+
n_failed=n_failed,
|
|
261
|
+
total_cost_usd=total_cost,
|
|
262
|
+
wall_clock_ms=elapsed_ms,
|
|
263
|
+
budget_remaining_usd=None,
|
|
264
|
+
provider=provider_name,
|
|
265
|
+
timestamp=datetime.now(timezone.utc).isoformat(),
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
await self._broadcast_phase(broadcaster, node_id, workflow_id, "batch_complete", {
|
|
269
|
+
"provider": provider_name,
|
|
270
|
+
"n_succeeded": n_succeeded,
|
|
271
|
+
"n_failed": n_failed,
|
|
272
|
+
"total_cost_usd": total_cost,
|
|
273
|
+
"wall_clock_ms": elapsed_ms,
|
|
274
|
+
})
|
|
275
|
+
return result
|
|
276
|
+
|
|
277
|
+
async def cancel_workflow(self, workflow_id: str) -> int:
|
|
278
|
+
"""Cancel every active session for a workflow. Returns count cancelled."""
|
|
279
|
+
cancelled = 0
|
|
280
|
+
async with self._lock:
|
|
281
|
+
keys = [k for k in self._active_sessions if k[0] == workflow_id]
|
|
282
|
+
sessions: List[AICliSession] = []
|
|
283
|
+
for k in keys:
|
|
284
|
+
sessions.extend(self._active_sessions[k])
|
|
285
|
+
for sess in sessions:
|
|
286
|
+
try:
|
|
287
|
+
await sess.cleanup()
|
|
288
|
+
cancelled += 1
|
|
289
|
+
except Exception as exc:
|
|
290
|
+
logger.debug("[CC-Agent service] cancel: %s", exc)
|
|
291
|
+
return cancelled
|
|
292
|
+
|
|
293
|
+
async def cancel_node(self, node_id: str) -> int:
|
|
294
|
+
"""Cancel every active session for a node. Returns count cancelled."""
|
|
295
|
+
cancelled = 0
|
|
296
|
+
async with self._lock:
|
|
297
|
+
keys = [k for k in self._active_sessions if k[1] == node_id]
|
|
298
|
+
sessions: List[AICliSession] = []
|
|
299
|
+
for k in keys:
|
|
300
|
+
sessions.extend(self._active_sessions[k])
|
|
301
|
+
for sess in sessions:
|
|
302
|
+
try:
|
|
303
|
+
await sess.cleanup()
|
|
304
|
+
cancelled += 1
|
|
305
|
+
except Exception as exc:
|
|
306
|
+
logger.debug("[CC-Agent service] cancel: %s", exc)
|
|
307
|
+
return cancelled
|
|
308
|
+
|
|
309
|
+
# ------------------------------------------------------------------
|
|
310
|
+
# Internals
|
|
311
|
+
# ------------------------------------------------------------------
|
|
312
|
+
|
|
313
|
+
@staticmethod
|
|
314
|
+
async def _clear_stale_session_id(
|
|
315
|
+
connected_memory: Dict[str, Any],
|
|
316
|
+
) -> None:
|
|
317
|
+
"""Wipe a stale ``last_session_id`` that no longer maps to any
|
|
318
|
+
JSONL under the current cwd's project dir.
|
|
319
|
+
|
|
320
|
+
Triggered when claude reports ``No conversation found with
|
|
321
|
+
session ID: <UUID>``. Preserves ``memory_content`` (the
|
|
322
|
+
user-visible markdown mirror) since that's informational; only
|
|
323
|
+
the resume UUID is broken.
|
|
324
|
+
"""
|
|
325
|
+
from services.plugin.deps import get_database
|
|
326
|
+
|
|
327
|
+
db = get_database()
|
|
328
|
+
memory_node_id = connected_memory["node_id"]
|
|
329
|
+
params = await db.get_node_parameters(memory_node_id) or {}
|
|
330
|
+
prior = params.get("last_session_id")
|
|
331
|
+
if not prior:
|
|
332
|
+
return # already cleared
|
|
333
|
+
params["last_session_id"] = None
|
|
334
|
+
await db.save_node_parameters(memory_node_id, params)
|
|
335
|
+
logger.warning(
|
|
336
|
+
"[CC-Agent _persist_memory] cleared stale last_session_id=%s "
|
|
337
|
+
"from memory_node=%s; next run will spawn a fresh claude "
|
|
338
|
+
"session and persist its new UUID.",
|
|
339
|
+
prior, memory_node_id,
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
@staticmethod
|
|
343
|
+
async def _persist_memory(
|
|
344
|
+
connected_memory: Dict[str, Any],
|
|
345
|
+
results: List[SessionResult],
|
|
346
|
+
broadcaster: Any = None,
|
|
347
|
+
) -> None:
|
|
348
|
+
"""Append each successful run's user prompt + assistant response
|
|
349
|
+
to ``simpleMemory.memory_content`` (markdown). Mirrors aiAgent /
|
|
350
|
+
chatAgent / deep_agent / rlm_agent's persistence pattern exactly
|
|
351
|
+
— same helpers (``append_to_memory_markdown``,
|
|
352
|
+
``trim_markdown_window``), same field. One DB write.
|
|
353
|
+
"""
|
|
354
|
+
successful = [r for r in results if r.success]
|
|
355
|
+
logger.info(
|
|
356
|
+
"[CC-Agent _persist_memory] memory_node=%s results=%d "
|
|
357
|
+
"successful=%d session_ids=%s",
|
|
358
|
+
connected_memory.get("node_id"),
|
|
359
|
+
len(results),
|
|
360
|
+
len(successful),
|
|
361
|
+
[r.session_id for r in successful],
|
|
362
|
+
)
|
|
363
|
+
if not successful:
|
|
364
|
+
logger.warning(
|
|
365
|
+
"[CC-Agent _persist_memory] no successful runs; skipping "
|
|
366
|
+
"save (memory_node=%s). Per-result: %s",
|
|
367
|
+
connected_memory.get("node_id"),
|
|
368
|
+
[
|
|
369
|
+
{"success": r.success, "session_id": r.session_id,
|
|
370
|
+
"error": (r.error or "")[:80]}
|
|
371
|
+
for r in results
|
|
372
|
+
],
|
|
373
|
+
)
|
|
374
|
+
# Auto-recovery: claude returns
|
|
375
|
+
# ``No conversation found with session ID: <UUID>`` when the
|
|
376
|
+
# `--resume <UUID>` we passed doesn't exist under the current
|
|
377
|
+
# cwd's project dir (most often: a `last_session_id` saved
|
|
378
|
+
# before the cwd-stability fix landed, or a session JSONL
|
|
379
|
+
# that was wiped). Without this clear the same stale UUID
|
|
380
|
+
# would re-fire on every retry and lock the user out
|
|
381
|
+
# forever. The next run after this point will spawn a
|
|
382
|
+
# fresh session and `_persist_memory` will save its UUID.
|
|
383
|
+
stale = any(
|
|
384
|
+
r.error and "No conversation found with session ID" in r.error
|
|
385
|
+
for r in results
|
|
386
|
+
)
|
|
387
|
+
if stale:
|
|
388
|
+
await AICliService._clear_stale_session_id(connected_memory)
|
|
389
|
+
return
|
|
390
|
+
|
|
391
|
+
from services.memory import (
|
|
392
|
+
append_to_memory_markdown,
|
|
393
|
+
trim_markdown_window,
|
|
394
|
+
)
|
|
395
|
+
from services.plugin.deps import get_database
|
|
396
|
+
|
|
397
|
+
db = get_database()
|
|
398
|
+
memory_node_id = connected_memory["node_id"]
|
|
399
|
+
params = await db.get_node_parameters(memory_node_id) or {}
|
|
400
|
+
|
|
401
|
+
# 1. Persist claude's returned session_id from the most recent
|
|
402
|
+
# successful run. Drives `--resume <UUID>` on the next spawn so
|
|
403
|
+
# claude finds and continues its own JSONL transcript on disk.
|
|
404
|
+
last_run = next((r for r in reversed(successful) if r.session_id), None)
|
|
405
|
+
if last_run is not None:
|
|
406
|
+
params["last_session_id"] = last_run.session_id
|
|
407
|
+
|
|
408
|
+
# 2. Update the user-visible markdown mirror.
|
|
409
|
+
content = params.get("memory_content") or (
|
|
410
|
+
"# Conversation History\n\n*No messages yet.*\n"
|
|
411
|
+
)
|
|
412
|
+
for r in successful:
|
|
413
|
+
content = append_to_memory_markdown(content, "human", r.prompt)
|
|
414
|
+
content = append_to_memory_markdown(
|
|
415
|
+
content, "ai", r.response or "",
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
window = int(connected_memory.get("window_size") or 100)
|
|
419
|
+
content, removed_texts = trim_markdown_window(content, window)
|
|
420
|
+
params["memory_content"] = content
|
|
421
|
+
|
|
422
|
+
await db.save_node_parameters(memory_node_id, params)
|
|
423
|
+
logger.info(
|
|
424
|
+
"[CC-Agent _persist_memory] saved memory_node=%s "
|
|
425
|
+
"last_session_id=%s appended_turns=%d archived_blocks=%d "
|
|
426
|
+
"content_length=%d",
|
|
427
|
+
memory_node_id, params.get("last_session_id"),
|
|
428
|
+
len(successful), len(removed_texts), len(content),
|
|
429
|
+
)
|
|
430
|
+
|
|
431
|
+
# Broadcast `node_parameters_updated` so the simpleMemory's
|
|
432
|
+
# parameter panel + memory editor refetch live without a page
|
|
433
|
+
# reload. Mirrors the pattern in
|
|
434
|
+
# `routers/websocket.py:handle_save_node_parameters`. Without
|
|
435
|
+
# this the DB has the latest conversation but the UI keeps
|
|
436
|
+
# showing the stale snapshot it loaded at workflow open.
|
|
437
|
+
if broadcaster is not None:
|
|
438
|
+
try:
|
|
439
|
+
await broadcaster.broadcast({
|
|
440
|
+
"type": "node_parameters_updated",
|
|
441
|
+
"node_id": memory_node_id,
|
|
442
|
+
"parameters": params,
|
|
443
|
+
"version": 1,
|
|
444
|
+
"timestamp": time.time(),
|
|
445
|
+
})
|
|
446
|
+
except Exception as exc:
|
|
447
|
+
logger.warning(
|
|
448
|
+
"[CC-Agent _persist_memory] broadcast failed: %s", exc,
|
|
449
|
+
)
|
|
450
|
+
|
|
451
|
+
if connected_memory.get("long_term_enabled") and removed_texts:
|
|
452
|
+
from services.memory.vector_store import get_memory_vector_store
|
|
453
|
+
|
|
454
|
+
store = get_memory_vector_store(
|
|
455
|
+
connected_memory.get("session_id") or "default",
|
|
456
|
+
)
|
|
457
|
+
if store is not None:
|
|
458
|
+
await asyncio.to_thread(store.add_texts, removed_texts)
|
|
459
|
+
|
|
460
|
+
@staticmethod
|
|
461
|
+
async def _resolve_repo_root(
|
|
462
|
+
*,
|
|
463
|
+
workspace_dir: Path,
|
|
464
|
+
override: Optional[Path],
|
|
465
|
+
) -> Optional[Path]:
|
|
466
|
+
"""Find the git repo root via `git rev-parse --show-toplevel`.
|
|
467
|
+
|
|
468
|
+
Contract:
|
|
469
|
+
- When `override` is given, only consider that subtree.
|
|
470
|
+
- When not given, try `workspace_dir` first, then `cwd`.
|
|
471
|
+
"""
|
|
472
|
+
starts: List[Path]
|
|
473
|
+
if override is not None:
|
|
474
|
+
starts = [Path(override).resolve()]
|
|
475
|
+
else:
|
|
476
|
+
starts = [Path(workspace_dir).resolve(), Path.cwd().resolve()]
|
|
477
|
+
|
|
478
|
+
for start in starts:
|
|
479
|
+
try:
|
|
480
|
+
result = await anyio.run_process(
|
|
481
|
+
["git", "-C", str(start), "rev-parse", "--show-toplevel"],
|
|
482
|
+
check=False,
|
|
483
|
+
)
|
|
484
|
+
except FileNotFoundError:
|
|
485
|
+
# `git` not on PATH at all — fail-fast, nothing to fall back to.
|
|
486
|
+
return None
|
|
487
|
+
if result.returncode == 0:
|
|
488
|
+
root_text = (result.stdout or b"").decode("utf-8", errors="replace").strip()
|
|
489
|
+
if root_text:
|
|
490
|
+
return Path(root_text)
|
|
491
|
+
return None
|
|
492
|
+
|
|
493
|
+
@staticmethod
|
|
494
|
+
def _derive_cost(
|
|
495
|
+
result: SessionResult,
|
|
496
|
+
tasks: List[BaseAICliTaskSpec],
|
|
497
|
+
) -> Optional[float]:
|
|
498
|
+
"""Compute USD cost from `canonical_usage` via the central
|
|
499
|
+
`PricingService`. Returns None when token counts are zero (the
|
|
500
|
+
provider didn't surface them) — keeps the contract that
|
|
501
|
+
``cost_usd is None`` means "we genuinely don't know the cost"."""
|
|
502
|
+
cu = result.canonical_usage
|
|
503
|
+
total_tokens = cu.input_tokens + cu.output_tokens + cu.cache_read + cu.cache_write
|
|
504
|
+
if total_tokens == 0:
|
|
505
|
+
return None
|
|
506
|
+
|
|
507
|
+
# Find the model the task requested (or the provider's default).
|
|
508
|
+
model = ""
|
|
509
|
+
for t in tasks:
|
|
510
|
+
if (t.task_id or "") == result.task_id:
|
|
511
|
+
model = t.model or ""
|
|
512
|
+
break
|
|
513
|
+
|
|
514
|
+
try:
|
|
515
|
+
from services.pricing import get_pricing_service
|
|
516
|
+
pricing = get_pricing_service()
|
|
517
|
+
breakdown = pricing.calculate_cost(
|
|
518
|
+
provider=result.provider,
|
|
519
|
+
model=model,
|
|
520
|
+
input_tokens=cu.input_tokens,
|
|
521
|
+
output_tokens=cu.output_tokens,
|
|
522
|
+
cache_read_tokens=cu.cache_read,
|
|
523
|
+
cache_creation_tokens=cu.cache_write,
|
|
524
|
+
reasoning_tokens=cu.reasoning_tokens,
|
|
525
|
+
)
|
|
526
|
+
total = breakdown.get("total_cost")
|
|
527
|
+
return float(total) if total else None
|
|
528
|
+
except Exception as exc: # pragma: no cover — pricing is non-critical
|
|
529
|
+
logger.debug("[CC-Agent service] pricing lookup failed: %s", exc)
|
|
530
|
+
return None
|
|
531
|
+
|
|
532
|
+
@staticmethod
|
|
533
|
+
def _fail_result(
|
|
534
|
+
provider_name: str,
|
|
535
|
+
task: BaseAICliTaskSpec,
|
|
536
|
+
task_id: str,
|
|
537
|
+
error: str,
|
|
538
|
+
) -> SessionResult:
|
|
539
|
+
return SessionResult(
|
|
540
|
+
task_id=task_id,
|
|
541
|
+
provider=provider_name,
|
|
542
|
+
prompt=task.prompt,
|
|
543
|
+
success=False,
|
|
544
|
+
error=error,
|
|
545
|
+
)
|
|
546
|
+
|
|
547
|
+
@staticmethod
|
|
548
|
+
async def _broadcast_phase(
|
|
549
|
+
broadcaster: Any,
|
|
550
|
+
node_id: str,
|
|
551
|
+
workflow_id: str,
|
|
552
|
+
phase: str,
|
|
553
|
+
data: dict,
|
|
554
|
+
) -> None:
|
|
555
|
+
if not broadcaster:
|
|
556
|
+
return
|
|
557
|
+
try:
|
|
558
|
+
await broadcaster.update_node_status(
|
|
559
|
+
node_id,
|
|
560
|
+
"executing",
|
|
561
|
+
{"phase": phase, **data},
|
|
562
|
+
workflow_id=workflow_id,
|
|
563
|
+
)
|
|
564
|
+
except Exception:
|
|
565
|
+
pass
|
|
566
|
+
|
|
567
|
+
def _abort_not_git_repo(
|
|
568
|
+
self,
|
|
569
|
+
*,
|
|
570
|
+
provider_name: str,
|
|
571
|
+
tasks: List[BaseAICliTaskSpec],
|
|
572
|
+
) -> BatchResult:
|
|
573
|
+
results: List[SessionResult] = [
|
|
574
|
+
SessionResult(
|
|
575
|
+
task_id=t.task_id or "t_unstarted",
|
|
576
|
+
provider=provider_name,
|
|
577
|
+
prompt=t.prompt,
|
|
578
|
+
success=False,
|
|
579
|
+
error="working_directory_not_git_repo",
|
|
580
|
+
)
|
|
581
|
+
for t in tasks
|
|
582
|
+
]
|
|
583
|
+
return BatchResult(
|
|
584
|
+
tasks=results,
|
|
585
|
+
n_tasks=len(results),
|
|
586
|
+
n_succeeded=0,
|
|
587
|
+
n_failed=len(results),
|
|
588
|
+
total_cost_usd=None,
|
|
589
|
+
wall_clock_ms=0,
|
|
590
|
+
budget_remaining_usd=None,
|
|
591
|
+
provider=provider_name,
|
|
592
|
+
timestamp=datetime.now(timezone.utc).isoformat(),
|
|
593
|
+
)
|
|
594
|
+
|
|
595
|
+
|
|
596
|
+
# ---------------------------------------------------------------------------
|
|
597
|
+
# Singleton accessor
|
|
598
|
+
# ---------------------------------------------------------------------------
|
|
599
|
+
|
|
600
|
+
_instance: Optional[AICliService] = None
|
|
601
|
+
|
|
602
|
+
|
|
603
|
+
def get_ai_cli_service() -> AICliService:
|
|
604
|
+
global _instance
|
|
605
|
+
if _instance is None:
|
|
606
|
+
_instance = AICliService()
|
|
607
|
+
return _instance
|