-
Notifications
You must be signed in to change notification settings - Fork 4k
fix(session): resumed threads keep their integration tools and tool_search #6589
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
senamakel
merged 35 commits into
tinyhumansai:main
from
senamakel:resumed-session-store
Sep 24, 2026
Merged
Changes from all commits
Commits
Show all changes
35 commits
Select commit
Hold shift + click to select a range
0f430ae
fix(session): hydrate integrations on every session start, not only c…
senamakel ad05dab
fix(test): add missing `tools` field to test transcript constructors
senamakel fd85718
chore(deps): add tinyagents vendor dependency
senamakel 94fbb14
chore(deps): update tinyagents submodule
senamakel 39283e2
chore(deps): add tinyagents vendor dependency
senamakel 62e3306
chore(deps): update tinyagents vendor dependency
senamakel 04c9b2d
chore: add recorded tools module
senamakel 34ec573
chore: add recorded tools tests
senamakel 5aab52f
fix(agent): handle session host runtime errors gracefully
senamakel 83ee6c0
chore(openhuman-core): remove unused prefix recovery helper
senamakel bc2a4f8
fix(runtime_session): restore session state after host restart
senamakel 6752e42
fix(scope): reduce visibility of internal methods and add test module
senamakel 9a636ae
fix(runtime_session): fix formatting of rehydrate_integration_actions…
senamakel 51b5cd9
chore(openhuman-core): update session host runtime imports
senamakel 4f0cb7b
fix(session_host): correct module path for recorded_integration_actions
senamakel de9108d
refactor(session-host): move delegation tool surface doc to its imple…
senamakel 2ae204b
chore: add blank line and Arc import in session host
senamakel 9f68947
docs(AGENTS.md): document tool list persistence in session transcripts
senamakel 20030cf
chore(AGENTS.md): remove duplicate bullet point about tool list recor…
senamakel 6f8bacf
chore: resolve tinyagents submodule merge
senamakel dcc4582
feat(session): filter rehydrated actions by current integration policy
senamakel 2db8081
feat(session): track whether connected integrations are authoritative
senamakel 527e21c
fix(recorded_tools): fix formatting of chained condition in rehydrate…
senamakel 6664e79
test(recorded_tools): add assertion that rebuilt declaration matches …
senamakel bca79b5
Merge remote-tracking branch 'upstream/main' into pr/6589
senamakel 8f4963d
fix(session): prune stale integration and MCP announcements on refresh
senamakel dc5b75b
fix(test): ensure rehydration test sets connected integrations
senamakel 02f10bf
fix(agent): reformat integration prelude and test code
senamakel 8ad1c03
chore(deps): update tinyagents submodule
senamakel 094e35e
chore(deps): update tinyagents submodule
senamakel ef23298
chore(deps): update tinyagents submodule
senamakel f0105e8
fix(session_host): skip non-integration tools during rehydration
senamakel d2349f1
test(recorded-tools): format test vector for readability
senamakel 0f4473b
fix(runtime_session): restore recorded tools on session resume
senamakel 1b9788f
fix(builder): remove authoritative flag from session host builder
senamakel File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
223 changes: 223 additions & 0 deletions
223
crates/openhuman-core/src/agent/session_host/prelude_integrations.rs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,223 @@ | ||
| //! Turn-boundary refresh of the prelude's integration state: hydrating the | ||
| //! connected integrations a session starts without, tracking connects and | ||
| //! revokes between turns, and adopting the integration actions the tinyagents | ||
| //! session restored for a resumed thread. | ||
|
|
||
| use std::sync::Arc; | ||
|
|
||
| use tinyagents_runtime::ToolSnapshot; | ||
|
|
||
| use super::OpenHumanTurnPrelude; | ||
|
|
||
| impl OpenHumanTurnPrelude { | ||
| /// Takes the declarations the tinyagents session restored for this | ||
| /// thread. Called before the boundary refresh so the rebuilt surface can | ||
| /// include them. | ||
| pub(super) fn adopt_recorded_tools(&self, recorded: Option<&ToolSnapshot>) { | ||
| let Some(recorded) = recorded else { | ||
| return; | ||
| }; | ||
| let actions = super::super::recorded_tools::recorded_integration_actions(recorded.specs()); | ||
|
senamakel marked this conversation as resolved.
|
||
| log::debug!( | ||
| "[session] adopting {} recorded integration action declaration(s) agent={}", | ||
| actions.len(), | ||
| self.agent_definition_id | ||
| ); | ||
| self.mutable | ||
| .lock() | ||
| .unwrap_or_else(std::sync::PoisonError::into_inner) | ||
| .recorded_integration_actions = actions; | ||
| } | ||
|
|
||
| pub(super) async fn refresh_turn_boundary(&self, cold: bool) { | ||
| // Hydrate on the first turn of *this session instance*, not only on a | ||
| // brand-new thread. A resumed thread is never `cold`, and a session | ||
| // rebuilt after a restart (or any rebuild past the 60 s integrations | ||
| // cache TTL) is seeded from an empty cache — gating the fetch on | ||
| // `cold` left it with zero integrations, no deferred Composio | ||
| // actions, and no `tool_search` bridge for the whole thread. | ||
| // `refresh_cold_integrations` is a no-op once hydrated. | ||
| self.refresh_cold_integrations().await; | ||
| if !cold { | ||
| self.refresh_dynamic_announcements().await; | ||
| } | ||
| // Integration changes are authority changes, not only display | ||
| // announcements. Refresh the delegation executable set and rebuild | ||
| // its schema/policy in the same hook pass before the driver sees it. | ||
| self.refresh_delegation_tool_surface(); | ||
| } | ||
|
|
||
| pub(super) async fn refresh_cold_integrations(&self) { | ||
| let should_fetch = !self | ||
| .mutable | ||
| .lock() | ||
| .unwrap_or_else(std::sync::PoisonError::into_inner) | ||
| .connected_integrations_initialized; | ||
| if !should_fetch { | ||
| return; | ||
| } | ||
| let config = match self.runtime_config.clone() { | ||
| Some(config) => Some(config), | ||
| None => crate::config::Config::load_or_init() | ||
| .await | ||
| .ok() | ||
| .map(Arc::new), | ||
| }; | ||
| let Some(config) = config else { | ||
| return; | ||
| }; | ||
| let Some((connected, authoritative)) = load_connected_integrations(&config).await else { | ||
| // Backend unreachable and nothing cached: stay un-hydrated so the | ||
| // next turn retries rather than pinning an empty surface. | ||
| log::warn!( | ||
| "[session] integrations unavailable and no cached snapshot; will retry next turn agent={}", | ||
| self.agent_definition_id | ||
| ); | ||
| return; | ||
| }; | ||
| log::info!( | ||
| "[session] hydrated connected integrations count={} agent={}", | ||
| connected.len(), | ||
| self.agent_definition_id | ||
| ); | ||
| let mcp_servers = crate::mcp::registry::connections::connected_overview() | ||
| .await | ||
| .into_iter() | ||
| .map(|server| server.qualified_name) | ||
| .collect::<std::collections::HashSet<_>>(); | ||
| let mut mutable = self | ||
| .mutable | ||
| .lock() | ||
| .unwrap_or_else(std::sync::PoisonError::into_inner); | ||
| mutable.connected_integrations = connected; | ||
| // A stale fallback is useful for announcements but cannot authorize | ||
| // restored executors. Leave hydration pending so a later turn retries | ||
| // the live lookup rather than pinning this session to the snapshot. | ||
| mutable.connected_integrations_initialized = authoritative; | ||
| mutable.connected_integrations_authoritative = authoritative; | ||
|
senamakel marked this conversation as resolved.
|
||
| mutable.announced_integrations = mutable | ||
| .connected_integrations | ||
| .iter() | ||
| .map(|item| item.toolkit.clone()) | ||
| .collect(); | ||
| mutable.announced_mcp_servers = mcp_servers; | ||
| } | ||
|
|
||
| pub(super) async fn refresh_dynamic_announcements(&self) { | ||
| let skills_changed = self.drain_host_events(); | ||
| let config = match self.runtime_config.clone() { | ||
| Some(config) => Some(config), | ||
| None => crate::config::Config::load_or_init() | ||
| .await | ||
| .ok() | ||
| .map(Arc::new), | ||
| }; | ||
| if let Some(config) = config.as_deref() { | ||
| // An expired cache is refetched rather than skipped, so a | ||
| // long-lived session keeps tracking connects/revokes. | ||
| let current = match crate::integrations::composio::cached_active_integrations(config) { | ||
|
senamakel marked this conversation as resolved.
|
||
| Some(current) => Some((current, true)), | ||
| None => load_connected_integrations(config).await, | ||
| }; | ||
| if let Some((current, authoritative)) = current { | ||
| let mut mutable = self | ||
| .mutable | ||
| .lock() | ||
| .unwrap_or_else(std::sync::PoisonError::into_inner); | ||
| let current_slugs: std::collections::HashSet<_> = | ||
| current.iter().map(|item| item.toolkit.clone()).collect(); | ||
| mutable | ||
| .announced_integrations | ||
| .retain(|slug| current_slugs.contains(slug)); | ||
| mutable | ||
| .pending_integration_announcement | ||
| .retain(|slug| current_slugs.contains(slug)); | ||
| for slug in ¤t_slugs { | ||
| if mutable.announced_integrations.insert(slug.clone()) | ||
| && !mutable.pending_integration_announcement.contains(slug) | ||
| { | ||
| mutable.pending_integration_announcement.push(slug.clone()); | ||
| } | ||
| } | ||
| mutable.connected_integrations = current; | ||
|
senamakel marked this conversation as resolved.
senamakel marked this conversation as resolved.
|
||
| mutable.connected_integrations_authoritative = authoritative; | ||
| } | ||
| } | ||
| let connected_mcp = crate::mcp::registry::connections::connected_overview() | ||
| .await | ||
| .into_iter() | ||
| .map(|server| server.qualified_name) | ||
| .collect::<Vec<_>>(); | ||
| let mut mutable = self | ||
| .mutable | ||
| .lock() | ||
| .unwrap_or_else(std::sync::PoisonError::into_inner); | ||
| let connected_mcp: std::collections::HashSet<_> = connected_mcp.into_iter().collect(); | ||
| mutable | ||
| .announced_mcp_servers | ||
| .retain(|server| connected_mcp.contains(server)); | ||
| mutable | ||
| .pending_mcp_announcement | ||
| .retain(|server| connected_mcp.contains(server)); | ||
| for server in connected_mcp { | ||
| if mutable.announced_mcp_servers.insert(server.clone()) | ||
| && !mutable.pending_mcp_announcement.contains(&server) | ||
| { | ||
| mutable.pending_mcp_announcement.push(server); | ||
| } | ||
| } | ||
| if !skills_changed { | ||
| return; | ||
| } | ||
| // Event-driven metadata refresh keeps the steady-state hot path free | ||
| // of the old per-turn filesystem scan. | ||
| let latest = crate::skills::load_workflow_metadata(&self.workspace_dir); | ||
| let id = |workflow: &crate::skills::Workflow| { | ||
| if workflow.dir_name.is_empty() { | ||
| workflow.name.clone() | ||
| } else { | ||
| workflow.dir_name.clone() | ||
| } | ||
| }; | ||
| let previous: std::collections::HashSet<_> = mutable.workflows.iter().map(&id).collect(); | ||
| let current: std::collections::HashSet<_> = latest.iter().map(&id).collect(); | ||
| for id in current.difference(&previous) { | ||
| if mutable.announced_skills.insert((*id).clone()) | ||
| && !mutable.pending_skill_announcement.contains(id) | ||
| { | ||
| mutable.pending_skill_announcement.push((*id).clone()); | ||
| } | ||
| } | ||
| for id in previous.difference(¤t) { | ||
| mutable.announced_skills.remove(id); | ||
| mutable | ||
| .pending_skill_announcement | ||
| .retain(|pending| pending != id); | ||
| if !mutable.pending_skill_retraction.contains(id) { | ||
| mutable.pending_skill_retraction.push((*id).clone()); | ||
| } | ||
| } | ||
| mutable.workflows = latest; | ||
| } | ||
| } | ||
|
|
||
| /// Live connected integrations, falling back to the last cached snapshot | ||
| /// (even past its TTL) when the backend is unreachable. `None` only when | ||
| /// there is neither a live answer nor any snapshot to fall back to. | ||
| async fn load_connected_integrations( | ||
| config: &crate::config::Config, | ||
| ) -> Option<(Vec<crate::agent::prompts::ConnectedIntegration>, bool)> { | ||
| use crate::integrations::composio::FetchConnectedIntegrationsStatus; | ||
| match crate::integrations::composio::fetch_connected_integrations_status(config).await { | ||
| FetchConnectedIntegrationsStatus::Authoritative(connected) => Some((connected, true)), | ||
| FetchConnectedIntegrationsStatus::Unavailable => { | ||
| let stale = | ||
|
senamakel marked this conversation as resolved.
|
||
| crate::integrations::composio::cached_active_integrations_including_expired(config); | ||
| log::warn!( | ||
|
senamakel marked this conversation as resolved.
|
||
| "[session] integrations fetch unavailable; using stale snapshot={}", | ||
| stale.as_ref().map_or(0, Vec::len) | ||
| ); | ||
| stale.map(|connected| (connected, false)) | ||
|
senamakel marked this conversation as resolved.
|
||
| } | ||
| } | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.