diff --git a/canvas/developers.mdx b/canvas/developers.mdx index de108a4af..48c239621 100644 --- a/canvas/developers.mdx +++ b/canvas/developers.mdx @@ -19,7 +19,8 @@ Two packages own this territory: ## The big picture -![DataManager architecture: the Actor calls primitives.data.*, which lands on DataManager (contract in base.py, implementations in ops/, schemas in types/); ContextRegistry supplies destinations; SimulatedDataManager mirrors the same contract in memory; persistence flows through unisdk HTTP to Orchestra, where contexts hold log events across personal Data/… and Teams/{id}/Data/… roots with federated reads between them.](/images/developers/datamanager-architecture.png) +DataManager architecture: the Actor calls primitives.data.*, which lands on DataManager (contract in base.py, implementations in ops/, schemas in types/); ContextRegistry supplies destinations; SimulatedDataManager mirrors the same contract in memory; persistence flows through unisdk HTTP to Orchestra, where contexts hold log events across personal Data/… and Teams/{id}/Data/… roots with federated reads between them. +DataManager architecture: the Actor calls primitives.data.*, which lands on DataManager (contract in base.py, implementations in ops/, schemas in types/); ContextRegistry supplies destinations; SimulatedDataManager mirrors the same contract in memory; persistence flows through unisdk HTTP to Orchestra, where contexts hold log events across personal Data/… and Teams/{id}/Data/… roots with federated reads between them. Both packages follow the same layered pattern, shared with every state manager in the repo: @@ -129,7 +130,8 @@ Context paths resolve through three private helpers in `DataManager`: `federated_ranked_search`, and `federated_reduce` from [`unify/common/federated_search.py`](https://github.com/unifyai/unify/blob/main/unify/common/federated_search.py). -![Destination scopes: write_root routes each write to exactly one destination — the personal root or a team root, each holding Data and Dashboards contexts — while read_roots federates reads across personal plus all teams; data_scope additionally lets a personal tile read team data.](/images/developers/destination-scopes.png) +Destination scopes: write_root routes each write to exactly one destination — the personal root or a team root, each holding Data and Dashboards contexts — while read_roots federates reads across personal plus all teams; data_scope additionally lets a personal tile read team data. +Destination scopes: write_root routes each write to exactly one destination — the personal root or a team root, each holding Data and Dashboards contexts — while read_roots federates reads across personal plus all teams; data_scope additionally lets a personal tile read team data. `ContextRegistry` ([`unify/common/context_registry.py`](https://github.com/unifyai/unify/blob/main/unify/common/context_registry.py)) @@ -315,7 +317,8 @@ bindings; *the Actor never writes bridge API calls*. ## Life of a live tile -![Live tile lifecycle: the Actor calls create_tile with HTML, data bindings, and on_data; DashboardManager validates and dry-runs the bindings, stores the tile row and registers its token; the Console TileViewer renders the iframe and injects the UnifyData bridge, which runs live filter/reduce/join queries against the Orchestra bridge at render time; results are handed to on_data so the chart updates with fresh data.](/images/developers/live-tile-lifecycle.png) +Live tile lifecycle: the Actor calls create_tile with HTML, data bindings, and on_data; DashboardManager validates and dry-runs the bindings, stores the tile row and registers its token; the Console TileViewer renders the iframe and injects the UnifyData bridge, which runs live filter/reduce/join queries against the Orchestra bridge at render time; results are handed to on_data so the chart updates with fresh data. +Live tile lifecycle: the Actor calls create_tile with HTML, data bindings, and on_data; DashboardManager validates and dry-runs the bindings, stores the tile row and registers its token; the Console TileViewer renders the iframe and injects the UnifyData bridge, which runs live filter/reduce/join queries against the Orchestra bridge at render time; results are handed to on_data so the chart updates with fresh data. The `create_tile` pipeline in `DashboardManager`, step by step: diff --git a/communication/behavior.mdx b/communication/behavior.mdx index cb8c3aa96..5a44c0ddd 100644 --- a/communication/behavior.mdx +++ b/communication/behavior.mdx @@ -28,9 +28,10 @@ Your assistant isn't purely reactive: tells you on an appropriate channel. - **Scheduled work** — [tasks](/tasks/overview) you've scheduled can end with a message, an email, or a call at the time you chose. -- **Gentle follow-ups** — if a conversation goes quiet while something needs - your input, it may send a friendly follow-up email. Ask it to stop and it - stops. +- **Gentle follow-ups** — if a conversation with your T-W1N goes quiet while + something needs your input, it may send a friendly follow-up email. This + re-engagement is specific to your T-W1N; hired teammates don't do it. Ask it + to stop and it stops. - **Ringing you** — when a live conversation beats a wall of text, it can ring you on [Unify Meet](/communication/unify-meet), and falls back to chat if you don't pick up. diff --git a/communication/developers/architecture.mdx b/communication/developers/architecture.mdx index c58e752e2..86e935629 100644 --- a/communication/developers/architecture.mdx +++ b/communication/developers/architecture.mdx @@ -25,7 +25,8 @@ The communication stack is two cooperating pieces: back through the gateway via [`unify/comms/primitives.py`](https://github.com/unifyai/unify/blob/main/unify/comms/primitives.py). -![unify.gateway architecture: providers feed adapters, which normalize to envelopes delivered through an EnvelopeSink to the ConversationManager; the ConversationManager sends outbound via channels](/images/developers/gateway-architecture.png) +unify.gateway architecture: providers feed adapters, which normalize to envelopes delivered through an EnvelopeSink to the ConversationManager; the ConversationManager sends outbound via channels +unify.gateway architecture: providers feed adapters, which normalize to envelopes delivered through an EnvelopeSink to the ConversationManager; the ConversationManager sends outbound via channels ## The wire contract: envelopes diff --git a/communication/developers/conversation-manager.mdx b/communication/developers/conversation-manager.mdx index ce4641612..60eb24ea9 100644 --- a/communication/developers/conversation-manager.mdx +++ b/communication/developers/conversation-manager.mdx @@ -10,7 +10,8 @@ state, consumes events, runs the reasoning loop, and delegates outbound I/O. Its package README describes it as the layer that interfaces with users while delegating complex reasoning to the `Actor`. -![Inbound event lifecycle: envelope → CommsManager → EventBroker → ConversationManager.wait_for_events → side effects → slow brain turn → comms tools / act / wait](/images/developers/event-lifecycle.png) +Inbound event lifecycle: envelope → CommsManager → EventBroker → ConversationManager.wait_for_events → side effects → slow brain turn → comms tools / act / wait +Inbound event lifecycle: envelope → CommsManager → EventBroker → ConversationManager.wait_for_events → side effects → slow brain turn → comms tools / act / wait ## The event model @@ -23,7 +24,10 @@ Subclasses auto-register via `__init_subclass__`, serialize with The catalogue is broad and symmetric: for most channels there's a `*Received` and a `*Sent` class (`SMSReceived`/`SMSSent`, -`EmailReceived`/`EmailSent`, `UnifyMessageReceived`/`UnifyMessageSent`…), +`EmailReceived`/`EmailSent`, `UnifyMessageReceived`/`UnifyMessageSent`, and — +for the org-installed Teams app — separate DM and channel pairs +`MsTeamsBotMessageReceived`/`MsTeamsBotMessageSent` and +`MsTeamsBotChannelMessageReceived`/`MsTeamsBotChannelMessageSent`…), plus call-lifecycle events (`PhoneCallReceived`, `PhoneCallAnswered`, `PhoneCallEnded`, `UnifyMeetStarted`…), per-utterance voice events (`InboundPhoneUtterance`, `OutboundUnifyMeetUtterance`…), reactions, @@ -52,7 +56,11 @@ Two mapping layers connect the world to handlers: defines the `Medium` enum — the single source of truth for channel types (`UNIFY_MESSAGE`, `EMAIL`, `SMS_MESSAGE`, `WHATSAPP_MESSAGE`, `WHATSAPP_CALL`, `PHONE_CALL`, `UNIFY_MEET`, `GOOGLE_MEET`, `TEAMS_MEET`, -Slack/Discord/Teams variants…). Each medium registers a `MediumInfo` with a +Slack/Discord variants, and both the delegated-Graph +(`TEAMS_MESSAGE`, `TEAMS_CHANNEL_MESSAGE`) and org-installed bot +(`MS_TEAMS_BOT_MESSAGE`, `MS_TEAMS_BOT_CHANNEL_MESSAGE`) Teams mediums…). +The two org-installed bot send tools are gated behind the assistant's +`has_ms_teams_bot` capability. Each medium registers a `MediumInfo` with a **mode**: `TEXT`, `CALL`, or `MEET` ([`cm_types/mode.py`](https://github.com/unifyai/unify/blob/main/unify/conversation_manager/cm_types/mode.py)). Mode is what gates behavior — voice tools like `guide_voice_agent` only diff --git a/communication/developers/gateway.mdx b/communication/developers/gateway.mdx index 7180e4807..5d42a21d2 100644 --- a/communication/developers/gateway.mdx +++ b/communication/developers/gateway.mdx @@ -51,7 +51,8 @@ its router(s): | [`gmail/`](https://github.com/unifyai/unify/blob/main/unify/gateway/channels/gmail/views.py) | `send_email` (threading headers), `watch_email` (Pub/Sub push watch), attachment fetch | `googleapiclient`, `google.oauth2` | | [`outlook/`](https://github.com/unifyai/unify/blob/main/unify/gateway/channels/outlook/views.py) | Same shape over Microsoft Graph subscriptions | `msgraph` via [`common/graph.py`](https://github.com/unifyai/unify/blob/main/unify/gateway/common/graph.py) | | [`email/`](https://github.com/unifyai/unify/blob/main/unify/gateway/channels/email/views.py) | Provider-agnostic dispatcher — routes `send_email` to Gmail or Outlook based on how the assistant's mailbox is connected | — | -| [`teams/`](https://github.com/unifyai/unify/blob/main/unify/gateway/channels/teams/views.py) | Chats, group chats, channels, watches, and meeting creation (`create_meeting.py`: instant links vs scheduled calendar events) | `msgraph` | +| [`teams/`](https://github.com/unifyai/unify/blob/main/unify/gateway/channels/teams/views.py) | Delegated Microsoft Graph Teams: chats, group chats, channels, watches, and meeting creation (`create_meeting.py`: instant links vs scheduled calendar events) | `msgraph` | +| [`ms_teams_bot/`](https://github.com/unifyai/unify/blob/main/unify/gateway/channels/ms_teams_bot/views.py) | Org-installed Bot Framework Teams app: pending-install / tenant-bind / install upsert, `send` (DM + channel/group), Bot Connector token minting from the bot's home tenant | Bot Framework Connector | | [`slack/`](https://github.com/unifyai/unify/blob/main/unify/gateway/channels/slack/views.py) | Workspace install upsert, `send_slack_message`, user lookup by email | Slack Web API | | [`discord/`](https://github.com/unifyai/unify/blob/main/unify/gateway/channels/discord/views.py) | `send_discord_message`, bot pool registration and status | Discord REST | | [`drive/`](https://github.com/unifyai/unify/blob/main/unify/gateway/channels/drive/views.py) / [`sharepoint/`](https://github.com/unifyai/unify/blob/main/unify/gateway/channels/sharepoint/views.py) | File browsing for the workspace file picker (not messaging) | Google Drive API, `msgraph` | @@ -63,7 +64,8 @@ its router(s): |---|---|---| | [`twilio.py`](https://github.com/unifyai/unify/blob/main/unify/gateway/adapters/twilio.py) | SMS, voice calls, call status, WhatsApp messages/reactions/calls/call status | `msg`, `call`, `call_answered`, `call_not_answered`, `whatsapp`, `whatsapp_reaction`, `whatsapp_call`, … | | [`google.py`](https://github.com/unifyai/unify/blob/main/unify/gateway/adapters/google.py) | Gmail Pub/Sub push notifications, Google OAuth callback and revoke | `email` | -| [`microsoft.py`](https://github.com/unifyai/unify/blob/main/unify/gateway/adapters/microsoft.py) | Outlook and Teams Graph notifications, Microsoft OAuth callback, a shared `/microsoft/router` delegator | `email`, `teams_chat`, `teams_channel` | +| [`microsoft.py`](https://github.com/unifyai/unify/blob/main/unify/gateway/adapters/microsoft.py) | Outlook and delegated-Teams Graph notifications, Microsoft OAuth callback, a shared `/microsoft/router` delegator | `email`, `teams_chat`, `teams_channel` | +| [`ms_teams_bot.py`](https://github.com/unifyai/unify/blob/main/unify/gateway/adapters/ms_teams_bot.py) | Bot Framework activity webhook for the org-installed Teams app; validates the inbound Bot Framework JWT ([`ms_teams_bot_auth.py`](https://github.com/unifyai/unify/blob/main/unify/gateway/adapters/ms_teams_bot_auth.py)), resolves the assistant via the dispatcher (token addressing + coordinator fallback) | `ms_teams_bot` | | [`slack.py`](https://github.com/unifyai/unify/blob/main/unify/gateway/adapters/slack.py) | Slack Events API, with a two-pass sender-identity resolution for coordinator routing and message dedup | `slack` | | [`internal.py`](https://github.com/unifyai/unify/blob/main/unify/gateway/adapters/internal.py) | Console/runtime routes: chat messages, reactions, attachments, Unify Meet rings, system events, pre-hire chat logging, wake-ups | `unify_message`, `unify_meet`, `api_message`, `unity_system_event`, … | diff --git a/communication/developers/voice.mdx b/communication/developers/voice.mdx index 95714bc65..9b5e718b4 100644 --- a/communication/developers/voice.mdx +++ b/communication/developers/voice.mdx @@ -8,7 +8,8 @@ sub-second conversational reflexes *and* deep reasoning, and no single loop provides both. The runtime splits the job between two brains that share one conversation. -![Voice call dual-brain architecture: the caller connects through a LiveKit room to the fast brain (Assistant agent with STT/VAD/TTS), which exchanges utterance events and FastBrainNotifications with the slow brain (ConversationManager), supported by fillers/barge-in handling and the LivekitCallManager](/images/developers/voice-dual-brain.png) +Voice call dual-brain architecture: the caller connects through a LiveKit room to the fast brain (Assistant agent with STT/VAD/TTS), which exchanges utterance events and FastBrainNotifications with the slow brain (ConversationManager), supported by fillers/barge-in handling and the LivekitCallManager +Voice call dual-brain architecture: the caller connects through a LiveKit room to the fast brain (Assistant agent with STT/VAD/TTS), which exchanges utterance events and FastBrainNotifications with the slow brain (ConversationManager), supported by fillers/barge-in handling and the LivekitCallManager ## The two brains diff --git a/communication/email.mdx b/communication/email.mdx index 2ccf4241c..533f691da 100644 --- a/communication/email.mdx +++ b/communication/email.mdx @@ -7,6 +7,13 @@ Your assistant can send and receive email from a real mailbox — replying in threads, handling attachments, and looping in cc'd colleagues, just like any teammate. + + **If this is your T-W1N** (your digital twin): its email is platform-managed + and already set up — there's nothing to create or configure. T-W1N acts on + *your* behalf, so it uses your own connected account rather than a mailbox of + its own. The setup below is for **hired teammates**. + + ## How your assistant gets an email address Your assistant uses a **Google (Gmail) or Microsoft (Outlook / Microsoft 365) @@ -32,12 +39,6 @@ mailbox, which means email from your assistant lands like email from a person. - - **T-W1N, your digital twin, is the exception**: it's meant to act on *your* - behalf, so you connect your own account to it. Its contact details are - platform-managed — there's nothing to create or configure. - - Connecting email is free — there are no setup or monthly credit costs for email contacts. diff --git a/communication/microsoft-teams.mdx b/communication/microsoft-teams.mdx index 19122912c..50e378f95 100644 --- a/communication/microsoft-teams.mdx +++ b/communication/microsoft-teams.mdx @@ -1,35 +1,61 @@ --- title: "Microsoft Teams" -description: "Chats, group chats, channels, and meetings with your assistant" +description: "Bring your assistants into Teams with the org-installed Unify app" --- -If your organization runs on Microsoft Teams, your assistant fits right in — -one-on-one chats, group chats, channel threads, and even creating and joining -Teams meetings. +If your organization runs on Microsoft Teams, the **Unify Teams app** brings +your assistants right into it — one-on-one chats, group chats, and channel +threads. It's an org-wide app your admin installs once, so everyone can reach +your assistants without any per-assistant setup. This is the Teams equivalent +of the [Slack app](/communication/slack): the assistant shows up as the shared +Unify bot, not as a person. + + + There's a second, separate way to use Teams: an assistant can act through + its **own connected Microsoft 365 account** (the same account as its + [mailbox](/workspace/email)) — appearing as that user rather than the shared + app. That path is part of the workspace connection and is documented under + [Workspace → Teams](/workspace/teams). + ## Setup -Teams comes with the **Microsoft 365 connection** — the same one used for -[Outlook email](/communication/email). When you connect a Microsoft account -for your assistant, grant the **Teams** feature and Teams chat is enabled. +A Microsoft admin installs the **Unify Teams app** into your organization's +Teams. Once it's installed, every assistant in the org — and your coordinator +— is reachable through it; there's nothing to configure per assistant. ## Messaging -- **1:1 chat** — message your assistant in Teams like any colleague. -- **Group chats** — your assistant can take part in group conversations with - two or more people, and can start them too. -- **Channels** — @mention your assistant in a channel and it replies in the - thread; it can also post to channels and even create new ones. -- **Attachments** — your assistant can send files in Teams messages. - -## Meetings - -Your assistant is unusually capable with Teams meetings: - -- **Create instant meetings** — ask for a meeting link and it generates one - on the spot. -- **Schedule meetings** — it can put a Teams meeting on the calendar with - invites sent to attendees. -- **Join meetings** — send your assistant a Teams meeting link and it joins - the call and participates with its voice, like any remote attendee. See - [Meetings](/communication/meetings). +- **1:1 chat** — message the Unify app in Teams and your coordinator picks up + the conversation. +- **Group chats** — add the app to a group chat and @mention it to bring it + in; it replies in the same conversation. +- **Channels** — @mention the app in a channel and it replies in the thread, + keeping channels tidy. + +## Reaching a specific teammate + +Because the app is shared, addressing works like Slack: + +- In a **channel or group chat**, @mention the app and include the teammate's + name to reach a specific assistant (for example, `@Unify Alex, can you…`). + @mention it with no name and the message goes to your **coordinator**, who + can hand off from there. +- In a **1:1 chat**, messages go to your coordinator — there's no name to add, + so naming a teammate isn't used here. + + + Teams only delivers channel and group-chat messages to the app when it's + **@mentioned**, so remember the mention when you want the assistant to see a + message in a shared space. In a 1:1 chat every message is delivered. + + +## Good to know + +- Your assistant only speaks in a channel when it's mentioned or directly + relevant — it won't spam your team's conversations. +- It's the same assistant across every surface: a task you discussed in Teams + can be delivered over email, and vice versa. +- **Meetings** are separate from the app. Your assistant can join and host + Teams meetings — see [Meetings](/workspace/meetings) for joining, and + [Workspace → Teams](/workspace/teams) for creating and scheduling them. diff --git a/communication/overview.mdx b/communication/overview.mdx index e49078dfe..4ab369fd9 100644 --- a/communication/overview.mdx +++ b/communication/overview.mdx @@ -20,9 +20,9 @@ assistant with the same memory of everything you've discussed. | [WhatsApp](/communication/whatsapp) | Yes | Yes | WhatsApp contact | | [Email](/communication/email) | Yes | Yes | Connected Gmail or Outlook | | [Slack](/communication/slack) | Yes | Yes | Slack app install | -| [Microsoft Teams](/communication/microsoft-teams) | Yes | Yes | Microsoft 365 connection | +| [Microsoft Teams](/communication/microsoft-teams) | Yes | Yes | Teams app install ([or Microsoft 365 connection](/workspace/teams)) | | [Discord](/communication/discord) | Yes | Yes | Discord ID on your profile | -| [Google Meet & Teams meetings](/communication/meetings) | Invite it to a meeting | It can create Teams meetings | Meeting link | +| [Google Meet & Teams meetings](/workspace/meetings) | Invite it to a meeting | It can create Teams meetings | Meeting link | ## Two channels work out of the box @@ -103,12 +103,12 @@ A few things that hold everywhere: DMs and channel mentions in your workspace. } href="/communication/microsoft-teams"> - Chats, group chats, channels, and meetings. + Chats, group chats, and channels via the org-installed Unify app. DMs and server channels. - } href="/communication/meetings"> + } href="/workspace/meetings"> Your assistant joins Google Meet and Teams meetings. diff --git a/communication/slack.mdx b/communication/slack.mdx index dcce45356..4b794c4a0 100644 --- a/communication/slack.mdx +++ b/communication/slack.mdx @@ -9,7 +9,7 @@ DM, @mention, and collaborate with in channels. ## Setup Connect Slack from the Console — your assistant can open the setup flow for -you, or you'll find it in the integrations area of your assistant's profile. +you, or you'll find it under your assistant's **Contact Details → Slack**. A workspace admin approves the Unify Slack app, and your assistant appears in your workspace. diff --git a/communication/voice.mdx b/communication/voice.mdx index e8147491e..6d40b4c70 100644 --- a/communication/voice.mdx +++ b/communication/voice.mdx @@ -6,7 +6,7 @@ description: "Choose how your assistant sounds on calls" Whenever your assistant speaks — on [Unify Meet](/communication/unify-meet), [phone calls](/communication/phone-calls), [WhatsApp calls](/communication/whatsapp), or in -[meetings](/communication/meetings) — it uses a voice you choose. +[meetings](/workspace/meetings) — it uses a voice you choose. ## Choosing a voice diff --git a/hiring/overview.mdx b/hiring/overview.mdx index 285ba773f..76eb1e5bf 100644 --- a/hiring/overview.mdx +++ b/hiring/overview.mdx @@ -63,6 +63,12 @@ For most organizations the right sequence is: +When you do hire, the dialog asks one ownership question up front: an +**owning team** (the default — the teammate belongs to the team itself, +everyone on it can work with it, and everything it learns is shared +there), or **Personal** (it reports to you alone). The trade-offs are +covered in [Team assistants](/teams/team-assistants). + Your T-W1N actively helps with this judgment — it's briefed to spot the moment: diff --git a/images/developers/architecture-flow-dark.svg b/images/developers/architecture-flow-dark.svg new file mode 100644 index 000000000..6eceb4525 --- /dev/null +++ b/images/developers/architecture-flow-dark.svg @@ -0,0 +1,7 @@ +ARCHITECTURE FLOWSTEERINGBUSUserMediumschat · voice · video · email · SMSEvent brokertyped pub/subConversationManagerinteraction loopact(…)CodeActActorbackground reasonerprimitives.*Back officeContactsKnowledgeTasksTranscriptsFilesImagesWebSecretsFunctionsGuidanceSteerableToolHandlehandle + notificationsstreamed responsesdispatchsteering diff --git a/images/developers/architecture-flow-light.svg b/images/developers/architecture-flow-light.svg new file mode 100644 index 000000000..e3f473ed4 --- /dev/null +++ b/images/developers/architecture-flow-light.svg @@ -0,0 +1,7 @@ +ARCHITECTURE FLOWSTEERINGBUSUserMediumschat · voice · video · email · SMSEvent brokertyped pub/subConversationManagerinteraction loopact(…)CodeActActorbackground reasonerprimitives.*Back officeContactsKnowledgeTasksTranscriptsFilesImagesWebSecretsFunctionsGuidanceSteerableToolHandlehandle + notificationsstreamed responsesdispatchsteering diff --git a/images/developers/datamanager-architecture-dark.svg b/images/developers/datamanager-architecture-dark.svg new file mode 100644 index 000000000..ce07eef43 --- /dev/null +++ b/images/developers/datamanager-architecture-dark.svg @@ -0,0 +1,7 @@ +DATAMANAGERActor (LLM)primitives.data.*DataManagerbase.py — contractops/ — implementationstypes/ — schemasContextRegistrydestinationsSimulatedDataManager(in-memory)same contractunisdk (HTTP)Orchestra— contexts + log eventsData/…Teams/{id}/Data/…federated reads diff --git a/images/developers/datamanager-architecture-light.svg b/images/developers/datamanager-architecture-light.svg new file mode 100644 index 000000000..ec611248e --- /dev/null +++ b/images/developers/datamanager-architecture-light.svg @@ -0,0 +1,7 @@ +DATAMANAGERActor (LLM)primitives.data.*DataManagerbase.py — contractops/ — implementationstypes/ — schemasContextRegistrydestinationsSimulatedDataManager(in-memory)same contractunisdk (HTTP)Orchestra— contexts + log eventsData/…Teams/{id}/Data/…federated reads diff --git a/images/developers/datamanager-architecture.png b/images/developers/datamanager-architecture.png deleted file mode 100644 index 4c921a9c3..000000000 Binary files a/images/developers/datamanager-architecture.png and /dev/null differ diff --git a/images/developers/demo-clarification-bubbling-technical-dark.svg b/images/developers/demo-clarification-bubbling-technical-dark.svg new file mode 100644 index 000000000..5b6055a4c --- /dev/null +++ b/images/developers/demo-clarification-bubbling-technical-dark.svg @@ -0,0 +1,7 @@ +UserConversationManagerActorContactManager"which Sarah? two matches""the one in Berlin"clarification up, answer back down diff --git a/images/developers/demo-clarification-bubbling-technical-light.svg b/images/developers/demo-clarification-bubbling-technical-light.svg new file mode 100644 index 000000000..4bfb480ec --- /dev/null +++ b/images/developers/demo-clarification-bubbling-technical-light.svg @@ -0,0 +1,7 @@ +UserConversationManagerActorContactManager"which Sarah? two matches""the one in Berlin"clarification up, answer back down diff --git a/images/developers/demo-course-correct-technical-dark.svg b/images/developers/demo-course-correct-technical-dark.svg new file mode 100644 index 000000000..e3a3f45b2 --- /dev/null +++ b/images/developers/demo-course-correct-technical-dark.svg @@ -0,0 +1,7 @@ +correction: "use WORK email"ConversationManagerstill runningActorstill runningTaskSchedulerstill runningContactManagerstill runninglive redirect, no restart diff --git a/images/developers/demo-course-correct-technical-light.svg b/images/developers/demo-course-correct-technical-light.svg new file mode 100644 index 000000000..c399b8919 --- /dev/null +++ b/images/developers/demo-course-correct-technical-light.svg @@ -0,0 +1,7 @@ +correction: "use WORK email"ConversationManagerstill runningActorstill runningTaskSchedulerstill runningContactManagerstill runninglive redirect, no restart diff --git a/images/developers/destination-scopes-dark.svg b/images/developers/destination-scopes-dark.svg new file mode 100644 index 000000000..2afa8d32e --- /dev/null +++ b/images/developers/destination-scopes-dark.svg @@ -0,0 +1,7 @@ +DESTINATION SCOPESwrite_root — exactly one destinationpersonalData/SalesDashboards/TilesDashboards/Layoutsteam:42Data/OpsDashboards/TilesDashboards/Layoutsread_roots — personal + all teamsdata_scope: a personal tile may read team data diff --git a/images/developers/destination-scopes-light.svg b/images/developers/destination-scopes-light.svg new file mode 100644 index 000000000..2a978b91f --- /dev/null +++ b/images/developers/destination-scopes-light.svg @@ -0,0 +1,7 @@ +DESTINATION SCOPESwrite_root — exactly one destinationpersonalData/SalesDashboards/TilesDashboards/Layoutsteam:42Data/OpsDashboards/TilesDashboards/Layoutsread_roots — personal + all teamsdata_scope: a personal tile may read team data diff --git a/images/developers/destination-scopes.png b/images/developers/destination-scopes.png deleted file mode 100644 index c31211b74..000000000 Binary files a/images/developers/destination-scopes.png and /dev/null differ diff --git a/images/developers/dev-computer-control-stack-dark.svg b/images/developers/dev-computer-control-stack-dark.svg new file mode 100644 index 000000000..b4e8e9fa4 --- /dev/null +++ b/images/developers/dev-computer-control-stack-dark.svg @@ -0,0 +1,7 @@ +COMPUTER CONTROLCodeActActor / ConversationManagerwrites plans · fast pathsact / observe / navigate / screenshotprimitives.computer.*desktopsingleton · one mousewebsession factoryuser_desktoplinked machines_vm_ready gate ·secret injectionHTTP · /act /extract /screenshotMagnitudeBackend → ComputerSessionasync HTTP · Bearer UNIFY_KEYcontainer_url → VMlocal_url → headless webHTTP · /act /extract /screenshotagent-serviceNode · Playwrightdesktop → noVNC pageweb-vm → visible Chromiumweb → headless ChromiumVM display: TigerVNC :5900 · noVNC :6080 · agent :3000 diff --git a/images/developers/dev-computer-control-stack-light.svg b/images/developers/dev-computer-control-stack-light.svg new file mode 100644 index 000000000..5f127ee31 --- /dev/null +++ b/images/developers/dev-computer-control-stack-light.svg @@ -0,0 +1,7 @@ +COMPUTER CONTROLCodeActActor / ConversationManagerwrites plans · fast pathsact / observe / navigate / screenshotprimitives.computer.*desktopsingleton · one mousewebsession factoryuser_desktoplinked machines_vm_ready gate ·secret injectionHTTP · /act /extract /screenshotMagnitudeBackend → ComputerSessionasync HTTP · Bearer UNIFY_KEYcontainer_url → VMlocal_url → headless webHTTP · /act /extract /screenshotagent-serviceNode · Playwrightdesktop → noVNC pageweb-vm → visible Chromiumweb → headless ChromiumVM display: TigerVNC :5900 · noVNC :6080 · agent :3000 diff --git a/images/developers/dev-computer-control-stack.png b/images/developers/dev-computer-control-stack.png deleted file mode 100644 index 1cafeaec1..000000000 Binary files a/images/developers/dev-computer-control-stack.png and /dev/null differ diff --git a/images/developers/dev-desktop-lifecycle-dark.svg b/images/developers/dev-desktop-lifecycle-dark.svg new file mode 100644 index 000000000..189860aa4 --- /dev/null +++ b/images/developers/dev-desktop-lifecycle-dark.svg @@ -0,0 +1,7 @@ +DESKTOP LIFECYCLEStartupEventdesktop_mode · user_desktops (no desktop_url yet)AssistantDesktopReadybinding_id · desktop_url · vm_typepublished when the VMpasses health checksvm_pending —desktop VM is still bootingapply_managed_desktop_ready()set desktop_url + _vm_readypublish liveview URLensure desktop sessionstart file sync (rclone bisync)FileSyncCompleteprior-session files now on disksync_pending —files do not exist on disk yetreadycomputer actions unlocked diff --git a/images/developers/dev-desktop-lifecycle-light.svg b/images/developers/dev-desktop-lifecycle-light.svg new file mode 100644 index 000000000..5fc955e25 --- /dev/null +++ b/images/developers/dev-desktop-lifecycle-light.svg @@ -0,0 +1,7 @@ +DESKTOP LIFECYCLEStartupEventdesktop_mode · user_desktops (no desktop_url yet)AssistantDesktopReadybinding_id · desktop_url · vm_typepublished when the VMpasses health checksvm_pending —desktop VM is still bootingapply_managed_desktop_ready()set desktop_url + _vm_readypublish liveview URLensure desktop sessionstart file sync (rclone bisync)FileSyncCompleteprior-session files now on disksync_pending —files do not exist on disk yetreadycomputer actions unlocked diff --git a/images/developers/dev-desktop-lifecycle.png b/images/developers/dev-desktop-lifecycle.png deleted file mode 100644 index 4cb291f2a..000000000 Binary files a/images/developers/dev-desktop-lifecycle.png and /dev/null differ diff --git a/images/developers/dev-screen-share-flow-dark.svg b/images/developers/dev-screen-share-flow-dark.svg new file mode 100644 index 000000000..be38ef032 --- /dev/null +++ b/images/developers/dev-screen-share-flow-dark.svg @@ -0,0 +1,7 @@ +SCREEN SHAREConsole (Unify Meet)Show assistant screenEnable mouse & keyboardnoVNC iframeview-only overlay by defaultConversationManagerassistant_screen_share_activeuser_remote_control_activescreenshot bufferfast paths: desktop_act / web_actflags gate prompts + toolsassistant_screen_share_started / stoppeduser_remote_control_started / stoppedactors (act sessions)interjected on controltake / releaseset_user_remote_control()slow brain LLMscreenshots attachedas vision input ·Screenshots/Assistant/JPEG frameslive desktop via noVNCsolid arrows: data flowdotted arrows: events / signals diff --git a/images/developers/dev-screen-share-flow-light.svg b/images/developers/dev-screen-share-flow-light.svg new file mode 100644 index 000000000..679603ec2 --- /dev/null +++ b/images/developers/dev-screen-share-flow-light.svg @@ -0,0 +1,7 @@ +SCREEN SHAREConsole (Unify Meet)Show assistant screenEnable mouse & keyboardnoVNC iframeview-only overlay by defaultConversationManagerassistant_screen_share_activeuser_remote_control_activescreenshot bufferfast paths: desktop_act / web_actflags gate prompts + toolsassistant_screen_share_started / stoppeduser_remote_control_started / stoppedactors (act sessions)interjected on controltake / releaseset_user_remote_control()slow brain LLMscreenshots attachedas vision input ·Screenshots/Assistant/JPEG frameslive desktop via noVNCsolid arrows: data flowdotted arrows: events / signals diff --git a/images/developers/dev-screen-share-flow.png b/images/developers/dev-screen-share-flow.png deleted file mode 100644 index 4ca0a8e68..000000000 Binary files a/images/developers/dev-screen-share-flow.png and /dev/null differ diff --git a/images/developers/event-lifecycle-dark.svg b/images/developers/event-lifecycle-dark.svg new file mode 100644 index 000000000..742dc6756 --- /dev/null +++ b/images/developers/event-lifecycle-dark.svg @@ -0,0 +1,7 @@ +EVENT LIFECYCLEenvelope arrives{thread, publish_timestamp, event}CommsManagerevents_map: thread → Event classcontact resolution via MEDIUM_TO_CONTACT_FIELDpublish app:comms:*EventBrokerin-memory pub/subConversationManager.wait_for_events()EventHandler.handle_event(event, cm)side effectsTranscriptManager.log_messages · ContactIndex · NotificationBarrequest_llm_run() → flushslow brain turnDebouncer → build_brain_spec() → single_shot_tool_decisioncomms toolssend_sms · send_email · …act(…)steerable Actor handlewaitend turn silently diff --git a/images/developers/event-lifecycle-light.svg b/images/developers/event-lifecycle-light.svg new file mode 100644 index 000000000..f277bcfe8 --- /dev/null +++ b/images/developers/event-lifecycle-light.svg @@ -0,0 +1,7 @@ +EVENT LIFECYCLEenvelope arrives{thread, publish_timestamp, event}CommsManagerevents_map: thread → Event classcontact resolution via MEDIUM_TO_CONTACT_FIELDpublish app:comms:*EventBrokerin-memory pub/subConversationManager.wait_for_events()EventHandler.handle_event(event, cm)side effectsTranscriptManager.log_messages · ContactIndex · NotificationBarrequest_llm_run() → flushslow brain turnDebouncer → build_brain_spec() → single_shot_tool_decisioncomms toolssend_sms · send_email · …act(…)steerable Actor handlewaitend turn silently diff --git a/images/developers/event-lifecycle.png b/images/developers/event-lifecycle.png deleted file mode 100644 index 42d06ff02..000000000 Binary files a/images/developers/event-lifecycle.png and /dev/null differ diff --git a/images/developers/federated-scopes-dark.svg b/images/developers/federated-scopes-dark.svg new file mode 100644 index 000000000..845fc442f --- /dev/null +++ b/images/developers/federated-scopes-dark.svg @@ -0,0 +1,7 @@ +FEDERATED SCOPESfederated_ranked_searchFederatedSearchContext per rootmerged globally by scorepersonal root{user}/{assistant}/Guidance ·Functions/Compositionalteam rootsTeams/{team_id}/... for eachSESSION_DETAILS.team_idsbuiltins catalogueread-only platform project ·seeded skillsContextRegistryread_roots(): personal first, then teamswrite_root(): exactly one destination,membership checkedresolveswrites:personal | team:<id>— never builtins diff --git a/images/developers/federated-scopes-light.svg b/images/developers/federated-scopes-light.svg new file mode 100644 index 000000000..84c893b34 --- /dev/null +++ b/images/developers/federated-scopes-light.svg @@ -0,0 +1,7 @@ +FEDERATED SCOPESfederated_ranked_searchFederatedSearchContext per rootmerged globally by scorepersonal root{user}/{assistant}/Guidance ·Functions/Compositionalteam rootsTeams/{team_id}/... for eachSESSION_DETAILS.team_idsbuiltins catalogueread-only platform project ·seeded skillsContextRegistryread_roots(): personal first, then teamswrite_root(): exactly one destination,membership checkedresolveswrites:personal | team:<id>— never builtins diff --git a/images/developers/federated-scopes.png b/images/developers/federated-scopes.png deleted file mode 100644 index c24ede8dd..000000000 Binary files a/images/developers/federated-scopes.png and /dev/null differ diff --git a/images/developers/gateway-architecture-dark.svg b/images/developers/gateway-architecture-dark.svg new file mode 100644 index 000000000..be7395d8c --- /dev/null +++ b/images/developers/gateway-architecture-dark.svg @@ -0,0 +1,7 @@ +GATEWAYTwilioSMS · voice · WhatsAppGoogle / MicrosoftGmail push · Graph notifications · OAuthSlack / Discord / Consoleevents API · gateway WS · internal routesunify.gateway— create_app() FastAPIadapters/inbound webhook normalizationtwilio.py · google.py · microsoft.pyslack.py · internal.pychannels/outbound + admin APIsphone · whatsapp · gmail · outlookteams · slack · discord · socialoutbound provider calls{thread, event} envelopeEnvelopeSinkHttpEnvelopeSink (local) · Pub/Sub (hosted)ConversationManager· separate processCommsManager → typed Event → slow brainoutbound HTTPvia comms_utils diff --git a/images/developers/gateway-architecture-light.svg b/images/developers/gateway-architecture-light.svg new file mode 100644 index 000000000..e28c36fa8 --- /dev/null +++ b/images/developers/gateway-architecture-light.svg @@ -0,0 +1,7 @@ +GATEWAYTwilioSMS · voice · WhatsAppGoogle / MicrosoftGmail push · Graph notifications · OAuthSlack / Discord / Consoleevents API · gateway WS · internal routesunify.gateway— create_app() FastAPIadapters/inbound webhook normalizationtwilio.py · google.py · microsoft.pyslack.py · internal.pychannels/outbound + admin APIsphone · whatsapp · gmail · outlookteams · slack · discord · socialoutbound provider calls{thread, event} envelopeEnvelopeSinkHttpEnvelopeSink (local) · Pub/Sub (hosted)ConversationManager· separate processCommsManager → typed Event → slow brainoutbound HTTPvia comms_utils diff --git a/images/developers/gateway-architecture.png b/images/developers/gateway-architecture.png deleted file mode 100644 index 1fec1a0d2..000000000 Binary files a/images/developers/gateway-architecture.png and /dev/null differ diff --git a/images/developers/knowledge-memory-dark.svg b/images/developers/knowledge-memory-dark.svg new file mode 100644 index 000000000..efcbe77c0 --- /dev/null +++ b/images/developers/knowledge-memory-dark.svg @@ -0,0 +1,7 @@ +KNOWLEDGE & MEMORYlive(Actor-driven)CodeActActorexecute_function(...)primitives.knowledgeask / update / refactorinner LLM tool loop_search · _filter · _add_rows+ schema toolsKnowledge/* tablestyped columns ·DataManager-backedbackground(every 50 events)EventBusMessage + ManagerMethod eventsMemoryManager.process_chunkunified extraction passContactManagerbios · summaries · response policiesKnowledgeManager.updatedurable domain factsTaskScheduler.updatetask commitments diff --git a/images/developers/knowledge-memory-light.svg b/images/developers/knowledge-memory-light.svg new file mode 100644 index 000000000..70ba4c3d2 --- /dev/null +++ b/images/developers/knowledge-memory-light.svg @@ -0,0 +1,7 @@ +KNOWLEDGE & MEMORYlive(Actor-driven)CodeActActorexecute_function(...)primitives.knowledgeask / update / refactorinner LLM tool loop_search · _filter · _add_rows+ schema toolsKnowledge/* tablestyped columns ·DataManager-backedbackground(every 50 events)EventBusMessage + ManagerMethod eventsMemoryManager.process_chunkunified extraction passContactManagerbios · summaries · response policiesKnowledgeManager.updatedurable domain factsTaskScheduler.updatetask commitments diff --git a/images/developers/knowledge-memory.png b/images/developers/knowledge-memory.png deleted file mode 100644 index ef65da3a8..000000000 Binary files a/images/developers/knowledge-memory.png and /dev/null differ diff --git a/images/developers/learning-loop-dark.svg b/images/developers/learning-loop-dark.svg new file mode 100644 index 000000000..2807baa5a --- /dev/null +++ b/images/developers/learning-loop-dark.svg @@ -0,0 +1,7 @@ +LEARNING LOOPConversationManagerslow brainact(...) →SteerableToolHandleCodeActActor.act()Phase 1 — doing loopdiscovery gate:FunctionManager_search_functions+ GuidanceManager_searchexecute_code / execute_functionstore_skills (proactive)_StorageCheckHandle: result()resolves after Phase 1Phase 2 — StorageCheckskill librarian LLM looplabel: "Storing reusable skills"often stores nothingFunctionManagerthe WHAT —executable skillsGuidanceManagerthe HOW —playbooks / SOPssearch firststore aftersearch firststore afterLegenddispatch / readstorage write diff --git a/images/developers/learning-loop-light.svg b/images/developers/learning-loop-light.svg new file mode 100644 index 000000000..27979e4a5 --- /dev/null +++ b/images/developers/learning-loop-light.svg @@ -0,0 +1,7 @@ +LEARNING LOOPConversationManagerslow brainact(...) →SteerableToolHandleCodeActActor.act()Phase 1 — doing loopdiscovery gate:FunctionManager_search_functions+ GuidanceManager_searchexecute_code / execute_functionstore_skills (proactive)_StorageCheckHandle: result()resolves after Phase 1Phase 2 — StorageCheckskill librarian LLM looplabel: "Storing reusable skills"often stores nothingFunctionManagerthe WHAT —executable skillsGuidanceManagerthe HOW —playbooks / SOPssearch firststore aftersearch firststore afterLegenddispatch / readstorage write diff --git a/images/developers/learning-loop.png b/images/developers/learning-loop.png deleted file mode 100644 index baab4b838..000000000 Binary files a/images/developers/learning-loop.png and /dev/null differ diff --git a/images/developers/live-tile-lifecycle-dark.svg b/images/developers/live-tile-lifecycle-dark.svg new file mode 100644 index 000000000..bb6c88308 --- /dev/null +++ b/images/developers/live-tile-lifecycle-dark.svg @@ -0,0 +1,7 @@ +LIVE TILE LIFECYCLE1Actorcreate_tile(html, data_bindings, on_data)2DashboardManagervalidate + dry-runbindings3Console TileVieweriframe +UnifyData bridge4Orchestra bridgefilter / reduce / joinlive queriesat render timestore tile row +register token5on_data(results)chart updates with fresh data diff --git a/images/developers/live-tile-lifecycle-light.svg b/images/developers/live-tile-lifecycle-light.svg new file mode 100644 index 000000000..88e3deff4 --- /dev/null +++ b/images/developers/live-tile-lifecycle-light.svg @@ -0,0 +1,7 @@ +LIVE TILE LIFECYCLE1Actorcreate_tile(html, data_bindings, on_data)2DashboardManagervalidate + dry-runbindings3Console TileVieweriframe +UnifyData bridge4Orchestra bridgefilter / reduce / joinlive queriesat render timestore tile row +register token5on_data(results)chart updates with fresh data diff --git a/images/developers/live-tile-lifecycle.png b/images/developers/live-tile-lifecycle.png deleted file mode 100644 index b76ca64ae..000000000 Binary files a/images/developers/live-tile-lifecycle.png and /dev/null differ diff --git a/images/developers/nested-steering-sequence-dark.svg b/images/developers/nested-steering-sequence-dark.svg new file mode 100644 index 000000000..0f4268f3d --- /dev/null +++ b/images/developers/nested-steering-sequence-dark.svg @@ -0,0 +1,7 @@ +NESTED STEERINGUserConversationManagerActorTranscriptManagerUSER INTERJECTS MID-FLIGHT"find when Sarah last mentioned Berlin"act(prompt)handle_Atranscripts.ask(…)handle_B"actually include emails too"interject(…)interject(…)refined resultsnotify(…)"scanning emails too…"final answer diff --git a/images/developers/nested-steering-sequence-light.svg b/images/developers/nested-steering-sequence-light.svg new file mode 100644 index 000000000..b8664f6ee --- /dev/null +++ b/images/developers/nested-steering-sequence-light.svg @@ -0,0 +1,7 @@ +NESTED STEERINGUserConversationManagerActorTranscriptManagerUSER INTERJECTS MID-FLIGHT"find when Sarah last mentioned Berlin"act(prompt)handle_Atranscripts.ask(…)handle_B"actually include emails too"interject(…)interject(…)refined resultsnotify(…)"scanning emails too…"final answer diff --git a/images/developers/storage-check-dark.svg b/images/developers/storage-check-dark.svg new file mode 100644 index 000000000..e029cfcad --- /dev/null +++ b/images/developers/storage-check-dark.svg @@ -0,0 +1,7 @@ +STORAGECHECKtrajectory snapshotmessages + tool calls + result from Phase 1skill librarian LLM loop1. review the trajectory2. search existing stores (dedup)3. decide what persists4. delete superseded entriesstore_skills (mid-run)ProactiveStorage loopsummariesfeed Phase 2FunctionManager_add_functionsreusable code thatran successfullyGuidanceManager_add_guidancerecipes, pitfalls,correctionsentrypoint certificationattach_entrypoint_to_recurring_task+ evidencestore nothingoften the right call diff --git a/images/developers/storage-check-light.svg b/images/developers/storage-check-light.svg new file mode 100644 index 000000000..0f297bd5a --- /dev/null +++ b/images/developers/storage-check-light.svg @@ -0,0 +1,7 @@ +STORAGECHECKtrajectory snapshotmessages + tool calls + result from Phase 1skill librarian LLM loop1. review the trajectory2. search existing stores (dedup)3. decide what persists4. delete superseded entriesstore_skills (mid-run)ProactiveStorage loopsummariesfeed Phase 2FunctionManager_add_functionsreusable code thatran successfullyGuidanceManager_add_guidancerecipes, pitfalls,correctionsentrypoint certificationattach_entrypoint_to_recurring_task+ evidencestore nothingoften the right call diff --git a/images/developers/storage-check.png b/images/developers/storage-check.png deleted file mode 100644 index 597818e53..000000000 Binary files a/images/developers/storage-check.png and /dev/null differ diff --git a/images/developers/tasks-activation-lanes-dark.svg b/images/developers/tasks-activation-lanes-dark.svg new file mode 100644 index 000000000..815490406 --- /dev/null +++ b/images/developers/tasks-activation-lanes-dark.svg @@ -0,0 +1,7 @@ +ACTIVATION LANESscheduled (live)activation projectedTasks/Activations rowtimer firesCloud Tasks (hosted) ·LocalActivationScheduler (self-host)TaskDue eventConversationManagervalidate · auto-start · silent_by_defaulttriggered (live)inbound messagesms · email · call …mechanical filtermedium + contact include/omitsemantic checkslow brain decides fitprimitives.tasks.executetask_id + trigger_attempt_tokenoffline (headless)due or triggeredexecution_mode = offlinejob dispatchedK8s job (hosted) ·LocalOfflineDispatcher (self-host)offline_runnerno live session · no clarificationsrun persistedTasks/RunsTaskScheduler.execute() → ActiveTasksteerable handle · pause / interject / stop diff --git a/images/developers/tasks-activation-lanes-light.svg b/images/developers/tasks-activation-lanes-light.svg new file mode 100644 index 000000000..78215774d --- /dev/null +++ b/images/developers/tasks-activation-lanes-light.svg @@ -0,0 +1,7 @@ +ACTIVATION LANESscheduled (live)activation projectedTasks/Activations rowtimer firesCloud Tasks (hosted) ·LocalActivationScheduler (self-host)TaskDue eventConversationManagervalidate · auto-start · silent_by_defaulttriggered (live)inbound messagesms · email · call …mechanical filtermedium + contact include/omitsemantic checkslow brain decides fitprimitives.tasks.executetask_id + trigger_attempt_tokenoffline (headless)due or triggeredexecution_mode = offlinejob dispatchedK8s job (hosted) ·LocalOfflineDispatcher (self-host)offline_runnerno live session · no clarificationsrun persistedTasks/RunsTaskScheduler.execute() → ActiveTasksteerable handle · pause / interject / stop diff --git a/images/developers/tasks-activation-lanes.png b/images/developers/tasks-activation-lanes.png deleted file mode 100644 index 84b274cce..000000000 Binary files a/images/developers/tasks-activation-lanes.png and /dev/null differ diff --git a/images/developers/tasks-anatomy-dark.svg b/images/developers/tasks-anatomy-dark.svg new file mode 100644 index 000000000..4a01360e0 --- /dev/null +++ b/images/developers/tasks-anatomy-dark.svg @@ -0,0 +1,7 @@ +TASK ANATOMYTask rowname · descriptionpriority · deadlinedestinationpersonal or team:<id>statusschedulestart_attriggermedium · from_contact_ids · recurringXORrepeat: List[RepeatPattern]RRULE subset: frequency · interval ·weekdays · time_of_day · count · untilentrypointNone = agentic · function_id = symbolicofflinelive lane vs headless laneone logical task = many instancestask_id stays fixed · instance_id increments on each re-arm clone diff --git a/images/developers/tasks-anatomy-light.svg b/images/developers/tasks-anatomy-light.svg new file mode 100644 index 000000000..836888dfd --- /dev/null +++ b/images/developers/tasks-anatomy-light.svg @@ -0,0 +1,7 @@ +TASK ANATOMYTask rowname · descriptionpriority · deadlinedestinationpersonal or team:<id>statusschedulestart_attriggermedium · from_contact_ids · recurringXORrepeat: List[RepeatPattern]RRULE subset: frequency · interval ·weekdays · time_of_day · count · untilentrypointNone = agentic · function_id = symbolicofflinelive lane vs headless laneone logical task = many instancestask_id stays fixed · instance_id increments on each re-arm clone diff --git a/images/developers/tasks-anatomy.png b/images/developers/tasks-anatomy.png deleted file mode 100644 index e8dc83660..000000000 Binary files a/images/developers/tasks-anatomy.png and /dev/null differ diff --git a/images/developers/tasks-lifecycle-dark.svg b/images/developers/tasks-lifecycle-dark.svg new file mode 100644 index 000000000..2c502a93d --- /dev/null +++ b/images/developers/tasks-lifecycle-dark.svg @@ -0,0 +1,7 @@ +TASK LIFECYCLEscheduledhas schedule.start_attriggerablearmed, waiting for eventactiveone runnable instanceactivated_by: schedule | trigger | explicitexecute()execute()completedresult() ok · info summary writtenfailedresult() errorcancelledstop() / interject(cancel)execute() also clones the next instancerecurring: next_repeated_start_at(…) · triggerable: identical re-arm · instance_id + 1 diff --git a/images/developers/tasks-lifecycle-light.svg b/images/developers/tasks-lifecycle-light.svg new file mode 100644 index 000000000..0adc7242e --- /dev/null +++ b/images/developers/tasks-lifecycle-light.svg @@ -0,0 +1,7 @@ +TASK LIFECYCLEscheduledhas schedule.start_attriggerablearmed, waiting for eventactiveone runnable instanceactivated_by: schedule | trigger | explicitexecute()execute()completedresult() ok · info summary writtenfailedresult() errorcancelledstop() / interject(cancel)execute() also clones the next instancerecurring: next_repeated_start_at(…) · triggerable: identical re-arm · instance_id + 1 diff --git a/images/developers/tasks-lifecycle.png b/images/developers/tasks-lifecycle.png deleted file mode 100644 index 047849c86..000000000 Binary files a/images/developers/tasks-lifecycle.png and /dev/null differ diff --git a/images/developers/teams-membership-runtime-dark.svg b/images/developers/teams-membership-runtime-dark.svg new file mode 100644 index 000000000..dfafd8b44 --- /dev/null +++ b/images/developers/teams-membership-runtime-dark.svg @@ -0,0 +1,7 @@ +MEMBERSHIP RUNTIMEsource of truthOrchestrateam memberships · closed sourcestartup payloadSESSION_DETAILSteam_ids · team_summariessystem promptbuild_accessible_teams_blockrouting rulesenv varsTEAM_IDS · TEAM_SUMMARIESworker subprocessesmid-session refreshAssistantUpdateEventupdate_kind='membership'forget_departed_team_rootsdrop cached roots · no stale accessmembership changes land live — a revoked team disappears without a restart diff --git a/images/developers/teams-membership-runtime-light.svg b/images/developers/teams-membership-runtime-light.svg new file mode 100644 index 000000000..c7d1ee0b5 --- /dev/null +++ b/images/developers/teams-membership-runtime-light.svg @@ -0,0 +1,7 @@ +MEMBERSHIP RUNTIMEsource of truthOrchestrateam memberships · closed sourcestartup payloadSESSION_DETAILSteam_ids · team_summariessystem promptbuild_accessible_teams_blockrouting rulesenv varsTEAM_IDS · TEAM_SUMMARIESworker subprocessesmid-session refreshAssistantUpdateEventupdate_kind='membership'forget_departed_team_rootsdrop cached roots · no stale accessmembership changes land live — a revoked team disappears without a restart diff --git a/images/developers/teams-membership-runtime.png b/images/developers/teams-membership-runtime.png deleted file mode 100644 index d1e61b556..000000000 Binary files a/images/developers/teams-membership-runtime.png and /dev/null differ diff --git a/images/developers/teams-scope-model-dark.svg b/images/developers/teams-scope-model-dark.svg new file mode 100644 index 000000000..6cd1714a6 --- /dev/null +++ b/images/developers/teams-scope-model-dark.svg @@ -0,0 +1,7 @@ +TEAM SCOPESstate managersGuidance · Knowledge · Functions · Tasks · Data · Secrets · Dashboardsdestination='personal' | 'team:<id>'ContextRegistrydestination → root · membership gatedefaultrequires team_id inSESSION_DETAILS.team_idspersonal root{user_id}/{assistant_id}/…privacy floor — visible to one assistantteam rootsTeams/{team_id}/…one root per membership — visible to current memberssame table names under every rootGuidance · Knowledge · Functions/* · Tasks · Data · Secrets · Dashboards/* · Transcriptswrites target exactly one root — reads fan out across all of them diff --git a/images/developers/teams-scope-model-light.svg b/images/developers/teams-scope-model-light.svg new file mode 100644 index 000000000..0b7c237ec --- /dev/null +++ b/images/developers/teams-scope-model-light.svg @@ -0,0 +1,7 @@ +TEAM SCOPESstate managersGuidance · Knowledge · Functions · Tasks · Data · Secrets · Dashboardsdestination='personal' | 'team:<id>'ContextRegistrydestination → root · membership gatedefaultrequires team_id inSESSION_DETAILS.team_idspersonal root{user_id}/{assistant_id}/…privacy floor — visible to one assistantteam rootsTeams/{team_id}/…one root per membership — visible to current memberssame table names under every rootGuidance · Knowledge · Functions/* · Tasks · Data · Secrets · Dashboards/* · Transcriptswrites target exactly one root — reads fan out across all of them diff --git a/images/developers/teams-scope-model.png b/images/developers/teams-scope-model.png deleted file mode 100644 index 2e877a148..000000000 Binary files a/images/developers/teams-scope-model.png and /dev/null differ diff --git a/images/developers/teams-write-read-paths-dark.svg b/images/developers/teams-write-read-paths-dark.svg new file mode 100644 index 000000000..8e3caa70c --- /dev/null +++ b/images/developers/teams-write-read-paths-dark.svg @@ -0,0 +1,7 @@ +WRITE & READ PATHSWRITEadd_guidance(…)destination='team:42'canonical_destinationnormalize · validate tokenmembership gateteam_id in team_ids?else invalid_destinationwrite_rootTeams/42/Guidanceexactly one rootrows stamped withauthoring_assistant_idREADsearch(…)no destination argread_rootspersonal + everymember teampersonal rootTeams/42Teams/57federated mergefederated_ranked_searchone global windowrows annotated with_federated_source /_federated_contextone destination per write — every reachable root per read diff --git a/images/developers/teams-write-read-paths-light.svg b/images/developers/teams-write-read-paths-light.svg new file mode 100644 index 000000000..039c3b219 --- /dev/null +++ b/images/developers/teams-write-read-paths-light.svg @@ -0,0 +1,7 @@ +WRITE & READ PATHSWRITEadd_guidance(…)destination='team:42'canonical_destinationnormalize · validate tokenmembership gateteam_id in team_ids?else invalid_destinationwrite_rootTeams/42/Guidanceexactly one rootrows stamped withauthoring_assistant_idREADsearch(…)no destination argread_rootspersonal + everymember teampersonal rootTeams/42Teams/57federated mergefederated_ranked_searchone global windowrows annotated with_federated_source /_federated_contextone destination per write — every reachable root per read diff --git a/images/developers/teams-write-read-paths.png b/images/developers/teams-write-read-paths.png deleted file mode 100644 index 1a50cc028..000000000 Binary files a/images/developers/teams-write-read-paths.png and /dev/null differ diff --git a/images/developers/user-desktop-architecture-dark.svg b/images/developers/user-desktop-architecture-dark.svg new file mode 100644 index 000000000..ec058af07 --- /dev/null +++ b/images/developers/user-desktop-architecture-dark.svg @@ -0,0 +1,7 @@ +LINKED DESKTOPConversationManagerslow brain — resolves acting useract(...)CodeActActorwrites Python plansprimitives.computer.user_desktop_UserDesktopFactorylist_linked() · session(user_id)UserDesktopHandleact · observe · click · type_text · get_screenshotresolve + consent checkUserDesktopLinkSESSION_DETAILSper owner_user_idHTTPS over outbound tunnelunify cloud relay — machine dials out, nothing listens inbounduser's own computermacOS · Windows · Ubuntuagent-service/act · /screenshot · /api/execlive desktop viewassistant perception onlySFTP $HOMEkey-gated file channelsolid arrows: dispatch flow · dashed line: outbound-only tunnel boundary diff --git a/images/developers/user-desktop-architecture-light.svg b/images/developers/user-desktop-architecture-light.svg new file mode 100644 index 000000000..c4dd4a19f --- /dev/null +++ b/images/developers/user-desktop-architecture-light.svg @@ -0,0 +1,7 @@ +LINKED DESKTOPConversationManagerslow brain — resolves acting useract(...)CodeActActorwrites Python plansprimitives.computer.user_desktop_UserDesktopFactorylist_linked() · session(user_id)UserDesktopHandleact · observe · click · type_text · get_screenshotresolve + consent checkUserDesktopLinkSESSION_DETAILSper owner_user_idHTTPS over outbound tunnelunify cloud relay — machine dials out, nothing listens inbounduser's own computermacOS · Windows · Ubuntuagent-service/act · /screenshot · /api/execlive desktop viewassistant perception onlySFTP $HOMEkey-gated file channelsolid arrows: dispatch flow · dashed line: outbound-only tunnel boundary diff --git a/images/developers/user-desktop-architecture.png b/images/developers/user-desktop-architecture.png deleted file mode 100644 index de4d2d01a..000000000 Binary files a/images/developers/user-desktop-architecture.png and /dev/null differ diff --git a/images/developers/user-desktop-consent-gates-dark.svg b/images/developers/user-desktop-consent-gates-dark.svg new file mode 100644 index 000000000..8a30bdb62 --- /dev/null +++ b/images/developers/user-desktop-consent-gates-dark.svg @@ -0,0 +1,7 @@ +CONSENT GATESLAYER 1standing config(from Console)UserDesktopLink.urlcontrol channel linkedfilesys_sync + tunnel= filesys_availablefile access enabledUserFilesysAccessStarted/ UserFilesysAccessStoppedcomms events → grant / revokeLAYER 2live consent(in-memory,per session)_user_desktop_revokedset[user_id] — blocks control_user_filesys_revokedset[user_id] — blocks filesLAYER 3every callre-checksUserDesktopHandle.act(...) · files.pull(...)_assert_user_desktop_allowed / _assert_user_filesys_allowed→ PermissionError on revokerevocation is enforced per call — a mid-session revoke stops the very next action diff --git a/images/developers/user-desktop-consent-gates-light.svg b/images/developers/user-desktop-consent-gates-light.svg new file mode 100644 index 000000000..11ffc9725 --- /dev/null +++ b/images/developers/user-desktop-consent-gates-light.svg @@ -0,0 +1,7 @@ +CONSENT GATESLAYER 1standing config(from Console)UserDesktopLink.urlcontrol channel linkedfilesys_sync + tunnel= filesys_availablefile access enabledUserFilesysAccessStarted/ UserFilesysAccessStoppedcomms events → grant / revokeLAYER 2live consent(in-memory,per session)_user_desktop_revokedset[user_id] — blocks control_user_filesys_revokedset[user_id] — blocks filesLAYER 3every callre-checksUserDesktopHandle.act(...) · files.pull(...)_assert_user_desktop_allowed / _assert_user_filesys_allowed→ PermissionError on revokerevocation is enforced per call — a mid-session revoke stops the very next action diff --git a/images/developers/user-desktop-consent-gates.png b/images/developers/user-desktop-consent-gates.png deleted file mode 100644 index dbd6ecd26..000000000 Binary files a/images/developers/user-desktop-consent-gates.png and /dev/null differ diff --git a/images/developers/user-desktop-files-flow-dark.svg b/images/developers/user-desktop-files-flow-dark.svg new file mode 100644 index 000000000..3b63299e0 --- /dev/null +++ b/images/developers/user-desktop-files-flow-dark.svg @@ -0,0 +1,7 @@ +USER FILE CHANNELassistant runtimeuser's home directory ($HOME)SFTP overraw-TCP tunneldocuments & foldersoriginals — never overwritten/.unity-edits/timestamped writeback copiesexcluded: caches · node_modules · .git.ssh · .gnupg · .awsprimitives.computer.user_desktop.fileslist · pull · sync · pushUserHomeSFTPon-demand rclone client · per-link private key~/Unity/Remote/<user_id>/read-only staging mirror of their homelist — browse metadata onlypull / sync — stage copiespush — new timestamped copyreads stage into a local mirror · writes land beside originals, never over them diff --git a/images/developers/user-desktop-files-flow-light.svg b/images/developers/user-desktop-files-flow-light.svg new file mode 100644 index 000000000..84db9d814 --- /dev/null +++ b/images/developers/user-desktop-files-flow-light.svg @@ -0,0 +1,7 @@ +USER FILE CHANNELassistant runtimeuser's home directory ($HOME)SFTP overraw-TCP tunneldocuments & foldersoriginals — never overwritten/.unity-edits/timestamped writeback copiesexcluded: caches · node_modules · .git.ssh · .gnupg · .awsprimitives.computer.user_desktop.fileslist · pull · sync · pushUserHomeSFTPon-demand rclone client · per-link private key~/Unity/Remote/<user_id>/read-only staging mirror of their homelist — browse metadata onlypull / sync — stage copiespush — new timestamped copyreads stage into a local mirror · writes land beside originals, never over them diff --git a/images/developers/user-desktop-files-flow.png b/images/developers/user-desktop-files-flow.png deleted file mode 100644 index 0792a522f..000000000 Binary files a/images/developers/user-desktop-files-flow.png and /dev/null differ diff --git a/images/developers/voice-dual-brain-dark.svg b/images/developers/voice-dual-brain-dark.svg new file mode 100644 index 000000000..d30495026 --- /dev/null +++ b/images/developers/voice-dual-brain-dark.svg @@ -0,0 +1,7 @@ +VOICE DUAL-BRAINcallerphone · WhatsApp · Unify MeetLiveKit roomunity_{assistant_id}_{medium}Fast Brain— Assistant(Agent)medium_scripts/call.pyDeepgram STT · Silero VADCartesia/ElevenLabs TTS · fast LLMSlow Brain— ConversationManagerpersistent reasoning loopguide_voice_agent · allow_hang_upact(…)InboundPhoneUtterance · eventsFastBrainNotificationSPEAK · guidance · IPC socketFillers & barge-inbuffer filler while slow brain thinksVoiceInterrupt on interruptionLivekitCallManagerdomains/call_manager.pydispatch · hang-up gate · speaker engagement diff --git a/images/developers/voice-dual-brain-light.svg b/images/developers/voice-dual-brain-light.svg new file mode 100644 index 000000000..d3f5c69d8 --- /dev/null +++ b/images/developers/voice-dual-brain-light.svg @@ -0,0 +1,7 @@ +VOICE DUAL-BRAINcallerphone · WhatsApp · Unify MeetLiveKit roomunity_{assistant_id}_{medium}Fast Brain— Assistant(Agent)medium_scripts/call.pyDeepgram STT · Silero VADCartesia/ElevenLabs TTS · fast LLMSlow Brain— ConversationManagerpersistent reasoning loopguide_voice_agent · allow_hang_upact(…)InboundPhoneUtterance · eventsFastBrainNotificationSPEAK · guidance · IPC socketFillers & barge-inbuffer filler while slow brain thinksVoiceInterrupt on interruptionLivekitCallManagerdomains/call_manager.pydispatch · hang-up gate · speaker engagement diff --git a/images/developers/voice-dual-brain.png b/images/developers/voice-dual-brain.png deleted file mode 100644 index 6ab24e0cc..000000000 Binary files a/images/developers/voice-dual-brain.png and /dev/null differ diff --git a/images/developers/workspace-email-identities-dark.svg b/images/developers/workspace-email-identities-dark.svg new file mode 100644 index 000000000..05e75c0f3 --- /dev/null +++ b/images/developers/workspace-email-identities-dark.svg @@ -0,0 +1,7 @@ +EMAIL IDENTITIESprimitives.workspace_emailacts as the userprimitives.comms.send_emailacts as the assistantsame assistant, two send identitiesWorkspaceEmailManagersend · list_messages · search · get_messagegateway email channelschannels/gmail · channels/outlook · /email/senduser's connected mailboxGmail API / Microsoft Graph — trusted tokenassistant's managed mailboxcontact graph · threading · outbound historyinbound emailGmail push → /email/gmail · Graph subscriptions → /microsoft/routerwatch / subscriptions wake the assistantworkspace_email sends from the connected account; comms.send_email sends as the assistant. diff --git a/images/developers/workspace-email-identities-light.svg b/images/developers/workspace-email-identities-light.svg new file mode 100644 index 000000000..5e0686b66 --- /dev/null +++ b/images/developers/workspace-email-identities-light.svg @@ -0,0 +1,7 @@ +EMAIL IDENTITIESprimitives.workspace_emailacts as the userprimitives.comms.send_emailacts as the assistantsame assistant, two send identitiesWorkspaceEmailManagersend · list_messages · search · get_messagegateway email channelschannels/gmail · channels/outlook · /email/senduser's connected mailboxGmail API / Microsoft Graph — trusted tokenassistant's managed mailboxcontact graph · threading · outbound historyinbound emailGmail push → /email/gmail · Graph subscriptions → /microsoft/routerwatch / subscriptions wake the assistantworkspace_email sends from the connected account; comms.send_email sends as the assistant. diff --git a/images/developers/workspace-email-identities.png b/images/developers/workspace-email-identities.png deleted file mode 100644 index 8efcef979..000000000 Binary files a/images/developers/workspace-email-identities.png and /dev/null differ diff --git a/images/developers/workspace-oauth-plumbing-dark.svg b/images/developers/workspace-oauth-plumbing-dark.svg new file mode 100644 index 000000000..e60195908 --- /dev/null +++ b/images/developers/workspace-oauth-plumbing-dark.svg @@ -0,0 +1,7 @@ +WORKSPACE OAUTHConsoleuser approves scopesgateway OAuth callbacksadapters/google.py · adapters/microsoft.pyOrchestra secretsGOOGLE_* / MICROSOFT_* tokensSecretManager synctokens stay in memory · metadata to envin-memory tokensnever written to diskgranted scopes + expiryenv vars onlyreal tokenactor sandboxexecute_codeBearer nonceprovider proxylocalhost · swaps nonce for real tokenGoogle APIs · Microsoft Graphgoogleapis.comgraph.microsoft.comsolid arrows: request flow — dotted arrows: trusted token access. The sandbox never sees a real token. diff --git a/images/developers/workspace-oauth-plumbing-light.svg b/images/developers/workspace-oauth-plumbing-light.svg new file mode 100644 index 000000000..4d7aea7b8 --- /dev/null +++ b/images/developers/workspace-oauth-plumbing-light.svg @@ -0,0 +1,7 @@ +WORKSPACE OAUTHConsoleuser approves scopesgateway OAuth callbacksadapters/google.py · adapters/microsoft.pyOrchestra secretsGOOGLE_* / MICROSOFT_* tokensSecretManager synctokens stay in memory · metadata to envin-memory tokensnever written to diskgranted scopes + expiryenv vars onlyreal tokenactor sandboxexecute_codeBearer nonceprovider proxylocalhost · swaps nonce for real tokenGoogle APIs · Microsoft Graphgoogleapis.comgraph.microsoft.comsolid arrows: request flow — dotted arrows: trusted token access. The sandbox never sees a real token. diff --git a/images/developers/workspace-oauth-plumbing.png b/images/developers/workspace-oauth-plumbing.png deleted file mode 100644 index fa9b30f44..000000000 Binary files a/images/developers/workspace-oauth-plumbing.png and /dev/null differ diff --git a/images/developers/workspace-proxy-policy-dark.svg b/images/developers/workspace-proxy-policy-dark.svg new file mode 100644 index 000000000..bc81cb759 --- /dev/null +++ b/images/developers/workspace-proxy-policy-dark.svg @@ -0,0 +1,7 @@ +PROXY POLICYsandbox requestGET /google/drive/v3/files…classify()classify.py — path template matchnon_filemail · calendar · contactsfile_readget · list · search · contentfile_writecreate · move · copy · uploadunknown_fileunmatched file-shaped pathpolicy evaluationpolicy.py + ancestry.pywalk parent chain,nearest decision winspassthrough403 default-denyallowedmaskedupstream APIreal token injectedfilter.py masks listings404 / 403item invisible to the assistantNo Console policy configured → everything passes. Once a policy exists, the allowlist governs every file endpoint. diff --git a/images/developers/workspace-proxy-policy-light.svg b/images/developers/workspace-proxy-policy-light.svg new file mode 100644 index 000000000..7848cf95f --- /dev/null +++ b/images/developers/workspace-proxy-policy-light.svg @@ -0,0 +1,7 @@ +PROXY POLICYsandbox requestGET /google/drive/v3/files…classify()classify.py — path template matchnon_filemail · calendar · contactsfile_readget · list · search · contentfile_writecreate · move · copy · uploadunknown_fileunmatched file-shaped pathpolicy evaluationpolicy.py + ancestry.pywalk parent chain,nearest decision winspassthrough403 default-denyallowedmaskedupstream APIreal token injectedfilter.py masks listings404 / 403item invisible to the assistantNo Console policy configured → everything passes. Once a policy exists, the allowlist governs every file endpoint. diff --git a/images/developers/workspace-proxy-policy.png b/images/developers/workspace-proxy-policy.png deleted file mode 100644 index 86ea43a35..000000000 Binary files a/images/developers/workspace-proxy-policy.png and /dev/null differ diff --git a/images/integrations-dev-execution-dark.svg b/images/integrations-dev-execution-dark.svg new file mode 100644 index 000000000..e22c42e1b --- /dev/null +++ b/images/integrations-dev-execution-dark.svg @@ -0,0 +1,7 @@ +EXECUTION & GOVERNANCEActor sandboxawait primitives.integrations.hubspot.search_contacts(…)IntegrationPrimitivesdynamic app namespace → execute_tool(tool_id, args)ops.run_tool → UniSDK run_integration_toolHTTPSOrchestra— governed executiontool policy check (allow / ask / block)audit recordprovider dispatchresult envelopeokconnect_requiredmissing_scopeblocked_by_policyconfirmation_requiredbuild_pending_approval_payload →user approves or deniesresolve_tool_execution(audit_id) + retry diff --git a/images/integrations-dev-execution-light.svg b/images/integrations-dev-execution-light.svg new file mode 100644 index 000000000..e35e5e22d --- /dev/null +++ b/images/integrations-dev-execution-light.svg @@ -0,0 +1,7 @@ +EXECUTION & GOVERNANCEActor sandboxawait primitives.integrations.hubspot.search_contacts(…)IntegrationPrimitivesdynamic app namespace → execute_tool(tool_id, args)ops.run_tool → UniSDK run_integration_toolHTTPSOrchestra— governed executiontool policy check (allow / ask / block)audit recordprovider dispatchresult envelopeokconnect_requiredmissing_scopeblocked_by_policyconfirmation_requiredbuild_pending_approval_payload →user approves or deniesresolve_tool_execution(audit_id) + retry diff --git a/images/integrations-dev-execution.png b/images/integrations-dev-execution.png deleted file mode 100644 index dd957da18..000000000 Binary files a/images/integrations-dev-execution.png and /dev/null differ diff --git a/images/integrations-dev-lanes-dark.svg b/images/integrations-dev-lanes-dark.svg new file mode 100644 index 000000000..7da890f30 --- /dev/null +++ b/images/integrations-dev-lanes-dark.svg @@ -0,0 +1,7 @@ +INTEGRATION LANESDiscoveryActivationExecutionProvider-backed(third-party)Builtins catalog —Integrations/Apps + ToolsConsole connect(OAuth or API key)then tool syncprimitives.integrations.<app>.<tool> →Orchestra governed runNative packagesDisk manifest discovery(unity-deploy packages)Required secrets present →functions + guidanceregisteredCustom functions inFunctions/CompositionalSDK + secrets(no gallery entry)Actor checksprimitives.secrets.ask(…)install SDK ·env credentials ·OAuth proxyexecute_code —direct API callsFunctionManager search— one discovery surface for every executable tool diff --git a/images/integrations-dev-lanes-light.svg b/images/integrations-dev-lanes-light.svg new file mode 100644 index 000000000..7f0e709e8 --- /dev/null +++ b/images/integrations-dev-lanes-light.svg @@ -0,0 +1,7 @@ +INTEGRATION LANESDiscoveryActivationExecutionProvider-backed(third-party)Builtins catalog —Integrations/Apps + ToolsConsole connect(OAuth or API key)then tool syncprimitives.integrations.<app>.<tool> →Orchestra governed runNative packagesDisk manifest discovery(unity-deploy packages)Required secrets present →functions + guidanceregisteredCustom functions inFunctions/CompositionalSDK + secrets(no gallery entry)Actor checksprimitives.secrets.ask(…)install SDK ·env credentials ·OAuth proxyexecute_code —direct API callsFunctionManager search— one discovery surface for every executable tool diff --git a/images/integrations-dev-lanes.png b/images/integrations-dev-lanes.png deleted file mode 100644 index ba865230a..000000000 Binary files a/images/integrations-dev-lanes.png and /dev/null differ diff --git a/images/integrations-dev-sync-dark.svg b/images/integrations-dev-sync-dark.svg new file mode 100644 index 000000000..31d35f6b7 --- /dev/null +++ b/images/integrations-dev-sync-dark.svg @@ -0,0 +1,7 @@ +PROVIDER TOOL SYNC1Consoleuser connects app (OAuth or API key)integration_tools_sync_requested (event)2IntegrationSyncCoordinatorstatus: syncingprompt block: <integration_tool_sync>also runs at startup:schedule_connected_apps()3FunctionManager.sync_provider_integration_toolsread BuiltinsIntegrations/Toolscompare per-app hash(Functions/Meta)delete + upsertchanged rows4Functions/Primitives rowsprimitives.integrations.<app>.<tool>metadata.source = provider_backedstatus: ready5User notified:“<App> tools are ready.”disconnect → operation: cleanup(rows removed) diff --git a/images/integrations-dev-sync-light.svg b/images/integrations-dev-sync-light.svg new file mode 100644 index 000000000..02231bf19 --- /dev/null +++ b/images/integrations-dev-sync-light.svg @@ -0,0 +1,7 @@ +PROVIDER TOOL SYNC1Consoleuser connects app (OAuth or API key)integration_tools_sync_requested (event)2IntegrationSyncCoordinatorstatus: syncingprompt block: <integration_tool_sync>also runs at startup:schedule_connected_apps()3FunctionManager.sync_provider_integration_toolsread BuiltinsIntegrations/Toolscompare per-app hash(Functions/Meta)delete + upsertchanged rows4Functions/Primitives rowsprimitives.integrations.<app>.<tool>metadata.source = provider_backedstatus: ready5User notified:“<App> tools are ready.”disconnect → operation: cleanup(rows removed) diff --git a/images/integrations-dev-sync.png b/images/integrations-dev-sync.png deleted file mode 100644 index 14355b907..000000000 Binary files a/images/integrations-dev-sync.png and /dev/null differ diff --git a/images/unify-docs-banner-dark.svg b/images/unify-docs-banner-dark.svg new file mode 100644 index 000000000..bdafeeeeb --- /dev/null +++ b/images/unify-docs-banner-dark.svg @@ -0,0 +1,7 @@ +UNIFY · DOCSUnifyEverything you can do with your AI teammate diff --git a/images/unify-docs-banner-light.svg b/images/unify-docs-banner-light.svg new file mode 100644 index 000000000..a0292d3ab --- /dev/null +++ b/images/unify-docs-banner-light.svg @@ -0,0 +1,7 @@ +UNIFY · DOCSUnifyEverything you can do with your AI teammate diff --git a/integrations/developers.mdx b/integrations/developers.mdx index 837e64447..e7f86583e 100644 --- a/integrations/developers.mdx +++ b/integrations/developers.mdx @@ -23,7 +23,8 @@ on one rule: **FunctionManager search is the single discovery surface for executable tools**. Whatever lane a capability arrives through, the actor finds it the same way it finds everything else. -![Three integration lanes: provider-backed third-party apps discovered via the Builtins catalog and executed through Orchestra; native unity-deploy packages activated by secrets; and the SDK-plus-secrets pattern for apps with no gallery entry — all converging on FunctionManager search](/images/integrations-dev-lanes.png) +Three integration lanes: provider-backed third-party apps discovered via the Builtins catalog and executed through Orchestra; native unity-deploy packages activated by secrets; and the SDK-plus-secrets pattern for apps with no gallery entry — all converging on FunctionManager search +Three integration lanes: provider-backed third-party apps discovered via the Builtins catalog and executed through Orchestra; native unity-deploy packages activated by secrets; and the SDK-plus-secrets pattern for apps with no gallery entry — all converging on FunctionManager search | Lane | Discovery | Activation | Execution | |---|---|---|---| @@ -135,7 +136,8 @@ What it triggers is **materialization**: provider tools from the Builtins catalog are written into the assistant's own `Functions/Primitives` context as first-class FunctionManager rows, so ordinary FM search finds them. -![Provider tool sync and materialization: a Console connect emits an integration_tools_sync_requested event, the IntegrationSyncCoordinator marks the app as syncing, FunctionManager.sync_provider_integration_tools reads the Builtins tools catalog, compares per-app hashes, upserts changed rows into Functions/Primitives, and the user is notified the tools are ready — with a cleanup path on disconnect and a startup pass over all connected apps](/images/integrations-dev-sync.png) +Provider tool sync and materialization: a Console connect emits an integration_tools_sync_requested event, the IntegrationSyncCoordinator marks the app as syncing, FunctionManager.sync_provider_integration_tools reads the Builtins tools catalog, compares per-app hashes, upserts changed rows into Functions/Primitives, and the user is notified the tools are ready — with a cleanup path on disconnect and a startup pass over all connected apps +Provider tool sync and materialization: a Console connect emits an integration_tools_sync_requested event, the IntegrationSyncCoordinator marks the app as syncing, FunctionManager.sync_provider_integration_tools reads the Builtins tools catalog, compares per-app hashes, upserts changed rows into Functions/Primitives, and the user is notified the tools are ready — with a cleanup path on disconnect and a startup pass over all connected apps ### The materialized row @@ -213,7 +215,8 @@ At call time, the runtime is a thin, honest client — every policy decision happens server-side, and every non-`ok` outcome comes back as a typed status the actor is prompted to handle rather than swallow. -![Execution and governance: actor sandbox code calls the dynamic integration namespace, which resolves execute_tool, flows through ops.run_tool and UniSDK to Orchestra's governed execution — policy check, audit record, provider dispatch — returning a result envelope whose statuses include ok, connect_required, missing_scope, blocked_by_policy, and confirmation_required, the last of which loops through a pending-approval payload and resolve_tool_execution retry](/images/integrations-dev-execution.png) +Execution and governance: actor sandbox code calls the dynamic integration namespace, which resolves execute_tool, flows through ops.run_tool and UniSDK to Orchestra's governed execution — policy check, audit record, provider dispatch — returning a result envelope whose statuses include ok, connect_required, missing_scope, blocked_by_policy, and confirmation_required, the last of which loops through a pending-approval payload and resolve_tool_execution retry +Execution and governance: actor sandbox code calls the dynamic integration namespace, which resolves execute_tool, flows through ops.run_tool and UniSDK to Orchestra's governed execution — policy check, audit record, provider dispatch — returning a result envelope whose statuses include ok, connect_required, missing_scope, blocked_by_policy, and confirmation_required, the last of which loops through a pending-approval payload and resolve_tool_execution retry The result envelope statuses form the contract: diff --git a/introduction.mdx b/introduction.mdx index 57cf01afb..1957b09f6 100644 --- a/introduction.mdx +++ b/introduction.mdx @@ -3,6 +3,9 @@ title: "Welcome" description: "Meet your AI assistant" --- +Unify — everything you can do with your AI teammate +Unify — everything you can do with your AI teammate + Unify gives you a fully remote virtual employee — an AI assistant you can talk to the way you'd talk to any other colleague. Message it, email it, call it, or hop on a video call and share your screen. It remembers your conversations, works on @@ -54,4 +57,8 @@ of working together. Your private T-W1N versus dedicated teammates with their own accounts, identities, and audiences. + + Run the open-source runtime on your own machine — one teammate, fully + traceable, against the hosted backend. + diff --git a/learning/developers/architecture.mdx b/learning/developers/architecture.mdx index b90b4396a..182a995f7 100644 --- a/learning/developers/architecture.mdx +++ b/learning/developers/architecture.mdx @@ -9,7 +9,8 @@ reading, extending, or embedding the code. It covers the two skill libraries, the actor loop that reads and writes them, and the knowledge and memory subsystems — with symbol names and links into the source throughout. -![The learning loop: ConversationManager dispatches act() to CodeActActor, whose Phase 1 doing loop is gated on FunctionManager and GuidanceManager discovery and whose Phase 2 StorageCheck writes back to both libraries](/images/developers/learning-loop.png) +The learning loop: ConversationManager dispatches act() to CodeActActor, whose Phase 1 doing loop is gated on FunctionManager and GuidanceManager discovery and whose Phase 2 StorageCheck writes back to both libraries +The learning loop: ConversationManager dispatches act() to CodeActActor, whose Phase 1 doing loop is gated on FunctionManager and GuidanceManager discovery and whose Phase 2 StorageCheck writes back to both libraries ## The dual library @@ -82,7 +83,8 @@ sandbox. Both libraries (and Knowledge) read across every scope the assistant can see and write to exactly one: -![Federated scopes: federated_ranked_search fans out to the personal root, team roots, and the read-only builtins catalogue, with ContextRegistry resolving read and write roots](/images/developers/federated-scopes.png) +Federated scopes: federated_ranked_search fans out to the personal root, team roots, and the read-only builtins catalogue, with ContextRegistry resolving read and write roots +Federated scopes: federated_ranked_search fans out to the personal root, team roots, and the read-only builtins catalogue, with ContextRegistry resolving read and write roots - [`ContextRegistry.read_roots`](https://github.com/unifyai/unify/blob/main/unify/common/context_registry.py) returns the personal root first, then `Teams/{team_id}/…` for each id in diff --git a/learning/developers/knowledge-and-memory.mdx b/learning/developers/knowledge-and-memory.mdx index cb267dc3f..a9f6b05fd 100644 --- a/learning/developers/knowledge-and-memory.mdx +++ b/learning/developers/knowledge-and-memory.mdx @@ -10,7 +10,8 @@ and a **background consolidation** pass in [`MemoryManager`](https://github.com/unifyai/unify/blob/main/unify/memory_manager/memory_manager.py) that distills recent traffic into durable state. -![Knowledge acquisition pipelines: the live path runs CodeActActor through primitives.knowledge into an inner LLM tool loop over Knowledge tables; the background path runs EventBus events through MemoryManager.process_chunk into ContactManager, KnowledgeManager, and TaskScheduler](/images/developers/knowledge-memory.png) +Knowledge acquisition pipelines: the live path runs CodeActActor through primitives.knowledge into an inner LLM tool loop over Knowledge tables; the background path runs EventBus events through MemoryManager.process_chunk into ContactManager, KnowledgeManager, and TaskScheduler +Knowledge acquisition pipelines: the live path runs CodeActActor through primitives.knowledge into an inner LLM tool loop over Knowledge tables; the background path runs EventBus events through MemoryManager.process_chunk into ContactManager, KnowledgeManager, and TaskScheduler ## KnowledgeManager diff --git a/learning/developers/storage-check.mdx b/learning/developers/storage-check.mdx index c666b9a49..fa015e3d7 100644 --- a/learning/developers/storage-check.mdx +++ b/learning/developers/storage-check.mdx @@ -11,7 +11,8 @@ persists. This page covers that pass, its mid-run sibling `store_skills`, and the certification gate that turns stored functions into recurring-task executors. -![StorageCheck flow: the trajectory snapshot feeds a skill librarian LLM loop that can add functions, add guidance, certify a task entrypoint, or store nothing; a mid-run store_skills loop feeds summaries into it](/images/developers/storage-check.png) +StorageCheck flow: the trajectory snapshot feeds a skill librarian LLM loop that can add functions, add guidance, certify a task entrypoint, or store nothing; a mid-run store_skills loop feeds summaries into it +StorageCheck flow: the trajectory snapshot feeds a skill librarian LLM loop that can add functions, add guidance, certify a task entrypoint, or store nothing; a mid-run store_skills loop feeds summaries into it ## `_StorageCheckHandle`: the two-phase wrapper diff --git a/local-deployment/custom-overrides.mdx b/local-deployment/custom-overrides.mdx new file mode 100644 index 000000000..4b7908a59 --- /dev/null +++ b/local-deployment/custom-overrides.mdx @@ -0,0 +1,243 @@ +--- +title: "Custom overrides" +description: "Seed and manage state-manager content from version-controlled files" +--- + +Every state manager in the runtime can be seeded from **source files** — +plain JSONL and JSON files (plus decorated Python for functions) that you +keep in version control. At sync time the runtime reconciles each manager's +backing store against these files: entries you declare are inserted, +entries you edit are updated, and entries you delete are removed. Content +the assistant created on its own is left alone. + +This turns a local deployment into something you can configure like code: +check your assistant's starting contacts, guidance, secrets, tasks, and +reference data into a repo, and every fresh deployment converges to the +same state. + +## How syncing works + +Each manager exposes the same pair of building blocks (in +`unify//custom_*.py` in the +[`unifyai/unify`](https://github.com/unifyai/unify) repo): + +- **Collectors** read the source files from one or more directories and + return the declared entries. +- **`sync_custom()`** on the manager reconciles the store against those + entries. + +Reconciliation is **hash-based and idempotent**: + +1. Every source entry carries a stable `key` and gets a content hash. An + aggregate hash over all entries is compared against the last synced + value — if nothing changed, sync is a no-op, so it's cheap to run on + every startup. +2. On a mismatch, entries are reconciled by `key`: new keys are inserted, + changed hashes are updated in place, and keys that disappeared from the + source are deleted from the store. +3. Rows the assistant or user created through normal conversation are + untouched — sync only manages rows it created. The one exception: if a + manually-created row collides with a declared `key`, the source + definition adopts it. +4. Runtime-owned state survives updates. A recurring task keeps its + execution history and status when you edit its definition, and a task + that is actively running is skipped and picked up on the next sync. +5. Invalid lines are skipped with a logged warning — one bad row never + blocks the rest of the file. + +Two fields are understood by every entry type: + +| Field | Default | Meaning | +|---|---|---| +| `destination` | `"personal"` | Where the entry lives — `"personal"` for the assistant's own context, `"team:"` for a shared team context | +| `auto_sync` | `true` | Set `false` to stop managing this entry from source (it's skipped entirely) | + +Blank lines and `#`-prefixed comment lines are allowed in every `.jsonl` +file. + +## The override cascade + +Collectors accept a **list of directories** and merge them in order — when +the same `key` appears in more than one directory, **the later directory +wins**. This gives you a layered override model: + +```text +overrides/ + org/ # baseline shared by everyone + guidance.jsonl + contacts.jsonl + me/ # your personal layer — overrides org on key collision + guidance.jsonl + tasks.jsonl +``` + +Pass `[org, me]` and an entry keyed `crm-runbook` in `me/guidance.jsonl` +replaces the org-level definition of the same key. The hosted product uses +this same mechanism to cascade org → user → assistant configuration; a +local deployment can use as many or as few layers as it wants. + +## Source file formats + +### Guidance — `guidance.jsonl` + +One JSON object per line. `function_names` link the guidance to custom +functions by name (resolved to ids at sync time): + +```json +{"key": "crm-runbook", "title": "CRM escalation runbook", "content": "When a customer reports...", "function_names": ["create_crm_ticket"]} +``` + +### Contacts — `contacts.jsonl` + +```json +{"key": "sarah-chen", "first_name": "Sarah", "surname": "Chen", "email_address": "sarah@example.com", "job_title": "CTO", "should_respond": true, "response_policy": "Always reply same-day."} +``` + +Other supported fields: `phone_number`, `whatsapp_number`, `discord_id`, +`slack_user_id`, `bio`, `timezone`. `key` may be omitted — it defaults to +the lowercased `first_name|surname` pair (secrets similarly default to +their `name`). + +### Secrets — `secrets.jsonl` + +```json +{"key": "crm-api", "name": "CRM_API_KEY", "value": "…", "description": "Read-write CRM access"} +``` + + + Secret values live in the file, so keep secrets source files out of + shared version control (or template them in from your secret store at + deploy time). + + +### Tasks — `tasks.jsonl` + +Declare recurring or event-triggered work. `schedule` and `trigger` are +mutually exclusive; runtime status and execution metadata stay owned by +the scheduler: + +```json +{"key": "monday-digest", "name": "Weekly GitHub digest", "description": "Digest this week's GitHub notifications.", "schedule": {"start_at": "2026-07-13T09:00:00Z"}, "repeat": [{"frequency": "weekly", "weekdays": ["MO"], "time_of_day": "09:00"}]} +{"key": "vip-email-watch", "name": "VIP email watch", "description": "Summarize and flag anything urgent.", "trigger": {"medium": "email", "recurring": true}} +``` + +`schedule` holds an ISO-8601 `start_at`; `repeat` is a list of +RFC-5545-style patterns (`frequency`, `interval`, `weekdays`, `count`, +`until`, `time_of_day`); `trigger` names an inbound communication event +(`medium` such as `email` or `sms_message`, optional +`from_contact_ids`/`omit_contact_ids`, and `recurring` to re-arm after +each run). Other supported fields: `deadline`, `priority`, +`response_policy`, `entrypoint_function` (run a stored function instead of +re-planning), `offline`. + +### Blacklist — `blacklist.jsonl` + +```json +{"key": "spam-caller", "medium": "phone", "contact_detail": "+15551234567", "reason": "Repeated spam"} +``` + +### Knowledge tables — directory tree + +Each table is a subdirectory holding `meta.json` (description, column +types, and the `seed_key` used as the per-row merge key) plus `rows.jsonl`. +The relative path to `meta.json` becomes the table name, so nesting is +allowed: + +```text +knowledge/ + Companies/ + meta.json # {"description": "...", "columns": {...}, "seed_key": "name"} + rows.jsonl + CRM/OperatingRules/ + meta.json + rows.jsonl +``` + +### Reference data tables — directory tree + +Same `meta.json` + `rows.jsonl` shape as knowledge, for DataManager-owned +reference tables. `meta.json` additionally supports `context` (target +context path), `unique_keys`, and `auto_counting`. + +### Dashboards — directory tree + +Tiles and layouts live under `tiles/` and `layouts/` namespaces, each with +the `meta.json` + `rows.jsonl` shape. + +### Functions and venvs — Python and TOML + +Custom functions are ordinary Python decorated with `@custom_function`; +venvs are `pyproject.toml`-style files whose filename becomes the venv +name: + +```python +# functions/acme_workflows.py +from unify.function_manager.custom import custom_function + +@custom_function(venv_name="acme_ml") +def score_lead(company: str) -> float: + """Score a sales lead using the ACME model.""" + ... +``` + +```toml +# venvs/acme_ml.toml +[project] +dependencies = ["scikit-learn>=1.4"] +``` + +Files starting with `_` are ignored. See the +[FunctionManager README](https://github.com/unifyai/unify/blob/main/unify/function_manager/README.md) +for the full decorator reference. + +## Running a sync + +Syncing is **explicit** — nothing watches the files. Collect from your +directory layers and call `sync_custom()` on each manager. Sync functions +first, since guidance and tasks resolve function names to ids: + +```python +from pathlib import Path + +from unify.manager_registry import ManagerRegistry +from unify.function_manager.custom_functions import ( + collect_functions_from_directories, + collect_venvs_from_directories, +) +from unify.guidance_manager.custom_guidance import collect_guidance_from_directories +from unify.task_scheduler.custom_tasks import collect_tasks_from_directories + +layers = [Path("overrides/org"), Path("overrides/me")] + +fm = ManagerRegistry.get_function_manager() +fm.sync_custom( + source_functions=collect_functions_from_directories(layers), + source_venvs=collect_venvs_from_directories(layers), +) + +name_to_id = { + name: data["function_id"] + for name, data in fm.list_functions().items() + if data.get("function_id") is not None +} + +ManagerRegistry.get_guidance_manager().sync_custom( + source_guidance=collect_guidance_from_directories(layers), + function_name_to_id=name_to_id, +) +ManagerRegistry.get_task_scheduler().sync_custom( + source_tasks=collect_tasks_from_directories(layers), + function_name_to_id=name_to_id, +) +``` + +Contacts, secrets, blacklist, knowledge, data, and dashboards follow the +same collect-then-sync shape with their own collectors +(`collect_contacts_from_directories`, `collect_secrets_from_directories`, +and so on). Because every sync is hash-guarded, running the whole pass on +every startup costs almost nothing when the files haven't changed. + +In the hosted product this reconcile runs automatically when an assistant +starts, cascading org, user, and assistant configuration layers. In a +local deployment you decide when it runs — a startup script that calls the +snippet above is the usual shape. diff --git a/local-deployment/day-to-day.mdx b/local-deployment/day-to-day.mdx new file mode 100644 index 000000000..eb06e0b40 --- /dev/null +++ b/local-deployment/day-to-day.mdx @@ -0,0 +1,82 @@ +--- +title: "Day-to-day usage" +description: "The CLI, chatting, and steering work while it's in flight" +--- + +## Commands + +```text +unify Interactive local chat (alias: unify chat) +unify serve Run headless: ConversationManager + gateway +unify stop Stop the local runtime +unify status Show runtime status +unify logs Follow the runtime log +unify doctor Gateway/config checks +unify setup Re-run the key/credential wizard +unify update Update the checkout and re-sync deps +``` + +## Steering while work is in-flight + +When your teammate is mid-task in the local chat REPL, steer it the same +way you would in the hosted Console: **send another message**. There are no +special slash commands — steering is a first-class part of the runtime, not +a REPL feature. + +**Text:** use `msg` at the `>` prompt: + +```text +> msg Actually, narrow it to ones with Rust bindings. +> msg What step are you on? +``` + +Each `msg` publishes a normal inbound chat message. The ConversationManager +wakes the slow brain, which can answer you or redirect in-flight Actor work +through its usual action-steering tools. + +**Voice:** type `meet` to open a LiveKit browser session, then speak +through your mic — see [Voice](/local-deployment/voice). + +**Debugging:** use `trace`, `tree`, or `show_logs` to inspect what's +happening while work is in-flight. + +## What this feels like + +```text +You ▸ "Find me high-throughput vector DBs under Apache 2." +They ▸ (start searching) +You ▸ "Actually, narrow it to ones with Rust bindings." +They ▸ (adjust the in-flight search — don't restart) +You ▸ "Pause that, something urgent." +They ▸ (freeze exactly where they are) +... five minutes later ... +You ▸ "OK, resume. How's it going?" +They ▸ (pick up where they left off, give you a status update) +``` + +Every operation — at every level of the call stack — returns a live +steerable handle, so corrections propagate down into whatever the runtime +is currently doing without restarting it. The mechanics are covered in +[Developers](/local-deployment/developers). + +## Importing skills from other assistants + +OpenClaw and Hermes Agent both represent skills as `SKILL.md` files (the +[agentskills.io](https://agentskills.io) standard). That maps almost +one-to-one onto a GuidanceManager entry, so either skill library can be +imported off-the-shelf as guidance: + +```bash +# Dry run (the default): print what would be imported, write nothing +.venv/bin/python -m scripts.skill_migration.openclaw_to_guidance +.venv/bin/python -m scripts.skill_migration.hermes_to_guidance + +# Import for real (titles are namespaced "[openclaw] …" / "[hermes] …") +.venv/bin/python -m scripts.skill_migration.openclaw_to_guidance --execute +.venv/bin/python -m scripts.skill_migration.hermes_to_guidance --execute +``` + +Each script looks for a sibling checkout (`../openclaw`, +`../hermes-agent`) by default; pass `--repo-root` to point elsewhere. +Re-runs skip titles that already exist; pass `--conflict overwrite` to +update them in place instead. diff --git a/local-deployment/developers.mdx b/local-deployment/developers.mdx new file mode 100644 index 000000000..14dab6098 --- /dev/null +++ b/local-deployment/developers.mdx @@ -0,0 +1,137 @@ +--- +title: "Developers" +description: "How the local runtime is put together, and how to hack on it" +--- + +This page is for anyone reading, extending, or debugging the open-source +[`unifyai/unify`](https://github.com/unifyai/unify) runtime they just +deployed locally. The per-feature developer sub-sections elsewhere in these +docs (communication, workspace, tasks, learning, …) go deep on individual +subsystems; this is the top-level map. + +## How it works + +A persistent **interaction loop** (`ConversationManager`) stays present +across every medium and keeps thinking while work is in flight. When +something needs deeper reasoning, it dispatches a **background reasoner** +(`Actor`) that writes Python plans over a back office of typed state +managers. Every operation returns a live, steerable handle, and those +handles nest — a correction you make in chat propagates *down* through the +dispatched action into whatever manager call is currently running. + +```text +ConversationManager (interaction loop, event-driven scheduling) + │ + │ Slow Brain ◄── IPC ──► Fast Brain (real-time voice + video, LiveKit) + │ + ▼ +CodeActActor (generates Python plans, calls primitives.* APIs) + │ + ▼ +State Managers (each runs its own async LLM tool loop) + │ + ├── ContactManager — people and relationships + ├── KnowledgeManager — domain facts, structured knowledge + ├── TaskScheduler — durable tasks, schedules, triggers + ├── TranscriptManager — conversation history and search + ├── GuidanceManager — procedures, SOPs, how-to knowledge + ├── FileManager — file parsing and registry + ├── ImageManager — image storage, vision queries + ├── FunctionManager — user-defined functions, primitives registry + ├── WebSearcher — web research orchestration + ├── SecretManager — encrypted secret storage + ├── BlacklistManager — blocked contact details + └── DataManager — low-level data operations + │ + ├── EventBus — typed pub/sub backbone (Pydantic events) + └── MemoryManager — offline consolidation every 50 messages +``` + +### Steerable handles — the universal protocol + +Every public manager method returns one — the same `ask`, `interject`, +`pause`, `resume`, `stop` surface at every level of the call stack: + +```python +handle = await actor.act("Survey high-throughput vector DBs and draft a comparison") +await handle.interject("Only ones with Rust bindings") # mid-flight redirect +await handle.pause(); ...; await handle.resume() # freeze and resume +``` + +When the Actor calls `primitives.contacts.ask(...)`, the `ContactManager` +returns its own handle — nested inside the Actor's, which is nested inside +the `ConversationManager`'s. Steering at any level propagates down through +the live call stack as a typed signal any inner loop can act on, not as an +abort or a queued prompt. + +### CodeAct — the Actor writes Python programs + +Rather than emitting one JSON tool call at a time, the Actor writes a +single sandboxed Python program per turn over typed `primitives.*`: + +```python +deps = await primitives.knowledge.ask( + "Which Python deps am I tracking for security updates?" +) +for dep in deps: + latest = await primitives.web.ask( + f"What's the latest released version of {dep}?" + ) + await primitives.knowledge.update( + f"Record that {dep}'s latest known release is {latest}." + ) +``` + +A memory lookup → external check → memory write becomes one coherent plan +with real variables, loops, and control flow. + +## Project structure + +```text +unify/ +├── unify/ # Main package — actor, conversation_manager, common, and one folder per state manager +├── sandboxes/ # Dev / eval playgrounds, one per manager; backs the `unify` CLI +├── tests/ # Pytest suite (cached LLM responses) +├── agent-service/ # Node.js desktop / browser automation +└── deploy/ # Dockerfile, Cloud Build, virtual desktop +``` + +## Where to start reading + +| File | What's there | +|---|---| +| `unify/common/async_tool_loop.py` | `SteerableToolHandle` — the protocol everything returns | +| `unify/common/_async_tool/loop.py` | The async tool loop engine — nesting, steering, context propagation | +| `unify/actor/code_act_actor.py` | CodeAct — plan generation, sandbox, primitives | +| `unify/conversation_manager/conversation_manager.py` | Dual-brain orchestration, debouncing, in-flight actions | +| `unify/conversation_manager/domains/brain_action_tools.py` | How the brain starts, steers, and tracks concurrent work | +| `unify/conversation_manager/domains/call_manager.py` | LiveKit subprocess + voice/video event wiring | +| `unify/function_manager/primitives/registry.py` | How primitives are assembled into the typed API surface | +| `unify/events/event_bus.py` | Typed event backbone | +| `unify/memory_manager/memory_manager.py` | Offline consolidation pipeline | +| `unify//custom_*.py` | File-source collectors behind [custom overrides](/local-deployment/custom-overrides) | + +The full breakdown — async tool loop internals, event bus, primitive +registry, hosted deployment SPI — lives in the repo's +[`ARCHITECTURE.md`](https://github.com/unifyai/unify/blob/main/ARCHITECTURE.md). + +## Running the tests + +Tests exercise the real system — steerable handles, CodeAct, manager +composition, nested tool loops — against real LLMs whose responses are +cached per unique input, not mocked: + +```bash +uv sync --all-groups +source .venv/bin/activate + +tests/parallel_run.sh tests/ # everything +tests/parallel_run.sh tests/actor/ # one module +tests/parallel_run.sh tests/contact_manager/ # another +``` + +First run makes live LLM calls; subsequent runs replay from cache in +milliseconds. See the repo's +[`tests/README.md`](https://github.com/unifyai/unify/blob/main/tests/README.md) +for the full philosophy — delete the cache and you're re-evaluating +against live models. diff --git a/local-deployment/installation.mdx b/local-deployment/installation.mdx new file mode 100644 index 000000000..8b0cc097d --- /dev/null +++ b/local-deployment/installation.mdx @@ -0,0 +1,89 @@ +--- +title: "Installation" +description: "One command to a locally-running teammate" +--- + +## Prerequisites + +- **Python 3.12+** +- **macOS, Linux, or WSL2** +- An **LLM provider key** — OpenAI, Anthropic, or DeepSeek +- A **Unify API key** and an assistant — create both at + [console.unify.ai](https://console.unify.ai) + +## Install + +```bash +curl -fsSL https://raw.githubusercontent.com/unifyai/unify/staging/scripts/install.sh | bash +``` + +The installer clones `unify`, syncs Python dependencies with `uv` (which +resolves the sibling `unisdk` and `unillm` packages), runs a key wizard, +and installs a `unify` CLI shim in `~/.local/bin/`. **Open a new +terminal**, then start chatting: + +```bash +unify +``` + +`unify` opens an interactive local chat with one teammate, powered by the +full local runtime and the hosted Orchestra backend. Run headless +(ConversationManager + gateway) with `unify serve`. + +```text +> What did I leave half-finished on the indexer rewrite last week? +> Watch my open PRs and ping me when one gets reviewed. +> Remind me to send Sarah the benchmark numbers on Thursday. +``` + + + Clones `unify` under `~/.unity/`, runs `uv sync --all-groups`, and writes + `~/.unity/unity/.env` with your `UNIFY_KEY`, `ASSISTANT_ID`, + `ORCHESTRA_URL` (the hosted backend), and an LLM provider key — plus + optional voice and research keys via the bring-your-own-keys wizard. It + installs a `unify` CLI shim in `~/.local/bin/` with a clearly-marked PATH + block appended to your shell rc. + + Get your `UNIFY_KEY` and `ASSISTANT_ID` from + [console.unify.ai](https://console.unify.ai). If you skip a key at + install time (or pipe through a non-interactive shell), add it to + `~/.unity/unity/.env` and run `unify setup`. + + +## Configuration + +All bring-your-own-keys (LLM, voice, research) live in +`~/.unity/unity/.env`. Edit the file and run `unify setup` to apply +changes. The keys the runtime understands: + +| Variable | Purpose | Required? | +|---|---|---| +| `UNIFY_KEY` | Authenticates against the hosted Orchestra backend | Yes | +| `ASSISTANT_ID` | Which assistant record the runtime drives | Yes | +| `ORCHESTRA_URL` | Backend URL (defaults to the hosted backend) | Set by installer | +| `OPENAI_API_KEY` / `ANTHROPIC_API_KEY` / `DEEPSEEK_API_KEY` | Lets the assistant think and reply | One of these | +| `TAVILY_API_KEY` | Web search while researching (free tier) | Optional | +| `ANTI_CAPTCHA_KEY` | CAPTCHA solving during computer use | Optional | +| LiveKit + speech keys | Browser voice calls | See [Voice](/local-deployment/voice) | + +## Running from a checkout + +If you cloned the repo yourself rather than using the installer: + +```bash +pip install uv && uv sync --all-groups +``` + +The interactive chat is `.venv/bin/python -m +sandboxes.conversation_manager.sandbox`, and the headless runtime is `bash +scripts/local.sh start --full`. Always use the project virtualenv at +`.venv/` — the key wizard is available as `scripts/prompt_byok_keys.sh`. + +## Updating + +```bash +unify update +``` + +This updates the checkout and re-syncs dependencies. Your `.env` is +preserved. diff --git a/local-deployment/overview.mdx b/local-deployment/overview.mdx new file mode 100644 index 000000000..a6c9523e3 --- /dev/null +++ b/local-deployment/overview.mdx @@ -0,0 +1,76 @@ +--- +title: "Overview" +description: "Run the open-source unify runtime on your own machine" +--- + +Everything your assistant's brain does — the reasoning loop, memory, +steerable tasks, voice — lives in the MIT-licensed +[`unifyai/unify`](https://github.com/unifyai/unify) repository, and you can +run it locally. A local deployment gives you **one teammate** running +entirely on your laptop, talking to the hosted Orchestra backend for +persistence, so you can trace exactly how it thinks while keeping the +zero-maintenance storage layer. + +The shape is deliberately simple: install once, and the runtime lives on +your machine, accumulating state across every session. No Docker, no +Kubernetes, no local database. + +## What runs where + +The local runtime drives the assistant record whose `ASSISTANT_ID` you set, +provisioned at [console.unify.ai](https://console.unify.ai). Locally you +get: + +- **Chat** — an interactive REPL with the full ConversationManager + Actor + runtime behind it. An LLM key (OpenAI, Anthropic, or DeepSeek) is what + lets it think and reply. +- **Browser voice calls** — the production fast-brain (interruption-handling, + telephony-aware) running locally with sub-second latency. See + [Voice](/local-deployment/voice). +- **Web search** — a free [Tavily](https://tavily.com) key lets it look + things up while researching. +- **Computer use** — it drives a real browser and desktop; an optional + [AntiCaptcha](https://anti-captcha.com) key lets it get past CAPTCHAs + instead of stalling. + +The onboarding flow, inbound messaging channels (SMS / WhatsApp / phone, +Slack, Gmail, Outlook, Teams, Discord), Google / Microsoft workspace +connect, third-party app integrations, screen-share, and the +multi-teammate experience are part of the hosted product at +[console.unify.ai](https://console.unify.ai). + +## The runtime stack + +The open one-teammate runtime is three MIT-licensed repos. They talk to the +hosted Orchestra backend; you can also use any of them independently. + +| Repo | Role | +|---|---| +| [**unify**](https://github.com/unifyai/unify) | Agent runtime — managers, tool loops, CodeAct, voice, orchestration | +| [**unisdk**](https://github.com/unifyai/unisdk) | Python SDK — how the runtime talks to Orchestra | +| [**unillm**](https://github.com/unifyai/unillm) | LLM access layer — OpenAI, Anthropic, or any compatible endpoint | +| **orchestra** | Persistence backend — hosted at [console.unify.ai](https://console.unify.ai) | + +## Start here + + + + One command, a key wizard, and a `unify` CLI in your PATH — a few + minutes on macOS, Linux, or WSL2. + + + Real browser voice calls with sub-second latency — LiveKit Cloud plus + free-tier speech keys. + + + The CLI commands, chatting, and steering work while it's mid-task. + + + Seed contacts, guidance, tasks, secrets, and reference data from + version-controlled files. + + + How the runtime is put together, running the tests, and where to start + reading the code. + + diff --git a/local-deployment/voice.mdx b/local-deployment/voice.mdx new file mode 100644 index 000000000..fd12851c1 --- /dev/null +++ b/local-deployment/voice.mdx @@ -0,0 +1,40 @@ +--- +title: "Voice" +description: "Talk to your locally-running teammate in the browser" +--- + +Real voice calls run the production fast-brain — interruption-handling, +telephony-aware — locally with sub-second latency. From the local chat +REPL, type `meet` to open a LiveKit browser session and speak through your +mic. Utterances follow the same Unify Meet path as production voice. + +## What you need + +Voice uses **LiveKit Cloud** for browser Meet sessions, plus one +speech-to-text and one text-to-speech provider (both have free tiers; pick +**one** TTS provider). The install wizard prompts for these; to add them +later, edit `~/.unity/unity/.env` and run `unify setup`. + +| Variable | Purpose | Where to get it | +|---|---|---| +| `LIVEKIT_URL`, `LIVEKIT_API_KEY`, `LIVEKIT_API_SECRET` | Browser voice media | [cloud.livekit.io](https://cloud.livekit.io) | +| `DEEPGRAM_API_KEY` | Speech-to-text | [console.deepgram.com](https://console.deepgram.com) — free tier | +| `CARTESIA_API_KEY` *or* `ELEVEN_API_KEY` | Text-to-speech (pick one) | [play.cartesia.ai](https://play.cartesia.ai) or [elevenlabs.io](https://elevenlabs.io) — free credits | + +## How local voice works + +Live calls run two coordinated brains: + +- **Slow brain** (`ConversationManager`) — sees everything, decides + deliberately, runs in the main process. +- **Fast brain** — a real-time LiveKit voice agent in a subprocess, + sub-second latency, handles turn-taking autonomously. + +They communicate over IPC. The slow brain steers the fast brain with +**SPEAK** (say exactly this), **NOTIFY** (here's context, decide what to +do), or **BLOCK** (do nothing; carry on). This is the same dual-brain +architecture the hosted product runs — locally you're just running both +halves on your own machine. + +Phone calls over real telephony, video calls, and screen-share are part of +the hosted product at [console.unify.ai](https://console.unify.ai). diff --git a/mint.json b/mint.json index bd1150a9e..1668e868e 100644 --- a/mint.json +++ b/mint.json @@ -32,6 +32,17 @@ "introduction" ] }, + { + "group": "Local Deployment", + "pages": [ + "local-deployment/overview", + "local-deployment/installation", + "local-deployment/voice", + "local-deployment/day-to-day", + "local-deployment/custom-overrides", + "local-deployment/developers" + ] + }, { "group": "Communication", "pages": [ @@ -45,7 +56,6 @@ "communication/slack", "communication/microsoft-teams", "communication/discord", - "communication/meetings", "communication/setup", "communication/voice", "communication/behavior", @@ -68,6 +78,8 @@ "workspace/overview", "workspace/connecting", "workspace/email", + "workspace/teams", + "workspace/meetings", "workspace/calendar", "workspace/files", "workspace/contacts-and-tasks", @@ -164,6 +176,7 @@ "pages": [ "teams/overview", "teams/membership", + "teams/team-assistants", "teams/shared-context", "teams/managing", "teams/developers" diff --git a/tasks/developers.mdx b/tasks/developers.mdx index c1590e04b..d0c914bd3 100644 --- a/tasks/developers.mdx +++ b/tasks/developers.mdx @@ -27,7 +27,8 @@ Three design commitments shape everything below: ## The task model -Anatomy of a Task: the task row with name/description, priority/deadline, destination, status; schedule XOR trigger with repeat patterns; entrypoint and offline flags; and the task_id/instance_id split +Anatomy of a Task: the task row with name/description, priority/deadline, destination, status; schedule XOR trigger with repeat patterns; entrypoint and offline flags; and the task_id/instance_id split +Anatomy of a Task: the task row with name/description, priority/deadline, destination, status; schedule XOR trigger with repeat patterns; entrypoint and offline flags; and the task_id/instance_id split The model lives in [`unify/task_scheduler/types/task.py`](https://github.com/unifyai/unify/blob/main/unify/task_scheduler/types/task.py). @@ -75,7 +76,8 @@ the real work: ## The lifecycle -Task lifecycle state machine: scheduled and triggerable states flow into active via execute(), which fans out to completed, failed, and cancelled; execute() also clones the next instance back into scheduled/triggerable +Task lifecycle state machine: scheduled and triggerable states flow into active via execute(), which fans out to completed, failed, and cancelled; execute() also clones the next instance back into scheduled/triggerable +Task lifecycle state machine: scheduled and triggerable states flow into active via execute(), which fans out to completed, failed, and cancelled; execute() also clones the next instance back into scheduled/triggerable [`Status`](https://github.com/unifyai/unify/blob/main/unify/task_scheduler/types/status.py) is a six-value enum: `scheduled`, `triggerable`, `active`, `completed`, @@ -158,7 +160,8 @@ rows) inherit the task's scope. ## Activation: three lanes -Activation lanes: scheduled-live via activations, timers, and TaskDue into the ConversationManager; triggered-live via inbound messages, mechanical filter, semantic check, and primitives.tasks.execute; offline via dispatched jobs running offline_runner — all converging on TaskScheduler.execute returning an ActiveTask +Activation lanes: scheduled-live via activations, timers, and TaskDue into the ConversationManager; triggered-live via inbound messages, mechanical filter, semantic check, and primitives.tasks.execute; offline via dispatched jobs running offline_runner — all converging on TaskScheduler.execute returning an ActiveTask +Activation lanes: scheduled-live via activations, timers, and TaskDue into the ConversationManager; triggered-live via inbound messages, mechanical filter, semantic check, and primitives.tasks.execute; offline via dispatched jobs running offline_runner — all converging on TaskScheduler.execute returning an ActiveTask A task row does nothing by itself. Activation — deciding *now is the moment* — arrives through one of three lanes, all converging on diff --git a/teams/developers.mdx b/teams/developers.mdx index 0dc8cf6f5..d43c52971 100644 --- a/teams/developers.mdx +++ b/teams/developers.mdx @@ -24,9 +24,11 @@ namespaces addressed by path. Team sharing is implemented as a pure *namespace* concern: the same tables exist under multiple **roots**, and one class decides which root any given operation touches. -![Two scopes, one registry: state managers pass a destination string to ContextRegistry, which resolves it to either the personal root or a membership-gated team root — with the same table names under every root](/images/developers/teams-scope-model.png) +Two scopes, one registry: state managers pass a destination string to ContextRegistry, which resolves it to either the personal root or a membership-gated team root — with the same table names under every root +Two scopes, one registry: state managers pass a destination string to ContextRegistry, which resolves it to either the personal root or a membership-gated team root — with the same table names under every root -There are exactly two kinds of root: +There are two kinds of root for a personally-supervised assistant, and a +third shape for [team assistants](/teams/team-assistants): - **Personal root** — the assistant's own base context, `{user_id}/{assistant_id}/…`. This is the *privacy floor*: content here @@ -34,6 +36,19 @@ There are exactly two kinds of root: - **Team roots** — `Teams/{team_id}/…`, one per team the assistant belongs to. Contexts under a team root are owned by the team, not by any assistant, and are visible to every current member. +- **Team-owned home** — a team assistant + (`SESSION_DETAILS.owner_team_id is not None`, exposed as + `SESSION_DETAILS.team_owned`) has **no personal root at all**. Its + shared tables home at the owning team's flat root (`Teams/{owner}/…`, + the same surfaces every member reads), while per-assistant runtime + internals (events, caches, non-shared tables) live under a namespaced + subtree, `Teams/{owner}/Assistants/{agent_id}/…` — still team-owned, + just collision-free when a team owns several assistants. Task-machine + state (`unify/task_scheduler/machine_state.py`) routes to the team's + `Teams/{owner}/Tasks/…` tree for the same reason. The root itself is + resolved in + [`unify/common/runtime_context.py`](https://github.com/unifyai/unify/blob/main/unify/common/runtime_context.py)'s + `resolve_runtime_context_root()`. The arbiter is `ContextRegistry` in [`unify/common/context_registry.py`](https://github.com/unifyai/unify/blob/main/unify/common/context_registry.py). @@ -50,6 +65,16 @@ the two resolution primitives everything else is built on: every team in `SESSION_DETAILS.team_ids` (sorted), provisioning any that don't exist yet. Reads always fan out. +For a **team-owned** session both primitives collapse onto the team shape: +`destination=None` (and, forgivingly, `"personal"`) resolves shared tables +to the owning team's root instead of a personal one, `read_roots` returns +the owning team first followed by other member teams — with no personal +root anywhere — and non-shared tables resolve to the +`Teams/{owner}/Assistants/{agent}` subtree. The owning team also passes +the membership gate unconditionally and is protected from +`forget_departed_team_roots` eviction: it's the assistant's home, not a +revocable membership. + Destination parsing is centralized in `ContextRegistry.canonical_destination(...)`: `None` and `"personal"` normalize to the personal root; `"team:"` is validated for shape @@ -106,7 +131,8 @@ this: ## Writes and reads, end to end -![Write path: a destination string is normalized, membership-gated, and resolved to exactly one root. Read path: read_roots fans out across the personal root and every member team, merged by federated search into one global window](/images/developers/teams-write-read-paths.png) +Write path: a destination string is normalized, membership-gated, and resolved to exactly one root. Read path: read_roots fans out across the personal root and every member team, merged by federated search into one global window +Write path: a destination string is normalized, membership-gated, and resolved to exactly one root. Read path: read_roots fans out across the personal root and every member team, merged by federated search into one global window ### The write path @@ -134,7 +160,9 @@ deletion semantics simple. **One exception, by design:** transcripts and images. Conversation history isn't authored *at* a destination — it happens — so `ContextRegistry.implicit_shared_destinations()` returns the full list of -team destinations (or `[None]` when the assistant has no teams), and the +team destinations (or `[None]` when the assistant has no teams; a +team-owned assistant's owning team is always included, membership payload +or not), and the transcript/screenshot publishing paths in [`unify/conversation_manager/domains/managers_utils.py`](https://github.com/unifyai/unify/blob/main/unify/conversation_manager/domains/managers_utils.py) and the screenshot capture path in @@ -152,29 +180,43 @@ goes through it: - **`FederatedSearchContext`** — one participating context: the concrete path, a `source` label, an optional `row_filter`, optional - `allowed_fields` projection, and an optional foreign `project` (used for - the read-only platform builtins catalogue, which lives outside the - active project). -- **`federated_ranked_search(contexts, references, ...)`** — exact - federated top-k semantic search. Each context is fetched with a local - window of `offset + limit` (which makes the global merge provably - exact — a row outside its local window can't be in the global one), - merged by ascending embedding distance via `merge_ranked_batches`, and - sliced once. Optional `backfill` tops up short results with - deterministic recent rows. + `allowed_fields` / `excluded_fields` projections, and an optional + foreign `project` (used for the read-only platform builtins catalogue, + which lives outside the active project). - **`federated_filter(contexts, filter=..., sorting=...)`** — exact - federated structured reads, with `SortSpec` handling the subtle - NULLs-ordering problem: the backend always sorts NULLs last, so a sort - key asking for `missing="first"` forces a full per-context fetch instead - of windowed fetching. + federated structured reads. These are **server-side**: one call to the + backend's federated logs endpoint (`unisdk.get_logs_federated`, i.e. + `POST /logs/federated`) runs every context through the ordinary + single-context query pipeline and performs the exact global merge on + the server — one round trip, globally-ordered window, exact totals — + instead of paging each context over HTTP and merging client-side. + `SortSpec` handles the subtle NULLs-ordering problem: the backend sorts + NULLs last, so a sort key asking for `missing="first"` forces full + branch fetches. A `fetcher` escape hatch retains the client-side merge + path for rows that don't come from the logs API (local stores, + impl-specific reads). +- **`federated_count(contexts, key=...)`** — a count-only federated read + (one server call with `limit=0`). +- **`federated_ranked_search(contexts, references, ...)`** — exact + federated top-k semantic search. This path stays client-orchestrated + (it depends on per-context embedding columns): each context is fetched + with a local window of `offset + limit` (which makes the global merge + provably exact — a row outside its local window can't be in the global + one), merged by ascending embedding distance via + `merge_ranked_batches`, and sliced once. Optional `backfill` tops up + short results with deterministic recent rows. - **`federated_reduce(contexts, metric=..., ...)`** — aggregations. Decomposable ungrouped metrics (`count`, `sum`, `min`, `max`, `mean`) are pushed down per context and combined exactly; grouped or - non-decomposable metrics (`median`, `mode`, `var`, `std`) fall back to - fetching merged rows and reducing client-side. -- Missing contexts (404s — a table not yet provisioned under some root) - are tolerated everywhere via `is_missing_context_error`, so fan-out - reads don't require every root to be fully provisioned. + non-decomposable metrics (`median`, `mode`, `var`, `std`) fetch the + merged rows through the federated endpoint and reduce client-side. +- Missing contexts (a table not yet provisioned under some root) + contribute nothing, so fan-out reads don't require every root to be + fully provisioned. + +The same server endpoint powers the Console's merged **All** scope views +(Functions, Transcripts, and friends), so what the UI shows and what the +runtime reads are the same exact merge. Merged rows are annotated with `_federated_source` and `_federated_context` (and `_federated_score` for ranked reads), so callers @@ -223,12 +265,13 @@ different semantics worth knowing: ## How membership reaches the runtime -![Membership flow: Orchestra owns team membership and delivers team_ids and team_summaries in the startup payload to SESSION_DETAILS, which feeds the system prompt block and worker env vars; a mid-session AssistantUpdateEvent refreshes membership live and forget_departed_team_roots drops cached roots](/images/developers/teams-membership-runtime.png) +Membership flow: Orchestra owns team membership and delivers team_ids and team_summaries in the startup payload to SESSION_DETAILS, which feeds the system prompt block and worker env vars; a mid-session AssistantUpdateEvent refreshes membership live and forget_departed_team_roots drops cached roots +Membership flow: Orchestra owns team membership and delivers team_ids and team_summaries in the startup payload to SESSION_DETAILS, which feeds the system prompt block and worker env vars; a mid-session AssistantUpdateEvent refreshes membership live and forget_departed_team_roots drops cached roots ### Session state [`unify/session_details.py`](https://github.com/unifyai/unify/blob/main/unify/session_details.py) -is the single source of runtime truth. `AssistantDetails` carries two +is the single source of runtime truth. `AssistantDetails` carries three team fields, surfaced as convenience properties on the global `SESSION_DETAILS`: @@ -237,13 +280,21 @@ team fields, surfaced as convenience properties on the global - `team_summaries: list[TeamSummary]` — display/routing metadata (`team_id`, `name`, `description`) used to *teach* the model where content belongs. +- `owner_team_id: int | None` — the owning team for [team + assistants](/teams/team-assistants) (`None` = user-owned), with + `SESSION_DETAILS.team_owned` as the derived flag that switches the + registry, root resolution, task-machine routing, and prompt layer into + team-owned mode. Because actor plans and manager tool-loops can run in worker -subprocesses, both fields round-trip through the environment: -`export_team_ids_to_env` / `export_team_summaries_to_env` encode them into -`TEAM_IDS` (CSV) and `TEAM_SUMMARIES` (JSON), and `SESSION_DETAILS` -re-hydrates from those vars on the other side via -`normalize_team_summaries`. +subprocesses, all of it round-trips through the environment: +`export_team_ids_to_env` / `export_team_summaries_to_env` encode +memberships into `TEAM_IDS` (CSV) and `TEAM_SUMMARIES` (JSON), +`OWNER_TEAM_ID` carries ownership, and `SESSION_DETAILS` re-hydrates from +those vars on the other side via `normalize_team_summaries` / +`populate_from_env`. All three arrive in the startup payload from +Orchestra's assistant record (which carries `owner_team_id` for team-owned +assistants). ### Live membership updates @@ -280,6 +331,13 @@ content clearly matches its described domain, ask a brief clarifying question when the audience is unclear, never invent a team id, and never pass a `team:` token where a `contact_id` belongs. +Team-owned sessions render a distinct variant: the identity line becomes +*"You are a team-owned assistant: your owning team's shared root is your +home memory… You have no private personal memory"*, the owning team's +bullet is marked as the default destination, the `personal` bullet +disappears, and the routing rules swap "default to personal" for "writes +default to your owning team's shared root." + Two design details matter here: - **Team descriptions are load-bearing.** The model routes content by @@ -342,9 +400,12 @@ A few recipes that follow directly from the architecture: Everything on this page is the runtime's half of the story. The other - half — the `Team`/`TeamAssistantMembership` data model, membership - endpoints, coordinator auto-enrollment, org-wide sharing, and the purge - of `Teams/{id}/…` trees on team deletion — lives in the closed-source - Orchestra backend, which the runtime reaches through + half — the `Team`/`TeamAssistantMembership` data model, the + `owner_team_id` ownership column and its lifecycle guards (an owning + team refuses deletion while it owns assistants; a team assistant can't + be removed from its owning team), membership endpoints, coordinator + auto-enrollment, org-wide sharing, per-team spend attribution, and the + purge of `Teams/{id}/…` trees on team deletion — lives in the + closed-source Orchestra backend, which the runtime reaches through [UniSDK](https://github.com/unifyai/unisdk). diff --git a/teams/managing.mdx b/teams/managing.mdx index aa522aefb..9da77b220 100644 --- a/teams/managing.mdx +++ b/teams/managing.mdx @@ -20,6 +20,10 @@ permissions (Owner or Admin role). - **Update or delete** from the team's row menu. Deleting a team is consequential: it permanently removes the team's entire shared pool — the knowledge, playbooks, tasks, and data accumulated in it. +- **Teams that own assistants can't be deleted.** If the team has [team + assistants](/teams/team-assistants), its pool is their entire memory — + deletion is refused until those assistants are deleted first, so a + team's AI staff can never be wiped out as a side effect. ## Managing members @@ -32,15 +36,29 @@ From each team's menu: their own right. The simplest way is to ask your org's T-W1N — org-shaped setup like team creation, membership, and shared credentials is exactly the work it's for. +- **Hire directly into the team** — the team's **Members** panel has a + **Hire for this team** action that onboards a new [team + assistant](/teams/team-assistants) with this team preselected as its + owner. (A team assistant can't be removed from its owning team — that + membership is structural.) Access changes take effect immediately, including for assistants that are mid-session. -## Org-wide sharing: the one-team shortcut +## Team spend -When you create an organization (and any time after, from the Teams tab), -you choose a data-sharing posture. The Console explains the trade -directly: +Assistant spending is attributable per team: the organization's spending +breakdown can be scoped to any team, aggregating across every [team +assistant](/teams/team-assistants) that team owns. Their caps are managed +by org admins, bounded by the organization's cap, and limit alerts go to +the org's Owners and Admins — the full model is described in +[Team assistants](/teams/team-assistants#spend-caps-and-attribution). + +## Org-wide sharing: on by default + +New organizations start with org-wide sharing **enabled** — choose +**private** at creation (or toggle later from the Teams tab) to opt out. +The Console explains the trade directly: > By default, all skills acquired and knowledge retained are personal to > each user's own private teammate… Each user's teammate learns in @@ -53,7 +71,7 @@ directly: > controlled sharing within specific teams, rather than blanket org-wide > sharing. -Enabling **org-wide sharing** creates a managed team named **Org** that +Org-wide sharing runs through a managed team named **Org** that automatically includes every current and future member and assistant. A few things to know about it: @@ -64,16 +82,21 @@ A few things to know about it: and files are never shared** through the org pool. Only knowledge, skills, and general know-how flow into it, creating what the Console calls "a faster hive-mind approach to learning across the team." +- **It's the natural home for company-wide assistants.** Hiring a [team + assistant](/teams/team-assistants) into the Org team gives you an + assistant that belongs to — and is usable by — the entire organization, + present and future. - **Turning it off deletes the pool.** Disabling org-wide sharing removes the Org team *and all shared content in it* — the Console warns you, and - it cannot be undone. + it cannot be undone. (Like any team, it can't be disabled while it owns + team assistants.) ## Choosing your structure | Setup | Fits | |---|---| -| **No teams** (all private) | Solo users; orgs where assistants genuinely shouldn't share | -| **Org-wide sharing only** | Small orgs where everyone works on everything | +| **Fully private** (opt out at creation) | Solo users; orgs where assistants genuinely shouldn't share | +| **Org-wide sharing only** (the default) | Small orgs where everyone works on everything | | **Dedicated teams** | Any org with units, divisions, or sensitivity boundaries — the recommended shape as you grow | The options combine: you can run org-wide sharing for general know-how diff --git a/teams/membership.mdx b/teams/membership.mdx index 7d5a3794b..316f28cb4 100644 --- a/teams/membership.mdx +++ b/teams/membership.mdx @@ -3,10 +3,10 @@ title: "People & assistants" description: "Who belongs to a team, and how assistant access follows" --- -Teams contain **people** and **assistants**, and the platform treats the two -kinds of assistants — your private T-W1N and publicly visible teammates — -very differently. Getting this distinction right is most of understanding -teams. +Teams contain **people** and **assistants**, and the platform treats the +kinds of assistants — your private T-W1N, personally-supervised hires, and +[team assistants](/teams/team-assistants) — very differently. Getting +these distinctions right is most of understanding teams. ## Your T-W1N: private, and it follows you @@ -27,25 +27,36 @@ team *you're* on — the sales team's playbooks, the ops team's data — while remaining completely invisible to your colleagues. They benefit from what it shares into team pools without ever interacting with it directly. -## Public assistants: visible, and members in their own right +## Hired assistants: visible, and members in their own right -Regular hired assistants are the opposite on both counts: +Hired assistants are the opposite on both counts: 1. **They're visible across the organization.** Colleagues (with the appropriate org permissions) can find them in the assistant list, open their profiles, and message them — they're shared team members in the fullest sense. -2. **They're added to teams directly.** A public assistant's team - memberships are its own — granted explicitly, not inherited from - whoever hired it. The person who onboarded an assistant might not even - be on the teams it serves. +2. **Their team memberships are their own** — granted explicitly, not + inherited from any person. One assistant can serve several teams at + once (the sidebar shows a badge listing all of them). -This is what makes public assistants useful as **specialists**: a repairs +Within that, two ownership models: + +- **Team assistants** — owned by a team rather than a person. Hiring + defaults to this: you pick an **owning team** during the hire, the team + becomes the assistant's home, and membership in that team is + *structural* — it can't be removed from its owning team, and the team + can't be deleted while it owns assistants. Everything about them is + covered in [Team assistants](/teams/team-assistants). +- **Personally-supervised assistants** — owned by the person who hired + them (choose **Personal** during the hire). They keep a private memory + as their [privacy floor](/teams/shared-context) and join teams as an + add-on: each membership layers that team's shared pool on top. + +This is what makes hired assistants useful as **specialists**: a repairs coordinator serving the whole repairs team, a reporting assistant embedded -in finance. They belong to the unit, not to a person, and one assistant can -serve several teams at once (the sidebar shows a badge listing all of -them). For how these teammates come to exist — and how their identity -differs from a T-W1N's — see [Hiring](/hiring/overview). +in finance. They belong to the unit, not to a person. For how these +teammates come to exist — and how their identity differs from a T-W1N's — +see [Hiring](/hiring/overview). ## Joining and leaving @@ -53,11 +64,16 @@ Membership is live — access tracks it immediately: - **Joining** a team gives a member (human or assistant) access to the team's whole [shared pool](/teams/shared-context) — knowledge, guidance, - skills, tasks, data, credentials — from that moment. + skills, tasks, data, credentials — from that moment. It also puts them + in the team's group chat. - **Leaving** (or being removed) ends that access just as cleanly. The shared content itself **stays with the team** for the remaining members — people take their access away with them, not the team's accumulated know-how. +- **The one structural exception:** a [team + assistant](/teams/team-assistants) cannot leave or be removed from its + *owning* team — that team's pool is its entire memory. It can join and + leave any *other* team freely, like any assistant. - **Leaving the organization** removes a person from all its teams — and their T-W1N from all of them too, automatically. @@ -66,9 +82,10 @@ revocation doesn't wait for the next session. ## Who's who at a glance -| | Your T-W1N | Public assistants | -|---|---|---| -| Visible to colleagues | Never | Yes — listed, searchable, messageable | -| Team membership | Automatic — mirrors yours | Explicit — granted per team | -| Access ceiling | Exactly your teams | Exactly its teams | -| Serves | You | The team(s) it belongs to | +| | Your T-W1N | Personally-supervised | Team assistant | +|---|---|---|---| +| Visible to colleagues | Never | Yes — listed, searchable, messageable | Yes — listed, searchable, messageable | +| Owned by | You | The person who hired it | The owning team | +| Team membership | Automatic — mirrors yours | Explicit — granted per team | Owning team is structural; other teams explicit | +| Private memory | Yes — its privacy floor | Yes — its privacy floor | None — the team pool is its memory | +| Answers to | You | Its supervisor (and teams it joins) | Anyone on the team | diff --git a/teams/overview.mdx b/teams/overview.mdx index 981f94ea7..43eb69b89 100644 --- a/teams/overview.mdx +++ b/teams/overview.mdx @@ -6,9 +6,11 @@ description: "Group people and their assistants — and give them a shared brain An organization rarely works as one undifferentiated blob. Sales runs differently from engineering; the finance team's data is not the marketing team's business. **Teams** bring that structure to your assistants: a team -groups **people and assistants** within your organization, and gives them a +groups **people and assistants** within your organization, gives them a **shared memory pool** — knowledge, playbooks, skills, tasks, data, and -credentials that every member can use, and nobody outside can. +credentials that every member can use, and nobody outside can — and a +**group chat** where all of them, human and AI, work together in one +thread. ## Why teams exist @@ -25,61 +27,102 @@ Teams unlock the middle ground that real organizations need: *current members only*. Division-specific data, sensitive procedures, and elevated-access credentials stay inside the unit rather than spilling across the whole org. -- **The default is still private.** Assistants keep personal memory as - their [privacy floor](/teams/shared-context) — they share into a team - pool only when content clearly belongs to that team, and ask when unsure. +- **Privacy floors are respected.** Personally-supervised assistants keep + personal memory as their [privacy floor](/teams/shared-context) — they + share into a team pool only when content clearly belongs to that team, + and ask when unsure. ([Team assistants](/teams/team-assistants) are the + deliberate exception: they belong to the team, so the team pool *is* + their memory.) +- **A team can hire its own assistants.** A [team + assistant](/teams/team-assistants) is owned by the team itself — no + single supervisor, everyone can work with it, and everything it learns + is shared with the team by construction. This shared-context machinery is the **most important thing teams do** — covered in depth in [Shared context](/teams/shared-context). ## Who's in a team -Two kinds of members, with different rules: +Three kinds of members, with different rules: - **People** — colleagues from your organization, added by an org admin. -- **Assistants** — in two flavors: - - **Everyone's T-W1N follows its person.** Your digital twin is - [private to you](/teams/membership) — hidden from everyone else in the - org — and it automatically joins and leaves teams *with you*, so it - always has exactly your team access, never more. - - **Regular assistants join directly.** Publicly visible teammates - (the ones anyone in the org can find and message) are added to teams - in their own right — their access comes from their team memberships, - not from whoever hired them. + Humans appear in the workspace roster with live online indicators, and + you can message any of them directly. +- **Everyone's T-W1N follows its person.** Your digital twin is + [private to you](/teams/membership) — hidden from everyone else in the + org — and it automatically joins and leaves teams *with you*, so it + always has exactly your team access, never more. +- **Hired assistants join in their own right** — and come in two + ownership models: + - **Team assistants** (the default when hiring) are *owned by a team*: + the team's shared pool is their entire memory, anyone on the team can + work with them, and no individual holds a supervisor position. See + [Team assistants](/teams/team-assistants). + - **Personally-supervised assistants** are owned by one person but can + still be added to any number of teams, layering team pools on top of + their private memory. Details and edge cases in [People & assistants](/teams/membership). ## Where you see teams -In the **Assistants** sidebar, colleagues are grouped under the teams they -belong to — your T-W1N pinned at the top, team groups below, and -independent colleagues listed outside any group. An assistant serving -several teams shows a badge with all of them. +The **Assistants** sidebar is a full org roster, grouped into three +sections: **Teams** (each team with its members beneath it — an assistant +serving several teams shows a badge listing them, and team-owned +assistants carry a **Team-owned** badge under their owning team), **Real** +(your human colleagues, with online indicators), and **Virtual** +(assistants that aren't in any team). Your own T-W1N stays pinned at the +top. + +**Selecting a team** opens the team's workspace: + +- **Chat** — the team's group chat, one thread shared by every member, + human and AI. Messages behave like a conversation in any group channel: + every member sees them, assistants read the room and reply when a + message is genuinely for them (an @mention helps), and their replies are + visible to — and can wake — their AI teammates. +- **Members** — the roster with online status, and a **Hire for this + team** action to onboard a new [team assistant](/teams/team-assistants) + directly into it. +- **Every brain section, team-scoped** — Tasks, Dashboards, Contacts, + Transcripts, Knowledge, Functions, Guidance, and Data all show the + *team's* shared pool rather than any single assistant's memory. + +**Selecting an assistant** gives the complementary view: each brain pane +has scope chips — **All**, **Personal**, and one per team — so you can see +its private memory, any single team pool, or the exact merged picture the +assistant itself works from. Team administration lives under **Organizations → Teams** — see [Managing teams](/teams/managing). -## One org-wide team, if you want it +## One org-wide team, on by default -For small organizations that just want everything shared, there's a -shortcut: **org-wide sharing** creates a managed team called **Org** that -automatically includes every current and future member and assistant. It's -a blunt instrument by design — most organizations outgrow it and move to -dedicated teams for the control they offer. Both are covered in +New organizations start with **org-wide sharing enabled**: a managed team +called **Org** that automatically includes every current and future member +and assistant. It's the simplest useful structure — one shared pool for +general know-how, and the natural home for company-wide [team +assistants](/teams/team-assistants). Organizations that need boundaries +add dedicated teams on top (the narrower pool always keeps its boundary), +or opt out to fully private at creation. All covered in [Managing teams](/teams/managing). ## What's in this section - How membership works — T-W1N inheritance, public assistants, joining + How membership works — T-W1N inheritance, hired assistants, joining and leaving. + + Assistants owned by the team itself — no single supervisor, shared + memory by construction. + The heart of teams: personal vs. team memory across knowledge, guidance, skills, tasks, data, and credentials. - Creating teams, membership admin, and org-wide sharing. + Creating teams, membership admin, org-wide sharing, and team spend. diff --git a/teams/shared-context.mdx b/teams/shared-context.mdx index a53dcb6ce..5a11e4b10 100644 --- a/teams/shared-context.mdx +++ b/teams/shared-context.mdx @@ -11,6 +11,13 @@ Teams exist so that pool doesn't have to be all-or-nothing: every piece of context is either **personal** (belonging to one assistant) or **shared in a team** (accessible to every current member). +One deliberate exception up front: [team +assistants](/teams/team-assistants) have no personal scope at all — their +owning team's pool *is* their memory. Everything below about choosing +between personal and team applies to T-W1Ns and personally-supervised +assistants; a team assistant simply defaults to its owning team and can +additionally write into other teams it belongs to. + ## The two scopes @@ -94,10 +101,20 @@ org": ## Seeing scopes in the Console -Shared context is visible where you'd expect: **Guidance** and -**Knowledge** entries carry scope badges showing whether they're personal -or belong to a team, and the [Data pane](/canvas/data) groups team-owned -tables under their team alongside the assistant's personal folders. +Every brain pane on an assistant — Contacts, Transcripts, Knowledge, +Functions, Guidance, Tasks, Data, Dashboards — carries **scope chips**: + +- **All** (the default) — the exact merged picture the assistant itself + works from: its personal pool plus every team it belongs to, combined + into one correctly-ordered view. Merged entries show which scope they + came from, and in Transcripts each thread is labeled with its team. +- **Personal** — just its private memory. +- **One chip per team** — just that team's shared pool. + +The complementary view is the team itself: **selecting a team in the +sidebar** shows every one of those sections scoped to the team's pool — +the same data any member's assistant sees when it reads that team, without +going through any individual assistant. A good habit when teaching an assistant something reusable: say who it's diff --git a/teams/team-assistants.mdx b/teams/team-assistants.mdx new file mode 100644 index 000000000..5e99d9730 --- /dev/null +++ b/teams/team-assistants.mdx @@ -0,0 +1,134 @@ +--- +title: "Team assistants" +description: "Assistants owned by the team itself — no single supervisor, shared memory by construction" +--- + +Sometimes an assistant shouldn't belong to anyone. A repairs coordinator +for the whole repairs unit, a reporting assistant the entire finance team +leans on, a company-wide helper in the Org team — for these, "pick someone +to own it and tell them to share everything" is the wrong shape. **Team +assistants** make the team itself the owner: no single supervisor, no +private memory, and everyone on the team can work with it as naturally as +with a colleague. + +## What makes a team assistant different + + + + No individual holds a supervisor position. The person who ran the hire + is recorded only as the hiring member; management rights belong to the + org's admins, like any shared resource. + + + A team assistant has **no personal contexts at all**. Everything it + knows, learns, schedules, and stores lives in its owning team's + shared pool — visible to every member, by construction. + + + +The memory point is worth dwelling on, because it's a genuine privacy +model, not just a default: + +- A personally-supervised assistant keeps a private pool and *chooses* + when something belongs to a team. A team assistant has nothing to + choose between — its knowledge, playbooks, skills, tasks, data, + transcripts, and credentials all live at the team scope. There is no + hidden layer only one person can see. +- Its working state defaults to the **owning team's** pool. If it also + joins other teams, it can read and contribute to those pools like any + member — but its home never moves. +- Because everything is team-visible, a team assistant is exactly as + auditable as the team itself: open the team in the sidebar and every + brain section shows the full picture. + +## Hiring one + +Team-first is the default. The hire dialog includes an **Owning team** +selector, pre-set to the team you hired from (or the managed **Org** team +where org-wide sharing is on): + +- **Pick a team** → you get a team assistant, born into that team: it's + enrolled automatically, appears under the team in the sidebar with a + **Team-owned** badge, and its memory starts at team scope from the + first message. +- **Pick "Personal (only you)"** → you get the classic + personally-supervised assistant instead, with a private memory and + optional team memberships layered on. + +The fastest route for a specific team: open the team in the sidebar, +switch to **Members**, and use **Hire for this team** — the owning team +comes preselected. + +Everything else about hiring — profile, voice, its own accounts, phone +numbers, and mailboxes — is identical to any hired teammate; see +[Hiring](/hiring/overview). A team assistant still gets **its own +identity** in your org's tools; what changes is who it answers to. + +## Working with one + +Anyone on the team can message a team assistant directly, work with it in +the [team group chat](/teams/overview#where-you-see-teams), assign it +tasks, and see everything it's doing. There's no privileged channel — the +hiring member's messages carry no more weight than anyone else's. + +Because its transcripts and tasks live at team scope, the whole team sees +one coherent history: what it was asked, what it did, what it scheduled. +Its scheduled work runs on the team's task surfaces, so runs and results +are team-visible too. + +## Teams, plural + +A team assistant always has **exactly one owning team** — that's where its +memory lives and who it belongs to. On top of that it can join any number +of other teams, exactly like any assistant: each extra membership adds +that team's shared pool to what it can read and contribute to. + +Two structural rules keep ownership coherent: + +- **It can't leave its owning team.** Removing it from the owning team is + refused — that pool *is* its memory. (Other memberships can be added + and removed freely.) +- **The owning team can't be deleted while it owns assistants.** Deleting + the team would delete their entire minds; the platform refuses until + the owned assistants are deleted first. + + + Want an assistant the *whole company* can use? Hire into the managed + **Org** team (on by default for new organizations). Its membership is + automatically everyone — current and future — so "owned by the Org team" + means "belongs to the company." + + +## Spend, caps, and attribution + +Team assistants bill the **organization's** account, and their cost +controls are team-shaped rather than person-shaped: + +- **Caps are managed by org admins.** Any member with assistant-management + permissions can set or change a team assistant's monthly spending cap — + the hiring member holds no special position. +- **The org cap is the ceiling.** A team assistant's cap is bounded by the + organization's spending cap only — no individual member's personal cap + constrains a team asset. +- **Limit alerts go to the org's Owners and Admins** (alongside the hiring + member), so the people who can act on a cap are the ones told when it's + reached. +- **Spend is attributable per team.** The organization's spending + breakdown can be scoped to a team, aggregating across every assistant + that team owns — so "what does the repairs team's AI staff cost?" is one + query, not a spreadsheet. + +## Which ownership model to choose + +| | Personally-supervised | Team assistant | +|---|---|---| +| Best for | An assistant that works *for you* with private context | A shared specialist the whole unit relies on | +| Memory | Private floor + team pools it joins | Owning team's pool, entirely | +| Who can direct it | Its supervisor; teams it joins | Anyone on the team | +| Management rights | Its supervisor | Org admins | +| Billing anchor | Supervisor's caps apply | Org cap; per-team attribution | +| Lifecycle | Supervisor manages | Tied to the owning team | + +Both models coexist freely in one org — and in one team. The question to +ask is simply: *if the hiring person left tomorrow, should this assistant +carry on unchanged?* If yes, it's a team assistant. diff --git a/their-computer/developers.mdx b/their-computer/developers.mdx index 2959fc383..30ed48e49 100644 --- a/their-computer/developers.mdx +++ b/their-computer/developers.mdx @@ -17,7 +17,8 @@ are separate closed-source components that speak to these interfaces. ## The control stack -![The computer control stack](/images/developers/dev-computer-control-stack.png) +The computer control stack +The computer control stack Computer control is layered, with a clean seam at each level: @@ -168,7 +169,8 @@ automation API is `{base}/api`. ## Lifecycle: from cold start to ready -![Managed desktop lifecycle](/images/developers/dev-desktop-lifecycle.png) +Managed desktop lifecycle +Managed desktop lifecycle Desktop state lives on the global session singleton ([`session_details.py`](https://github.com/unifyai/unify/blob/main/unify/session_details.py)): @@ -272,7 +274,8 @@ and `USER_DESKTOP` (a linked user machine). Each has a target class in ## Screen share & remote control -![Screen share and remote control flow](/images/developers/dev-screen-share-flow.png) +Screen share and remote control flow +Screen share and remote control flow The Console's Meet controls arrive as system events — `AssistantScreenShareStarted/Stopped`, `UserScreenShareStarted/Stopped`, diff --git a/workspace/calendar.mdx b/workspace/calendar.mdx index 15915c678..d11840418 100644 --- a/workspace/calendar.mdx +++ b/workspace/calendar.mdx @@ -22,7 +22,7 @@ connected account's calendar — Google Calendar or Outlook Calendar. ## Calendar + meetings Calendar access pairs naturally with your assistant's [meeting -abilities](/communication/meetings): +abilities](/workspace/meetings): - On Microsoft 365, it can **schedule a Teams meeting** directly onto the calendar — invites go out automatically and the join link is included. diff --git a/workspace/connecting.mdx b/workspace/connecting.mdx index 73830d7e0..91697a549 100644 --- a/workspace/connecting.mdx +++ b/workspace/connecting.mdx @@ -26,7 +26,7 @@ open it from several places — they all lead to the same dialog: |---|---|---| | Email | Always included | Always included | | Drive / files | Always included | Always included | - | Teams | — | Always included | + | [Teams](/workspace/teams) | — | Always included | | SharePoint | — | Always included | | Calendar | Optional | Optional | | Contacts | Optional | Optional | diff --git a/workspace/developers/architecture.mdx b/workspace/developers/architecture.mdx index d7c3ef9a2..91b739343 100644 --- a/workspace/developers/architecture.mdx +++ b/workspace/developers/architecture.mdx @@ -38,7 +38,8 @@ The workspace integration is built from four cooperating pieces: [`sharepoint`](https://github.com/unifyai/unify/blob/main/unify/gateway/channels/sharepoint/views.py)) that back sending, inbox watches, and the Console file picker. -![Workspace OAuth plumbing: the Console approves scopes, gateway OAuth callbacks persist GOOGLE_*/MICROSOFT_* tokens to Orchestra secrets, SecretManager syncs them into the runtime keeping raw tokens in memory only, and the localhost provider proxy swaps sandbox nonces for real tokens before calling Google APIs and Microsoft Graph](/images/developers/workspace-oauth-plumbing.png) +Workspace OAuth plumbing: the Console approves scopes, gateway OAuth callbacks persist GOOGLE_*/MICROSOFT_* tokens to Orchestra secrets, SecretManager syncs them into the runtime keeping raw tokens in memory only, and the localhost provider proxy swaps sandbox nonces for real tokens before calling Google APIs and Microsoft Graph +Workspace OAuth plumbing: the Console approves scopes, gateway OAuth callbacks persist GOOGLE_*/MICROSOFT_* tokens to Orchestra secrets, SecretManager syncs them into the runtime keeping raw tokens in memory only, and the localhost provider proxy swaps sandbox nonces for real tokens before calling Google APIs and Microsoft Graph ## The core design decision diff --git a/workspace/developers/gateway-channels.mdx b/workspace/developers/gateway-channels.mdx index 74e24b8d2..ff385c70a 100644 --- a/workspace/developers/gateway-channels.mdx +++ b/workspace/developers/gateway-channels.mdx @@ -13,7 +13,8 @@ Email is the place where the twin-vs-teammate identity split ([user docs](/hiring/twin-vs-teammates)) becomes concrete in code — there are two entirely separate send paths: -![Two email identities: primitives.workspace_email acts as the user through WorkspaceEmailManager calling Gmail API or Microsoft Graph directly with a trusted token; primitives.comms.send_email acts as the assistant through the gateway email channels to its managed mailbox; inbound email arrives via Gmail push to /email/gmail and Graph subscriptions to /microsoft/router, waking the assistant](/images/developers/workspace-email-identities.png) +Two email identities: primitives.workspace_email acts as the user through WorkspaceEmailManager calling Gmail API or Microsoft Graph directly with a trusted token; primitives.comms.send_email acts as the assistant through the gateway email channels to its managed mailbox; inbound email arrives via Gmail push to /email/gmail and Graph subscriptions to /microsoft/router, waking the assistant +Two email identities: primitives.workspace_email acts as the user through WorkspaceEmailManager calling Gmail API or Microsoft Graph directly with a trusted token; primitives.comms.send_email acts as the assistant through the gateway email channels to its managed mailbox; inbound email arrives via Gmail push to /email/gmail and Graph subscriptions to /microsoft/router, waking the assistant ### `primitives.workspace_email` — acts as the user diff --git a/workspace/developers/provider-proxy.mdx b/workspace/developers/provider-proxy.mdx index 2098debd0..e8c7e5b28 100644 --- a/workspace/developers/provider-proxy.mdx +++ b/workspace/developers/provider-proxy.mdx @@ -10,7 +10,8 @@ at once: give LLM-authored code the **complete** provider REST surface without a real token, and enforce the per-assistant **file-access allowlist** at a single choke point. -![Provider proxy policy pipeline: a sandbox request is classified by classify.py into non_file (passthrough), file_read, file_write, or unknown_file (403 default-deny); file reads and writes go through policy evaluation in policy.py and ancestry.py, with allowed requests forwarded to the upstream API and masked items returned as 404/403](/images/developers/workspace-proxy-policy.png) +Provider proxy policy pipeline: a sandbox request is classified by classify.py into non_file (passthrough), file_read, file_write, or unknown_file (403 default-deny); file reads and writes go through policy evaluation in policy.py and ancestry.py, with allowed requests forwarded to the upstream API and masked items returned as 404/403 +Provider proxy policy pipeline: a sandbox request is classified by classify.py into non_file (passthrough), file_read, file_write, or unknown_file (403 default-deny); file reads and writes go through policy evaluation in policy.py and ancestry.py, with allowed requests forwarded to the upstream API and masked items returned as 404/403 ## Module map diff --git a/communication/meetings.mdx b/workspace/meetings.mdx similarity index 95% rename from communication/meetings.mdx rename to workspace/meetings.mdx index e1cec2a02..768ffdb5d 100644 --- a/communication/meetings.mdx +++ b/workspace/meetings.mdx @@ -24,7 +24,7 @@ status") and it handles the rest, reporting back to you afterwards. ## Your assistant can host, too -With a [Microsoft 365 connection](/communication/microsoft-teams), your +With a [Microsoft 365 connection](/workspace/teams), your assistant can **create** Teams meetings: - **Instant links** — "create a meeting link for me and Sam" gets you a diff --git a/workspace/overview.mdx b/workspace/overview.mdx index 4ce591a96..76ff05226 100644 --- a/workspace/overview.mdx +++ b/workspace/overview.mdx @@ -27,11 +27,19 @@ there are no credit costs for connecting or keeping a workspace connected. Look people up in your address book and manage your to-do lists. + + On Microsoft 365, chat, post in channels, and host meetings as the + connected account. + + + Join Google Meet and Teams meetings, and host Teams meetings on + Microsoft 365. + On Microsoft 365, the connection also powers -[Microsoft Teams](/communication/microsoft-teams) — chats, channels, and -meetings. +[Microsoft Teams](/workspace/teams) — chats, channels, and meetings, with your +assistant acting as the connected account. The workspace connection covers the Google/Microsoft suite. Other apps — @@ -83,7 +91,7 @@ because the ecosystems do: | Files | Google Drive | OneDrive + SharePoint | | Contacts | Google Contacts | Microsoft contacts | | Tasks | Google Tasks | Microsoft To Do | -| Teams chat & meetings | — | Microsoft Teams | +| Teams chat & meetings | — | [Microsoft Teams](/workspace/teams) | Ready to connect? Head to [Connecting your workspace](/workspace/connecting). diff --git a/workspace/teams.mdx b/workspace/teams.mdx new file mode 100644 index 000000000..3a78ee653 --- /dev/null +++ b/workspace/teams.mdx @@ -0,0 +1,57 @@ +--- +title: "Microsoft Teams" +description: "Your assistant acts in Teams as its own connected Microsoft account" +--- + +When you connect a **Microsoft 365** account for your assistant, it can work in +Microsoft Teams as *that account* — chatting, posting in channels, and hosting +meetings under its own Microsoft identity, the same way it works in the +[connected mailbox](/workspace/email). + +This is the account-bound path. It's set up per assistant as part of the +[workspace connection](/workspace/connecting): pick **Microsoft 365** and the +**Teams** feature is included, so Teams is enabled the moment the account is +linked. It's distinct from the org-wide [Unify Teams app](/communication/microsoft-teams), +where the assistant appears as a shared bot rather than as a Microsoft user. + +## Messaging + +- **1:1 chat** — message your assistant in Teams like any colleague, and it + replies as its connected account. +- **Group chats** — it can take part in group conversations with two or more + people, and can start them too. +- **Channels** — @mention your assistant in a channel and it replies in the + thread; it can also post to channels and even create new ones. +- **Attachments** — it can send files in Teams messages. + +## Hosting meetings + +Because it acts as a real Microsoft account, your assistant can **create** +Teams meetings, not just attend them: + +- **Instant links** — "create a meeting link for me and Sam" gets you a + ready-to-share Teams link on the spot. +- **Scheduled meetings** — it can put a Teams meeting on the calendar with a + link and send invites to all attendees. + +Joining a meeting is separate and doesn't require this connection — send any +Teams meeting link and your assistant joins with its voice. See +[Meetings](/workspace/meetings). + +## Whose name is on it? + +Just like [email](/workspace/email#whose-name-is-on-the-email), it depends on +the account you connected — see [whose account gets +connected](/workspace/overview#whose-account-gets-connected): + +- **T-W1N** is connected to *your* account, so it chats and hosts meetings + **as you** — your digital twin acting in your name. +- **A hired assistant** has its own dedicated account, so it shows up as + **itself** — a named team member with its own Teams presence. + + + If you'd rather everyone in the org reach your assistants through one shared + bot — including people who never see the assistant's Microsoft account — use + the org-installed [Unify Teams app](/communication/microsoft-teams) instead. + You can use either path, or both. + diff --git a/your-computer/developers.mdx b/your-computer/developers.mdx index 12c346e8b..db8722c89 100644 --- a/your-computer/developers.mdx +++ b/your-computer/developers.mdx @@ -25,7 +25,8 @@ Console live view, every call re-checks consent, file access is pull-on-request rather than continuously synced, and writebacks never touch originals. -Architecture of linked user-desktop control: ConversationManager dispatches act(...) to CodeActActor, which reaches primitives.computer.user_desktop; _UserDesktopFactory resolves a UserDesktopLink from SESSION_DETAILS and hands back a UserDesktopHandle whose calls travel over an outbound-only tunnel to the user's machine, where an agent-service, a live desktop view for assistant perception, and a key-gated SFTP file channel run +Architecture of linked user-desktop control: ConversationManager dispatches act(...) to CodeActActor, which reaches primitives.computer.user_desktop; _UserDesktopFactory resolves a UserDesktopLink from SESSION_DETAILS and hands back a UserDesktopHandle whose calls travel over an outbound-only tunnel to the user's machine, where an agent-service, a live desktop view for assistant perception, and a key-gated SFTP file channel run +Architecture of linked user-desktop control: ConversationManager dispatches act(...) to CodeActActor, which reaches primitives.computer.user_desktop; _UserDesktopFactory resolves a UserDesktopLink from SESSION_DETAILS and hands back a UserDesktopHandle whose calls travel over an outbound-only tunnel to the user's machine, where an agent-service, a live desktop view for assistant perception, and a key-gated SFTP file channel run ## The link: session state @@ -126,7 +127,8 @@ Three deliberate differences from the assistant-desktop path: ## The file channel: `user_desktop.files` -File flow between the assistant runtime and the user's home directory: the files namespace calls UserHomeSFTP, list browses metadata only, pull and sync stage copies into the read-only mirror at ~/Unity/Remote/<user_id>/, and push writes a new timestamped copy into the remote /.unity-edits/ tree, never touching originals; noise and credential directories are excluded +File flow between the assistant runtime and the user's home directory: the files namespace calls UserHomeSFTP, list browses metadata only, pull and sync stage copies into the read-only mirror at ~/Unity/Remote/<user_id>/, and push writes a new timestamped copy into the remote /.unity-edits/ tree, never touching originals; noise and credential directories are excluded +File flow between the assistant runtime and the user's home directory: the files namespace calls UserHomeSFTP, list browses metadata only, pull and sync stage copies into the read-only mirror at ~/Unity/Remote/<user_id>/, and push writes a new timestamped copy into the remote /.unity-edits/ tree, never touching originals; noise and credential directories are excluded File access is a **separate channel with separate consent**, implemented in [`unify/file_manager/sync/user_sftp.py`](https://github.com/unifyai/unify/blob/main/unify/file_manager/sync/user_sftp.py) @@ -219,7 +221,8 @@ and the computer environment context ## Consent: three gates, checked at three times -Three consent layers for the linked desktop: standing config from the Console (UserDesktopLink.url for control, filesys_sync plus tunnel coordinates for files), live in-memory revoke sets (_user_desktop_revoked and _user_filesys_revoked, driven by UserFilesysAccessStarted/Stopped comms events), and per-call assertions in UserDesktopHandle and the files namespace that raise PermissionError on revoke +Three consent layers for the linked desktop: standing config from the Console (UserDesktopLink.url for control, filesys_sync plus tunnel coordinates for files), live in-memory revoke sets (_user_desktop_revoked and _user_filesys_revoked, driven by UserFilesysAccessStarted/Stopped comms events), and per-call assertions in UserDesktopHandle and the files namespace that raise PermissionError on revoke +Three consent layers for the linked desktop: standing config from the Console (UserDesktopLink.url for control, filesys_sync plus tunnel coordinates for files), live in-memory revoke sets (_user_desktop_revoked and _user_filesys_revoked, driven by UserFilesysAccessStarted/Stopped comms events), and per-call assertions in UserDesktopHandle and the files namespace that raise PermissionError on revoke Access control is layered, and each layer answers a different question: