polyrob 0.5.0__py3-none-any.whl
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.
- agents/README.md +453 -0
- agents/__init__.py +164 -0
- agents/base_agent.py +743 -0
- agents/personality/character.py +119 -0
- agents/personality/character_manager.py +309 -0
- agents/personality/characters/rob.character.json +56 -0
- agents/personality/characters/trump.character.json +63 -0
- agents/personality/persona_render.py +78 -0
- agents/personality/persona_resolver.py +115 -0
- agents/prompt/__init__.py +58 -0
- agents/prompt/base_prompt.py +92 -0
- agents/prompt/system.py +417 -0
- agents/task/README_PATH_MANAGEMENT.md +141 -0
- agents/task/__init__.py +162 -0
- agents/task/agent/__init__.py +70 -0
- agents/task/agent/agent_state.py +408 -0
- agents/task/agent/async_delegation.py +274 -0
- agents/task/agent/autonomy_state.py +275 -0
- agents/task/agent/conversation.py +75 -0
- agents/task/agent/core/__init__.py +6 -0
- agents/task/agent/core/aux_metering.py +61 -0
- agents/task/agent/core/background_review.py +157 -0
- agents/task/agent/core/construction.py +1179 -0
- agents/task/agent/core/conversational_exit.py +49 -0
- agents/task/agent/core/correspondent_gate.py +227 -0
- agents/task/agent/core/curator.py +246 -0
- agents/task/agent/core/episodic_digest.py +103 -0
- agents/task/agent/core/error_recovery.py +480 -0
- agents/task/agent/core/history_io.py +257 -0
- agents/task/agent/core/llm_provisioning.py +287 -0
- agents/task/agent/core/llm_runner.py +547 -0
- agents/task/agent/core/logging_io.py +276 -0
- agents/task/agent/core/loop_detection.py +262 -0
- agents/task/agent/core/memory_prefetch.py +325 -0
- agents/task/agent/core/memory_writer.py +667 -0
- agents/task/agent/core/model_introspection.py +82 -0
- agents/task/agent/core/model_swap.py +177 -0
- agents/task/agent/core/next_action_internal.py +1331 -0
- agents/task/agent/core/output_validation.py +303 -0
- agents/task/agent/core/project_context.py +272 -0
- agents/task/agent/core/resources.py +93 -0
- agents/task/agent/core/result_processing.py +389 -0
- agents/task/agent/core/run_loop.py +661 -0
- agents/task/agent/core/safety_lifecycle.py +228 -0
- agents/task/agent/core/secret_guard.py +293 -0
- agents/task/agent/core/self_wake.py +280 -0
- agents/task/agent/core/session_metadata.py +104 -0
- agents/task/agent/core/step.py +781 -0
- agents/task/agent/core/step_execution.py +398 -0
- agents/task/agent/core/step_telemetry.py +222 -0
- agents/task/agent/core/test_aux_metering.py +50 -0
- agents/task/agent/core/test_billing_failover.py +68 -0
- agents/task/agent/core/test_fatal_error_classifier.py +38 -0
- agents/task/agent/core/test_judge_validation.py +275 -0
- agents/task/agent/core/test_model_swap.py +190 -0
- agents/task/agent/core/test_reflection_offload.py +54 -0
- agents/task/agent/core/turn_input.py +8 -0
- agents/task/agent/core/untrusted_wrap.py +103 -0
- agents/task/agent/core/user_ingress.py +228 -0
- agents/task/agent/hitl_manager.py +396 -0
- agents/task/agent/log_sanitize.py +36 -0
- agents/task/agent/message_manager/config.py +97 -0
- agents/task/agent/message_manager/service.py +758 -0
- agents/task/agent/message_manager/tests.py +305 -0
- agents/task/agent/message_manager/tool_call_builder.py +539 -0
- agents/task/agent/message_manager/tool_message_repair.py +364 -0
- agents/task/agent/message_manager/views.py +143 -0
- agents/task/agent/messages/__init__.py +8 -0
- agents/task/agent/messages/builders.py +309 -0
- agents/task/agent/messages/compactor.py +611 -0
- agents/task/agent/messages/context_references.py +271 -0
- agents/task/agent/messages/filters.py +333 -0
- agents/task/agent/messages/guidance.py +328 -0
- agents/task/agent/messages/persistence.py +448 -0
- agents/task/agent/messages/retrieval.py +506 -0
- agents/task/agent/messages/sqlite_persistence.py +85 -0
- agents/task/agent/messages/test_compaction_upgrade.py +349 -0
- agents/task/agent/messages/test_persistence_subdir_fix.py +28 -0
- agents/task/agent/messages/test_sqlite_persistence.py +54 -0
- agents/task/agent/messages/token_counter.py +532 -0
- agents/task/agent/orchestrator.py +776 -0
- agents/task/agent/profile_manager.py +105 -0
- agents/task/agent/profile_registry.py +173 -0
- agents/task/agent/prompts.py +1090 -0
- agents/task/agent/scenario_registry.py +226 -0
- agents/task/agent/service.py +509 -0
- agents/task/agent/session.py +984 -0
- agents/task/agent/skill_discovery.py +110 -0
- agents/task/agent/skill_frontmatter.py +94 -0
- agents/task/agent/skill_manager.py +1201 -0
- agents/task/agent/skill_store.py +627 -0
- agents/task/agent/skill_validation.py +56 -0
- agents/task/agent/skill_writer.py +492 -0
- agents/task/agent/sub_agent_manager.py +1137 -0
- agents/task/agent/test_session_disk_recovery.py +34 -0
- agents/task/agent/tests.py +513 -0
- agents/task/agent/tool_call_tracker.py +511 -0
- agents/task/agent/views.py +469 -0
- agents/task/config.py +432 -0
- agents/task/constants.py +1384 -0
- agents/task/flag_defaults.py +63 -0
- agents/task/goals/__init__.py +1 -0
- agents/task/goals/autonomy_marker.py +28 -0
- agents/task/goals/board.py +690 -0
- agents/task/goals/completion_judge.py +247 -0
- agents/task/goals/context.py +62 -0
- agents/task/goals/dispatcher.py +848 -0
- agents/task/goals/escalation.py +102 -0
- agents/task/goals/planner.py +114 -0
- agents/task/logging_config.py +155 -0
- agents/task/path.py +991 -0
- agents/task/robust_parse_config.py +400 -0
- agents/task/runtime/__init__.py +0 -0
- agents/task/runtime/run_as_session.py +130 -0
- agents/task/runtime_safety.py +308 -0
- agents/task/session/__init__.py +5 -0
- agents/task/session/browser_pool.py +130 -0
- agents/task/session/cleanup.py +612 -0
- agents/task/session/execution.py +397 -0
- agents/task/session/feed.py +200 -0
- agents/task/session/hitl_ingress.py +201 -0
- agents/task/session/hooks.py +83 -0
- agents/task/session/multi_agent.py +243 -0
- agents/task/session/workspace.py +91 -0
- agents/task/session_registry.py +91 -0
- agents/task/session_route.py +35 -0
- agents/task/sqlite_session_registry.py +261 -0
- agents/task/surface_config.py +185 -0
- agents/task/telemetry/__init__.py +58 -0
- agents/task/telemetry/event_log.py +180 -0
- agents/task/telemetry/formatters.py +642 -0
- agents/task/telemetry/manager.py +571 -0
- agents/task/telemetry/memory_events.py +66 -0
- agents/task/telemetry/self_events.py +53 -0
- agents/task/telemetry/sequence.py +160 -0
- agents/task/telemetry/service.py +1265 -0
- agents/task/telemetry/views.py +882 -0
- agents/task/templates.py +98 -0
- agents/task/test_aux_chain.py +151 -0
- agents/task/test_aux_router.py +41 -0
- agents/task/tests/test_parse_robustness.py +170 -0
- agents/task/tool_defaults.py +110 -0
- agents/task/utils.py +762 -0
- agents/task/utils_json.py +768 -0
- agents/task/utils_webview.py +86 -0
- agents/task/workspace_context.py +229 -0
- agents/task_agent_lite.py +2398 -0
- api/README.md +683 -0
- api/TASK_API_DOCS.md +151 -0
- api/__init__.py +0 -0
- api/a2a/__init__.py +37 -0
- api/a2a/agent_card.py +337 -0
- api/a2a/client.py +657 -0
- api/a2a/endpoints.py +375 -0
- api/a2a/models.py +268 -0
- api/a2a/streaming.py +419 -0
- api/a2a/task_handler.py +929 -0
- api/admin_endpoints.py +1584 -0
- api/app.py +923 -0
- api/auth_constants.py +28 -0
- api/auth_endpoints.py +402 -0
- api/auth_state.py +36 -0
- api/chat_via_task.py +42 -0
- api/conversation_manager.py +231 -0
- api/dependencies.py +186 -0
- api/eip8004_endpoints.py +452 -0
- api/hyperliquid_models.py +174 -0
- api/hyperliquid_routes.py +450 -0
- api/interfaces.py +207 -0
- api/jwt_middleware.py +83 -0
- api/kb/__init__.py +1 -0
- api/kb/endpoints.py +314 -0
- api/mcp_models.py +279 -0
- api/mcp_routes.py +438 -0
- api/middleware.py +526 -0
- api/models.py +176 -0
- api/openai_compat/__init__.py +0 -0
- api/openai_compat/model_map.py +25 -0
- api/openai_compat/models.py +59 -0
- api/openai_compat/router.py +140 -0
- api/payment_endpoints.py +444 -0
- api/payment_verification.py +138 -0
- api/polymarket_models.py +155 -0
- api/polymarket_routes.py +501 -0
- api/pricing_endpoints.py +89 -0
- api/session_routing.py +54 -0
- api/skill_endpoints.py +453 -0
- api/task_http_api.py +1856 -0
- api/webhooks.py +52 -0
- api/x402_endpoints.py +280 -0
- avatar/README.md +44 -0
- avatar/__init__.py +1 -0
- avatar/config/rob.json +7 -0
- avatar/mindprint.js +395 -0
- avatar/renders/rob.meta.json +20 -0
- avatar/renders/rob.png +0 -0
- avatar/studio.html +373 -0
- avatar/webview/avatar-live.js +44 -0
- cli/README.md +89 -0
- cli/__init__.py +0 -0
- cli/cli_surface.py +67 -0
- cli/commands/__init__.py +0 -0
- cli/commands/_bootstrap.py +49 -0
- cli/commands/_errors.py +53 -0
- cli/commands/chat.py +973 -0
- cli/commands/config.py +100 -0
- cli/commands/dashboard.py +103 -0
- cli/commands/doctor.py +222 -0
- cli/commands/email.py +174 -0
- cli/commands/gateway.py +346 -0
- cli/commands/goals.py +509 -0
- cli/commands/init.py +276 -0
- cli/commands/journey.py +30 -0
- cli/commands/kb.py +280 -0
- cli/commands/model.py +107 -0
- cli/commands/owner.py +431 -0
- cli/commands/pfp.py +232 -0
- cli/commands/run.py +414 -0
- cli/commands/serve.py +41 -0
- cli/commands/session.py +684 -0
- cli/commands/skill_install.py +786 -0
- cli/commands/skills.py +128 -0
- cli/commands/subagents.py +100 -0
- cli/commands/surface.py +81 -0
- cli/commands/telegram.py +185 -0
- cli/commands/todos.py +209 -0
- cli/commands/tools.py +102 -0
- cli/commands/update.py +316 -0
- cli/commands/whatsapp.py +175 -0
- cli/config_store.py +276 -0
- cli/gitignore.py +43 -0
- cli/inventory.py +46 -0
- cli/keys.py +101 -0
- cli/persona.py +17 -0
- cli/polyrob.py +137 -0
- cli/ui/__init__.py +64 -0
- cli/ui/activity.py +141 -0
- cli/ui/app.py +538 -0
- cli/ui/banner.py +233 -0
- cli/ui/blocks.py +534 -0
- cli/ui/bootstrap_notice.py +47 -0
- cli/ui/commands/__init__.py +41 -0
- cli/ui/commands/h_cron.py +64 -0
- cli/ui/commands/h_journey.py +164 -0
- cli/ui/commands/h_kb.py +121 -0
- cli/ui/commands/h_learn.py +100 -0
- cli/ui/commands/h_mcp.py +215 -0
- cli/ui/commands/h_self.py +113 -0
- cli/ui/commands/h_skills.py +314 -0
- cli/ui/commands/handlers.py +1672 -0
- cli/ui/commands/registry.py +426 -0
- cli/ui/dialog.py +425 -0
- cli/ui/event_registry.py +110 -0
- cli/ui/event_specs.py +45 -0
- cli/ui/events.py +433 -0
- cli/ui/identity.py +15 -0
- cli/ui/lifecycle.py +159 -0
- cli/ui/live_hooks.py +43 -0
- cli/ui/model_selector.py +532 -0
- cli/ui/persistent_loop.py +181 -0
- cli/ui/pick.py +113 -0
- cli/ui/plain_renderer.py +351 -0
- cli/ui/renderer.py +384 -0
- cli/ui/rich_renderer.py +533 -0
- cli/ui/secrets.py +115 -0
- cli/ui/state.py +376 -0
- cli/ui/statusbar.py +231 -0
- cli/ui/streaming.py +254 -0
- cli/ui/terminal_render.py +105 -0
- cli/ui/theme.py +132 -0
- cli/update/__init__.py +6 -0
- cli/update/context.py +121 -0
- cli/update/detect.py +176 -0
- cli/update/engine.py +95 -0
- cli/update/migrate_guarded.py +55 -0
- cli/update/process_guard.py +215 -0
- cli/update/runners.py +78 -0
- cli/update/snapshot.py +270 -0
- cli/update/versions.py +205 -0
- core/README.md +377 -0
- core/__init__.py +174 -0
- core/assets.py +87 -0
- core/async_bridge.py +76 -0
- core/autonomy_runtime.py +259 -0
- core/base_component.py +302 -0
- core/bootstrap.py +681 -0
- core/bot.py +310 -0
- core/config.py +833 -0
- core/constants.py +126 -0
- core/container.py +462 -0
- core/db_manifest.py +101 -0
- core/embedding.py +57 -0
- core/env.py +39 -0
- core/exceptions.py +287 -0
- core/flags.py +144 -0
- core/flags_catalog.py +345 -0
- core/home_migration.py +62 -0
- core/identity.py +118 -0
- core/initialization.py +1004 -0
- core/instance.py +537 -0
- core/interactive_gate.py +96 -0
- core/logging.py +436 -0
- core/owner_doc_writer.py +176 -0
- core/pairing.py +193 -0
- core/path_safety.py +20 -0
- core/paths.py +27 -0
- core/payment_config.py +21 -0
- core/permissions.py +322 -0
- core/runtime_config.py +96 -0
- core/runtime_paths.py +135 -0
- core/seams.py +35 -0
- core/secret_scan.py +25 -0
- core/secret_scrub.py +71 -0
- core/secrets.py +35 -0
- core/security_logging_filter.py +148 -0
- core/self_context_writer.py +333 -0
- core/self_evolution.py +294 -0
- core/session_context.py +47 -0
- core/sqlite_util.py +59 -0
- core/surfaces/__init__.py +16 -0
- core/surfaces/access.py +118 -0
- core/surfaces/binding.py +88 -0
- core/surfaces/bootstrap.py +78 -0
- core/surfaces/circuit.py +168 -0
- core/surfaces/continuity.py +77 -0
- core/surfaces/correspondents.py +228 -0
- core/surfaces/dispatcher.py +216 -0
- core/surfaces/envelopes.py +75 -0
- core/surfaces/gc.py +30 -0
- core/surfaces/idempotency.py +57 -0
- core/surfaces/inbound_webhook.py +124 -0
- core/surfaces/media.py +32 -0
- core/surfaces/message_router.py +91 -0
- core/surfaces/outbound_allowlist.py +80 -0
- core/surfaces/outbound_dispatcher.py +110 -0
- core/surfaces/outbound_mirror.py +37 -0
- core/surfaces/outbound_queue.py +107 -0
- core/surfaces/outbound_target.py +12 -0
- core/surfaces/owner_admin.py +29 -0
- core/surfaces/proactive.py +78 -0
- core/surfaces/progress.py +108 -0
- core/surfaces/rate_bucket.py +21 -0
- core/surfaces/registry.py +51 -0
- core/surfaces/rendering.py +53 -0
- core/surfaces/send_policy.py +14 -0
- core/surfaces/serialize.py +24 -0
- core/surfaces/session_chat_registry.py +112 -0
- core/surfaces/session_policy.py +60 -0
- core/surfaces/surface.py +288 -0
- core/surfaces/transcription.py +88 -0
- core/surfaces/voice_echo.py +26 -0
- core/surfaces/voice_guard.py +23 -0
- core/tickers.py +174 -0
- core/tool_catalog.py +192 -0
- core/version.py +75 -0
- core/wallet/__init__.py +5 -0
- core/wallet/agent_wallet.py +59 -0
- core/wallet/audit_sink.py +61 -0
- core/wallet/config.py +70 -0
- core/wallet/factory.py +87 -0
- core/wallet/policy.py +104 -0
- core/wallet/signer.py +51 -0
- cron/README.md +65 -0
- cron/__init__.py +0 -0
- cron/delivery.py +277 -0
- cron/digest.py +124 -0
- cron/jobs.py +207 -0
- cron/runner.py +297 -0
- cron/schedule.py +194 -0
- cron/scheduler.py +201 -0
- cron/service.py +57 -0
- cron/wake_gate.py +223 -0
- data/__init__.py +1 -0
- data/prompts/autov2_prompts.json +87 -0
- data/prompts/skills/browser-automation/SKILL.md +43 -0
- data/prompts/skills/coding-workflow/SKILL.md +36 -0
- data/prompts/skills/crypto-trading-safety/SKILL.md +53 -0
- data/prompts/skills/document-writing/SKILL.md +33 -0
- data/prompts/skills/email-comms/SKILL.md +33 -0
- data/prompts/skills/file-data-ops/SKILL.md +35 -0
- data/prompts/skills/hyperliquid-account-review/SKILL.md +48 -0
- data/prompts/skills/hyperliquid-market-data/SKILL.md +52 -0
- data/prompts/skills/hyperliquid-trading/SKILL.md +59 -0
- data/prompts/skills/lead-research/SKILL.md +86 -0
- data/prompts/skills/market-research-brief/SKILL.md +104 -0
- data/prompts/skills/person-analyzer/SKILL.md +124 -0
- data/prompts/skills/polymarket-market-research/SKILL.md +54 -0
- data/prompts/skills/polymarket-portfolio-review/SKILL.md +50 -0
- data/prompts/skills/polymarket-trading/SKILL.md +55 -0
- data/prompts/skills/presentation-creator/SKILL.md +36 -0
- data/prompts/skills/project-analyzer/SKILL.md +169 -0
- data/prompts/skills/rules.json +268 -0
- data/prompts/skills/secret-handling/SKILL.md +20 -0
- data/prompts/skills/skill-authoring/SKILL.md +31 -0
- data/prompts/skills/skill-security-review/SKILL.md +27 -0
- data/prompts/skills/social-discovery/SKILL.md +57 -0
- data/prompts/skills/task-planning/SKILL.md +33 -0
- data/prompts/skills/web-research/SKILL.md +39 -0
- data/prompts/skills/web-scraping/SKILL.md +73 -0
- data/prompts/skills/x-engagement/SKILL.md +69 -0
- data/prompts/system_prompts.json +4 -0
- data/subscription_channels.json +5 -0
- modules/README.md +503 -0
- modules/__init__.py +124 -0
- modules/auth/__init__.py +13 -0
- modules/auth/api_key_manager.py +157 -0
- modules/auth/identity_mapper.py +380 -0
- modules/auth/siwe_auth.py +273 -0
- modules/auth/tier_manager.py +173 -0
- modules/base_module.py +190 -0
- modules/credits/__init__.py +29 -0
- modules/credits/balance_manager.py +237 -0
- modules/credits/cost_utils.py +129 -0
- modules/credits/pricing.py +139 -0
- modules/credits/unified_ledger.py +148 -0
- modules/credits/usage_meter.py +310 -0
- modules/credits/usage_tracker.py +674 -0
- modules/database/__init__.py +33 -0
- modules/database/audit_log.py +322 -0
- modules/database/auth_tables.py +276 -0
- modules/database/connection.py +644 -0
- modules/database/connection_pool.py +305 -0
- modules/database/conversation_contexts.py +446 -0
- modules/database/database_manager.py +237 -0
- modules/database/hyperliquid.py +375 -0
- modules/database/polymarket.py +574 -0
- modules/database/user_mcp_servers.py +728 -0
- modules/database/user_profiles.py +562 -0
- modules/database/utils.py +107 -0
- modules/database/x402_tables.py +92 -0
- modules/eip8004/README.md +1069 -0
- modules/eip8004/__init__.py +65 -0
- modules/eip8004/contracts.py +498 -0
- modules/eip8004/models.py +271 -0
- modules/eip8004/registration.py +148 -0
- modules/eip8004/reputation.py +367 -0
- modules/eip8004/validation.py +277 -0
- modules/llm/__init__.py +97 -0
- modules/llm/adapters.py +963 -0
- modules/llm/anthropic_client.py +831 -0
- modules/llm/available_models.py +106 -0
- modules/llm/brain_scrubber.py +137 -0
- modules/llm/cache_hints.py +162 -0
- modules/llm/deepseek_client.py +562 -0
- modules/llm/gemini_client.py +1367 -0
- modules/llm/llm_client.py +1139 -0
- modules/llm/llm_client_registry.py +188 -0
- modules/llm/llm_factory.py +237 -0
- modules/llm/llm_manager.py +1036 -0
- modules/llm/messages.py +374 -0
- modules/llm/model_registry.py +1935 -0
- modules/llm/nvidia_client.py +125 -0
- modules/llm/openai_client.py +800 -0
- modules/llm/openrouter_client.py +969 -0
- modules/llm/profiles.py +178 -0
- modules/llm/test_anthropic_cache_capture.py +35 -0
- modules/llm/test_cached_pricing_derivation.py +24 -0
- modules/llm/think_scrubber.py +444 -0
- modules/llm/token_counter.py +268 -0
- modules/memory/__init__.py +35 -0
- modules/memory/backend_factory.py +83 -0
- modules/memory/cache_manager.py +186 -0
- modules/memory/episodic.py +108 -0
- modules/memory/local_vector_memory_provider.py +552 -0
- modules/memory/memory_manager.py +225 -0
- modules/memory/models.py +312 -0
- modules/memory/provider.py +210 -0
- modules/memory/registry.py +205 -0
- modules/memory/sqlite_memory_provider.py +767 -0
- modules/memory/task/__init__.py +52 -0
- modules/memory/task/compaction_manager.py +157 -0
- modules/memory/task/context_retriever.py +809 -0
- modules/memory/task/hierarchical_memory.py +1002 -0
- modules/memory/task/lexical_retriever.py +71 -0
- modules/memory/task/null_context_manager.py +213 -0
- modules/memory/task/phase_manager.py +625 -0
- modules/memory/task/reflection_service.py +157 -0
- modules/memory/task/semantic_retriever.py +218 -0
- modules/memory/task/task_context_manager.py +1165 -0
- modules/memory/task/test_forgetting_engages.py +24 -0
- modules/memory/task/test_reflection_llm.py +39 -0
- modules/memory/task/test_threat_scan.py +53 -0
- modules/memory/task/threat_scan.py +90 -0
- modules/memory/test_sqlite_memory_provider.py +106 -0
- modules/memory/user_profile_manager.py +527 -0
- modules/payments/__init__.py +11 -0
- modules/payments/deposit_monitor.py +478 -0
- modules/payments/price_oracle.py +70 -0
- modules/payments/treasury_sweeper.py +401 -0
- modules/payments/wallet_generator.py +145 -0
- modules/pfp/__init__.py +1 -0
- modules/pfp/config.py +92 -0
- modules/pfp/mesh.py +565 -0
- modules/pfp/push.py +69 -0
- modules/pfp/renderer.py +85 -0
- modules/pfp/store.py +82 -0
- modules/skills/__init__.py +1 -0
- modules/skills/skill_usage.py +248 -0
- modules/transcription/__init__.py +24 -0
- modules/transcription/base.py +20 -0
- modules/transcription/faster_whisper_transcriber.py +55 -0
- modules/x402/README.md +249 -0
- modules/x402/__init__.py +22 -0
- modules/x402/invoicing.py +416 -0
- modules/x402/middleware.py +364 -0
- modules/x402/settlement_watcher.py +145 -0
- modules/x402/x402_integration.py +344 -0
- polyrob-0.5.0.dist-info/METADATA +482 -0
- polyrob-0.5.0.dist-info/RECORD +745 -0
- polyrob-0.5.0.dist-info/WHEEL +5 -0
- polyrob-0.5.0.dist-info/entry_points.txt +2 -0
- polyrob-0.5.0.dist-info/licenses/LICENSE +21 -0
- polyrob-0.5.0.dist-info/top_level.txt +12 -0
- surfaces/README.md +70 -0
- surfaces/__init__.py +0 -0
- surfaces/email/__init__.py +0 -0
- surfaces/email/dedup.py +44 -0
- surfaces/email/harness.py +187 -0
- surfaces/email/inbound.py +159 -0
- surfaces/email/seed.py +60 -0
- surfaces/email/surface.py +105 -0
- surfaces/telegram/__init__.py +0 -0
- surfaces/telegram/dedup.py +83 -0
- surfaces/telegram/harness.py +886 -0
- surfaces/telegram/inbound.py +145 -0
- surfaces/telegram/interactive_tools.py +49 -0
- surfaces/telegram/markdown.py +347 -0
- surfaces/telegram/rate_limit.py +167 -0
- surfaces/telegram/surface.py +187 -0
- surfaces/telegram/voice.py +52 -0
- surfaces/whatsapp/__init__.py +0 -0
- surfaces/whatsapp/client.py +51 -0
- surfaces/whatsapp/harness.py +62 -0
- surfaces/whatsapp/inbound.py +132 -0
- surfaces/whatsapp/surface.py +64 -0
- surfaces/whatsapp/window.py +34 -0
- tools/README.md +578 -0
- tools/__init__.py +424 -0
- tools/alchemy/__init__.py +5 -0
- tools/alchemy/alchemy_tool.py +389 -0
- tools/anysite/__init__.py +17 -0
- tools/anysite/client.py +88 -0
- tools/anysite/tool.py +89 -0
- tools/base_tool.py +468 -0
- tools/browser/__init__.py +22 -0
- tools/browser/actions.py +118 -0
- tools/browser/browser.py +1457 -0
- tools/browser/browser_manager.py +690 -0
- tools/browser/context.py +1748 -0
- tools/browser/playwright_utils.py +137 -0
- tools/browser/views.py +56 -0
- tools/code_exec/SANDBOX_SECURITY.md +144 -0
- tools/code_exec/__init__.py +178 -0
- tools/code_exec/backend.py +73 -0
- tools/code_exec/backends/__init__.py +1 -0
- tools/code_exec/backends/docker.py +779 -0
- tools/code_exec/backends/local_subprocess.py +154 -0
- tools/code_exec/env_policy.py +45 -0
- tools/code_exec/result.py +42 -0
- tools/code_exec/sandbox_guard.py +59 -0
- tools/code_exec/tool.py +227 -0
- tools/coding/__init__.py +50 -0
- tools/coding/edit.py +107 -0
- tools/coding/search.py +113 -0
- tools/coding/tool.py +353 -0
- tools/collabland/__init__.py +5 -0
- tools/collabland/collabland_tool.py +471 -0
- tools/controller/__init__.py +0 -0
- tools/controller/_helpers.py +165 -0
- tools/controller/action_registration.py +1765 -0
- tools/controller/approval.py +244 -0
- tools/controller/approval_interactive.py +90 -0
- tools/controller/delegation.py +195 -0
- tools/controller/execution.py +797 -0
- tools/controller/execution_context.py +141 -0
- tools/controller/hooks.py +135 -0
- tools/controller/introspection.py +344 -0
- tools/controller/mcp_registrar.py +190 -0
- tools/controller/message_send.py +31 -0
- tools/controller/registry/__init__.py +4 -0
- tools/controller/registry/schema_generators.py +395 -0
- tools/controller/registry/schema_sanitizer.py +406 -0
- tools/controller/registry/service.py +1266 -0
- tools/controller/registry/views.py +151 -0
- tools/controller/service.py +409 -0
- tools/controller/tool_management.py +334 -0
- tools/controller/types.py +28 -0
- tools/controller/views.py +450 -0
- tools/cronjob_tools.py +166 -0
- tools/crypto_trade_gate.py +59 -0
- tools/descriptors.py +452 -0
- tools/dom/__init__.py +0 -0
- tools/dom/history_tree_processor/service.py +244 -0
- tools/dom/history_tree_processor/view.py +99 -0
- tools/dom/service.py +471 -0
- tools/dom/views.py +346 -0
- tools/email_tool.py +403 -0
- tools/exceptions.py +169 -0
- tools/filesystem.py +1038 -0
- tools/filesystem_docproc.py +331 -0
- tools/filesystem_pdf.py +755 -0
- tools/git/__init__.py +42 -0
- tools/git/tool.py +258 -0
- tools/github/__init__.py +33 -0
- tools/github/client.py +94 -0
- tools/github/tool.py +237 -0
- tools/goal_tools.py +309 -0
- tools/hyperliquid/__init__.py +56 -0
- tools/hyperliquid/models.py +233 -0
- tools/hyperliquid/service.py +1680 -0
- tools/knowledge_ingest.py +950 -0
- tools/mcp/README.md +535 -0
- tools/mcp/__init__.py +52 -0
- tools/mcp/catalog.py +115 -0
- tools/mcp/config.py +337 -0
- tools/mcp/mcp_tool.py +1415 -0
- tools/mcp/param_coercion.py +289 -0
- tools/mcp/protocol.py +1520 -0
- tools/mcp/rate_limit.py +50 -0
- tools/mcp/security.py +424 -0
- tools/mcp/self_install.py +111 -0
- tools/mcp/server_manager.py +829 -0
- tools/mcp/subscriptions.py +72 -0
- tools/mcp/user_mcp_service.py +899 -0
- tools/mcp/validation_tracker.py +89 -0
- tools/mcp/views.py +346 -0
- tools/oauth/__init__.py +17 -0
- tools/oauth/manager.py +75 -0
- tools/oauth/provider.py +69 -0
- tools/oauth/providers/__init__.py +1 -0
- tools/oauth/providers/generic_oauth2.py +96 -0
- tools/perplexity_tool.py +280 -0
- tools/polymarket/__init__.py +41 -0
- tools/polymarket/clob_adapter.py +76 -0
- tools/polymarket/models.py +182 -0
- tools/polymarket/service.py +2082 -0
- tools/self_env/__init__.py +61 -0
- tools/self_env/tool.py +274 -0
- tools/shell/__init__.py +80 -0
- tools/shell/backend_pool.py +118 -0
- tools/shell/discipline.py +62 -0
- tools/shell/executor.py +157 -0
- tools/shell/loopback_allow.py +87 -0
- tools/shell/process_registry.py +113 -0
- tools/shell/process_tool.py +131 -0
- tools/shell/state.py +129 -0
- tools/shell/tool.py +151 -0
- tools/task_tool.py +637 -0
- tools/twitter_tool.py +2086 -0
- tools/user_directory.py +179 -0
- tools/web_fetch/__init__.py +3 -0
- tools/web_fetch/fetcher.py +144 -0
- tools/web_fetch/render.py +44 -0
- tools/web_fetch/tool.py +60 -0
- tools/x402/__init__.py +60 -0
- tools/x402/client.py +47 -0
- tools/x402/invoice_tool.py +143 -0
- tools/x402/real_client.py +250 -0
- tools/x402/service.py +131 -0
- utils/README.md +498 -0
- utils/__init__.py +46 -0
- utils/auth_utils.py +114 -0
- utils/bounded_collections.py +162 -0
- utils/circuit_breaker.py +389 -0
- utils/gif_utils.py +435 -0
- utils/markdown_utils.py +291 -0
- utils/message_utils.py +147 -0
- utils/metrics.py +36 -0
- utils/path_validator.py +227 -0
- utils/rate_limit_manager.py +399 -0
- utils/result_size.py +296 -0
- utils/time_utils.py +206 -0
- utils/user_utils.py +188 -0
- webview/README.md +111 -0
- webview/RENAME_AND_ALIGN_HANDOFF.md +200 -0
- webview/__init__.py +8 -0
- webview/activity.py +636 -0
- webview/owner_auth.py +108 -0
- webview/pages.py +448 -0
- webview/repair_sessions.py +399 -0
- webview/server.py +4323 -0
- webview/server_launcher.py +112 -0
- webview/static/css/activity.css +89 -0
- webview/static/css/chat.css +1772 -0
- webview/static/css/components.css +1190 -0
- webview/static/css/config-panel.css +651 -0
- webview/static/css/pages/admin.css +766 -0
- webview/static/css/pages/profile.css +231 -0
- webview/static/css/pages/settings.css +297 -0
- webview/static/css/pages/signin.css +316 -0
- webview/static/css/style.css +4250 -0
- webview/static/css/variables.css +147 -0
- webview/static/css/workspace-fullwidth-fix.css +109 -0
- webview/static/img/favicon.ico +0 -0
- webview/static/js/activity.js +394 -0
- webview/static/js/admin/activity.js +186 -0
- webview/static/js/admin/dashboard.js +163 -0
- webview/static/js/admin/user_detail.js +400 -0
- webview/static/js/admin/users.js +174 -0
- webview/static/js/admin/utils.js +214 -0
- webview/static/js/chat.js +3178 -0
- webview/static/js/config-panel.js +522 -0
- webview/static/js/constants.js +20 -0
- webview/static/js/error-handler.js +226 -0
- webview/static/js/ethers.min.js +1 -0
- webview/static/js/event-filter.js +137 -0
- webview/static/js/event-store.js +351 -0
- webview/static/js/file-attachments.js +374 -0
- webview/static/js/file-loader.js +163 -0
- webview/static/js/index.js +103 -0
- webview/static/js/performance-utils.js +196 -0
- webview/static/js/profile.js +396 -0
- webview/static/js/screenshot.js +386 -0
- webview/static/js/session.js +1527 -0
- webview/static/js/settings.js +1288 -0
- webview/static/js/sidebar-data.js +766 -0
- webview/static/js/sidebar-toggle.js +296 -0
- webview/static/js/socket.io.min.js +7 -0
- webview/static/js/stats.js +355 -0
- webview/static/js/ui-utils.js +420 -0
- webview/static/js/workspace.js +966 -0
- webview/stats_service.py +613 -0
- webview/templates/__init__.py +1 -0
- webview/templates/__pycache__/__init__.cpython-311.pyc +0 -0
- webview/templates/activity.html +36 -0
- webview/templates/admin/activity.html +122 -0
- webview/templates/admin/dashboard.html +112 -0
- webview/templates/admin/user_detail.html +228 -0
- webview/templates/admin/users.html +105 -0
- webview/templates/autonomy.html +60 -0
- webview/templates/error.html +133 -0
- webview/templates/finance.html +90 -0
- webview/templates/identity.html +73 -0
- webview/templates/index.html +42 -0
- webview/templates/layout.html +125 -0
- webview/templates/memory.html +59 -0
- webview/templates/owner_login.html +15 -0
- webview/templates/profile.html +121 -0
- webview/templates/session.html +312 -0
- webview/templates/settings.html +534 -0
- webview/templates/sidebar.html +112 -0
- webview/templates/signin.html +213 -0
- webview/templates/status.html +14 -0
- webview/templates/system.html +45 -0
- webview/webgate.py +196 -0
api/a2a/task_handler.py
ADDED
|
@@ -0,0 +1,929 @@
|
|
|
1
|
+
"""A2A Task Handler - Maps A2A tasks to POLYROB sessions.
|
|
2
|
+
|
|
3
|
+
This module handles the mapping between A2A protocol concepts
|
|
4
|
+
and POLYROB's internal session management:
|
|
5
|
+
|
|
6
|
+
A2A Concept → POLYROB Equivalent
|
|
7
|
+
---------------------------------
|
|
8
|
+
Task → Session
|
|
9
|
+
TaskState → SessionStatus
|
|
10
|
+
Message → UserMessage/feed events
|
|
11
|
+
Artifact → Workspace files
|
|
12
|
+
contextId → user_id grouping
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import logging
|
|
16
|
+
import os
|
|
17
|
+
from typing import Optional, Dict, Any, List, Tuple
|
|
18
|
+
|
|
19
|
+
from core.env import bool_env as _bool_env
|
|
20
|
+
from core.exceptions import SessionOwnershipError
|
|
21
|
+
from datetime import datetime
|
|
22
|
+
import uuid
|
|
23
|
+
import json
|
|
24
|
+
import asyncio
|
|
25
|
+
|
|
26
|
+
from api.dependencies import resolve_orchestrator
|
|
27
|
+
|
|
28
|
+
from api.a2a.models import (
|
|
29
|
+
A2ATaskState, A2ATask, A2ATaskStatus, A2AMessage, A2AArtifact,
|
|
30
|
+
SendMessageRequest, Part, TextPart, FilePart, DataPart,
|
|
31
|
+
PushNotificationConfig
|
|
32
|
+
)
|
|
33
|
+
from agents.task.agent.session import SessionStatus
|
|
34
|
+
|
|
35
|
+
logger = logging.getLogger(__name__)
|
|
36
|
+
|
|
37
|
+
# H2 FIX: hold strong references to fire-and-forget session tasks. The event loop
|
|
38
|
+
# keeps only a weak reference to a bare asyncio.Task, so a task whose only reference
|
|
39
|
+
# was the create_task() return value can be garbage-collected mid-run and silently
|
|
40
|
+
# cancelled. These tasks run entire agent sessions, so that is a real session-death risk.
|
|
41
|
+
_BACKGROUND_SESSION_TASKS: set = set()
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _spawn_session_task(coro) -> "asyncio.Task":
|
|
45
|
+
"""Schedule a background coroutine while retaining a strong reference to it."""
|
|
46
|
+
task = asyncio.create_task(coro)
|
|
47
|
+
_BACKGROUND_SESSION_TASKS.add(task)
|
|
48
|
+
task.add_done_callback(_BACKGROUND_SESSION_TASKS.discard)
|
|
49
|
+
return task
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
# =============================================================================
|
|
53
|
+
# Status Mapping
|
|
54
|
+
# =============================================================================
|
|
55
|
+
|
|
56
|
+
# Map POLYROB SessionStatus to A2A TaskState
|
|
57
|
+
# Note: "paused" was removed from SessionStatus - use cancelled for user interruption
|
|
58
|
+
ROB_TO_A2A_STATE: Dict[str, A2ATaskState] = {
|
|
59
|
+
"created": A2ATaskState.SUBMITTED,
|
|
60
|
+
"running": A2ATaskState.WORKING,
|
|
61
|
+
"resumed": A2ATaskState.WORKING,
|
|
62
|
+
"completed": A2ATaskState.COMPLETED,
|
|
63
|
+
"suspended": A2ATaskState.INPUT_REQUIRED,
|
|
64
|
+
"failed": A2ATaskState.FAILED,
|
|
65
|
+
"error": A2ATaskState.FAILED,
|
|
66
|
+
"cancelled": A2ATaskState.CANCELED,
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
# Map A2A TaskState to POLYROB SessionStatus
|
|
70
|
+
A2A_TO_ROB_STATE: Dict[A2ATaskState, str] = {
|
|
71
|
+
A2ATaskState.SUBMITTED: "created",
|
|
72
|
+
A2ATaskState.WORKING: "running",
|
|
73
|
+
A2ATaskState.INPUT_REQUIRED: "suspended",
|
|
74
|
+
A2ATaskState.AUTH_REQUIRED: "suspended",
|
|
75
|
+
A2ATaskState.COMPLETED: "completed",
|
|
76
|
+
A2ATaskState.FAILED: "failed",
|
|
77
|
+
A2ATaskState.CANCELED: "cancelled",
|
|
78
|
+
A2ATaskState.REJECTED: "cancelled",
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
# =============================================================================
|
|
83
|
+
# Task Handler
|
|
84
|
+
# =============================================================================
|
|
85
|
+
|
|
86
|
+
class A2ATaskHandler:
|
|
87
|
+
"""Handles A2A task operations by delegating to TaskAgent."""
|
|
88
|
+
|
|
89
|
+
def __init__(self, container):
|
|
90
|
+
"""Initialize handler with dependency container.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
container: DependencyContainer with TaskAgent and services
|
|
94
|
+
"""
|
|
95
|
+
self.container = container
|
|
96
|
+
self.logger = logging.getLogger("a2a.task_handler")
|
|
97
|
+
|
|
98
|
+
# Cache for push notification configs
|
|
99
|
+
self._push_configs: Dict[str, PushNotificationConfig] = {}
|
|
100
|
+
|
|
101
|
+
def _get_task_agent(self):
|
|
102
|
+
"""Get TaskAgent from container."""
|
|
103
|
+
return self.container.get_agent("task_agent")
|
|
104
|
+
|
|
105
|
+
def _get_session_manager(self):
|
|
106
|
+
"""Get SessionManager from container or TaskAgent."""
|
|
107
|
+
sm = self.container.get_service("session_manager")
|
|
108
|
+
if not sm:
|
|
109
|
+
agent = self._get_task_agent()
|
|
110
|
+
if agent:
|
|
111
|
+
sm = agent.session_manager
|
|
112
|
+
return sm
|
|
113
|
+
|
|
114
|
+
# =========================================================================
|
|
115
|
+
# Message/Part Extraction
|
|
116
|
+
# =========================================================================
|
|
117
|
+
|
|
118
|
+
def _extract_text_from_message(self, message: A2AMessage) -> str:
|
|
119
|
+
"""Extract text content from A2A message parts.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
message: A2A message with parts
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
Concatenated text from all text parts
|
|
126
|
+
"""
|
|
127
|
+
text_parts = []
|
|
128
|
+
for part in message.parts:
|
|
129
|
+
# Handle both dict and model formats
|
|
130
|
+
if isinstance(part, dict):
|
|
131
|
+
if part.get("kind") == "text" or "text" in part:
|
|
132
|
+
text_parts.append(part.get("text", ""))
|
|
133
|
+
elif hasattr(part, 'text'):
|
|
134
|
+
text_parts.append(part.text)
|
|
135
|
+
text = "\n".join(filter(None, text_parts))
|
|
136
|
+
|
|
137
|
+
# C1/A3: opt-in context-reference expansion for remote A2A intake.
|
|
138
|
+
# Filesystem refs are refused (no trusted session workspace here); only @url
|
|
139
|
+
# expands, behind the SSRF guard. Fails soft — never breaks task creation.
|
|
140
|
+
if text:
|
|
141
|
+
from agents.task.constants import AutonomyConfig
|
|
142
|
+
if AutonomyConfig.context_references_enabled():
|
|
143
|
+
try:
|
|
144
|
+
from agents.task.agent.messages.context_references import (
|
|
145
|
+
preprocess_context_references,
|
|
146
|
+
)
|
|
147
|
+
text = preprocess_context_references(
|
|
148
|
+
text, confine_to_root=True, allow_filesystem=False
|
|
149
|
+
)
|
|
150
|
+
except Exception as e:
|
|
151
|
+
self.logger.debug(f"context-ref expansion skipped: {e}")
|
|
152
|
+
return text
|
|
153
|
+
|
|
154
|
+
def _extract_files_from_message(self, message: A2AMessage) -> List[Dict[str, Any]]:
|
|
155
|
+
"""Extract file parts from A2A message.
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
message: A2A message with parts
|
|
159
|
+
|
|
160
|
+
Returns:
|
|
161
|
+
List of file part dictionaries
|
|
162
|
+
"""
|
|
163
|
+
files = []
|
|
164
|
+
for part in message.parts:
|
|
165
|
+
if isinstance(part, dict):
|
|
166
|
+
if part.get("kind") in ["file", "data"]:
|
|
167
|
+
files.append(part)
|
|
168
|
+
elif hasattr(part, 'kind') and part.kind in ["file", "data"]:
|
|
169
|
+
files.append(part.dict() if hasattr(part, 'dict') else part)
|
|
170
|
+
return files
|
|
171
|
+
|
|
172
|
+
def _extract_images_from_message(self, message: A2AMessage) -> List[Dict[str, Any]]:
|
|
173
|
+
"""Extract image attachments for vision processing.
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
message: A2A message with parts
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
List of image data dicts compatible with vision models
|
|
180
|
+
"""
|
|
181
|
+
images = []
|
|
182
|
+
for part in message.parts:
|
|
183
|
+
part_dict = part if isinstance(part, dict) else (part.dict() if hasattr(part, 'dict') else {})
|
|
184
|
+
|
|
185
|
+
if part_dict.get("kind") == "file":
|
|
186
|
+
file_data = part_dict.get("file", {})
|
|
187
|
+
mime_type = file_data.get("mimeType", "")
|
|
188
|
+
|
|
189
|
+
# Check if it's an image
|
|
190
|
+
if mime_type.startswith("image/"):
|
|
191
|
+
if "bytes" in file_data:
|
|
192
|
+
# Inline base64 image
|
|
193
|
+
images.append({
|
|
194
|
+
'type': 'image_url',
|
|
195
|
+
'image_url': {
|
|
196
|
+
'url': f'data:{mime_type};base64,{file_data["bytes"]}'
|
|
197
|
+
}
|
|
198
|
+
})
|
|
199
|
+
elif "uri" in file_data:
|
|
200
|
+
# URI reference
|
|
201
|
+
images.append({
|
|
202
|
+
'type': 'image_url',
|
|
203
|
+
'image_url': {
|
|
204
|
+
'url': file_data["uri"]
|
|
205
|
+
}
|
|
206
|
+
})
|
|
207
|
+
return images
|
|
208
|
+
|
|
209
|
+
# =========================================================================
|
|
210
|
+
# State Conversion
|
|
211
|
+
# =========================================================================
|
|
212
|
+
|
|
213
|
+
def _session_status_to_a2a_state(self, status: str) -> A2ATaskState:
|
|
214
|
+
"""Convert POLYROB session status to A2A task state.
|
|
215
|
+
|
|
216
|
+
Args:
|
|
217
|
+
status: POLYROB session status string
|
|
218
|
+
|
|
219
|
+
Returns:
|
|
220
|
+
Corresponding A2ATaskState
|
|
221
|
+
"""
|
|
222
|
+
return ROB_TO_A2A_STATE.get(
|
|
223
|
+
status.lower() if status else "unknown",
|
|
224
|
+
A2ATaskState.UNKNOWN
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
def _a2a_state_to_session_status(self, state: A2ATaskState) -> str:
|
|
228
|
+
"""Convert A2A task state to POLYROB session status.
|
|
229
|
+
|
|
230
|
+
Args:
|
|
231
|
+
state: A2A task state
|
|
232
|
+
|
|
233
|
+
Returns:
|
|
234
|
+
Corresponding POLYROB session status string
|
|
235
|
+
"""
|
|
236
|
+
return A2A_TO_ROB_STATE.get(state, "unknown")
|
|
237
|
+
|
|
238
|
+
# =========================================================================
|
|
239
|
+
# Task Operations
|
|
240
|
+
# =========================================================================
|
|
241
|
+
|
|
242
|
+
async def create_task(
|
|
243
|
+
self,
|
|
244
|
+
request: SendMessageRequest,
|
|
245
|
+
user_id: str,
|
|
246
|
+
context_id: Optional[str] = None
|
|
247
|
+
) -> A2ATask:
|
|
248
|
+
"""Create a new A2A task (maps to POLYROB session).
|
|
249
|
+
|
|
250
|
+
Args:
|
|
251
|
+
request: SendMessageRequest with initial message
|
|
252
|
+
user_id: User/payer identifier
|
|
253
|
+
context_id: Optional context ID for grouping
|
|
254
|
+
|
|
255
|
+
Returns:
|
|
256
|
+
Created A2ATask
|
|
257
|
+
|
|
258
|
+
Raises:
|
|
259
|
+
RuntimeError: If TaskAgent unavailable
|
|
260
|
+
ValueError: If message has no text content
|
|
261
|
+
"""
|
|
262
|
+
agent = self._get_task_agent()
|
|
263
|
+
if not agent:
|
|
264
|
+
raise RuntimeError("TaskAgent not available")
|
|
265
|
+
|
|
266
|
+
# Extract task text from message
|
|
267
|
+
task_text = self._extract_text_from_message(request.message)
|
|
268
|
+
if not task_text:
|
|
269
|
+
raise ValueError("No text content in message")
|
|
270
|
+
|
|
271
|
+
# Extract images for vision
|
|
272
|
+
image_attachments = self._extract_images_from_message(request.message)
|
|
273
|
+
|
|
274
|
+
# Get configuration from request
|
|
275
|
+
config = request.configuration or {}
|
|
276
|
+
metadata = request.message.metadata or {}
|
|
277
|
+
|
|
278
|
+
# Prepare session config
|
|
279
|
+
from agents.task_agent_lite import SessionRequest
|
|
280
|
+
session_request = SessionRequest(
|
|
281
|
+
task=task_text,
|
|
282
|
+
model=metadata.get("model", config.get("model", "gpt-5")),
|
|
283
|
+
provider=metadata.get("provider", config.get("provider", "openai")),
|
|
284
|
+
tools=metadata.get("tools", config.get("tools", ["browser", "filesystem"])),
|
|
285
|
+
max_steps=metadata.get("max_steps", config.get("max_steps", 50)),
|
|
286
|
+
use_vision=metadata.get("use_vision", config.get("use_vision", True))
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
# Allow client to specify task ID
|
|
290
|
+
task_id = request.message.taskId
|
|
291
|
+
|
|
292
|
+
# Create session
|
|
293
|
+
try:
|
|
294
|
+
session_info = await agent.create_session(
|
|
295
|
+
user_id=user_id,
|
|
296
|
+
request=session_request,
|
|
297
|
+
session_id=task_id
|
|
298
|
+
)
|
|
299
|
+
except SessionOwnershipError:
|
|
300
|
+
# C4: client-supplied taskId belongs to another tenant. Stay
|
|
301
|
+
# probe-resistant (mirror _authorize_owner: never a distinguishable 403).
|
|
302
|
+
raise ValueError(f"Task {task_id} could not be created")
|
|
303
|
+
|
|
304
|
+
session_id = session_info["id"]
|
|
305
|
+
|
|
306
|
+
# Store context ID mapping
|
|
307
|
+
if context_id or request.message.contextId:
|
|
308
|
+
ctx_id = context_id or request.message.contextId
|
|
309
|
+
sm = self._get_session_manager()
|
|
310
|
+
if sm:
|
|
311
|
+
sm.update_session_metadata(session_id, {
|
|
312
|
+
"a2a_context_id": ctx_id
|
|
313
|
+
})
|
|
314
|
+
|
|
315
|
+
# Store push notification config if provided
|
|
316
|
+
push_config = config.get("pushNotificationConfig")
|
|
317
|
+
if push_config:
|
|
318
|
+
self._push_configs[session_id] = PushNotificationConfig(**push_config)
|
|
319
|
+
sm = self._get_session_manager()
|
|
320
|
+
if sm:
|
|
321
|
+
sm.update_session_metadata(session_id, {
|
|
322
|
+
"a2a_push_url": push_config.get("url")
|
|
323
|
+
})
|
|
324
|
+
|
|
325
|
+
# Queue initial message with images if present
|
|
326
|
+
orchestrator = await resolve_orchestrator(session_id, agent)
|
|
327
|
+
if orchestrator and image_attachments:
|
|
328
|
+
try:
|
|
329
|
+
await orchestrator.submit_user_message(
|
|
330
|
+
agent_id=None,
|
|
331
|
+
text=task_text,
|
|
332
|
+
kind="a2a_initial",
|
|
333
|
+
metadata={"image_attachments": image_attachments}
|
|
334
|
+
)
|
|
335
|
+
self.logger.info(f"Queued initial message with {len(image_attachments)} images")
|
|
336
|
+
except Exception as e:
|
|
337
|
+
self.logger.error(f"Failed to queue initial message: {e}")
|
|
338
|
+
|
|
339
|
+
# Start session execution in background (strong ref retained — see
|
|
340
|
+
# _spawn_session_task). Wrapped so the registered webhook (if any) is
|
|
341
|
+
# notified when the run settles (T4-11).
|
|
342
|
+
_spawn_session_task(self._run_session_with_push(agent, user_id, session_id))
|
|
343
|
+
|
|
344
|
+
# Build A2A task response
|
|
345
|
+
return A2ATask(
|
|
346
|
+
id=session_id,
|
|
347
|
+
contextId=context_id or request.message.contextId or user_id,
|
|
348
|
+
status=A2ATaskStatus(
|
|
349
|
+
state=A2ATaskState.SUBMITTED,
|
|
350
|
+
timestamp=datetime.now().isoformat()
|
|
351
|
+
),
|
|
352
|
+
metadata={
|
|
353
|
+
"session_id": session_id,
|
|
354
|
+
"task": task_text,
|
|
355
|
+
"created_at": datetime.now().isoformat(),
|
|
356
|
+
"model": session_request.model,
|
|
357
|
+
"tools": session_request.tools
|
|
358
|
+
}
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
@staticmethod
|
|
362
|
+
def _authorize_owner(session_info: dict, task_id: str, user_id: Optional[str]) -> None:
|
|
363
|
+
"""Enforce task ownership. Raises not-found (never a distinguishable 403)
|
|
364
|
+
when ``user_id`` is supplied and does not own the session, so a caller
|
|
365
|
+
cannot use the error to probe another tenant's task-IDs. ``user_id=None``
|
|
366
|
+
means the caller already authorized (internal re-fetch) — no check."""
|
|
367
|
+
if user_id is not None and session_info.get("user_id") != user_id:
|
|
368
|
+
raise ValueError(f"Task {task_id} not found")
|
|
369
|
+
|
|
370
|
+
async def get_task(
|
|
371
|
+
self,
|
|
372
|
+
task_id: str,
|
|
373
|
+
history_length: Optional[int] = None,
|
|
374
|
+
user_id: Optional[str] = None
|
|
375
|
+
) -> A2ATask:
|
|
376
|
+
"""Get current status of an A2A task.
|
|
377
|
+
|
|
378
|
+
Args:
|
|
379
|
+
task_id: Task/session ID
|
|
380
|
+
history_length: Number of history messages to include
|
|
381
|
+
None = all, 0 = none, >0 = last N
|
|
382
|
+
|
|
383
|
+
Returns:
|
|
384
|
+
A2ATask with current status
|
|
385
|
+
|
|
386
|
+
Raises:
|
|
387
|
+
ValueError: If task not found
|
|
388
|
+
"""
|
|
389
|
+
agent = self._get_task_agent()
|
|
390
|
+
if not agent:
|
|
391
|
+
raise RuntimeError("TaskAgent not available")
|
|
392
|
+
|
|
393
|
+
session_info = await agent.get_session_by_id(task_id)
|
|
394
|
+
if not session_info:
|
|
395
|
+
raise ValueError(f"Task {task_id} not found")
|
|
396
|
+
self._authorize_owner(session_info, task_id, user_id)
|
|
397
|
+
|
|
398
|
+
# Map status to A2A state
|
|
399
|
+
rob_status = session_info.get("status", "unknown")
|
|
400
|
+
a2a_state = self._session_status_to_a2a_state(rob_status)
|
|
401
|
+
|
|
402
|
+
# Collect artifacts if completed
|
|
403
|
+
artifacts = []
|
|
404
|
+
if A2ATaskState.is_terminal(a2a_state):
|
|
405
|
+
artifacts = await self._collect_artifacts(task_id, session_info)
|
|
406
|
+
|
|
407
|
+
# Build history if requested
|
|
408
|
+
history = None
|
|
409
|
+
if history_length is None or history_length > 0:
|
|
410
|
+
history = await self._build_history(task_id, session_info, history_length)
|
|
411
|
+
|
|
412
|
+
# Get context ID
|
|
413
|
+
context_id = session_info.get("metadata", {}).get("a2a_context_id")
|
|
414
|
+
if not context_id:
|
|
415
|
+
context_id = session_info.get("user_id", task_id)
|
|
416
|
+
|
|
417
|
+
return A2ATask(
|
|
418
|
+
id=task_id,
|
|
419
|
+
contextId=context_id,
|
|
420
|
+
status=A2ATaskStatus(
|
|
421
|
+
state=a2a_state,
|
|
422
|
+
timestamp=session_info.get("updated_at", datetime.now().isoformat())
|
|
423
|
+
),
|
|
424
|
+
history=history,
|
|
425
|
+
artifacts=artifacts,
|
|
426
|
+
metadata={
|
|
427
|
+
"session_id": task_id,
|
|
428
|
+
"internal_status": rob_status,
|
|
429
|
+
"task": session_info.get("task"),
|
|
430
|
+
"created_at": session_info.get("created_at"),
|
|
431
|
+
"updated_at": session_info.get("updated_at"),
|
|
432
|
+
"model": session_info.get("config", {}).get("model"),
|
|
433
|
+
"tools": session_info.get("config", {}).get("tools", [])
|
|
434
|
+
}
|
|
435
|
+
)
|
|
436
|
+
|
|
437
|
+
async def send_message(
|
|
438
|
+
self,
|
|
439
|
+
task_id: str,
|
|
440
|
+
message: A2AMessage,
|
|
441
|
+
user_id: str
|
|
442
|
+
) -> A2ATask:
|
|
443
|
+
"""Send a message to an existing A2A task.
|
|
444
|
+
|
|
445
|
+
Args:
|
|
446
|
+
task_id: Task/session ID
|
|
447
|
+
message: Message to send
|
|
448
|
+
user_id: User identifier
|
|
449
|
+
|
|
450
|
+
Returns:
|
|
451
|
+
Updated A2ATask
|
|
452
|
+
|
|
453
|
+
Raises:
|
|
454
|
+
ValueError: If task not found or in terminal state
|
|
455
|
+
"""
|
|
456
|
+
agent = self._get_task_agent()
|
|
457
|
+
if not agent:
|
|
458
|
+
raise RuntimeError("TaskAgent not available")
|
|
459
|
+
|
|
460
|
+
session_info = await agent.get_session_by_id(task_id)
|
|
461
|
+
if not session_info:
|
|
462
|
+
raise ValueError(f"Task {task_id} not found")
|
|
463
|
+
# Ownership guard BEFORE any write/inject/resume: a non-owner must not be
|
|
464
|
+
# able to inject a message into or resume another tenant's session.
|
|
465
|
+
self._authorize_owner(session_info, task_id, user_id)
|
|
466
|
+
|
|
467
|
+
# Check if task is in terminal state
|
|
468
|
+
current_state = self._session_status_to_a2a_state(
|
|
469
|
+
session_info.get("status", "unknown")
|
|
470
|
+
)
|
|
471
|
+
if A2ATaskState.is_terminal(current_state):
|
|
472
|
+
raise ValueError(
|
|
473
|
+
f"Cannot send message to task in terminal state: {current_state.value}"
|
|
474
|
+
)
|
|
475
|
+
|
|
476
|
+
# Extract text and images
|
|
477
|
+
text = self._extract_text_from_message(message)
|
|
478
|
+
image_attachments = self._extract_images_from_message(message)
|
|
479
|
+
|
|
480
|
+
# Get orchestrator and queue message
|
|
481
|
+
orchestrator = await resolve_orchestrator(task_id, agent)
|
|
482
|
+
if orchestrator:
|
|
483
|
+
metadata = message.metadata or {}
|
|
484
|
+
if image_attachments:
|
|
485
|
+
metadata["image_attachments"] = image_attachments
|
|
486
|
+
|
|
487
|
+
await orchestrator.submit_user_message(
|
|
488
|
+
agent_id=None,
|
|
489
|
+
text=text,
|
|
490
|
+
kind="a2a_message",
|
|
491
|
+
metadata=metadata
|
|
492
|
+
)
|
|
493
|
+
self.logger.info(f"Queued message to task {task_id}")
|
|
494
|
+
|
|
495
|
+
# Resume if suspended
|
|
496
|
+
status = session_info.get("status", "").lower()
|
|
497
|
+
if status in ["completed", "suspended", "failed", "error"]:
|
|
498
|
+
self.logger.info(f"Resuming task {task_id} from {status}")
|
|
499
|
+
_spawn_session_task(self._run_session_with_push(
|
|
500
|
+
agent,
|
|
501
|
+
session_info.get("user_id"),
|
|
502
|
+
task_id
|
|
503
|
+
))
|
|
504
|
+
|
|
505
|
+
return await self.get_task(task_id)
|
|
506
|
+
|
|
507
|
+
async def cancel_task(self, task_id: str, user_id: str) -> A2ATask:
|
|
508
|
+
"""Cancel an A2A task.
|
|
509
|
+
|
|
510
|
+
Args:
|
|
511
|
+
task_id: Task/session ID
|
|
512
|
+
user_id: User identifier
|
|
513
|
+
|
|
514
|
+
Returns:
|
|
515
|
+
Updated A2ATask with canceled state
|
|
516
|
+
|
|
517
|
+
Raises:
|
|
518
|
+
ValueError: If task not found or already terminal
|
|
519
|
+
"""
|
|
520
|
+
agent = self._get_task_agent()
|
|
521
|
+
if not agent:
|
|
522
|
+
raise RuntimeError("TaskAgent not available")
|
|
523
|
+
|
|
524
|
+
session_info = await agent.get_session_by_id(task_id)
|
|
525
|
+
if not session_info:
|
|
526
|
+
raise ValueError(f"Task {task_id} not found")
|
|
527
|
+
|
|
528
|
+
# Check if already terminal
|
|
529
|
+
current_state = self._session_status_to_a2a_state(
|
|
530
|
+
session_info.get("status", "unknown")
|
|
531
|
+
)
|
|
532
|
+
if A2ATaskState.is_terminal(current_state):
|
|
533
|
+
raise ValueError(
|
|
534
|
+
f"Cannot cancel task in terminal state: {current_state.value}"
|
|
535
|
+
)
|
|
536
|
+
|
|
537
|
+
# Cancel via TaskAgent
|
|
538
|
+
success = await agent.cancel_session(
|
|
539
|
+
user_id=user_id,
|
|
540
|
+
session_id=task_id,
|
|
541
|
+
force=True
|
|
542
|
+
)
|
|
543
|
+
|
|
544
|
+
if not success:
|
|
545
|
+
raise ValueError(f"Failed to cancel task {task_id}")
|
|
546
|
+
|
|
547
|
+
# T4-11: the cancel outcome is known here — push CANCELED directly to the
|
|
548
|
+
# registered webhook (fail-open; a webhook failure never breaks the cancel).
|
|
549
|
+
try:
|
|
550
|
+
await self.send_push_notification(task_id, A2ATaskState.CANCELED)
|
|
551
|
+
except Exception as e:
|
|
552
|
+
self.logger.debug(f"push notify on cancel skipped: {e}")
|
|
553
|
+
|
|
554
|
+
return await self.get_task(task_id)
|
|
555
|
+
|
|
556
|
+
async def list_tasks(
|
|
557
|
+
self,
|
|
558
|
+
user_id: str,
|
|
559
|
+
context_id: Optional[str] = None,
|
|
560
|
+
page_token: Optional[str] = None,
|
|
561
|
+
page_size: int = 20
|
|
562
|
+
) -> Tuple[List[A2ATask], Optional[str]]:
|
|
563
|
+
"""List tasks for a user.
|
|
564
|
+
|
|
565
|
+
Args:
|
|
566
|
+
user_id: User identifier
|
|
567
|
+
context_id: Optional context filter
|
|
568
|
+
page_token: Pagination token
|
|
569
|
+
page_size: Number of results per page
|
|
570
|
+
|
|
571
|
+
Returns:
|
|
572
|
+
Tuple of (tasks, next_page_token)
|
|
573
|
+
"""
|
|
574
|
+
sm = self._get_session_manager()
|
|
575
|
+
if not sm:
|
|
576
|
+
return [], None
|
|
577
|
+
|
|
578
|
+
# Get all sessions
|
|
579
|
+
all_sessions = sm._sessions
|
|
580
|
+
|
|
581
|
+
# Filter by user
|
|
582
|
+
user_sessions = [
|
|
583
|
+
(sid, info) for sid, info in all_sessions.items()
|
|
584
|
+
if info.get("user_id") == user_id
|
|
585
|
+
]
|
|
586
|
+
|
|
587
|
+
# Filter by context if specified
|
|
588
|
+
if context_id:
|
|
589
|
+
user_sessions = [
|
|
590
|
+
(sid, info) for sid, info in user_sessions
|
|
591
|
+
if info.get("metadata", {}).get("a2a_context_id") == context_id
|
|
592
|
+
]
|
|
593
|
+
|
|
594
|
+
# Sort by creation time (newest first)
|
|
595
|
+
user_sessions.sort(
|
|
596
|
+
key=lambda x: x[1].get("created_at", ""),
|
|
597
|
+
reverse=True
|
|
598
|
+
)
|
|
599
|
+
|
|
600
|
+
# Handle pagination
|
|
601
|
+
start_idx = 0
|
|
602
|
+
if page_token:
|
|
603
|
+
try:
|
|
604
|
+
start_idx = int(page_token)
|
|
605
|
+
except ValueError:
|
|
606
|
+
start_idx = 0
|
|
607
|
+
|
|
608
|
+
end_idx = start_idx + page_size
|
|
609
|
+
page_sessions = user_sessions[start_idx:end_idx]
|
|
610
|
+
|
|
611
|
+
# Build tasks
|
|
612
|
+
tasks = []
|
|
613
|
+
for session_id, session_info in page_sessions:
|
|
614
|
+
try:
|
|
615
|
+
task = await self.get_task(session_id, history_length=0)
|
|
616
|
+
tasks.append(task)
|
|
617
|
+
except Exception as e:
|
|
618
|
+
self.logger.warning(f"Failed to build task for {session_id}: {e}")
|
|
619
|
+
|
|
620
|
+
# Calculate next page token
|
|
621
|
+
next_token = None
|
|
622
|
+
if end_idx < len(user_sessions):
|
|
623
|
+
next_token = str(end_idx)
|
|
624
|
+
|
|
625
|
+
return tasks, next_token
|
|
626
|
+
|
|
627
|
+
# =========================================================================
|
|
628
|
+
# Push Notifications
|
|
629
|
+
# =========================================================================
|
|
630
|
+
|
|
631
|
+
async def _authorize_task(self, task_id: str, user_id: Optional[str]) -> None:
|
|
632
|
+
"""Ownership gate for push-config ops: the caller must own ``task_id``.
|
|
633
|
+
|
|
634
|
+
Mirrors ``_authorize_owner`` (raises not-found, never a distinguishable 403,
|
|
635
|
+
so a caller can't probe another tenant's task ids). ``user_id=None`` means
|
|
636
|
+
the caller already authorized (internal use) — no check.
|
|
637
|
+
"""
|
|
638
|
+
if user_id is None:
|
|
639
|
+
return
|
|
640
|
+
agent = self._get_task_agent()
|
|
641
|
+
if not agent:
|
|
642
|
+
raise RuntimeError("TaskAgent not available")
|
|
643
|
+
session_info = await agent.get_session_by_id(task_id)
|
|
644
|
+
if not session_info:
|
|
645
|
+
raise ValueError(f"Task {task_id} not found")
|
|
646
|
+
self._authorize_owner(session_info, task_id, user_id)
|
|
647
|
+
|
|
648
|
+
async def set_push_notification_config(
|
|
649
|
+
self,
|
|
650
|
+
task_id: str,
|
|
651
|
+
config: PushNotificationConfig,
|
|
652
|
+
user_id: Optional[str] = None
|
|
653
|
+
) -> bool:
|
|
654
|
+
"""Set push notification config for a task.
|
|
655
|
+
|
|
656
|
+
Args:
|
|
657
|
+
task_id: Task/session ID
|
|
658
|
+
config: Push notification configuration
|
|
659
|
+
user_id: Authenticated caller — must own the task (IDOR gate)
|
|
660
|
+
|
|
661
|
+
Returns:
|
|
662
|
+
True if successful
|
|
663
|
+
"""
|
|
664
|
+
await self._authorize_task(task_id, user_id)
|
|
665
|
+
|
|
666
|
+
self._push_configs[task_id] = config
|
|
667
|
+
|
|
668
|
+
sm = self._get_session_manager()
|
|
669
|
+
if sm:
|
|
670
|
+
sm.update_session_metadata(task_id, {
|
|
671
|
+
"a2a_push_url": config.url,
|
|
672
|
+
"a2a_push_token": config.token
|
|
673
|
+
})
|
|
674
|
+
|
|
675
|
+
return True
|
|
676
|
+
|
|
677
|
+
async def get_push_notification_config(
|
|
678
|
+
self,
|
|
679
|
+
task_id: str,
|
|
680
|
+
user_id: Optional[str] = None
|
|
681
|
+
) -> Optional[PushNotificationConfig]:
|
|
682
|
+
"""Get push notification config for a task (caller must own the task)."""
|
|
683
|
+
await self._authorize_task(task_id, user_id)
|
|
684
|
+
return self._push_configs.get(task_id)
|
|
685
|
+
|
|
686
|
+
async def delete_push_notification_config(
|
|
687
|
+
self,
|
|
688
|
+
task_id: str,
|
|
689
|
+
user_id: Optional[str] = None
|
|
690
|
+
) -> bool:
|
|
691
|
+
"""Delete push notification config for a task (caller must own the task)."""
|
|
692
|
+
await self._authorize_task(task_id, user_id)
|
|
693
|
+
if task_id in self._push_configs:
|
|
694
|
+
del self._push_configs[task_id]
|
|
695
|
+
return True
|
|
696
|
+
return False
|
|
697
|
+
|
|
698
|
+
def _get_push_config(
|
|
699
|
+
self,
|
|
700
|
+
task_id: str,
|
|
701
|
+
session_info: Optional[Dict[str, Any]] = None
|
|
702
|
+
) -> Optional[PushNotificationConfig]:
|
|
703
|
+
"""Resolve the push config: in-memory first, then the session-metadata
|
|
704
|
+
mirror (restart-survivable — the in-memory dict dies with the process
|
|
705
|
+
while the metadata written at registration does not). T4-11."""
|
|
706
|
+
config = self._push_configs.get(task_id)
|
|
707
|
+
if config:
|
|
708
|
+
return config
|
|
709
|
+
try:
|
|
710
|
+
meta = (session_info or {}).get("metadata") or {}
|
|
711
|
+
url = meta.get("a2a_push_url")
|
|
712
|
+
if url:
|
|
713
|
+
return PushNotificationConfig(url=url, token=meta.get("a2a_push_token"))
|
|
714
|
+
except Exception:
|
|
715
|
+
pass
|
|
716
|
+
return None
|
|
717
|
+
|
|
718
|
+
async def _notify_push_state(
|
|
719
|
+
self,
|
|
720
|
+
task_id: str,
|
|
721
|
+
message: Optional[str] = None
|
|
722
|
+
) -> bool:
|
|
723
|
+
"""Push the task's CURRENT state to its registered webhook (T4-11).
|
|
724
|
+
|
|
725
|
+
Refetches the session so the delivered state reflects the real outcome
|
|
726
|
+
(completed/failed/suspended). No-op (False) when no webhook is
|
|
727
|
+
registered. Fail-open — a webhook failure never breaks the task."""
|
|
728
|
+
try:
|
|
729
|
+
agent = self._get_task_agent()
|
|
730
|
+
session_info = await agent.get_session_by_id(task_id) if agent else None
|
|
731
|
+
if not session_info:
|
|
732
|
+
return False
|
|
733
|
+
if self._get_push_config(task_id, session_info) is None:
|
|
734
|
+
return False
|
|
735
|
+
state = self._session_status_to_a2a_state(
|
|
736
|
+
session_info.get("status", "unknown"))
|
|
737
|
+
return await self.send_push_notification(task_id, state, message)
|
|
738
|
+
except Exception as e:
|
|
739
|
+
self.logger.debug(f"push state notify skipped for {task_id}: {e}")
|
|
740
|
+
return False
|
|
741
|
+
|
|
742
|
+
async def _run_session_with_push(self, agent, user_id: str, session_id: str) -> None:
|
|
743
|
+
"""Run the session and, when it settles (normally or by crash), deliver
|
|
744
|
+
the terminal state to the registered webhook (T4-11 — the agent card
|
|
745
|
+
advertises pushNotifications, so a registered client must hear back)."""
|
|
746
|
+
try:
|
|
747
|
+
await agent.run_session(user_id, session_id)
|
|
748
|
+
finally:
|
|
749
|
+
try:
|
|
750
|
+
await self._notify_push_state(session_id)
|
|
751
|
+
except Exception as e: # never mask the run outcome
|
|
752
|
+
self.logger.debug(f"push notify after run skipped: {e}")
|
|
753
|
+
|
|
754
|
+
async def send_push_notification(
|
|
755
|
+
self,
|
|
756
|
+
task_id: str,
|
|
757
|
+
state: A2ATaskState,
|
|
758
|
+
message: Optional[str] = None
|
|
759
|
+
) -> bool:
|
|
760
|
+
"""Send push notification for task update.
|
|
761
|
+
|
|
762
|
+
Args:
|
|
763
|
+
task_id: Task/session ID
|
|
764
|
+
state: Current task state
|
|
765
|
+
message: Optional status message
|
|
766
|
+
|
|
767
|
+
Returns:
|
|
768
|
+
True if notification sent successfully
|
|
769
|
+
"""
|
|
770
|
+
config = self._push_configs.get(task_id)
|
|
771
|
+
if not config:
|
|
772
|
+
# Restart-survivable fallback: rebuild from the metadata mirror.
|
|
773
|
+
try:
|
|
774
|
+
agent = self._get_task_agent()
|
|
775
|
+
session_info = await agent.get_session_by_id(task_id) if agent else None
|
|
776
|
+
config = self._get_push_config(task_id, session_info)
|
|
777
|
+
except Exception:
|
|
778
|
+
config = None
|
|
779
|
+
if not config:
|
|
780
|
+
return False
|
|
781
|
+
|
|
782
|
+
try:
|
|
783
|
+
import httpx
|
|
784
|
+
|
|
785
|
+
payload = {
|
|
786
|
+
"taskId": task_id,
|
|
787
|
+
"state": state.value,
|
|
788
|
+
"timestamp": datetime.now().isoformat()
|
|
789
|
+
}
|
|
790
|
+
if message:
|
|
791
|
+
payload["message"] = message
|
|
792
|
+
|
|
793
|
+
headers = {}
|
|
794
|
+
if config.token:
|
|
795
|
+
headers["X-A2A-Notification-Token"] = config.token
|
|
796
|
+
if config.authentication:
|
|
797
|
+
if "Bearer" in config.authentication.schemes:
|
|
798
|
+
headers["Authorization"] = f"Bearer {config.authentication.credentials}"
|
|
799
|
+
|
|
800
|
+
async with httpx.AsyncClient(timeout=10.0) as client:
|
|
801
|
+
response = await client.post(
|
|
802
|
+
config.url,
|
|
803
|
+
json=payload,
|
|
804
|
+
headers=headers
|
|
805
|
+
)
|
|
806
|
+
response.raise_for_status()
|
|
807
|
+
|
|
808
|
+
self.logger.info(f"Sent push notification for task {task_id}: {state.value}")
|
|
809
|
+
return True
|
|
810
|
+
|
|
811
|
+
except Exception as e:
|
|
812
|
+
self.logger.error(f"Failed to send push notification: {e}")
|
|
813
|
+
return False
|
|
814
|
+
|
|
815
|
+
# =========================================================================
|
|
816
|
+
# Helper Methods
|
|
817
|
+
# =========================================================================
|
|
818
|
+
|
|
819
|
+
async def _collect_artifacts(
|
|
820
|
+
self,
|
|
821
|
+
task_id: str,
|
|
822
|
+
session_info: Dict[str, Any]
|
|
823
|
+
) -> List[A2AArtifact]:
|
|
824
|
+
"""Collect artifacts from session workspace.
|
|
825
|
+
|
|
826
|
+
Args:
|
|
827
|
+
task_id: Task/session ID
|
|
828
|
+
session_info: Session metadata
|
|
829
|
+
|
|
830
|
+
Returns:
|
|
831
|
+
List of A2AArtifacts from workspace
|
|
832
|
+
"""
|
|
833
|
+
from agents.task.path import pm
|
|
834
|
+
import mimetypes
|
|
835
|
+
|
|
836
|
+
artifacts = []
|
|
837
|
+
user_id = session_info.get("user_id")
|
|
838
|
+
|
|
839
|
+
workspace_dir = pm().get_workspace_dir(task_id, user_id)
|
|
840
|
+
if not workspace_dir or not workspace_dir.exists():
|
|
841
|
+
return artifacts
|
|
842
|
+
|
|
843
|
+
for file_path in workspace_dir.rglob("*"):
|
|
844
|
+
if file_path.is_file():
|
|
845
|
+
mime_type, _ = mimetypes.guess_type(str(file_path))
|
|
846
|
+
relative_path = str(file_path.relative_to(workspace_dir))
|
|
847
|
+
|
|
848
|
+
artifacts.append(A2AArtifact(
|
|
849
|
+
artifactId=str(uuid.uuid4()),
|
|
850
|
+
name=file_path.name,
|
|
851
|
+
description=f"Generated file: {relative_path}",
|
|
852
|
+
parts=[{
|
|
853
|
+
"kind": "file",
|
|
854
|
+
"file": {
|
|
855
|
+
"uri": f"/api/task/sessions/{task_id}/workspace/{relative_path}",
|
|
856
|
+
"name": file_path.name,
|
|
857
|
+
"mimeType": mime_type or "application/octet-stream"
|
|
858
|
+
}
|
|
859
|
+
}],
|
|
860
|
+
metadata={
|
|
861
|
+
"path": relative_path,
|
|
862
|
+
"size": file_path.stat().st_size
|
|
863
|
+
}
|
|
864
|
+
))
|
|
865
|
+
|
|
866
|
+
return artifacts
|
|
867
|
+
|
|
868
|
+
async def _build_history(
|
|
869
|
+
self,
|
|
870
|
+
task_id: str,
|
|
871
|
+
session_info: Dict[str, Any],
|
|
872
|
+
limit: Optional[int] = None
|
|
873
|
+
) -> List[A2AMessage]:
|
|
874
|
+
"""Build message history from session feed.
|
|
875
|
+
|
|
876
|
+
Args:
|
|
877
|
+
task_id: Task/session ID
|
|
878
|
+
session_info: Session metadata
|
|
879
|
+
limit: Max messages to return (None = all)
|
|
880
|
+
|
|
881
|
+
Returns:
|
|
882
|
+
List of A2AMessages
|
|
883
|
+
"""
|
|
884
|
+
from agents.task.path import pm
|
|
885
|
+
|
|
886
|
+
messages = []
|
|
887
|
+
user_id = session_info.get("user_id")
|
|
888
|
+
|
|
889
|
+
feed_dir = pm().get_subdir(task_id, "feed", user_id=user_id)
|
|
890
|
+
if not feed_dir or not feed_dir.exists():
|
|
891
|
+
return messages
|
|
892
|
+
|
|
893
|
+
# Collect feed files
|
|
894
|
+
feed_files = sorted(feed_dir.glob("*.json"))
|
|
895
|
+
|
|
896
|
+
# Apply limit
|
|
897
|
+
if limit and limit > 0:
|
|
898
|
+
feed_files = feed_files[-limit:]
|
|
899
|
+
|
|
900
|
+
for feed_file in feed_files:
|
|
901
|
+
try:
|
|
902
|
+
with open(feed_file) as f:
|
|
903
|
+
event = json.load(f)
|
|
904
|
+
|
|
905
|
+
event_type = event.get("type", "")
|
|
906
|
+
event_data = event.get("data", {})
|
|
907
|
+
|
|
908
|
+
# Convert to A2A message based on event type
|
|
909
|
+
if event_type in ["user_message", "message"]:
|
|
910
|
+
messages.append(A2AMessage(
|
|
911
|
+
messageId=str(uuid.uuid4()),
|
|
912
|
+
role="user",
|
|
913
|
+
parts=[{"kind": "text", "text": event_data.get("text", "")}],
|
|
914
|
+
metadata=event_data.get("metadata")
|
|
915
|
+
))
|
|
916
|
+
elif event_type in ["agent_message", "step_result", "action_result"]:
|
|
917
|
+
text = event_data.get("text") or event_data.get("result", "")
|
|
918
|
+
if text:
|
|
919
|
+
messages.append(A2AMessage(
|
|
920
|
+
messageId=str(uuid.uuid4()),
|
|
921
|
+
role="agent",
|
|
922
|
+
parts=[{"kind": "text", "text": str(text)}],
|
|
923
|
+
metadata={"event_type": event_type}
|
|
924
|
+
))
|
|
925
|
+
|
|
926
|
+
except Exception as e:
|
|
927
|
+
self.logger.warning(f"Failed to parse feed file {feed_file}: {e}")
|
|
928
|
+
|
|
929
|
+
return messages
|