@otto-code/protocol 0.6.2 → 0.6.4
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.
- package/dist/agent-types.d.ts +37 -0
- package/dist/browser-automation/capabilities.d.ts +1 -1
- package/dist/browser-automation/rpc-schemas.d.ts +199 -1
- package/dist/browser-automation/rpc-schemas.js +71 -0
- package/dist/generated/validation/ws-outbound.aot.js +31906 -28601
- package/dist/messages.d.ts +2690 -15
- package/dist/messages.js +500 -6
- package/dist/observed-subagent-title.d.ts +26 -0
- package/dist/observed-subagent-title.js +63 -0
- package/dist/provider-config.d.ts +12 -0
- package/dist/provider-config.js +15 -0
- package/dist/provider-manifest.js +13 -0
- package/dist/tool-call-display.d.ts +9 -0
- package/dist/tool-call-display.js +60 -15
- package/dist/tool-name-normalization.d.ts +16 -0
- package/dist/tool-name-normalization.js +26 -0
- package/dist/validation/ws-outbound-schema-metadata.d.ts +464 -1
- package/package.json +2 -1
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/** Hard cap so no provider summary can produce a wall-of-text label. */
|
|
2
|
+
export declare const OBSERVED_SUBAGENT_TITLE_MAX = 60;
|
|
3
|
+
export declare function normalizeObservedTitleSource(value: string | undefined | null): string | null;
|
|
4
|
+
export declare function normalizeObservedSubagentType(value: string | undefined | null): string | null;
|
|
5
|
+
/**
|
|
6
|
+
* Derive the frozen row name for an observed subagent. Prefer the stable
|
|
7
|
+
* `subAgentType` (e.g. "code-explorer") over the description — a
|
|
8
|
+
* `task_progress` description is the ever-changing AI summary, which must
|
|
9
|
+
* never become the label. Generic catch-all types ("general-purpose") are
|
|
10
|
+
* skipped in favor of the description. Callers freeze the result at the first
|
|
11
|
+
* named update.
|
|
12
|
+
*/
|
|
13
|
+
export declare function deriveObservedSubagentTitle(update: {
|
|
14
|
+
subAgentType?: string;
|
|
15
|
+
description?: string;
|
|
16
|
+
}): string;
|
|
17
|
+
/**
|
|
18
|
+
* True when this update carries a real name source we can freeze the title on.
|
|
19
|
+
* A generic catch-all type alone doesn't count — freezing "general-purpose"
|
|
20
|
+
* would lock out the description a later update may carry.
|
|
21
|
+
*/
|
|
22
|
+
export declare function observedUpdateHasTitleSource(update: {
|
|
23
|
+
subAgentType?: string;
|
|
24
|
+
description?: string;
|
|
25
|
+
}): boolean;
|
|
26
|
+
//# sourceMappingURL=observed-subagent-title.d.ts.map
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// Shared naming rule for provider-managed (observed) subagents. This is the
|
|
2
|
+
// single source of truth for how an observed subagent is titled — the daemon
|
|
3
|
+
// uses it to freeze the track-row label (agent-projections.ts), and the app's
|
|
4
|
+
// visualizer uses it so page-side child labels (subagent_dispatch/return
|
|
5
|
+
// particles ride an edge keyed by child NAME) resolve to exactly the same
|
|
6
|
+
// string as the child node the daemon-titled agent row spawned. If the two
|
|
7
|
+
// ever diverge, the dispatch/return visuals silently stop rendering.
|
|
8
|
+
// See docs/agent-lifecycle.md (Item 4) and docs/visualizer.md.
|
|
9
|
+
/** Hard cap so no provider summary can produce a wall-of-text label. */
|
|
10
|
+
export const OBSERVED_SUBAGENT_TITLE_MAX = 60;
|
|
11
|
+
/**
|
|
12
|
+
* Catch-all subagent types that make lousy row labels — Claude's default Task
|
|
13
|
+
* runs as "general-purpose". These never become the title; the task
|
|
14
|
+
* description names the row instead (user-locked).
|
|
15
|
+
*/
|
|
16
|
+
const GENERIC_OBSERVED_SUBAGENT_TYPES = new Set([
|
|
17
|
+
"general-purpose",
|
|
18
|
+
"general",
|
|
19
|
+
"task",
|
|
20
|
+
"agent",
|
|
21
|
+
"subagent",
|
|
22
|
+
]);
|
|
23
|
+
export function normalizeObservedTitleSource(value) {
|
|
24
|
+
if (typeof value !== "string") {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
const collapsed = value.replace(/\s+/g, " ").trim();
|
|
28
|
+
return collapsed.length > 0 ? collapsed : null;
|
|
29
|
+
}
|
|
30
|
+
export function normalizeObservedSubagentType(value) {
|
|
31
|
+
const normalized = normalizeObservedTitleSource(value);
|
|
32
|
+
if (normalized === null || GENERIC_OBSERVED_SUBAGENT_TYPES.has(normalized.toLowerCase())) {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
return normalized;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Derive the frozen row name for an observed subagent. Prefer the stable
|
|
39
|
+
* `subAgentType` (e.g. "code-explorer") over the description — a
|
|
40
|
+
* `task_progress` description is the ever-changing AI summary, which must
|
|
41
|
+
* never become the label. Generic catch-all types ("general-purpose") are
|
|
42
|
+
* skipped in favor of the description. Callers freeze the result at the first
|
|
43
|
+
* named update.
|
|
44
|
+
*/
|
|
45
|
+
export function deriveObservedSubagentTitle(update) {
|
|
46
|
+
const base = normalizeObservedSubagentType(update.subAgentType) ??
|
|
47
|
+
normalizeObservedTitleSource(update.description) ??
|
|
48
|
+
"Subagent";
|
|
49
|
+
if (base.length <= OBSERVED_SUBAGENT_TITLE_MAX) {
|
|
50
|
+
return base;
|
|
51
|
+
}
|
|
52
|
+
return `${base.slice(0, OBSERVED_SUBAGENT_TITLE_MAX - 1).trimEnd()}…`;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* True when this update carries a real name source we can freeze the title on.
|
|
56
|
+
* A generic catch-all type alone doesn't count — freezing "general-purpose"
|
|
57
|
+
* would lock out the description a later update may carry.
|
|
58
|
+
*/
|
|
59
|
+
export function observedUpdateHasTitleSource(update) {
|
|
60
|
+
return (normalizeObservedSubagentType(update.subAgentType) !== null ||
|
|
61
|
+
normalizeObservedTitleSource(update.description) !== null);
|
|
62
|
+
}
|
|
63
|
+
//# sourceMappingURL=observed-subagent-title.js.map
|
|
@@ -86,6 +86,16 @@ export declare const MCP_TOOL_PERMISSION_MODES: readonly ["always-ask", "trust-r
|
|
|
86
86
|
* conversation is compacted automatically.
|
|
87
87
|
*/
|
|
88
88
|
export declare const COMPACTION_THRESHOLD_PERCENTS: readonly [50, 60, 70, 80, 90];
|
|
89
|
+
/**
|
|
90
|
+
* Max model→tool→model rounds per turn for providers whose tool loop the
|
|
91
|
+
* daemon owns (openai-compat). The turn stops with an error after this many
|
|
92
|
+
* rounds without a final answer — a runaway-loop safety valve, most often hit
|
|
93
|
+
* by smaller local models that keep calling tools instead of converging.
|
|
94
|
+
* Bounds keep the setting a sane guard rail rather than an off switch.
|
|
95
|
+
*/
|
|
96
|
+
export declare const MAX_TOOL_ROUNDS_DEFAULT = 50;
|
|
97
|
+
export declare const MAX_TOOL_ROUNDS_MIN = 1;
|
|
98
|
+
export declare const MAX_TOOL_ROUNDS_MAX = 1000;
|
|
89
99
|
/**
|
|
90
100
|
* Compaction tuning for providers whose conversation the daemon owns
|
|
91
101
|
* (openai-compat). These set the provider-level defaults; the per-agent
|
|
@@ -167,6 +177,7 @@ export declare const ProviderOverrideSchema: z.ZodObject<{
|
|
|
167
177
|
keepRecentTokens: z.ZodOptional<z.ZodNumber>;
|
|
168
178
|
hideSelector: z.ZodOptional<z.ZodBoolean>;
|
|
169
179
|
}, z.core.$strip>>;
|
|
180
|
+
maxToolRounds: z.ZodOptional<z.ZodNumber>;
|
|
170
181
|
enabled: z.ZodOptional<z.ZodBoolean>;
|
|
171
182
|
order: z.ZodOptional<z.ZodNumber>;
|
|
172
183
|
}, z.core.$strip>;
|
|
@@ -239,6 +250,7 @@ export declare const ProviderOverridesSchema: z.ZodRecord<z.ZodString, z.ZodObje
|
|
|
239
250
|
keepRecentTokens: z.ZodOptional<z.ZodNumber>;
|
|
240
251
|
hideSelector: z.ZodOptional<z.ZodBoolean>;
|
|
241
252
|
}, z.core.$strip>>;
|
|
253
|
+
maxToolRounds: z.ZodOptional<z.ZodNumber>;
|
|
242
254
|
enabled: z.ZodOptional<z.ZodBoolean>;
|
|
243
255
|
order: z.ZodOptional<z.ZodNumber>;
|
|
244
256
|
}, z.core.$strip>>;
|
package/dist/provider-config.js
CHANGED
|
@@ -117,6 +117,16 @@ export const MCP_TOOL_PERMISSION_MODES = ["always-ask", "trust-read-only"];
|
|
|
117
117
|
* conversation is compacted automatically.
|
|
118
118
|
*/
|
|
119
119
|
export const COMPACTION_THRESHOLD_PERCENTS = [50, 60, 70, 80, 90];
|
|
120
|
+
/**
|
|
121
|
+
* Max model→tool→model rounds per turn for providers whose tool loop the
|
|
122
|
+
* daemon owns (openai-compat). The turn stops with an error after this many
|
|
123
|
+
* rounds without a final answer — a runaway-loop safety valve, most often hit
|
|
124
|
+
* by smaller local models that keep calling tools instead of converging.
|
|
125
|
+
* Bounds keep the setting a sane guard rail rather than an off switch.
|
|
126
|
+
*/
|
|
127
|
+
export const MAX_TOOL_ROUNDS_DEFAULT = 50;
|
|
128
|
+
export const MAX_TOOL_ROUNDS_MIN = 1;
|
|
129
|
+
export const MAX_TOOL_ROUNDS_MAX = 1000;
|
|
120
130
|
/**
|
|
121
131
|
* Compaction tuning for providers whose conversation the daemon owns
|
|
122
132
|
* (openai-compat). These set the provider-level defaults; the per-agent
|
|
@@ -165,6 +175,11 @@ export const ProviderOverrideSchema = z.object({
|
|
|
165
175
|
* (openai-compat). Per-agent feature values win over these.
|
|
166
176
|
*/
|
|
167
177
|
compaction: ProviderCompactionConfigSchema.optional(),
|
|
178
|
+
/**
|
|
179
|
+
* Max model→tool→model rounds per turn for daemon-hosted providers
|
|
180
|
+
* (openai-compat). Omitted = the built-in default (MAX_TOOL_ROUNDS_DEFAULT).
|
|
181
|
+
*/
|
|
182
|
+
maxToolRounds: z.number().int().min(MAX_TOOL_ROUNDS_MIN).max(MAX_TOOL_ROUNDS_MAX).optional(),
|
|
168
183
|
enabled: z.boolean().optional(),
|
|
169
184
|
order: z.number().optional(),
|
|
170
185
|
});
|
|
@@ -124,6 +124,19 @@ const MOCK_LOAD_TEST_MODES = [
|
|
|
124
124
|
icon: "ShieldOff",
|
|
125
125
|
colorTier: "dangerous",
|
|
126
126
|
},
|
|
127
|
+
{
|
|
128
|
+
id: "dontAsk",
|
|
129
|
+
label: "Don't Ask",
|
|
130
|
+
description: "Runs without prompting — actions not pre-approved are denied",
|
|
131
|
+
icon: "ShieldCheck",
|
|
132
|
+
colorTier: "moderate",
|
|
133
|
+
// Dev-only mirror of Claude's guarded unattended mode so deterministic E2E
|
|
134
|
+
// specs can exercise the system-assigned surfaces: the unattended coercion
|
|
135
|
+
// target for mock schedule runs, and the locked mode badge on a live agent
|
|
136
|
+
// stuck in a non-user-selectable mode.
|
|
137
|
+
isUnattended: true,
|
|
138
|
+
userSelectable: false,
|
|
139
|
+
},
|
|
127
140
|
];
|
|
128
141
|
const MOCK_SLOW_MODES = [
|
|
129
142
|
{
|
|
@@ -7,5 +7,14 @@ export interface ToolCallDisplayModel {
|
|
|
7
7
|
summary?: string;
|
|
8
8
|
errorText?: string;
|
|
9
9
|
}
|
|
10
|
+
/**
|
|
11
|
+
* Friendly display name for a bare tool identifier, used at every surface that
|
|
12
|
+
* shows a tool/action name without a full timeline item to run through
|
|
13
|
+
* {@link buildToolCallDisplayModel} (the visualizer's action labels, sub-agent
|
|
14
|
+
* activity rows). Strips the MCP/Otto namespace, consults the known-tool
|
|
15
|
+
* registry, then title-cases as a fallback — so "mcp__otto__spawn_task",
|
|
16
|
+
* "otto.spawn_task", and a bare "spawn_task" all render as "Spawn Task".
|
|
17
|
+
*/
|
|
18
|
+
export declare function getToolDisplayName(name: string): string;
|
|
10
19
|
export declare function buildToolCallDisplayModel(input: ToolCallDisplayInput): ToolCallDisplayModel;
|
|
11
20
|
//# sourceMappingURL=tool-call-display.d.ts.map
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { getMcpToolLeafName, getOttoToolLeafName } from "./tool-name-normalization.js";
|
|
2
2
|
import { stripCwdPrefix } from "./path-utils.js";
|
|
3
3
|
function isRecord(value) {
|
|
4
4
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
@@ -6,26 +6,71 @@ function isRecord(value) {
|
|
|
6
6
|
function readString(value) {
|
|
7
7
|
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
8
8
|
}
|
|
9
|
+
// Curated display names — the registry of tools we explicitly "know about".
|
|
10
|
+
// Keyed by the lowercased leaf name (transport namespace already stripped).
|
|
11
|
+
//
|
|
12
|
+
// Only tools whose bare id does NOT title-case cleanly on its own need an entry
|
|
13
|
+
// here: lowercase compound names ("websearch") a splitter can't segment, or
|
|
14
|
+
// ones we want to word deliberately. Well-formed snake_case ("spawn_task") and
|
|
15
|
+
// camelCase ("WebSearch") names are handled by the algorithmic humanizer below
|
|
16
|
+
// and do NOT need listing.
|
|
17
|
+
//
|
|
18
|
+
// Anything not here falls through to the humanizer and still renders readably —
|
|
19
|
+
// so an unmapped tool shows up looking slightly generic (e.g. a stray provider
|
|
20
|
+
// tool), which is the cue to add it here. Extend this map to teach Otto a tool.
|
|
21
|
+
const KNOWN_TOOL_DISPLAY_NAMES = {
|
|
22
|
+
websearch: "Web Search",
|
|
23
|
+
web_search: "Web Search",
|
|
24
|
+
webfetch: "Web Fetch",
|
|
25
|
+
web_fetch: "Web Fetch",
|
|
26
|
+
todowrite: "Update Todos",
|
|
27
|
+
todoread: "Read Todos",
|
|
28
|
+
multiedit: "Multi Edit",
|
|
29
|
+
notebookedit: "Edit Notebook",
|
|
30
|
+
notebookread: "Read Notebook",
|
|
31
|
+
bashoutput: "Bash Output",
|
|
32
|
+
killshell: "Kill Shell",
|
|
33
|
+
exitplanmode: "Exit Plan Mode",
|
|
34
|
+
applypatch: "Apply Patch",
|
|
35
|
+
ls: "List Files",
|
|
36
|
+
};
|
|
37
|
+
// Split camelCase / PascalCase and separator-delimited identifiers into words,
|
|
38
|
+
// then Title-Case them: "WebSearch" -> "Web Search", "spawn_task" -> "Spawn
|
|
39
|
+
// Task", "HTTPServer" -> "HTTP Server".
|
|
40
|
+
function titleCaseToolId(value) {
|
|
41
|
+
return value
|
|
42
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1 $2")
|
|
43
|
+
.replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2")
|
|
44
|
+
.replace(/[._-]+/g, " ")
|
|
45
|
+
.split(" ")
|
|
46
|
+
.filter((segment) => segment.length > 0)
|
|
47
|
+
.map((segment) => `${segment[0]?.toUpperCase() ?? ""}${segment.slice(1)}`)
|
|
48
|
+
.join(" ");
|
|
49
|
+
}
|
|
9
50
|
function humanizeToolName(name) {
|
|
10
51
|
const trimmed = name.trim();
|
|
11
52
|
if (!trimmed) {
|
|
12
53
|
return name;
|
|
13
54
|
}
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
if (/[:./]/.test(trimmed) || trimmed.includes("__")) {
|
|
21
|
-
return trimmed;
|
|
55
|
+
// Strip the transport namespace first ("mcp__otto__spawn_task" ->
|
|
56
|
+
// "spawn_task", "otto.list_agents" -> "list_agents") so both the known-tool
|
|
57
|
+
// lookup and the fallback operate on the bare tool id.
|
|
58
|
+
const leaf = getMcpToolLeafName(trimmed) ?? getOttoToolLeafName(trimmed);
|
|
59
|
+
if (leaf) {
|
|
60
|
+
return humanizeToolName(leaf);
|
|
22
61
|
}
|
|
23
|
-
return trimmed
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
62
|
+
return KNOWN_TOOL_DISPLAY_NAMES[trimmed.toLowerCase()] ?? titleCaseToolId(trimmed);
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Friendly display name for a bare tool identifier, used at every surface that
|
|
66
|
+
* shows a tool/action name without a full timeline item to run through
|
|
67
|
+
* {@link buildToolCallDisplayModel} (the visualizer's action labels, sub-agent
|
|
68
|
+
* activity rows). Strips the MCP/Otto namespace, consults the known-tool
|
|
69
|
+
* registry, then title-cases as a fallback — so "mcp__otto__spawn_task",
|
|
70
|
+
* "otto.spawn_task", and a bare "spawn_task" all render as "Spawn Task".
|
|
71
|
+
*/
|
|
72
|
+
export function getToolDisplayName(name) {
|
|
73
|
+
return humanizeToolName(name);
|
|
29
74
|
}
|
|
30
75
|
function formatErrorText(error) {
|
|
31
76
|
if (error === null || error === undefined) {
|
|
@@ -5,5 +5,21 @@ export declare function isSpeakToolName(name: string): boolean;
|
|
|
5
5
|
export declare function isLikelyNamespacedToolName(name: string): boolean;
|
|
6
6
|
export declare function isOttoToolName(name: string): boolean;
|
|
7
7
|
export declare function getOttoToolLeafName(name: string): string | null;
|
|
8
|
+
/**
|
|
9
|
+
* Strip a leading MCP namespace for DISPLAY, returning the bare tool id.
|
|
10
|
+
*
|
|
11
|
+
* Tools hosted over MCP arrive namespaced as `mcp__<server>__<tool>` (Claude
|
|
12
|
+
* Code format). The `mcp__<server>__` part is transport plumbing that means
|
|
13
|
+
* nothing to a reader — "Create Issue", not "mcp__linear__create_issue". This
|
|
14
|
+
* generalizes {@link getOttoToolLeafName} to ANY server so every MCP tool reads
|
|
15
|
+
* cleanly, not just Otto's own.
|
|
16
|
+
*
|
|
17
|
+
* Returns `null` when the name carries no `mcp__…__` namespace, so callers keep
|
|
18
|
+
* plain tool names (`Read`, `bash`) and dotted/other forms untouched. Case is
|
|
19
|
+
* preserved — MCP tool ids are already snake_case and callers humanize after.
|
|
20
|
+
* `speak` is never treated as namespaced (it renders as a chat bubble, not an
|
|
21
|
+
* action row).
|
|
22
|
+
*/
|
|
23
|
+
export declare function getMcpToolLeafName(name: string): string | null;
|
|
8
24
|
export declare function isLikelyExternalToolName(name: string): boolean;
|
|
9
25
|
//# sourceMappingURL=tool-name-normalization.d.ts.map
|
|
@@ -69,6 +69,32 @@ export function getOttoToolLeafName(name) {
|
|
|
69
69
|
}
|
|
70
70
|
return null;
|
|
71
71
|
}
|
|
72
|
+
/**
|
|
73
|
+
* Strip a leading MCP namespace for DISPLAY, returning the bare tool id.
|
|
74
|
+
*
|
|
75
|
+
* Tools hosted over MCP arrive namespaced as `mcp__<server>__<tool>` (Claude
|
|
76
|
+
* Code format). The `mcp__<server>__` part is transport plumbing that means
|
|
77
|
+
* nothing to a reader — "Create Issue", not "mcp__linear__create_issue". This
|
|
78
|
+
* generalizes {@link getOttoToolLeafName} to ANY server so every MCP tool reads
|
|
79
|
+
* cleanly, not just Otto's own.
|
|
80
|
+
*
|
|
81
|
+
* Returns `null` when the name carries no `mcp__…__` namespace, so callers keep
|
|
82
|
+
* plain tool names (`Read`, `bash`) and dotted/other forms untouched. Case is
|
|
83
|
+
* preserved — MCP tool ids are already snake_case and callers humanize after.
|
|
84
|
+
* `speak` is never treated as namespaced (it renders as a chat bubble, not an
|
|
85
|
+
* action row).
|
|
86
|
+
*/
|
|
87
|
+
export function getMcpToolLeafName(name) {
|
|
88
|
+
const trimmed = name.trim();
|
|
89
|
+
if (!trimmed || isSpeakToolName(trimmed) || !trimmed.includes("__")) {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
const segments = trimmed.split("__").filter((segment) => segment.length > 0);
|
|
93
|
+
if (segments.length >= 3 && segments[0].toLowerCase() === "mcp") {
|
|
94
|
+
return segments.slice(2).join("__");
|
|
95
|
+
}
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
72
98
|
export function isLikelyExternalToolName(name) {
|
|
73
99
|
const normalized = normalizeToolName(name);
|
|
74
100
|
if (!normalized) {
|