@hanzo/build 0.2.79 → 0.2.80

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/README.md CHANGED
@@ -226,10 +226,15 @@ are any member's.
226
226
  - Plugins: TypeScript connectors built on the platform, from source or from a
227
227
  description of an API; a failed build says why. Browse offers building one and
228
228
  lists what the deployment mounts.
229
- - Agents: Browse is the platform's presets whose tool calls run on the platform,
230
- each a Use that opens a new agent written from it; Yours is the org's own —
231
- model, instructions, tools, budget — created, edited (only what changed is
232
- sent) and deleted.
229
+ - Agents: Browse is every agent to start from — the host's default agents
230
+ (`cast`) and personas (`personas`), and the platform's presets whose tool
231
+ calls run on the platform — searched by name and filtered by category. Use
232
+ makes one agent of the org's own from one template (a name taken becomes
233
+ `-2`, `-3`), with its face, model and instructions, and opens its profile;
234
+ nothing is added in bulk. Start from scratch, Clone yourself (the person
235
+ signed in, with writing pasted or uploaded) and Create from a person make one
236
+ too. Yours is the org's own — model, instructions, tools, budget — created,
237
+ edited (only what changed is sent) and deleted.
233
238
 
234
239
  **New.** "What's up next?", and at the foot the composer: where the run runs
235
240
  (Cloud is the platform's sandbox with the codebase's environment; the org's
@@ -1,6 +1,6 @@
1
1
  /**
2
- * Agents: the org's own — a model, instructions, the tools it may call and a
3
- * budget — and the presets one can start from.
2
+ * Agents: the org's own — a model, instructions, the tools it may call, a
3
+ * budget and a face — and the templates one is made from.
4
4
  *
5
5
  * GET /v1/agent {agents: [agent]}, without instructions
6
6
  * POST /v1/agent create → 201 agent
@@ -35,6 +35,8 @@ export interface Agent {
35
35
  task: number;
36
36
  spent: number;
37
37
  period: Period | '';
38
+ /** Its picture: an https link or a data URL; '' draws its emoji or initial. */
39
+ avatar: string;
38
40
  emoji: string;
39
41
  }
40
42
  export interface Preset {
@@ -52,6 +54,8 @@ export interface Draft {
52
54
  cap: string;
53
55
  task: string;
54
56
  period: Period;
57
+ /** Sent on create only: an https link or a data URL. */
58
+ avatar: string;
55
59
  }
56
60
  /** An agent's name, as the handler takes it. */
57
61
  export declare const NAME: RegExp;
@@ -86,29 +90,45 @@ export declare function profile(t: Target, ref: string): Promise<{
86
90
  agent: Agent;
87
91
  runs: Run[];
88
92
  }>;
89
- /** A default agent a host offers: a name, its line, its model and its instructions. */
93
+ /** An agent to start from: a default agent, a persona or a preset. */
90
94
  export interface Cast {
95
+ /** The name it is made under. */
91
96
  name: string;
97
+ /** What Browse calls it. */
98
+ title: string;
92
99
  description: string;
100
+ /** What Browse lists it under. */
101
+ category: string;
93
102
  model: string;
94
103
  instructions: string;
95
- /** Its face, an address of the host, when it has one. */
104
+ /** Reads the instructions, where they are a file the host serves. */
105
+ read?: () => Promise<string>;
106
+ /** Its face, an address of the host. */
96
107
  avatar?: string;
108
+ tools?: string[];
97
109
  }
98
- /** What a default agent may spend until its owner says otherwise, in dollars: each month, and one run. */
110
+ /** What an agent made from a template may spend until its owner says otherwise, in dollars: each month, and one run. */
99
111
  export declare const CAST_CAP = "10";
100
112
  export declare const CAST_TASK = "1";
101
- /** The cast members the org has no agent for, by name in any case. */
102
- export declare const missing: (cast: readonly Cast[], have: readonly {
103
- name: string;
104
- }[]) => Cast[];
105
- /** Create every cast member the org lacks, one at a time; the names made, and the first refusal, if any. */
106
- export declare function hire(t: Target, cast: readonly Cast[], have: readonly {
113
+ /**
114
+ * A name the API takes: accents dropped, any other run of characters a dash,
115
+ * from a letter or digit, up to 64. "Spärck Jones" is Sparck-Jones.
116
+ */
117
+ export declare const handle: (name: string) => string;
118
+ /** `name`, or `name-2`, `name-3`… : the first the org has no agent called, in any case. */
119
+ export declare function free(name: string, have: readonly {
107
120
  name: string;
108
- }[]): Promise<{
109
- made: string[];
110
- error: unknown;
111
- }>;
121
+ }[]): string;
122
+ /** An address of the page as an https link the API keeps, or '' (a page served over http has none). */
123
+ export declare const link: (at: string | undefined, base: string) => string;
124
+ /**
125
+ * Make the org's own agent from a template: under the first name the org has
126
+ * free (read now, so it is never one that exists), with its instructions read
127
+ * and its face linked. The template is not touched.
128
+ */
129
+ export declare function use(t: Target, c: Cast, base: string): Promise<Agent>;
130
+ /** Anyone as a template: their name, who they are, and writing of theirs to take the voice from. */
131
+ export declare function person(name: string, about: string, writing: string, avatar?: string): Cast;
112
132
  export declare function create(t: Target, d: Draft): Promise<Agent>;
113
133
  /** The fields that differ from `was`, and nothing else: an omitted field keeps its value. */
114
134
  export declare function changes(was: Agent, d: Draft): Record<string, unknown>;
@@ -116,5 +136,5 @@ export declare function update(t: Target, was: Agent, d: Draft): Promise<Agent>;
116
136
  export declare function remove(t: Target, ref: string): Promise<void>;
117
137
  /** The presets that make a working agent. */
118
138
  export declare function presets(t: Target): Promise<Preset[]>;
119
- /** A new agent's draft from a preset. */
120
- export declare function fromPreset(p: Preset): Draft;
139
+ /** A preset as a template. */
140
+ export declare function fromPreset(p: Preset): Cast;
package/lib/api/agents.js CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
- * Agents: the org's own — a model, instructions, the tools it may call and a
3
- * budget — and the presets one can start from.
2
+ * Agents: the org's own — a model, instructions, the tools it may call, a
3
+ * budget and a face — and the templates one is made from.
4
4
  *
5
5
  * GET /v1/agent {agents: [agent]}, without instructions
6
6
  * POST /v1/agent create → 201 agent
@@ -47,6 +47,7 @@ export function agent(raw) {
47
47
  task: num(o.max_task_micro_usd),
48
48
  spent: num(o.consumed_micro_usd),
49
49
  period: PERIODS.includes(period) ? period : '',
50
+ avatar: str(o.avatar),
50
51
  emoji: str(o.emoji),
51
52
  };
52
53
  }
@@ -61,9 +62,10 @@ export function draft(a) {
61
62
  cap: dollars(a.cap),
62
63
  task: dollars(a.task),
63
64
  period: a.period || 'month',
65
+ avatar: a.avatar,
64
66
  };
65
67
  }
66
- export const EMPTY = { name: '', description: '', model: '', instructions: '', tools: [], cap: '', task: '', period: 'month' };
68
+ export const EMPTY = { name: '', description: '', model: '', instructions: '', tools: [], cap: '', task: '', period: 'month', avatar: '' };
67
69
  /** Why a draft cannot be saved as it stands, or '' when it can. */
68
70
  export function refuse(d) {
69
71
  if (!NAME.test(d.name))
@@ -95,27 +97,52 @@ export async function profile(t, ref) {
95
97
  const raw = await call(t, 'GET', `/v1/agent/${seg(ref)}`);
96
98
  return { agent: agent(raw), runs: rows(obj(raw).recentRuns).map(run).filter((r) => r.id) };
97
99
  }
98
- /** What a default agent may spend until its owner says otherwise, in dollars: each month, and one run. */
100
+ /** What an agent made from a template may spend until its owner says otherwise, in dollars: each month, and one run. */
99
101
  export const CAST_CAP = '10';
100
102
  export const CAST_TASK = '1';
101
- /** The cast members the org has no agent for, by name in any case. */
102
- export const missing = (cast, have) => {
103
- const names = new Set(have.map((a) => a.name.toLowerCase()));
104
- return cast.filter((c) => !names.has(c.name.toLowerCase()));
105
- };
106
- /** Create every cast member the org lacks, one at a time; the names made, and the first refusal, if any. */
107
- export async function hire(t, cast, have) {
108
- const made = [];
109
- for (const c of missing(cast, have)) {
110
- try {
111
- await create(t, { ...EMPTY, name: c.name, description: c.description, model: c.model, instructions: c.instructions, cap: CAST_CAP, task: CAST_TASK, period: 'month' });
112
- made.push(c.name);
113
- }
114
- catch (error) {
115
- return { made, error };
116
- }
103
+ /**
104
+ * A name the API takes: accents dropped, any other run of characters a dash,
105
+ * from a letter or digit, up to 64. "Spärck Jones" is Sparck-Jones.
106
+ */
107
+ export const handle = (name) => name
108
+ .normalize('NFD')
109
+ .replace(/[\u0300-\u036f]/g, '')
110
+ .replace(/[^A-Za-z0-9._-]+/g, '-')
111
+ .replace(/^[^A-Za-z0-9]+/, '')
112
+ .slice(0, 64)
113
+ .replace(/-+$/, '');
114
+ /** `name`, or `name-2`, `name-3`… : the first the org has no agent called, in any case. */
115
+ export function free(name, have) {
116
+ const taken = new Set(have.map((a) => a.name.toLowerCase()));
117
+ if (!taken.has(name.toLowerCase()))
118
+ return name;
119
+ for (let i = 2;; i++) {
120
+ const next = `${name.slice(0, 63 - String(i).length)}-${i}`;
121
+ if (!taken.has(next.toLowerCase()))
122
+ return next;
117
123
  }
118
- return { made, error: null };
124
+ }
125
+ /** An address of the page as an https link the API keeps, or '' (a page served over http has none). */
126
+ export const link = (at, base) => {
127
+ if (!at)
128
+ return '';
129
+ const url = new URL(at, base).href;
130
+ return url.startsWith('https://') || url.startsWith('data:') ? url : '';
131
+ };
132
+ /**
133
+ * Make the org's own agent from a template: under the first name the org has
134
+ * free (read now, so it is never one that exists), with its instructions read
135
+ * and its face linked. The template is not touched.
136
+ */
137
+ export async function use(t, c, base) {
138
+ const [have, instructions] = await Promise.all([agents(t), c.read ? c.read() : c.instructions]);
139
+ return create(t, { ...EMPTY, name: free(handle(c.name), have), description: c.description, model: c.model, instructions, tools: c.tools ?? [], cap: CAST_CAP, task: CAST_TASK, avatar: link(c.avatar, base) });
140
+ }
141
+ /** Anyone as a template: their name, who they are, and writing of theirs to take the voice from. */
142
+ export function person(name, about, writing, avatar = '') {
143
+ const who = name.trim();
144
+ const lines = [`You are ${who}.`, about.trim(), writing.trim() && `Writing by ${who}:\n\n${writing.trim()}`];
145
+ return { name: who, title: who, description: about.trim(), category: 'Person', model: '', instructions: lines.filter(Boolean).join('\n\n'), avatar };
119
146
  }
120
147
  export async function create(t, d) {
121
148
  const why = refuse(d);
@@ -132,6 +159,8 @@ export async function create(t, d) {
132
159
  };
133
160
  if (d.model)
134
161
  body.model = d.model;
162
+ if (d.avatar)
163
+ body.avatar = d.avatar;
135
164
  return agent(await call(t, 'POST', '/v1/agent', body));
136
165
  }
137
166
  /** The fields that differ from `was`, and nothing else: an omitted field keeps its value. */
@@ -173,7 +202,7 @@ export async function presets(t) {
173
202
  .map((p) => ({ id: str(p.id), title: str(p.title), prompt: str(p.systemPrompt) }))
174
203
  .filter((p) => p.id);
175
204
  }
176
- /** A new agent's draft from a preset. */
205
+ /** A preset as a template. */
177
206
  export function fromPreset(p) {
178
- return { ...EMPTY, name: p.id, description: p.title, instructions: p.prompt };
207
+ return { name: p.id, title: p.title || p.id, description: p.title, category: 'Preset', model: '', instructions: p.prompt };
179
208
  }
@@ -1,4 +1,7 @@
1
+ import { type Cast } from '../api/agents.ts';
1
2
  import { type Pane } from './ui.tsx';
3
+ /** Categories as Browse offers them: the default agents' and presets' first, as listed, then the rest by how many each holds. */
4
+ export declare function kinds(first: readonly Cast[], rest: readonly Cast[]): string[];
2
5
  export declare function Agents({ view, q, adding, onAdding, onView, onCount }: Pane): import("react").JSX.Element;
3
6
  /** One agent's page: who it is, what it runs on and reads, what it may call and spend, and what it has done. */
4
7
  export declare function Profile({ name }: {
@@ -3,10 +3,12 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
3
3
  * Agents: the org's own — a model, instructions, the tools it may call, and
4
4
  * what it may spend.
5
5
  *
6
- * Browse is the platform's presets whose tool calls run on the platform;
7
- * starting from one opens a new agent written from it. Yours lists the org's
8
- * agents, and Add default agents makes every member of the host's cast the org
9
- * has none for (an org starts with none). Opening one is its profile page
6
+ * Browse is every agent there is to start from: the host's default agents
7
+ * (`cast`) and personas (`personas`), and the platform's presets whose tool
8
+ * calls run on the platform, by name and category. Use makes the org's own
9
+ * agent from one and opens it; nothing is added any other way, so an org starts
10
+ * with none. An agent is also made from scratch, from the person signed in
11
+ * (Clone yourself) or from anyone named. Yours lists the org's agents. Opening one is its profile page
10
12
  * (`-/customize/agents/<name>`): its face, part, model, instructions, tools,
11
13
  * budget and runs, edited in place, and only what changed is sent. An
12
14
  * agent calls only the tools it names, and only those that are on for the org,
@@ -15,11 +17,11 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
15
17
  * spend each period, and in one run.
16
18
  */
17
19
  import { SizableText, XStack, YStack } from '@hanzo/gui';
18
- import { ArrowLeft, Bot, Users } from '@hanzogui/lucide-icons-2';
20
+ import { ArrowLeft, Upload } from '@hanzogui/lucide-icons-2';
19
21
  import { Button, Input, Switch, Textarea } from '@hanzo/ui';
20
22
  import { ModelPicker } from '@hanzo/ui/models';
21
- import { useEffect, useMemo, useState } from 'react';
22
- import { agents, ALL, create, draft, EMPTY, fromPreset, hire, missing, one, PERIODS, presets, profile, remove, update } from '../api/agents.js';
23
+ import { useEffect, useMemo, useRef, useState } from 'react';
24
+ import { agents, ALL, create, draft, EMPTY, fromPreset, one, PERIODS, person, presets, profile, remove, update, use } from '../api/agents.js';
23
25
  import { models } from '../api/models.js';
24
26
  import { tools } from '../api/tools.js';
25
27
  import { useHost, useTarget } from '../host.js';
@@ -27,58 +29,106 @@ import { path } from '../route.js';
27
29
  import { useLoad } from './load.js';
28
30
  import { say } from './say.js';
29
31
  import { Add, Choice, Confirm, Empty, Failed, Field, Grid, Line, Mark, matches, mono, Part, Sheet, Soft, Tile, useCount, Visitor } from './ui.js';
32
+ /** How many cards Browse draws at a time. */
33
+ const PAGE = 60;
34
+ const ANY = 'All';
35
+ /** Categories as Browse offers them: the default agents' and presets' first, as listed, then the rest by how many each holds. */
36
+ export function kinds(first, rest) {
37
+ const lead = [...new Set(first.map((c) => c.category))];
38
+ const count = new Map();
39
+ for (const c of rest)
40
+ if (!lead.includes(c.category))
41
+ count.set(c.category, (count.get(c.category) ?? 0) + 1);
42
+ return [ANY, ...lead, ...[...count.keys()].sort((a, b) => count.get(b) - count.get(a) || a.localeCompare(b))];
43
+ }
30
44
  const money = (m) => `$${(m / 1_000_000).toFixed(m % 10_000 ? 4 : 2).replace(/\.?0+$/, '')}`;
31
45
  export function Agents({ view, q, adding, onAdding, onView, onCount }) {
32
46
  const host = useHost();
33
47
  const t = useTarget();
34
48
  const signed = Boolean(host.person);
49
+ const browsing = view === 'discover';
35
50
  const mine = useLoad(signed ? () => agents(t) : null, [], [t, signed]);
36
- const offered = useLoad(view === 'discover' ? () => presets(t) : null, [], [t, view]);
51
+ const offered = useLoad(browsing ? () => presets(t) : null, [], [t, view]);
52
+ const figures = useLoad(browsing && host.personas ? host.personas : null, [], [view]);
37
53
  useCount(mine.value.length, onCount);
38
- const [start, setStart] = useState(null);
39
54
  const [note, setNote] = useState('');
40
- const [hiring, setHiring] = useState(false);
41
- const cast = host.cast ?? [];
42
- const lacking = missing(cast, mine.value);
43
- // The host's cast, made in one press: each member the org has no agent for.
44
- const addCast = async () => {
45
- setHiring(true);
55
+ const [kind, setKind] = useState(ANY);
56
+ const [more, setMore] = useState(PAGE);
57
+ const [using, setUsing] = useState(null);
58
+ const [making, setMaking] = useState(null);
59
+ useEffect(() => setMore(PAGE), [q, kind]);
60
+ const opened = (a) => host.go(path({ kind: 'agent', name: a.name }));
61
+ const take = async (c) => {
62
+ setUsing(c);
46
63
  setNote('');
47
- const { made, error } = await hire(t, cast, mine.value);
48
- setHiring(false);
49
- mine.reload();
50
- setNote(error ? `${made.length ? `Added ${made.length}. ` : ''}${say(error)}` : `Added ${made.length} default ${made.length === 1 ? 'agent' : 'agents'}`);
51
- };
52
- const castButton = signed && lacking.length ? (_jsxs(Button, { size: "sm", variant: "outline", disabled: hiring, onPress: () => void addCast(), children: [_jsx(Users, { size: 14 }), " ", hiring ? 'Adding…' : lacking.length === cast.length ? 'Add default agents' : `Add ${lacking.length} default ${lacking.length === 1 ? 'agent' : 'agents'}`] })) : null;
53
- const closeNew = () => {
54
- setStart(null);
55
- onAdding(false);
64
+ try {
65
+ opened(await use(t, c, window.location.href));
66
+ }
67
+ catch (e) {
68
+ setNote(say(e));
69
+ }
70
+ finally {
71
+ setUsing(null);
72
+ }
56
73
  };
57
- const editor = adding || start ? (_jsx(Editor, { start: start ?? EMPTY, onClose: closeNew, onSaved: (a) => {
58
- closeNew();
74
+ const editor = adding ? (_jsx(Editor, { onClose: () => onAdding(false), onSaved: (a) => {
75
+ onAdding(false);
59
76
  onView('yours');
60
77
  mine.reload();
61
78
  setNote(`${a.name} is saved`);
62
79
  } })) : null;
63
- if (view === 'discover') {
64
- const names = new Set(mine.value.map((a) => a.name));
65
- const shown = offered.value.filter((p) => matches(q, p.id, p.title, p.prompt));
66
- return (_jsxs(YStack, { gap: "$3", children: [_jsx(Line, { children: note }), _jsx(Part, { title: "Start from a preset", detail: "A ready agent from Hanzo. You can change it before you save it.", children: offered.error && !offered.value.length ? (_jsx(Failed, { error: offered.error, onRetry: offered.reload })) : offered.loading && !offered.value.length ? (_jsx(Soft, { children: "Loading presets\u2026" })) : !shown.length ? (_jsx(Soft, { children: offered.value.length ? `No preset matches “${q.trim()}”.` : 'There are no presets yet. Start from a blank agent with New agent.' })) : (_jsx(Grid, { label: "Presets", children: shown.map((p) => (_jsx(Tile, { title: p.title || p.id, detail: p.prompt, meta: `Preset ${p.id}`, mark: _jsx(Mark, { name: p.title || p.id, icon: _jsx(Bot, { size: 15 }) }), action: signed ? _jsx(Add, { name: p.title || p.id, label: "Use", added: names.has(p.id), onPress: () => setStart(fromPreset(p)) }) : undefined }, p.id))) })) }), editor] }));
80
+ if (browsing) {
81
+ const first = [...(host.cast ?? []), ...offered.value.map(fromPreset)];
82
+ const all = [...first, ...figures.value];
83
+ const shown = all.filter((c) => (kind === ANY || c.category === kind) && matches(q, c.title, c.name, c.description, c.category));
84
+ const failed = figures.error ?? offered.error;
85
+ return (_jsxs(YStack, { gap: "$3", children: [signed ? (_jsxs(XStack, { gap: "$2", flexWrap: "wrap", children: [_jsx(Button, { size: "sm", variant: "outline", onPress: () => onAdding(true), children: "Start from scratch" }), _jsx(Button, { size: "sm", variant: "outline", onPress: () => setMaking('self'), children: "Clone yourself" }), _jsx(Button, { size: "sm", variant: "outline", onPress: () => setMaking('person'), children: "Create from a person" })] })) : null, _jsx(XStack, { overflowX: "auto", overflowY: "hidden", pb: "$1", children: _jsx(Choice, { label: "Category", value: kind, options: kinds(first, figures.value).map((id) => ({ id, label: id })), onChange: setKind }) }), _jsx(Line, { children: note }), shown.length ? (_jsx(Grid, { label: "Agents to start from", children: shown.slice(0, more).map((c) => (_jsx(Tile, { title: c.title, detail: c.description === c.title || c.description === c.category ? undefined : c.description, meta: c.category, mark: _jsx(Mark, { name: c.title, size: 40, logo: c.avatar }), action: signed ? _jsx(Add, { name: c.title, label: "Use", added: false, busy: using !== null, onPress: () => void take(c) }) : undefined }, `${c.category}/${c.name}/${c.title}`))) })) : figures.loading || offered.loading ? null : (_jsx(Soft, { children: `No agent matches “${q.trim()}”.` })), shown.length > more ? (_jsx(XStack, { children: _jsx(Button, { size: "sm", variant: "ghost", onPress: () => setMore(more + PAGE), children: "More" }) })) : null, failed ? _jsx(Failed, { error: failed, onRetry: figures.error ? figures.reload : offered.reload }) : figures.loading || offered.loading ? _jsx(Soft, { children: "Loading\u2026" }) : null, editor, making ? _jsx(Person, { self: making === 'self', onClose: () => setMaking(null), onMade: opened }) : null] }));
67
86
  }
68
87
  if (!signed) {
69
88
  return _jsx(Visitor, { children: "Sign in to see your agents." });
70
89
  }
71
90
  const shown = mine.value.filter((a) => matches(q, a.name, a.description, a.model));
72
- return (_jsxs(YStack, { gap: "$3", children: [_jsx(Line, { children: note }), mine.error && !mine.value.length ? (_jsx(Failed, { error: mine.error, onRetry: mine.reload })) : mine.loading && !mine.value.length ? (_jsx(Soft, { children: "Loading your agents\u2026" })) : !mine.value.length ? (_jsxs(Empty, { title: "No agents yet", detail: cast.length ? 'Add the default team, start from a preset, or give a new one a model, instructions and tools.' : 'Start from a preset, or give a new one a model, instructions and tools.', children: [castButton, _jsx(Button, { size: "sm", variant: "outline", onPress: () => onView('discover'), children: "Browse presets" }), _jsx(Button, { size: "sm", onPress: () => onAdding(true), children: "New agent" })] })) : !shown.length ? (_jsx(Soft, { children: `No agent of yours matches “${q.trim()}”.` })) : (_jsxs(_Fragment, { children: [castButton ? _jsx(XStack, { children: castButton }) : null, _jsx(Grid, { label: "Your agents", children: shown.map((a) => (_jsx(Tile, { title: a.name, detail: a.description || 'No description.', meta: [a.model, `${a.runs} ${a.runs === 1 ? 'run' : 'runs'}`, reach(a), a.cap ? `${money(a.spent)} of ${money(a.cap)} this ${a.period || 'period'}` : ''].filter(Boolean).join(' · '), mark: _jsx(Face, { agent: a, cast: cast }), onOpen: () => host.go(path({ kind: 'agent', name: a.name })) }, a.id || a.name))) })] })), editor] }));
91
+ return (_jsxs(YStack, { gap: "$3", children: [_jsx(Line, { children: note }), mine.error && !mine.value.length ? (_jsx(Failed, { error: mine.error, onRetry: mine.reload })) : mine.loading && !mine.value.length ? (_jsx(Soft, { children: "Loading your agents\u2026" })) : !mine.value.length ? (_jsxs(Empty, { title: "No agents yet", detail: "", children: [_jsx(Button, { size: "sm", variant: "outline", onPress: () => onView('discover'), children: "Browse agents" }), _jsx(Button, { size: "sm", onPress: () => onAdding(true), children: "New agent" })] })) : !shown.length ? (_jsx(Soft, { children: `No agent of yours matches “${q.trim()}”.` })) : (_jsx(Grid, { label: "Your agents", children: shown.map((a) => (_jsx(Tile, { title: a.name, detail: a.description || 'No description.', meta: [a.model, `${a.runs} ${a.runs === 1 ? 'run' : 'runs'}`, reach(a), a.cap ? `${money(a.spent)} of ${money(a.cap)} this ${a.period || 'period'}` : ''].filter(Boolean).join(' · '), mark: _jsx(Face, { agent: a, cast: host.cast ?? [] }), onOpen: () => opened(a) }, a.id || a.name))) })), editor] }));
92
+ }
93
+ /** Clone yourself, or anyone named: a name, who they are, and writing of theirs, pasted or uploaded. */
94
+ function Person({ self, onClose, onMade }) {
95
+ const host = useHost();
96
+ const t = useTarget();
97
+ const me = self ? host.person : null;
98
+ const [name, setName] = useState(me?.name ?? '');
99
+ const [about, setAbout] = useState('');
100
+ const [writing, setWriting] = useState('');
101
+ const [working, setWorking] = useState(false);
102
+ const [note, setNote] = useState('');
103
+ const file = useRef(null);
104
+ const read = async (files) => {
105
+ const texts = await Promise.all([...(files ?? [])].map((f) => f.text()));
106
+ setWriting((w) => [w.trim(), ...texts.map((x) => x.trim())].filter(Boolean).join('\n\n'));
107
+ };
108
+ const save = async () => {
109
+ setWorking(true);
110
+ setNote('');
111
+ try {
112
+ onMade(await use(t, person(name, about, writing, me?.avatar), window.location.href));
113
+ }
114
+ catch (e) {
115
+ setNote(say(e));
116
+ setWorking(false);
117
+ }
118
+ };
119
+ return (_jsxs(Sheet, { title: self ? 'Clone yourself' : 'Create from a person', open: true, onOpenChange: (o) => !o && onClose(), children: [_jsx(Field, { label: "Name", children: _jsx(Input, { value: name, onChangeText: setName, "aria-label": "Name" }) }), _jsx(Field, { label: "Description", children: _jsx(Textarea, { value: about, onChangeText: setAbout, "aria-label": "Description", rows: 3 }) }), _jsx(Field, { label: "Writing", children: _jsx(Textarea, { value: writing, onChangeText: setWriting, "aria-label": "Writing", rows: 6 }) }), _jsxs(XStack, { children: [_jsxs(Button, { size: "sm", variant: "ghost", onPress: () => file.current?.click(), children: [_jsx(Upload, { size: 14 }), " Upload"] }), _jsx("input", { ref: file, type: "file", multiple: true, accept: ".txt,.md,.markdown,.csv,.json,.html,text/*", hidden: true, "aria-label": "Upload writing", onChange: (e) => {
120
+ void read(e.currentTarget.files);
121
+ e.currentTarget.value = '';
122
+ } })] }), _jsx(Line, { children: note }), _jsxs(XStack, { gap: "$2", items: "center", children: [_jsx(XStack, { flex: 1 }), _jsx(Button, { size: "sm", variant: "ghost", onPress: onClose, children: "Cancel" }), _jsx(Button, { size: "sm", disabled: working || !name.trim(), onPress: () => void save(), children: "Create" })] })] }));
73
123
  }
74
124
  function reach(a) {
75
125
  if (a.tools.includes(ALL))
76
126
  return 'every tool';
77
127
  return a.tools.length === 1 ? '1 tool' : `${a.tools.length} tools`;
78
128
  }
79
- /** An agent's face: the cast member's picture, its emoji, or its initial. */
129
+ /** An agent's face: its picture, the default agent's of its name, its emoji, or its initial. */
80
130
  function Face({ agent: a, cast, size = 32 }) {
81
- const avatar = cast.find((c) => c.name.toLowerCase() === a.name.toLowerCase())?.avatar;
131
+ const avatar = a.avatar || cast.find((c) => c.name.toLowerCase() === a.name.toLowerCase())?.avatar;
82
132
  return _jsx(Mark, { name: a.name, size: size, logo: avatar, icon: a.emoji ? _jsx(SizableText, { size: size > 40 ? '$8' : '$4', children: a.emoji }) : undefined });
83
133
  }
84
134
  /** One agent's page: who it is, what it runs on and reads, what it may call and spend, and what it has done. */
@@ -95,12 +145,12 @@ export function Profile({ name }) {
95
145
  }, onDeleted: back })) : null] }));
96
146
  }
97
147
  /** Write a new agent, or change one; an existing agent is read whole first, for its instructions. */
98
- function Editor({ agent, start, onClose, onSaved, onDeleted, }) {
148
+ function Editor({ agent, onClose, onSaved, onDeleted, }) {
99
149
  const t = useTarget();
100
150
  const full = useLoad(agent ? () => one(t, agent.id || agent.name) : null, null, [t, agent?.id]);
101
151
  const catalog = useLoad(() => models(t), [], [t]);
102
152
  const on = useLoad(() => tools(t, { activated: true }), [], [t]);
103
- const [d, setD] = useState(start ?? EMPTY);
153
+ const [d, setD] = useState(EMPTY);
104
154
  const [ready, setReady] = useState(!agent);
105
155
  const [working, setWorking] = useState(false);
106
156
  const [note, setNote] = useState('');
@@ -29,11 +29,9 @@ const LABEL = {
29
29
  plugins: { title: 'Plugins', says: 'Connectors you build in TypeScript.', add: 'Build plugin', find: 'Search plugins', icon: _jsx(Puzzle, { size: 15 }) },
30
30
  agents: { title: 'Agents', says: 'Agents with their own model, instructions and budget.', add: 'New agent', find: 'Search agents', icon: _jsx(Bot, { size: 15 }) },
31
31
  };
32
- /** Where a tab opens: an org's own agents (and the cast to add) for Agents; Browse for the rest, what there is to add being the page. */
33
- const opens = (tab) => (tab === 'agents' ? 'yours' : 'discover');
34
32
  export function Customize({ tab, view: first }) {
35
33
  const host = useHost();
36
- const [view, setView] = useState(first ?? opens(tab));
34
+ const [view, setView] = useState(first ?? 'discover');
37
35
  const [q, setQ] = useState('');
38
36
  const [adding, setAdding] = useState(false);
39
37
  const [count, setCount] = useState(0);
@@ -41,7 +39,7 @@ export function Customize({ tab, view: first }) {
41
39
  const pick = (next) => {
42
40
  setQ('');
43
41
  setAdding(false);
44
- setView(opens(next));
42
+ setView('discover');
45
43
  setCount(0);
46
44
  host.go(path({ kind: 'customize', tab: next }));
47
45
  };
package/lib/host.d.ts CHANGED
@@ -107,8 +107,10 @@ export interface Host {
107
107
  browser?: Browser;
108
108
  /** The host's sidebar lists Settings' sections and Customize's tabs, so the pages draw neither. */
109
109
  sections?: boolean;
110
- /** The default agents an org may add in one press (Customize › Agents). Absent, none are offered. */
110
+ /** The default agents Customize › Agents offers to start from. Absent, none. */
111
111
  cast?: readonly Cast[];
112
+ /** Every persona Customize › Agents offers to start from, read when Browse opens. Absent, none. */
113
+ personas?: () => Promise<readonly Cast[]>;
112
114
  }
113
115
  export declare function HostProvider({ host, children }: {
114
116
  host: Host;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hanzo/build",
3
- "version": "0.2.79",
3
+ "version": "0.2.80",
4
4
  "type": "module",
5
5
  "license": "Apache-2.0 OR MIT",
6
6
  "description": "Hanzo Build — say what you want, watch it built, deployed and running. The builder as a component, mounted by the Hanzo app at hanzo.ai.",