@otto-code/protocol 0.5.1 → 0.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -41,13 +41,59 @@ const buildNotificationPreview = (text) => {
41
41
  }
42
42
  return truncateNotificationText(normalized, NOTIFICATION_PREVIEW_LIMIT);
43
43
  };
44
- const safeStringify = (value) => {
45
- try {
46
- return JSON.stringify(value);
47
- }
48
- catch {
44
+ // Human-readable labels for common tool names, so a permission notification reads
45
+ // like a request ("Run command: …") instead of dumping the raw tool identifier.
46
+ const TOOL_NAME_LABELS = {
47
+ bash: "Run command",
48
+ exec: "Run command",
49
+ shell: "Run command",
50
+ read: "Read file",
51
+ write: "Write file",
52
+ edit: "Edit file",
53
+ multiedit: "Edit file",
54
+ grep: "Search",
55
+ glob: "Find files",
56
+ webfetch: "Fetch page",
57
+ websearch: "Web search",
58
+ task: "Start subagent",
59
+ external_directory: "Access directory",
60
+ };
61
+ const humanizeToolName = (name) => {
62
+ const trimmed = name.trim();
63
+ return TOOL_NAME_LABELS[trimmed.toLowerCase()] ?? trimmed;
64
+ };
65
+ // Input fields that carry the meaningful, user-legible part of a tool call, in
66
+ // priority order. The first non-empty string wins.
67
+ const PERMISSION_INPUT_DESCRIPTION_KEYS = [
68
+ "command",
69
+ "description",
70
+ "file_path",
71
+ "path",
72
+ "url",
73
+ "pattern",
74
+ "query",
75
+ "prompt",
76
+ ];
77
+ // Deterministic field extraction — NOT an AI summary. Picks an existing string
78
+ // field out of the tool's input (by the priority list, then any string field)
79
+ // so the notification can show it verbatim instead of raw JSON.
80
+ const describePermissionInput = (input) => {
81
+ if (!input) {
49
82
  return null;
50
83
  }
84
+ for (const key of PERMISSION_INPUT_DESCRIPTION_KEYS) {
85
+ const value = input[key];
86
+ if (typeof value === "string" && value.trim()) {
87
+ return value.trim();
88
+ }
89
+ }
90
+ // Fall back to the first string-valued field rather than raw JSON.
91
+ for (const value of Object.values(input)) {
92
+ if (typeof value === "string" && value.trim()) {
93
+ return value.trim();
94
+ }
95
+ }
96
+ return null;
51
97
  };
52
98
  const buildPermissionDetails = (request) => {
53
99
  if (!request) {
@@ -65,15 +111,17 @@ const buildPermissionDetails = (request) => {
65
111
  if (details.length > 0) {
66
112
  return details.join(" - ");
67
113
  }
68
- const inputPreview = request.input ? safeStringify(request.input) : null;
69
- if (inputPreview) {
70
- return inputPreview;
114
+ // No provider-supplied title/description: build a legible line from the tool
115
+ // name and a meaningful input field instead of stringifying the payload.
116
+ const name = request.name?.trim();
117
+ const inputDescription = describePermissionInput(request.input);
118
+ if (name && inputDescription) {
119
+ return `${humanizeToolName(name)}: ${inputDescription}`;
71
120
  }
72
- const metadataPreview = request.metadata ? safeStringify(request.metadata) : null;
73
- if (metadataPreview) {
74
- return metadataPreview;
121
+ if (inputDescription) {
122
+ return inputDescription;
75
123
  }
76
- return request.name?.trim() || request.kind;
124
+ return name ? humanizeToolName(name) : request.kind;
77
125
  };
78
126
  export function findLatestAssistantMessageFromTimeline(timeline) {
79
127
  // Providers may stream assistant content in consecutive chunks.
@@ -0,0 +1,49 @@
1
+ import type { AgentPersonality, AgentTeam, PersonalityRole } from "./messages.js";
2
+ /**
3
+ * The dynamic "Team's Scheduler" schedule binding. Stored in the schedule's
4
+ * `personality` field in place of a personality name; resolved at RUN time to
5
+ * the active team's first available member carrying the Scheduler role
6
+ * (member order). Deliberately contains "@" — personality names are sanitized
7
+ * to [A-Za-z0-9_-] at the authoring surface, so the sentinel can never collide
8
+ * with a real name. No active team, or no available Scheduler member, is a
9
+ * hard run failure with a named error (same loudness as a bound personality
10
+ * being out of commission).
11
+ */
12
+ export declare const TEAM_SCHEDULER_PERSONALITY_SENTINEL = "@team-scheduler";
13
+ export interface AgentTeamsConfigView {
14
+ teams?: readonly AgentTeam[] | undefined;
15
+ activeTeamId?: string | null | undefined;
16
+ }
17
+ export declare function findAgentTeam(teams: readonly AgentTeam[] | undefined, teamId: string | null | undefined): AgentTeam | null;
18
+ /**
19
+ * Resolve the host's active team. A dangling `activeTeamId` (team deleted, or
20
+ * a patch raced a delete) reads as "no team active" — teamlessness is a valid
21
+ * state and must never error. The daemon additionally heals a dangling id back
22
+ * to null on the next config patch; this helper is the read-side tolerance.
23
+ */
24
+ export declare function getActiveAgentTeam(section: AgentTeamsConfigView | undefined): AgentTeam | null;
25
+ export declare function isTeamMember(team: Pick<AgentTeam, "memberIds"> | null | undefined, personalityId: string | null | undefined): boolean;
26
+ /**
27
+ * Resolve a team's members against the personality roster, in `memberIds`
28
+ * order, deduped. A member id pointing at a deleted personality is tolerated
29
+ * and ignored (it is pruned opportunistically on the next save of the team,
30
+ * never eagerly cascaded on delete).
31
+ */
32
+ export declare function resolveTeamMembers(team: Pick<AgentTeam, "memberIds"> | null | undefined, personalities: readonly AgentPersonality[] | undefined): AgentPersonality[];
33
+ /**
34
+ * The save-time prune: drop member ids that no longer resolve to a personality
35
+ * (and dedupe), preserving order. Editors call this when persisting a team so
36
+ * dangling ids don't accumulate; readers never require it.
37
+ */
38
+ export declare function pruneTeamMemberIds(memberIds: readonly string[] | undefined, personalities: readonly AgentPersonality[] | undefined): string[];
39
+ /**
40
+ * The union of all members' roles, normalized and returned in canonical
41
+ * `PERSONALITY_ROLES` order — the team card's role-pill strip.
42
+ */
43
+ export declare function teamRoleUnion(team: Pick<AgentTeam, "memberIds"> | null | undefined, personalities: readonly AgentPersonality[] | undefined): PersonalityRole[];
44
+ /**
45
+ * A team prompt only stacks when it has content; a team with an empty or
46
+ * whitespace prompt is purely organizational (picker scoping, no prompt layer).
47
+ */
48
+ export declare function getEffectiveTeamPrompt(team: Pick<AgentTeam, "teamPrompt"> | null | undefined): string | null;
49
+ //# sourceMappingURL=agent-teams.d.ts.map
@@ -0,0 +1,99 @@
1
+ import { normalizePersonalityRoles } from "./agent-personalities.js";
2
+ // Pure, dependency-free team helpers shared by the daemon (spawn-time active
3
+ // team resolution, list_personalities scoping) and the app (team cards,
4
+ // pickers, the Active Team switcher). Availability is deliberately NOT here —
5
+ // a team is never "out of commission"; its members are individually available
6
+ // or not, judged by checkPersonalityAvailability per member.
7
+ /**
8
+ * The dynamic "Team's Scheduler" schedule binding. Stored in the schedule's
9
+ * `personality` field in place of a personality name; resolved at RUN time to
10
+ * the active team's first available member carrying the Scheduler role
11
+ * (member order). Deliberately contains "@" — personality names are sanitized
12
+ * to [A-Za-z0-9_-] at the authoring surface, so the sentinel can never collide
13
+ * with a real name. No active team, or no available Scheduler member, is a
14
+ * hard run failure with a named error (same loudness as a bound personality
15
+ * being out of commission).
16
+ */
17
+ export const TEAM_SCHEDULER_PERSONALITY_SENTINEL = "@team-scheduler";
18
+ export function findAgentTeam(teams, teamId) {
19
+ if (!teamId) {
20
+ return null;
21
+ }
22
+ return teams?.find((team) => team.id === teamId) ?? null;
23
+ }
24
+ /**
25
+ * Resolve the host's active team. A dangling `activeTeamId` (team deleted, or
26
+ * a patch raced a delete) reads as "no team active" — teamlessness is a valid
27
+ * state and must never error. The daemon additionally heals a dangling id back
28
+ * to null on the next config patch; this helper is the read-side tolerance.
29
+ */
30
+ export function getActiveAgentTeam(section) {
31
+ return findAgentTeam(section?.teams, section?.activeTeamId);
32
+ }
33
+ export function isTeamMember(team, personalityId) {
34
+ if (!team || !personalityId) {
35
+ return false;
36
+ }
37
+ return (team.memberIds ?? []).includes(personalityId);
38
+ }
39
+ /**
40
+ * Resolve a team's members against the personality roster, in `memberIds`
41
+ * order, deduped. A member id pointing at a deleted personality is tolerated
42
+ * and ignored (it is pruned opportunistically on the next save of the team,
43
+ * never eagerly cascaded on delete).
44
+ */
45
+ export function resolveTeamMembers(team, personalities) {
46
+ if (!team || !personalities || personalities.length === 0) {
47
+ return [];
48
+ }
49
+ const byId = new Map(personalities.map((personality) => [personality.id, personality]));
50
+ const seen = new Set();
51
+ const members = [];
52
+ for (const memberId of team.memberIds ?? []) {
53
+ if (seen.has(memberId)) {
54
+ continue;
55
+ }
56
+ seen.add(memberId);
57
+ const personality = byId.get(memberId);
58
+ if (personality) {
59
+ members.push(personality);
60
+ }
61
+ }
62
+ return members;
63
+ }
64
+ /**
65
+ * The save-time prune: drop member ids that no longer resolve to a personality
66
+ * (and dedupe), preserving order. Editors call this when persisting a team so
67
+ * dangling ids don't accumulate; readers never require it.
68
+ */
69
+ export function pruneTeamMemberIds(memberIds, personalities) {
70
+ if (!memberIds || memberIds.length === 0) {
71
+ return [];
72
+ }
73
+ const known = new Set((personalities ?? []).map((personality) => personality.id));
74
+ const seen = new Set();
75
+ return memberIds.filter((memberId) => {
76
+ if (seen.has(memberId) || !known.has(memberId)) {
77
+ return false;
78
+ }
79
+ seen.add(memberId);
80
+ return true;
81
+ });
82
+ }
83
+ /**
84
+ * The union of all members' roles, normalized and returned in canonical
85
+ * `PERSONALITY_ROLES` order — the team card's role-pill strip.
86
+ */
87
+ export function teamRoleUnion(team, personalities) {
88
+ const roles = resolveTeamMembers(team, personalities).flatMap((personality) => personality.roles ?? []);
89
+ return normalizePersonalityRoles(roles);
90
+ }
91
+ /**
92
+ * A team prompt only stacks when it has content; a team with an empty or
93
+ * whitespace prompt is purely organizational (picker scoping, no prompt layer).
94
+ */
95
+ export function getEffectiveTeamPrompt(team) {
96
+ const prompt = team?.teamPrompt?.trim();
97
+ return prompt && prompt.length > 0 ? prompt : null;
98
+ }
99
+ //# sourceMappingURL=agent-teams.js.map
@@ -67,6 +67,13 @@ export interface AgentMode {
67
67
  colorTier?: string;
68
68
  }
69
69
  export type ProviderStatus = "ready" | "loading" | "error" | "unavailable";
70
+ /**
71
+ * A model's capability tier, used to bind personality "brains" provider-
72
+ * agnostically (deep = flagship reasoning, standard = everyday, fast = cheap/
73
+ * high-volume). Assigned by the daemon at ingest (catalog → name pattern), with
74
+ * a user per-model override winning; see model-tiers.ts.
75
+ */
76
+ export type ModelTier = "deep" | "standard" | "fast";
70
77
  export interface AgentModelDefinition {
71
78
  provider: AgentProvider;
72
79
  id: string;
@@ -77,6 +84,19 @@ export interface AgentModelDefinition {
77
84
  contextWindowMaxTokens?: number;
78
85
  thinkingOptions?: AgentSelectOption[];
79
86
  defaultThinkingOptionId?: string;
87
+ /**
88
+ * Capability tier, stamped by the daemon when it ingests the provider's model
89
+ * list. Optional: absent from old daemons, and from models we can't classify
90
+ * and the user hasn't tagged. Consumers fall back to their own inference.
91
+ */
92
+ tier?: ModelTier;
93
+ /**
94
+ * False when this model cannot run the provider's "auto" permission mode
95
+ * (e.g. Claude's classifier-based Auto mode is unsupported on Haiku). Absent
96
+ * = supported or unknown (including old daemons); clients only hide the Auto
97
+ * option on an explicit false.
98
+ */
99
+ supportsAutoMode?: boolean;
80
100
  }
81
101
  export interface AgentSelectOption {
82
102
  id: string;
@@ -482,8 +482,8 @@ export declare const BrowserAutomationWaitResultSchema: z.ZodObject<{
482
482
  command: z.ZodLiteral<"wait">;
483
483
  browserId: z.ZodString;
484
484
  matched: z.ZodEnum<{
485
- text: "text";
486
485
  url: "url";
486
+ text: "text";
487
487
  }>;
488
488
  }, z.core.$strip>;
489
489
  export declare const BrowserAutomationTypeResultSchema: z.ZodObject<{
@@ -715,8 +715,8 @@ export declare const BrowserAutomationResultSchema: z.ZodDiscriminatedUnion<[z.Z
715
715
  command: z.ZodLiteral<"wait">;
716
716
  browserId: z.ZodString;
717
717
  matched: z.ZodEnum<{
718
- text: "text";
719
718
  url: "url";
719
+ text: "text";
720
720
  }>;
721
721
  }, z.core.$strip>, z.ZodObject<{
722
722
  command: z.ZodLiteral<"type">;
@@ -1122,8 +1122,8 @@ export declare const BrowserAutomationExecuteResponseSchema: z.ZodObject<{
1122
1122
  command: z.ZodLiteral<"wait">;
1123
1123
  browserId: z.ZodString;
1124
1124
  matched: z.ZodEnum<{
1125
- text: "text";
1126
1125
  url: "url";
1126
+ text: "text";
1127
1127
  }>;
1128
1128
  }, z.core.$strip>, z.ZodObject<{
1129
1129
  command: z.ZodLiteral<"type">;
@@ -1,3 +1,4 @@
1
- import type { AgentPersonality } from "./messages.js";
1
+ import type { AgentPersonality, AgentTeam } from "./messages.js";
2
2
  export declare const DEFAULT_AGENT_PERSONALITIES: readonly AgentPersonality[];
3
+ export declare const DEFAULT_AGENT_TEAMS: readonly AgentTeam[];
3
4
  //# sourceMappingURL=default-personalities.d.ts.map
@@ -127,4 +127,24 @@ export const DEFAULT_AGENT_PERSONALITIES = [
127
127
  voice: kokoroVoice("am_echo"),
128
128
  },
129
129
  ];
130
+ // The starter Agent Team shipped with Otto: every starter personality grouped
131
+ // under one operating template. Seeded the same first-run/absent-section way
132
+ // as the personalities (see seedDefaultTeamsIfAbsent), and re-addable from the
133
+ // Agent teams card. Deliberately NOT active on first run — activating a
134
+ // prompt-bearing team silently on install would change spawn behavior out
135
+ // from under existing users; the user opts in via the Active Team switcher.
136
+ // The stable `team_builtin_*` id makes restore idempotent, exactly like the
137
+ // personalities' `personality_builtin_*` ids.
138
+ export const DEFAULT_AGENT_TEAMS = [
139
+ {
140
+ id: "team_builtin_otto_crew",
141
+ name: "The Otto Crew",
142
+ avatar: { color: "#4F46E5" },
143
+ teamPrompt: "You are part of the Otto Crew, a coordinated team of specialists working one project " +
144
+ "together under Atlas's lead. Stay in your lane and trust your teammates' lanes: do your " +
145
+ "own role's work well, hand off cleanly with the context the next specialist needs, and " +
146
+ "flag anything you notice outside your remit instead of fixing it yourself.",
147
+ memberIds: DEFAULT_AGENT_PERSONALITIES.map((personality) => personality.id),
148
+ },
149
+ ];
130
150
  //# sourceMappingURL=default-personalities.js.map