Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
1a40d44
Hermes: an agent is told its role at every turn, beside its name
futurepaul Oct 30, 2026
6013b00
shell: New chat with its agent, two-line chat rows, ⌘K up top
futurepaul Oct 30, 2026
89f13af
Settings: Add agent (a name and a role), and each agent's role
futurepaul Oct 30, 2026
827abb0
e2e: a row whose words changed may be drawn again
futurepaul Oct 30, 2026
14f2791
Merge branch 'claude/shell-agents-threads' into claude/settings-add-a…
futurepaul Oct 30, 2026
4194934
The stub carries the fragment CLI, and its agents ask each other
futurepaul Oct 30, 2026
bde93fd
shell: delegation threads, a line each over the chat an agent asked from
futurepaul Oct 31, 2026
e2e0c7d
The stub's CLI: its dependencies a layer of their own, unoptimized
futurepaul Oct 31, 2026
d6955c6
Merge branch 'claude/stub-asks' into claude/shell-threads
futurepaul Oct 31, 2026
1b14f3c
docs: the stub's CLI, as it is built now
futurepaul Oct 31, 2026
d075a6a
Merge branch 'claude/stub-asks' into claude/shell-threads
futurepaul Oct 31, 2026
ea17a8f
threads: read again on any change of a row; check each newly ended th…
futurepaul Oct 31, 2026
22610b2
Add agent: a reload finishes an Add it cut short
futurepaul Oct 31, 2026
dce4c23
Merge branch 'claude/settings-add-agent' into claude/stub-asks
futurepaul Oct 31, 2026
3bff24a
Merge branch 'claude/stub-asks' into claude/shell-threads
futurepaul Oct 31, 2026
c58cd3e
Merge branch 'sat-778' into sat-779
futurepaul Oct 31, 2026
75625fd
Merge branch 'claude/stub-asks' into claude/shell-threads
futurepaul Oct 31, 2026
6a61bf1
Merge branch 'claude/stub-asks' into claude/shell-threads
futurepaul Oct 31, 2026
019df47
Merge remote-tracking branch 'origin/master' into claude/shell-threads
futurepaul Oct 31, 2026
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
8 changes: 4 additions & 4 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# The Hermes image builds from the repo root (images/hermes/Dockerfile): it
# carries the fragment CLI, of the root workspace. Its context is only what
# its build reads; everything else (targets, the cell's builds, node_modules,
# git) stays out. The stub image builds from images/ (images/.dockerignore).
# The computer images build from the repo root (images/hermes/Dockerfile,
# images/stub/Dockerfile): each carries the fragment CLI, of the root
# workspace. Their context is only what their builds read; everything else
# (targets, the cell's builds, node_modules, git) stays out.
*
!Cargo.toml
!Cargo.lock
Expand Down
6 changes: 5 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,10 @@ jobs:
# commit in eight changes one) is its layers fetched in seconds, not
# its bridge compiled (about 100 s alone, 250 beside the Rust); a
# changed one compiles here, before the Rust rather than beside it.
# Its CLI stage (the fragment CLI its agents ask with) is the same:
# cached until the CLI or a crate it builds from changes, then built
# beside the bridge's stage. The context is the repo's root, cut to
# what the images build from (.dockerignore).
# Saved from the first shard on master's pushes, every stage's layers
# (mode=max: the build stage's base too, 345 MB in all, each layer
# kept once by its digest).
Expand All @@ -287,7 +291,7 @@ jobs:
DOCKER_BUILD_SUMMARY: false
DOCKER_BUILD_RECORD_UPLOAD: false
with:
context: images
context: .
file: images/stub/Dockerfile
platforms: linux/amd64
provenance: false
Expand Down
7 changes: 4 additions & 3 deletions .github/workflows/images.yml
Original file line number Diff line number Diff line change
Expand Up @@ -155,10 +155,11 @@ jobs:
- run: cargo test -p fragment-bridge --test docker --locked --no-run
- name: the runner
run: nproc; free -g; df -h /; docker version --format '{{.Server.Version}}'
# as the tests' own `build` does (docker.rs), the stub from images/, the
# Hermes image from the repo's root
# as the tests' own `build` does (docker.rs), both from the repo's root
# (each carries the fragment CLI, of the root workspace)
- name: build the stub image
run: docker build --platform linux/amd64 -f stub/Dockerfile -t fragment-stub:test .
working-directory: .
run: docker build --platform linux/amd64 -f images/stub/Dockerfile -t fragment-stub:test .
- name: build the Hermes image
working-directory: .
run: docker build --platform linux/amd64 -f images/hermes/Dockerfile -t fragment-hermes:test .
Expand Down
87 changes: 84 additions & 3 deletions cell/shell/agent-settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,13 @@ import { avatar } from "./agent-identity.js";
import { providerName } from "./connections.js";

export const DEFAULT_AGENT_TIER = "cheap";
// An agent's role, at most (proto's `ROLE_MAX_CHARS`), and the agents one
// computer runs (the cell's `AGENTS_MAX`): copies, each held to its source
// by a host test (xtask/src/guards.rs, `the_shell_copies_its_limits`).
const ROLE_MAX_CHARS = 48;
const AGENTS_MAX = 16;

export function createAgentSettings({ api, el, seg, byName, agentFace, titleOf, agents, named, rename, load, nameScreen, renderNames, computer, homePicker }) {
export function createAgentSettings({ api, el, seg, byName, agentFace, titleOf, agents, named, rename, load, nameScreen, renderNames, computer, homePicker, addAgent }) {
// ---- an agent's name and picture (docs/api.md, "An agent's name and
// picture"): its agent fragment's title and its avatar.png, written and
// put live as any of its files are (POST …/files, then …/deploy). Every
Expand Down Expand Up @@ -140,6 +145,82 @@ export function createAgentSettings({ api, el, seg, byName, agentFace, titleOf,
agentJsonWrites.set(agent, written);
return written;
}
// ---- an agent's role (its fragment.json's `meta.role`; Paul, 2026-10-30):
// the tag its chats' rows show beside its name, one source, written and
// put live as its name is; a role taken away is none. The rows follow the
// list, never this field. ----
async function setRole(agent, role) {
const manifest = await api("GET", `/api/f/${seg(agent)}/manifest`);
const meta = { ...(manifest.meta ?? {}) };
if (role) meta.role = role;
else delete meta.role;
manifest.meta = meta;
await api("POST", `/api/f/${seg(agent)}/files`, { message: role ? "its role" : "no role", files: [{ path: "fragment.json", text: JSON.stringify(manifest, null, 2) + "\n" }] });
await api("POST", `/api/f/${seg(agent)}/deploy`, {});
}
function roleField(a, said) {
const field = el("input", "agent-role");
field.type = "text";
field.maxLength = ROLE_MAX_CHARS;
field.placeholder = "e.g. chief of staff";
field.dataset.roleFor = a.fragment;
field.value = byName(a.fragment)?.agentRole ?? "";
named(field, a.fragment, (t) => `${t}'s role`, "aria-label");
field.onchange = async () => {
const role = field.value.trim();
field.disabled = true;
said.textContent = "Saving…";
try {
await setRole(a.fragment, role);
said.textContent = "Saved.";
await load();
} catch (e) {
said.textContent = e.message;
field.value = byName(a.fragment)?.agentRole ?? "";
} finally {
field.disabled = false;
}
};
const label = settingLabel("Role", field);
label.classList.add("agent-role-label");
return label;
}
// ---- another agent (Paul, 2026-10-30): its name and role, then it is
// made as the first was, in its own chat (onboarding.js `addAgent`),
// which opens. None past what one computer runs. ----
function addAgentForm() {
if (agents().size >= AGENTS_MAX) return [];
const form = el("form", "agent-add");
const name = el("input", "agent-add-name");
name.type = "text";
name.maxLength = 48;
name.placeholder = "Name";
name.setAttribute("aria-label", "New agent's name");
const role = el("input", "agent-add-role");
role.type = "text";
role.maxLength = ROLE_MAX_CHARS;
role.placeholder = "Role, e.g. dev";
role.setAttribute("aria-label", "New agent's role");
const add = el("button", "quiet", "Add agent");
add.type = "submit";
const said = el("p", "muted agent-face-said");
said.hidden = true;
form.onsubmit = async (event) => {
event.preventDefault();
if (!name.value.trim()) return name.focus();
add.disabled = true;
try {
await addAgent({ name: name.value.trim(), role: role.value.trim() });
} catch (e) {
said.textContent = e.message;
said.hidden = false;
} finally {
add.disabled = false;
}
};
form.append(name, role, add);
return [form, said];
}
function agentSettings(a, row, modeled) {
const box = el("div", "agent-setting");
box.dataset.agent = a.fragment;
Expand All @@ -152,7 +233,7 @@ export function createAgentSettings({ api, el, seg, byName, agentFace, titleOf,
// names (Private, Standard) or a model of a provider the person
// connected; whether there is one, with a single route, is the agent's
// own tier's to say (once agent.json is read)
const picks = [...modelPickers(a, modeled, read, write, said), ...homePicker(a, said)];
const picks = [roleField(a, said), ...modelPickers(a, modeled, read, write, said), ...homePicker(a, said)];
box.append(row, ...picks, said);
return box;
}
Expand Down Expand Up @@ -325,5 +406,5 @@ export function createAgentSettings({ api, el, seg, byName, agentFace, titleOf,
read.then(show);
return [fastRow, deepRow];
}
return { faceControls, renderAgentFaces, agentSettings, agentJson, writeAgentJson, savePick, settingLabel, modelOption };
return { faceControls, renderAgentFaces, agentSettings, addAgentForm, agentJson, writeAgentJson, savePick, settingLabel, modelOption };
}
Loading
Loading