Skip to content

Commit 9012873

Browse files
committed
feat(analytics): let a session declare its task category
name_session gains an optional category argument from a fixed enum. The free-form name stays local and drives the tab group title exactly as before; only the enum category is emitted, on a new agent_session_task_declared event (task_category + client_name), so we can aggregate what kind of work sessions do without any free-form text leaving the machine. An unrecognized category is coerced to "other" so the declaration still counts; a non-string is dropped. The prompt and skill now nudge the agent to pass a category.
1 parent 195b15c commit 9012873

4 files changed

Lines changed: 103 additions & 8 deletions

File tree

packages/browseros-agent/apps/claw-server-rust/src/analytics/events.rs

Lines changed: 72 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ use std::{
1111
};
1212

1313
const CLIENT_NAME: &str = "client_name";
14+
const TASK_CATEGORY: &str = "task_category";
1415
const HARNESS: &str = "harness";
1516
const KIND: &str = "kind";
1617
const TOOL_NAME: &str = "tool_name";
@@ -72,9 +73,28 @@ pub(crate) const HARNESS_VALUES: [&str; 7] = [
7273

7374
pub(crate) const END_KIND_VALUES: [&str; 3] = ["closed", "errored", "cancelled"];
7475

76+
/// Fixed set of task kinds the agent may declare. Only these tokens leave the
77+
/// machine; the free-form session name never does. An unrecognized value is
78+
/// coerced to `other` rather than dropped, so the declaration still counts and a
79+
/// hot `other` signals a missing row.
80+
pub(crate) const TASK_CATEGORY_VALUES: [&str; 11] = [
81+
"shopping",
82+
"research",
83+
"email-and-messaging",
84+
"form-filling",
85+
"data-extraction",
86+
"testing-and-qa",
87+
"dev-tools",
88+
"social-media",
89+
"finance-and-admin",
90+
"internal-tools",
91+
"other",
92+
];
93+
7594
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7695
enum PropertyKind {
7796
ClientName,
97+
TaskCategory,
7898
Harness,
7999
EndKind,
80100
ToolName,
@@ -96,6 +116,15 @@ impl PropertyDefinition {
96116
fn normalize(self, value: &Value) -> Option<Value> {
97117
match self.kind {
98118
PropertyKind::ClientName => Some(Value::String(bucket_client_name(value.as_str()?))),
119+
PropertyKind::TaskCategory => {
120+
let raw = value.as_str()?;
121+
let category = if TASK_CATEGORY_VALUES.contains(&raw) {
122+
raw
123+
} else {
124+
"other"
125+
};
126+
Some(Value::String(category.to_string()))
127+
}
99128
PropertyKind::Harness => normalize_token(value, &HARNESS_VALUES),
100129
PropertyKind::EndKind => normalize_token(value, &END_KIND_VALUES),
101130
PropertyKind::ToolName => {
@@ -195,6 +224,13 @@ pub const AGENT_SESSION_STARTED: EventDefinition = EventDefinition::new(
195224
PropertyKind::ClientName,
196225
)],
197226
);
227+
pub const AGENT_SESSION_TASK_DECLARED: EventDefinition = EventDefinition::new(
228+
"agent_session_task_declared",
229+
&[
230+
PropertyDefinition::new(TASK_CATEGORY, PropertyKind::TaskCategory),
231+
PropertyDefinition::new(CLIENT_NAME, PropertyKind::ClientName),
232+
],
233+
);
198234
pub const AGENT_SESSION_ENDED: EventDefinition = EventDefinition::new(
199235
"agent_session_ended",
200236
&[
@@ -252,9 +288,10 @@ pub const AGENT_SESSION_EFFICIENCY_COMPUTED: EventDefinition = EventDefinition::
252288
],
253289
);
254290

255-
pub const ALL: [EventDefinition; 7] = [
291+
pub const ALL: [EventDefinition; 8] = [
256292
SERVER_STARTED,
257293
AGENT_SESSION_STARTED,
294+
AGENT_SESSION_TASK_DECLARED,
258295
AGENT_SESSION_ENDED,
259296
HARNESS_CONNECTED,
260297
HARNESS_DISCONNECTED,
@@ -322,19 +359,24 @@ mod tests {
322359

323360
#[test]
324361
fn catalog_pins_wire_names_and_required_properties() {
325-
assert_eq!(ALL.len(), 7);
362+
assert_eq!(ALL.len(), 8);
326363
assert_eq!(
327364
ALL.map(EventDefinition::name),
328365
[
329366
SERVER_STARTED.name(),
330367
AGENT_SESSION_STARTED.name(),
368+
AGENT_SESSION_TASK_DECLARED.name(),
331369
AGENT_SESSION_ENDED.name(),
332370
HARNESS_CONNECTED.name(),
333371
HARNESS_DISCONNECTED.name(),
334372
AGENT_SESSION_TOOL_USAGE.name(),
335373
AGENT_SESSION_EFFICIENCY_COMPUTED.name(),
336374
]
337375
);
376+
assert_eq!(
377+
AGENT_SESSION_TASK_DECLARED.property_names(),
378+
vec!["task_category", "client_name"]
379+
);
338380
assert_eq!(
339381
AGENT_SESSION_ENDED.property_names(),
340382
vec![
@@ -451,6 +493,34 @@ mod tests {
451493
}
452494
}
453495

496+
#[test]
497+
fn known_task_categories_pass_through_and_unknown_coerces_to_other() {
498+
for category in TASK_CATEGORY_VALUES {
499+
assert_eq!(
500+
AGENT_SESSION_TASK_DECLARED
501+
.sanitize(&json!({ "task_category": category, "client_name": "cursor" })),
502+
Some(json!({ "task_category": category, "client_name": "cursor" }))
503+
);
504+
}
505+
// Anything off-enum is coerced to `other` so the declaration still counts.
506+
for raw in ["crypto-trading", "", "SHOPPING", "shopping ", "acme corp"] {
507+
assert_eq!(
508+
AGENT_SESSION_TASK_DECLARED
509+
.sanitize(&json!({ "task_category": raw, "client_name": "codex" })),
510+
Some(json!({ "task_category": "other", "client_name": "codex" }))
511+
);
512+
}
513+
}
514+
515+
#[test]
516+
fn task_declared_drops_when_category_is_not_a_string() {
517+
assert_eq!(
518+
AGENT_SESSION_TASK_DECLARED
519+
.sanitize(&json!({ "task_category": 7, "client_name": "cursor" })),
520+
None
521+
);
522+
}
523+
454524
#[test]
455525
fn unlisted_client_names_surface_their_slug() {
456526
for (raw, expected) in [

packages/browseros-agent/apps/claw-server-rust/src/api/mcp/prompt.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@ Shared with other agents:
2222
tabs action="new" and work on that copy; leave the original untouched.
2323
- Preserve useful pages: leave anything the user may want to inspect open
2424
instead of closing it when the task ends.
25-
- Rename your session early with name_session using a 2-3 word task label;
26-
tabs group as <client>/<name>.
25+
- Name your session early with name_session: a 2-3 word task label plus the
26+
category that best fits the task; tabs group as <client>/<name>.
2727
- The user oversees this browser from the BrowserOS neo cockpit (live view,
2828
audit, replay).
2929

packages/browseros-agent/apps/claw-server-rust/src/api/mcp/service.rs

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,8 @@ use uuid::Uuid;
4646
const SERVER_NAME: &str = "browseros-neo";
4747
const SERVER_TITLE: &str = "BrowserOS neo";
4848
const NAME_SESSION_TOOL_NAME: &str = "name_session";
49-
const NAME_SESSION_DESCRIPTION: &str = "Rename this browser session: a small lowercase 2-3 word label for what this session is doing, e.g. \"invoice processing\". Tabs are grouped as <client>/<name>. Call again to rename.";
49+
const NAME_SESSION_DESCRIPTION: &str = "Name this browser session at the start of a task: a small lowercase 2-3 word label for what it is doing, e.g. \"invoice processing\", plus a `category` for the kind of task. Tabs are grouped as <client>/<name>; the label stays on this machine and only the category is used for anonymous aggregate analytics. Call again to update.";
50+
const NAME_SESSION_CATEGORY_DESCRIPTION: &str = "The kind of task, for anonymous aggregate analytics only; the free-form name is never sent. Pick the closest fit from the list.";
5051
const NAME_SESSION_INPUT_MAX_LEN: usize = 64;
5152
const SESSION_ARG_DESCRIPTION: &str = "Opaque session handle returned by the server. Pass it back on every call to keep working in the same browser session; omit it to start a new session.";
5253
const SAVE_SKILL_TOOL_NAME: &str = "save_skill";
@@ -143,6 +144,20 @@ impl ClawMcpService {
143144
.into_call_tool_result();
144145
}
145146
};
147+
if let Some(category) = raw_args
148+
.get("category")
149+
.and_then(Value::as_str)
150+
.map(str::trim)
151+
.filter(|category| !category.is_empty())
152+
{
153+
self.state.analytics.capture(
154+
crate::analytics::events::AGENT_SESSION_TASK_DECLARED,
155+
json!({
156+
"task_category": category,
157+
"client_name": started.session.client_name(),
158+
}),
159+
);
160+
}
146161
let browser = self.state.browser.session().await;
147162
apply_agent_tab_group_title(
148163
browser.as_ref(),
@@ -644,7 +659,12 @@ fn name_session_tool() -> Tool {
644659
let Value::Object(input_schema) = json!({
645660
"type": "object",
646661
"properties": {
647-
"name": { "type": "string", "maxLength": NAME_SESSION_INPUT_MAX_LEN }
662+
"name": { "type": "string", "maxLength": NAME_SESSION_INPUT_MAX_LEN },
663+
"category": {
664+
"type": "string",
665+
"enum": crate::analytics::events::TASK_CATEGORY_VALUES,
666+
"description": NAME_SESSION_CATEGORY_DESCRIPTION
667+
}
648668
},
649669
"required": ["name"]
650670
}) else {
@@ -1095,7 +1115,7 @@ mod tests {
10951115
assert!(instructions.contains("BrowserOS neo — the browser for agents"));
10961116
assert!(instructions.contains("Reach for run first"));
10971117
assert!(instructions.contains(
1098-
"- Rename your session early with name_session using a 2-3 word task label;\n tabs group as <client>/<name>."
1118+
"- Name your session early with name_session: a 2-3 word task label plus the\n category that best fits the task; tabs group as <client>/<name>."
10991119
));
11001120
assert!(instructions.contains(
11011121
"- If the user points you at a tab you don't own, open its URL with\n tabs action=\"new\" and work on that copy; leave the original untouched."
@@ -1156,6 +1176,11 @@ mod tests {
11561176
"type": "object",
11571177
"properties": {
11581178
"name": { "type": "string", "maxLength": 64 },
1179+
"category": {
1180+
"type": "string",
1181+
"enum": crate::analytics::events::TASK_CATEGORY_VALUES,
1182+
"description": NAME_SESSION_CATEGORY_DESCRIPTION
1183+
},
11591184
"session": { "type": "string", "description": SESSION_ARG_DESCRIPTION }
11601185
},
11611186
"required": ["name"]

packages/browseros-agent/resources/skills/browserclaw/SKILL.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ When a task needs a browser or a website (open it, read it, act on it, fill a fo
99

1010
## Shared browser etiquette
1111

12-
- Call `name_session` early with a 2-3 word task label; tabs group as `<client>/<name>` in the cockpit.
12+
- Call `name_session` early with a 2-3 word task label and the best-fit `category`; tabs group as `<client>/<name>` in the cockpit.
1313
- Open your own tab with `tabs` action `"new"`. Work only in task-owned tabs.
1414
- If the user points you at a tab you do not own, open its URL in your own tab and leave the original untouched.
1515
- Preserve useful pages that the user may want to inspect instead of closing them when the task ends.

0 commit comments

Comments
 (0)