Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 16 additions & 2 deletions packages/plugin-sdk/src/contracts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13462,7 +13462,16 @@ export const contracts = {
type: 'object',
properties: {
workspace: {
$ref: '#/$defs/AgentWorkspaceSelection',
description:
"Where the Bot Session is created on first use. Absent: the Host's first\nconfigured workspace root, so opening the Bot never needs another conversation.",
anyOf: [
{
$ref: '#/$defs/AgentWorkspaceSelection',
},
{
type: 'null',
},
],
},
title: {
type: ['string', 'null'],
Expand All @@ -13471,7 +13480,6 @@ export const contracts = {
},
},
additionalProperties: false,
required: ['workspace'],
$defs: {
AgentWorkspaceSelection: {
oneOf: [
Expand Down Expand Up @@ -13625,6 +13633,12 @@ export const contracts = {
type: 'string',
const: 'creation_rejected',
},
{
description:
'No workspace was given and the Host has no configured workspace root.',
type: 'string',
const: 'no_workspace',
},
],
},
},
Expand Down
11 changes: 9 additions & 2 deletions packages/plugin-sdk/src/generated/methods.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,10 @@ export type AgentCompactOutput_AgentCompactRejection =
| 'archived'
| 'backend_unavailable'
| 'workspace_changed';
export type AgentOpenBotOutput_AgentBotRejection = 'disabled' | 'creation_rejected';
export type AgentOpenBotOutput_AgentBotRejection =
| 'disabled'
| 'creation_rejected'
| 'no_workspace';
/**
* All parts are subscription items; only End admits a complete preview.
* Offsets and byte counts refer to original bytes, not Base64 or UTF-16 lengths.
Expand Down Expand Up @@ -557,7 +560,11 @@ export interface ServiceMethods {
* Open this installation's Bot Session, creating it on first use.
*/
input: {
workspace: Runtime.AgentWorkspaceSelection;
/**
* Where the Bot Session is created on first use. Absent: the Host's first
* configured workspace root, so opening the Bot never needs another conversation.
*/
workspace?: Runtime.AgentWorkspaceSelection | null;
title?: string | null;
};
output:
Expand Down
7 changes: 6 additions & 1 deletion plugins/agent/api/src/contract/bot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,10 @@ use super::*;
#[serde(rename_all = "camelCase", deny_unknown_fields)]
#[schemars(rename = "AgentBotOpen")]
pub struct BotOpen {
pub workspace: WorkspaceSelection,
/// Where the Bot Session is created on first use. Absent: the Host's first
/// configured workspace root, so opening the Bot never needs another conversation.
#[serde(default)]
pub workspace: Option<WorkspaceSelection>,
#[serde(default)]
#[schemars(length(min = 1))]
pub title: Option<String>,
Expand Down Expand Up @@ -59,6 +62,8 @@ pub enum BotRejection {
Disabled,
/// The Session could not be created; see the creation reason.
CreationRejected,
/// No workspace was given and the Host has no configured workspace root.
NoWorkspace,
}

#[derive(Deserialize, JsonSchema)]
Expand Down
7 changes: 2 additions & 5 deletions plugins/agent/conversations/ui/SessionSurface.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1080,11 +1080,8 @@ export function createConversationUi(
await switchSession(existing, { sessionId, instance: null });
return;
}
if (sessionId === null)
throw new UiError('Open a conversation first; the Bot starts in its workspace.');
const result = await control('openBot', {
workspace: { kind: 'session', sessionId },
});
// The Host picks the workspace of the Bot's first open itself.
const result = await control('openBot', {});
if (!alive) return;
if (result.kind !== 'opened')
throw new UiError('Bot could not be opened: {reason}', {
Expand Down
33 changes: 32 additions & 1 deletion plugins/agent/native/src/owner/bot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -606,12 +606,24 @@ impl Owner {
.filter(|name| self.tools.contains(name))
.map(|name| (*name).to_owned())
.collect();
let workspace = match input.workspace {
Some(workspace) => workspace,
None => match self.default_bot_workspace().await? {
Some(workspace) => workspace,
None => {
self.release_claim(&mine).await?;
return Ok(BotOpened::Rejected {
reason: BotRejection::NoWorkspace,
});
}
},
};
let create = Create {
automatic_title: false,
tools: Some(tools),
title: input.title.unwrap_or_else(|| "Bot".into()),
labels: vec![],
workspace: input.workspace,
workspace,
};
let operation = operation.clone();
let created = self
Expand Down Expand Up @@ -651,6 +663,25 @@ impl Owner {
}
}
}
/// The Host's first configured workspace root, as a directory selection.
async fn default_bot_workspace(&self) -> Result<Option<WorkspaceSelection>, String> {
let page = maka_workspace_support::query(
self.workspace_roots.clone(),
crate::contract::Directories::Roots,
)
.await?;
Ok(match page {
crate::contract::DirectoryPage::Roots { roots } => {
roots
.into_iter()
.next()
.map(|root| WorkspaceSelection::Directory {
selection: root.selection,
})
}
_ => None,
})
}
async fn storage_claim(&self, revision: Option<u64>, mine: &str) -> Result<(), StoreError> {
self.bot
.storage
Expand Down
40 changes: 40 additions & 0 deletions plugins/agent/native/src/owner/tests/bot_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -646,3 +646,43 @@ async fn a_live_slow_creator_keeps_its_claim() {
.await
.unwrap();
}

#[tokio::test]
async fn the_bot_opens_without_naming_a_workspace() {
tokio::time::timeout(Duration::from_secs(60), async {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let f = bot_fixture(&format!("http://{}/v1", listener.local_addr().unwrap())).await;
// No other conversation exists and no workspace is given.
let opened = f
.mutation(2, "openBot", json!({}), Uuid::new_v4())
.await
.unwrap();
assert_eq!(opened["kind"], "opened", "{opened}");
assert_eq!(opened["created"], true);
let bot = opened["sessionId"].as_str().unwrap().to_owned();
let roots = f
.query("directories", json!({"kind":"roots"}))
.await
.unwrap();
let workspace = f.owner.session(&bot).await.unwrap().configuration.workspace;
assert_eq!(
workspace.path,
roots["roots"][0]["path"].as_str().unwrap(),
"the Bot starts in the Host's first workspace root"
);
// A second open, also without a workspace, returns the same Bot.
let again = f
.mutation(2, "openBot", json!({}), Uuid::new_v4())
.await
.unwrap();
assert_eq!(
again,
json!({"kind":"opened","sessionId":bot,"created":false})
);
assert!(sees_send_user(&f, &listener, &bot).await);
f.app.shutdown().await.unwrap();
f.config.shutdown().await.unwrap();
})
.await
.unwrap();
}
5 changes: 0 additions & 5 deletions products/maka/workbench/app/locale-conversation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -276,11 +276,6 @@ export const conversationMessages = {
Chat: ['Chat', '对话', '對話'],
'Check original upload': ['Check original upload', '查询原上传结果', '查詢原上傳結果'],
'Maka Bot': ['Maka Bot', 'Maka Bot', 'Maka Bot'],
'Open a conversation first; the Bot starts in its workspace.': [
'Open a conversation first; the Bot starts in its workspace.',
'请先打开一个对话,Bot 会在该对话的工作区中创建。',
'請先開啟一個對話,Bot 會在該對話的工作區中建立。',
],
'Bot could not be opened: {reason}': [
'Bot could not be opened: {reason}',
'无法打开 Bot:{reason}',
Expand Down