Subagents and Compact on the server's /api/v2 facts (server 2.0.0-beta.7)
- Models' `subagents {offered, named_only}` and /models' `subagents_cap`; a chat's
`subagents {offered, named_only, cap}` decides the chip for an open chat (an agent chat
gets none where the server says so); `named_only` shows the spec's `own` line in the
popover and marks the chip. GET /chats/{chat}/team stays the fallback for older servers.
- Context: `auto_compact`, `until_compact` and `compactable` from the server (worked out from
`threshold` with an older one); Compact greyed while not compactable.
- "Tasks: N" for a task's nested fold, the server's sentence; the subagent words aligned with
the server's catalogues (Podagenti, Sottoagenti, Podagenci); hint says the same-model rule.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
8 files changed
+144
-116
No files matched your search
+17
-12
@@ -475,20 +475,25 @@ function contextPart(c) {
|
||||
}
|
||||
|
||||
/**
|
||||
* How far it is to the automatic compaction -- the server compacts before a turn once the context
|
||||
* would reach `threshold` % of the window; 0 is off -- and the button that compacts now.
|
||||
* How far it is to the automatic compaction, and the button that compacts now. The server says it
|
||||
* (2.0.0-beta.7: `auto_compact`, `until_compact` in tokens, `compactable` whether Compact can run
|
||||
* now); an older one gives only `threshold` -- the server compacts before a turn once the context
|
||||
* would reach that % of the window, 0 is off -- and it is worked out from that.
|
||||
*/
|
||||
function compactRow(c) {
|
||||
const limit = Number(c.limit) || 0
|
||||
const threshold = Number(c.threshold)
|
||||
const auto = "auto_compact" in c ? !!c.auto_compact : threshold > 0
|
||||
let until = c.until_compact
|
||||
if (until === undefined && auto && limit && threshold < 100) until = Math.floor((limit * threshold) / 100) - (Number(c.used) || 0)
|
||||
let words
|
||||
if (!(threshold > 0)) words = t("Automatic compaction is off.")
|
||||
else if (!limit || threshold >= 100) words = t("Compacts by itself once the window is known.")
|
||||
else {
|
||||
const until = Math.floor((limit * threshold) / 100) - (Number(c.used) || 0)
|
||||
words = until > 0 ? t("%(tokens)s until auto-compact", { tokens: compact(until) }) : t("Compacts before the next reply.")
|
||||
}
|
||||
if (!auto) words = t("Automatic compaction is off.")
|
||||
else if (until === null || until === undefined) words = t("Compacts by itself once the window is known.")
|
||||
else words = until > 0 ? t("%(tokens)s until auto-compact", { tokens: compact(until) }) : t("Compacts before the next reply.")
|
||||
const busy = compacting === chat?.id
|
||||
// `compactable` false: nothing to summarise yet, a reply being written, compaction turned off --
|
||||
// the button stays, greyed; pressing an older server's says why in its own words.
|
||||
const can = !("compactable" in c) || !!c.compactable
|
||||
return h(
|
||||
"div",
|
||||
{ class: "ctx__compact" },
|
||||
@@ -497,7 +502,7 @@ function compactRow(c) {
|
||||
type: "button",
|
||||
class: "btn btn--sm",
|
||||
text: busy ? t("Compacting…") : t("Compact"),
|
||||
disabled: busy,
|
||||
disabled: busy || !can,
|
||||
title: t("Summarise the earlier turns now and send the summary in their place."),
|
||||
onclick: compactNow,
|
||||
}),
|
||||
@@ -601,7 +606,7 @@ function roomPart() {
|
||||
)
|
||||
}
|
||||
|
||||
/** A folded list of tasks ("Show 3 more", "Finished 12", "2 tasks"), remembered across redraws. */
|
||||
/** A folded list of tasks ("Show 3 more", "Finished 12", "Tasks: 2"), remembered across redraws. */
|
||||
function fold(name, label, list, cls = "") {
|
||||
const el = h(
|
||||
"details",
|
||||
@@ -613,11 +618,11 @@ function fold(name, label, list, cls = "") {
|
||||
return el
|
||||
}
|
||||
|
||||
/** A task and, folded under it, the tasks it leads or started ("1 task", "3 tasks"). */
|
||||
/** A task and, folded under it, the tasks it leads or started ("Tasks: 3"). */
|
||||
function agentRows(a) {
|
||||
const kids = a.children || []
|
||||
if (!kids.length) return agentRow(a)
|
||||
const nested = fold(`under:${a.kind}:${a.id}`, kids.length === 1 ? t("1 task") : t("%(n)s tasks", { n: kids.length }), kids, "tasks__nested")
|
||||
const nested = fold(`under:${a.kind}:${a.id}`, t("Tasks: %(n)s", { n: kids.length }), kids, "tasks__nested")
|
||||
nested.style.setProperty("--depth", String(Math.max(0, Math.min(6, (Number(a.depth) || 0) + 1))))
|
||||
return [agentRow(a), nested]
|
||||
}
|
||||
|
||||
+15
-11
@@ -13,7 +13,7 @@ import { markdown } from "./markdown.js"
|
||||
import { openViewer, showKind } from "./viewer.js"
|
||||
import { readLocal } from "./explorer.js"
|
||||
import { menu, ask, showLine, popover, closePopover } from "./popup.js"
|
||||
import { avatar, agentsControl, teamPossible, subagentsCap, SUBAGENTS_HINT, levelsOf, levelWord, picture } from "./models.js"
|
||||
import { avatar, agentsControl, teamPossible, namedOnly, subagentsCap, SUBAGENTS_HINT, SUBAGENTS_OWN, levelsOf, levelWord, picture } from "./models.js"
|
||||
import { readyIds, sending, sent, discard } from "./attach.js"
|
||||
import { watchActivity, activityChanged, planList, activityButton } from "./activity.js"
|
||||
import { agentBlock, agentReady, agentState, onAgentChange } from "./agent.js"
|
||||
@@ -566,17 +566,20 @@ async function chatModels(chat) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether this chat is offered subagents, and the instance's most: the chat's own word where the
|
||||
* server gives one (`subagents` on the chat), else its team's (`GET /chats/{chat}/team`) -- only a
|
||||
* chat on the server. null where it is not offered.
|
||||
* Whether this chat is offered subagents: `{ cap, own }` (the instance's most; whether only a model
|
||||
* the person names could serve them), or null. The chat's own `subagents` where the server gives
|
||||
* it (2.0.0-beta.7), else its team's word (`GET /chats/{chat}/team`) -- only a chat on the server.
|
||||
*/
|
||||
async function teamOffered(chat) {
|
||||
const said = chat.subagents
|
||||
if (said !== undefined && said !== null) return (typeof said === "object" ? said.offered : said) ? { cap: subagentsCap(said, ctx.me()) } : null
|
||||
if (said !== undefined && said !== null) {
|
||||
const offered = typeof said === "object" ? said.offered : said
|
||||
return offered ? { cap: subagentsCap(said), own: namedOnly(said) } : null
|
||||
}
|
||||
if (chat.kind !== "chat") return null
|
||||
try {
|
||||
const team = (await get(`/chats/${encodeURIComponent(chat.id)}/team`)).team
|
||||
return team?.offered ? { cap: subagentsCap(team, ctx.me()) } : null
|
||||
return team?.offered ? { cap: subagentsCap(), own: false } : null
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
@@ -652,13 +655,13 @@ function effortChip(model, value, onChange) {
|
||||
* The subagents a chat may run: a chip saying the fewest and the most, opening the two fields.
|
||||
* `value` { min, max } or empty; `cap` the instance's most, where the server says it.
|
||||
*/
|
||||
function agentsChip(value, onChange, cap) {
|
||||
function agentsChip(value, onChange, { cap, own } = {}) {
|
||||
const said = value?.min || value?.max ? `${value.min ?? "…"}–${value.max ?? "…"}` : t("default")
|
||||
return chip({
|
||||
ico: ICONS.users,
|
||||
label: `${t("Subagents")} ${said}`,
|
||||
title: SUBAGENTS_HINT(),
|
||||
cls: "cchip--subagents",
|
||||
title: own ? SUBAGENTS_OWN() : SUBAGENTS_HINT(),
|
||||
cls: `cchip--subagents${own ? " is-warn" : ""}`,
|
||||
onclick: (e) => {
|
||||
const fields = agentsControl({ value, id: "agents-min", cap })
|
||||
popover(
|
||||
@@ -669,6 +672,7 @@ function agentsChip(value, onChange, cap) {
|
||||
h("label", { class: "pop__title", for: "agents-min", text: t("Subagents") }),
|
||||
h("div", { class: "agents__heads", "aria-hidden": "true" }, h("span", { text: t("Fewest") }), h("span"), h("span", { text: t("Most") })),
|
||||
fields,
|
||||
own ? h("p", { class: "hint is-warn", text: SUBAGENTS_OWN() }) : null,
|
||||
h("p", { class: "hint", text: SUBAGENTS_HINT() }),
|
||||
h("div", { class: "pop__actions" }, h("button", { type: "button", class: "btn btn--sm btn--primary", text: t("Save"), onclick: () => (closePopover(), onChange(fields.value)) })),
|
||||
),
|
||||
@@ -718,7 +722,7 @@ async function renderChips() {
|
||||
renderChips()
|
||||
}, mayPick))
|
||||
if (model) parts.push(effortChip(model, draft.effort, (l) => ((draft.effort = l), renderChips())))
|
||||
if (draft.kind === "chat" && teamPossible(model)) parts.push(agentsChip(draft.agents, (a) => ((draft.agents = a), renderChips()), subagentsCap(model?.subagents, ctx.me())))
|
||||
if (draft.kind === "chat" && teamPossible(model)) parts.push(agentsChip(draft.agents, (a) => ((draft.agents = a), renderChips()), { cap: subagentsCap(), own: namedOnly(model?.subagents) }))
|
||||
box.replaceChildren(...parts.filter(Boolean))
|
||||
return
|
||||
}
|
||||
@@ -747,7 +751,7 @@ async function renderChips() {
|
||||
box.replaceChildren(...parts.filter(Boolean))
|
||||
const offer = await teamOffered(chat)
|
||||
if (!offer || drawing !== chipsDrawn || open?.chat?.id !== chat.id) return
|
||||
box.append(agentsChip(open.chat.agents || chat.agents, (a) => save({ agents: a }), offer.cap))
|
||||
box.append(agentsChip(open.chat.agents || chat.agents, (a) => save({ agents: a }), offer))
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+4
-1
@@ -19,6 +19,7 @@ import { initActivity, showPanel, togglePanel, panelOpen } from "./activity.js"
|
||||
import { initAgent, link } from "./agent.js"
|
||||
import { initScheduled, showScheduled, scheduledCommands } from "./scheduled.js"
|
||||
import { closePopover } from "./popup.js"
|
||||
import { noteSubagentsCap } from "./models.js"
|
||||
|
||||
let status = null
|
||||
let view = "" // "setup" | "app"
|
||||
@@ -118,7 +119,9 @@ function stopApp() {
|
||||
|
||||
async function loadModels() {
|
||||
try {
|
||||
models = (await get("/models")).models || []
|
||||
const r = await get("/models")
|
||||
models = r.models || []
|
||||
noteSubagentsCap(r.subagents_cap)
|
||||
} catch {
|
||||
models = []
|
||||
}
|
||||
|
||||
+23
-11
@@ -193,13 +193,13 @@ export function effortControl({ model, value, onChange, id, compact }) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a new chat on `model` is offered subagents. The server says so where it can: a model's
|
||||
* `subagents` in `/models` (a flag, or `{offered}`), true when subagents are on for the model and
|
||||
* the instance and at least one model could serve as one -- the model itself with room for two
|
||||
* requests at once, or a usable model outside its one-model-at-a-time server. A server that does
|
||||
* not say it yet: a model that calls tools and serves two requests at once (`parallel` null is a
|
||||
* hosted API with no stated limit). The server decides again; this only keeps the chip away where
|
||||
* it could never apply. An open chat asks the server (`GET /chats/{chat}/team`).
|
||||
* Whether a new chat on `model` is offered subagents: the model's `subagents.offered` in `/models`
|
||||
* (server 2.0.0-beta.7: subagents on for the model and the instance, and at least one model could
|
||||
* serve as one -- the model itself with room for two requests at once, or a usable model outside
|
||||
* its one-model-at-a-time server). A server that does not say it: a model that calls tools and
|
||||
* serves two requests at once (`parallel` null is a hosted API with no stated limit). The server
|
||||
* decides again; this only keeps the chip away where it could never apply. An open chat has its
|
||||
* own `subagents` (chat.js).
|
||||
*/
|
||||
export function teamPossible(model) {
|
||||
const said = model?.subagents
|
||||
@@ -207,16 +207,28 @@ export function teamPossible(model) {
|
||||
return !!model?.tools && (model.parallel === null || model.parallel === undefined || model.parallel >= 2)
|
||||
}
|
||||
|
||||
/** The most subagents the instance allows a chat (`cap`), where the server says it; else 32. */
|
||||
/** Whether only a model the person names in the chat could serve its subagents (`named_only`). */
|
||||
export function namedOnly(said) {
|
||||
return !!(said && typeof said === "object" && said.named_only)
|
||||
}
|
||||
|
||||
// The instance's most, as `/models` said it last (`subagents_cap`); 32 where it says nothing.
|
||||
export const SUBAGENTS_CAP = 32
|
||||
export const SUBAGENTS_HINT = () => t("The fewest and the most subagents this chat runs. They work on the same model unless you name another one in the chat.")
|
||||
let knownCap = null
|
||||
export function noteSubagentsCap(n) {
|
||||
knownCap = Number(n) >= 1 ? Number(n) : null
|
||||
}
|
||||
/** The most subagents a chat may have: the first of `sources` that says it (`cap`), else the instance's. */
|
||||
export function subagentsCap(...sources) {
|
||||
for (const s of sources) {
|
||||
const n = Number(s?.cap ?? s?.subagents_cap ?? s?.subagents?.cap)
|
||||
const n = Number(s?.cap)
|
||||
if (Number.isFinite(n) && n >= 1) return n
|
||||
}
|
||||
return SUBAGENTS_CAP
|
||||
return knownCap || SUBAGENTS_CAP
|
||||
}
|
||||
export const SUBAGENTS_HINT = () => t("Counted with the chat's own agent. A subagent runs on the chat's model, or on another only when you name it in the chat.")
|
||||
// The spec's `subagents.setting.own`: beside the setting where the chat's model cannot serve them.
|
||||
export const SUBAGENTS_OWN = () => t("This model serves one request at a time: subagents need another model, on another server, named in the chat.")
|
||||
|
||||
/**
|
||||
* The subagents a chat may run: the fewest and the most, as two number fields on one shared
|
||||
|
||||
Reference in new issue
Block a user