Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
5706daa
Add tests for pinning tool packages through the agent-definition crea…
TheGreatAxios Aug 21, 2026
734e0a4
agent-directory: accept toolPackagePins on the create-agent-definitio…
TheGreatAxios Aug 21, 2026
821f9e0
Add tests for seeding Scout and Jimmy through workbench templates
TheGreatAxios Aug 21, 2026
f51ca42
workflow-catalog: seed Scout and Jimmy as workbench-template particip…
TheGreatAxios Aug 21, 2026
13ef586
tool-registry-publish: publish Scout's and Jimmy's own tool bundles
TheGreatAxios Aug 21, 2026
037d31c
Merge remote-tracking branch 'origin/main' into cl-6499-seed-agents
TheGreatAxios Aug 21, 2026
58b7876
Sync lockfile with main
TheGreatAxios Aug 21, 2026
17ae9fd
Split agent metadata out of the tool modules so the browser bundle st…
TheGreatAxios Aug 21, 2026
a3fb4b8
Merge remote-tracking branch 'origin/main' into cl-6499-seed-agents
TheGreatAxios Aug 21, 2026
914420a
Drop the now-unused tool-name import from Jimmy's agent module
TheGreatAxios Aug 21, 2026
87868fa
Merge remote-tracking branch 'origin/main' into cl-6499-seed-agents
TheGreatAxios Aug 21, 2026
1dab047
Drop the duplicated metadata import in Jimmy's tool test
TheGreatAxios Aug 21, 2026
95164e1
Merge remote-tracking branch 'origin/main' into cl-6499-seed-agents
TheGreatAxios Aug 21, 2026
54a166b
Add tests expecting Scout's artifact-list tool renamed to fit the too…
TheGreatAxios Aug 21, 2026
c231cc2
Rename Scout's list_recent_artifacts tool to list_artifacts
TheGreatAxios Aug 21, 2026
c8af0e3
Add tests for gif_search's missing-credential-detail contract
TheGreatAxios Aug 21, 2026
0abfa77
Wire gif_search's "not connected" result through missing-credential-d…
TheGreatAxios Aug 21, 2026
28a98e8
Remove the default-teammates workbench template
TheGreatAxios Aug 21, 2026
ff51475
Add tests for the invite dialog's Jimmy quick-create row
TheGreatAxios Aug 21, 2026
a078f88
Add an "Add Jimmy" quick-create row to the invite-agent dialog
TheGreatAxios Aug 21, 2026
87acc13
Update docs: Jimmy's quick-create path and connect-Giphy detail contract
TheGreatAxios Aug 21, 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
5 changes: 5 additions & 0 deletions apps/web/src/agents-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,11 @@ export type CreateAgentDefinitionInput = {
readonly systemPrompt: string;
readonly model?: string;
readonly skills?: readonly string[];
/** Tool packages to pin by name (no version — the create route
* resolves each to `*`). Used by a template-driven create
* (`instantiateWorkbenchTemplate`'s Scout/Jimmy requests), never by
* the hand-authored create form, which has no field for it. */
readonly toolPackagePins?: readonly string[];
};

const CreatedAgentDefinition = WorkflowDefinitionResponse.and({
Expand Down
20 changes: 19 additions & 1 deletion bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions packages/agent-directory/src/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,9 @@ export function createAgentDefinitionRoutes({
if (body.description !== undefined)
coreInput.description = body.description;
if (body.model !== undefined) coreInput.model = body.model;
if (body.toolPackagePins !== undefined && body.toolPackagePins.length > 0) {
coreInput.toolPackagePins = body.toolPackagePins;
}

let row: Awaited<ReturnType<typeof createAgentDefinitionCore>>["row"];
try {
Expand Down
32 changes: 24 additions & 8 deletions packages/agent-directory/src/validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,21 @@ const SkillNameArray = skillNameSchema.array().narrow((skills, ctx) => {
return true;
});

// A pinned tool package names a `@corbits/*` workspace package, the only
// namespace this catalog ever resolves a pin against.
const ToolPackageNamePattern = type(/^@corbits\/[a-z0-9-]+$/);
const ToolPackagePinArray = ToolPackageNamePattern.array().narrow(
(pins, ctx) => {
const seen = new Set<string>();
for (const name of pins) {
if (seen.has(name))
return ctx.mustBe(`a list without duplicate tool package "${name}"`);
seen.add(name);
}
return true;
},
);

export const CreateAgentDefinitionInput = type({
name: boundedNonBlankString(100),
handle: HANDLE_PATTERN.describe(
Expand All @@ -49,14 +64,15 @@ export const CreateAgentDefinitionInput = type({
systemPrompt: boundedNonBlankString(8000),
"model?": boundedNonBlankString(200),
"skills?": SkillNameArray,
// No `toolPackagePins` field, deliberately: this is the HTTP route
// for a person hand-authoring an agent through a form, which has no
// affordance for typing an arbitrary tool-package pin. The one
// caller that needs `buildAgentDefinitionWorkflow`'s optional
// `toolPackagePins` (CL-6051's `{create}` planner branch, see
// `@corbits/task-planner`) calls that builder directly, in-process,
// never through this REST boundary — so parity here isn't needed
// unless a future UI grows a "pin a tool package" field of its own.
// `toolPackagePins` names tool packages by name only (no version — the
// core resolves each to `*`, matching `./workflow-create-routes.ts`'s
// own handling of the same field). Absent for a person hand-authoring
// an agent through a form, which has no affordance for typing one; the
// one caller that supplies it is `@corbits/workflow-catalog`'s
// `instantiateWorkbenchTemplate`, installing a template participant
// (Scout, Jimmy) whose tools ship as pinned packages rather than
// inline capabilities.
"toolPackagePins?": ToolPackagePinArray,
});
export type CreateAgentDefinitionInput =
typeof CreateAgentDefinitionInput.infer;
Expand Down
47 changes: 47 additions & 0 deletions packages/agent-directory/test/routes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -555,6 +555,53 @@ test("a create request without skills records an empty skills list", async () =>
expect(await skillsStore.getSkills("ast_1")).toEqual([]);
});

test("a create request with toolPackagePins pins each named package at version *", async () => {
let writtenFiles: Record<string, string | Uint8Array> | undefined;
const app = buildApp(
fakeAssetService({
createAsset: () =>
Promise.resolve({
id: "ast_1",
tenantId: TENANT.id,
kind: "workflow" as const,
name: "scout",
displayName: "Scout",
creatorPrincipalId: PRINCIPAL.id,
createdAt: new Date(),
updatedAt: new Date(),
}),
populateAsset: (params) => {
writtenFiles = params.tree.files;
return Promise.resolve({ commitSha: "deadbeef" });
},
}),
fakeCreateDb(),
);
const response = await post(app, {
name: "Scout",
handle: "scout",
systemPrompt: "You are Scout.",
toolPackagePins: ["@corbits/memory-tools", "@corbits/web-search-tools"],
});
expect(response.status).toBe(201);
const workflowJson = definitionFrom(writtenFiles);
expect(pinsFrom(workflowJson)).toEqual([
{ name: "@corbits/memory-tools", version: "*" },
{ name: "@corbits/web-search-tools", version: "*" },
]);
});

test("a create request rejects a toolPackagePins entry outside the @corbits scope", async () => {
const app = buildApp(fakeAssetService());
const response = await post(app, {
name: "Scout",
handle: "scout",
systemPrompt: "You are Scout.",
toolPackagePins: ["not-a-corbits-package"],
});
expect(response.status).toBe(400);
});

function fakeSkillsDb(
row: { id: string; assetId: string | null } | undefined,
): DB["db"] {
Expand Down
2 changes: 1 addition & 1 deletion packages/chat-ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,13 @@
"@corbits/react-ui": "github:corbitsdev/react-ui#3b122812a307ccb35be31386f7696020c5a84635",
"@tanstack/react-query": "catalog:",
"@workbench/connections": "workspace:*",
"@corbits/workflow-catalog": "workspace:*",
"arktype": "catalog:",
"@corbits/icons": "workspace:*",
"react": "^19.2.0",
"react-dom": "^19.2.0"
},
"devDependencies": {
"@corbits/workflow-catalog": "workspace:*",
"@happy-dom/global-registrator": "^20.11.2",
"@intx/inference": "0.3.0",
"@types/bun": "catalog:",
Expand Down
27 changes: 27 additions & 0 deletions packages/chat-ui/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { Part } from "@corbits/chat/parts";
import { parseParticipants } from "@corbits/chat/participants";
import type { ParticipantRecord } from "@corbits/chat/participants";
import { UnauthenticatedError } from "@corbits/api-query";
import { jimmyAgentRequest } from "@corbits/workflow-catalog";
import { CHAT_STRINGS } from "./strings";

export {
Expand Down Expand Up @@ -697,6 +698,32 @@ export function inviteAgent(
);
}

// Jimmy's own request shape, the same `@corbits/workflow-catalog` object a
// workbench template's participant create used to resolve — CL-6499 removed
// Jimmy's template (he is not a "kind of workbench"), so this dialog's own
// "Add Jimmy" quick-create row (see `invite-agent-dialog.tsx`) is his only
// create path left. `jimmyAgentRequest()` is pure data (no tool bodies, no
// server-only imports), safe to call from browser code.
export const JIMMY_QUICK_CREATE = jimmyAgentRequest();

const CreatedAgentDefinition = type({ id: "string" });

/**
* Creates Jimmy's agent-directory definition in one call — the same
* one-shot `POST /agent-definitions` a template-driven participant create
* goes through. Idempotency is the caller's job: only offer this when
* `JIMMY_QUICK_CREATE.handle` is absent from the tenant's invitable list.
*/
export function quickCreateJimmy(
tenantId: string,
): Promise<{ readonly id: string }> {
return request(
`/api/tenants/${tenantId}/agent-definitions`,
CreatedAgentDefinition,
{ method: "POST", body: JSON.stringify(JIMMY_QUICK_CREATE) },
);
}

// `DELETE /workbenches/:id/participants/:address` (see
// `packages/chat/src/routes.ts`): the removal counterpart to
// `inviteAgent`/workbench creation's own join — drops the participant and,
Expand Down
54 changes: 52 additions & 2 deletions packages/chat-ui/src/invite-agent-dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
// host — the server-side list already excludes it), each with an
// "Invite" action that launches it into the current workbench. The list
// itself carries its own loading/empty/error states since it is fetched
// fresh every time the dialog opens.
// fresh every time the dialog opens. When Jimmy has never been created
// in this tenant, an extra "Add Jimmy" row offers to create and invite
// him in one click — see `quickCreateJimmy` in `./api`.

import {
Button,
Expand All @@ -22,11 +24,18 @@ import { useEffect, useState } from "react";
import {
ChatApiError,
describeChatError,
JIMMY_QUICK_CREATE,
listInvitableDefinitions,
quickCreateJimmy,
} from "./api";
import type { InvitableDefinition } from "./api";
import { CHAT_STRINGS } from "./strings";

// A sentinel `invitingId` distinct from any real definition id — lets the
// "Add Jimmy" row show its own "Adding…" state while `quickCreateJimmy`
// runs, before a real definition id exists to key off of.
const JIMMY_QUICK_CREATE_MARKER = "jimmy-quick-create";

type ListState =
| { readonly kind: "loading" }
| { readonly kind: "error"; readonly message: string }
Expand Down Expand Up @@ -88,6 +97,29 @@ export function InviteAgentDialog({
}
}

/**
* Jimmy is no longer seeded by a workbench template (CL-6499: he is not
* a "kind of workbench") — this row is his one remaining create path.
* Creating him mints a real, tenant-wide agent-directory definition,
* exactly like a template's participant create did; inviting him into
* this workbench reuses `handleInvite`'s own state and error handling.
*/
async function handleQuickCreateJimmy() {
setInvitingId(JIMMY_QUICK_CREATE_MARKER);
setInviteError(null);
try {
const created = await quickCreateJimmy(tenantId);
await handleInvite(created.id);
} catch {
setInviteError(CHAT_STRINGS.inviteAgentQuickCreateError);
setInvitingId(null);
}
}

const jimmyMissing =
state.kind === "ready" &&
!state.items.some((item) => item.name === JIMMY_QUICK_CREATE.handle);

return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent side="right">
Expand All @@ -111,7 +143,7 @@ export function InviteAgentDialog({
title={CHAT_STRINGS.inviteAgentLoadError}
description={state.message}
/>
) : state.items.length === 0 ? (
) : state.items.length === 0 && !jimmyMissing ? (
<EmptyState
icon={<Users />}
title={CHAT_STRINGS.inviteAgentEmptyTitle}
Expand All @@ -138,6 +170,24 @@ export function InviteAgentDialog({
</Button>
</li>
))}
{jimmyMissing && (
<li
className="chat-invitable-item"
data-testid="quick-create-jimmy"
>
<span>{JIMMY_QUICK_CREATE.description}</span>
<Button
variant="outline"
size="sm"
disabled={invitingId !== null}
onClick={() => void handleQuickCreateJimmy()}
>
{invitingId === JIMMY_QUICK_CREATE_MARKER
? CHAT_STRINGS.inviteAgentQuickCreating
: CHAT_STRINGS.inviteAgentQuickCreateAction}
</Button>
</li>
)}
</ul>
)}
</DialogBody>
Expand Down
3 changes: 3 additions & 0 deletions packages/chat-ui/src/strings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,9 @@ export const CHAT_STRINGS = {
inviteAgentInviting: "Inviting…",
inviteAgentInviteError: "Couldn't invite that agent — try again.",
inviteAgentConflictError: "This workbench already has its agent.",
inviteAgentQuickCreateAction: "Add",
inviteAgentQuickCreating: "Adding…",
inviteAgentQuickCreateError: "Couldn't add Jimmy — try again.",
forkThreadAction: "Fork",
forkThreadError: "Couldn't fork that message into a thread — try again.",
replyInThreadAction: "Reply in thread",
Expand Down
Loading
Loading