Show the Brain beside the AI chat and make schedules from chosen material

The Brain and AI page lists Brain material (failed documents hidden) with
search, open, delete and per-material or select-all ticks. New schedules use
only the ticked material, kept through topic edits and resumes; nothing ticked
means the whole Brain. With material ticked, the agent skips the content topic
question and names the material in the chat and plan.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Iqbal
2026-10-27 22:53:38 +07:00
parent ef4fb7d971
commit 3b56c27210
10 changed files with 298 additions and 102 deletions

View File

@@ -73,6 +73,11 @@ function languageOfAgent(agent: ExpressAgent): OutputLanguage | undefined {
}
export function ruleOf(agent: ExpressAgent): Rule {
const rule = baseRuleOf(agent)
return rule.type === 'content' && usesChosen(agent) ? { ...rule, chosen: true } : rule
}
function baseRuleOf(agent: ExpressAgent): Rule {
const topic = agent.name.split(' · ').slice(2).join(' · ')
if (agent.enabled_tools.includes('studio.create_video')) {
const daily = agent.run_schedule?.cadence === 'daily'
@@ -161,18 +166,23 @@ function goalText(rule: Rule): string {
return `Create new learning ${KIND_TEXT[rule.kind]}${about} from the L&D team's Brain, without repeating content that already exists.`
}
function instructionsFor(rule: Rule): string {
// A schedule made from chosen material says so in its brief. That keeps it to those documents:
// editing or resuming it attaches nothing else, while a whole-Brain schedule picks up new material.
const CHOSEN_SENTENCE = 'Use only the Brain material chosen for this schedule.'
const usesChosen = (agent: ExpressAgent) => !!agent.instructions?.includes(CHOSEN_SENTENCE)
function instructionsFor(rule: Rule, chosen = false): string {
const language = languageSentence((rule.type === 'content' && rule.language) || defaultLanguage())
const brief = rule.type === 'content' ? KIND_BRIEF[rule.kind] : undefined
return [rule.topic && `Focus on the topic: ${rule.topic}.`, brief, language].filter(Boolean).join(' ')
return [rule.topic && `Focus on the topic: ${rule.topic}.`, brief, chosen && CHOSEN_SENTENCE, language].filter(Boolean).join(' ')
}
export function configFor(rule: Rule, videoCredits = 0) {
export function configFor(rule: Rule, videoCredits = 0, chosen = false) {
const budget = budgetFor(rule, videoCredits)
const common = {
name: scheduleName(rule),
goal: goalText(rule),
instructions: instructionsFor(rule),
instructions: instructionsFor(rule, chosen),
timezone: TIMEZONE,
run_budget: budget.run,
monthly_budget: budget.monthly,
@@ -227,21 +237,23 @@ export function canSchedule(rule: Rule): rule is ResearchRule | ContentRule {
return SCHEDULABLE.has(rule.kind) && weeklyTarget(rule) <= 50
}
// The production corpus is the agent's own sources, so every ready Brain document is attached.
async function attachBrain(agentId: number) {
const docs = (await listDocuments()).filter(d => d.knowledge === 'ready')
for (const d of docs) {
await request(`${AGENTS}/${agentId}/knowledge-sources`, { method: 'PUT', json: { kind: 'document', source_id: d.docId } })
// The production corpus is the agent's own sources: the chosen documents, or every ready Brain document.
async function attachBrain(agentId: number, chosen?: number[]) {
const ids = chosen ?? (await listDocuments()).filter(d => d.knowledge === 'ready').map(d => d.docId)
for (const id of ids) {
await request(`${AGENTS}/${agentId}/knowledge-sources`, { method: 'PUT', json: { kind: 'document', source_id: id } })
.catch(() => undefined) // a document someone else cannot share is skipped, not fatal
}
return docs.length
return ids.length
}
export async function createSchedule(rule: Rule): Promise<ExpressAgent> {
/** With `chosen` document ids the schedule uses only those; without, the whole Brain. */
export async function createSchedule(rule: Rule, chosen?: number[]): Promise<ExpressAgent> {
const only = chosen?.length ? chosen : undefined
// A video schedule's run budget is today's price of one video, so it is fetched first.
const credits = rule.type === 'content' && rule.kind === 'video' ? (await videoQuote()).max_credits : 0
const agent = await request<ExpressAgent>(AGENTS, { method: 'POST', json: configFor(rule, credits) })
await attachBrain(agent.id)
const agent = await request<ExpressAgent>(AGENTS, { method: 'POST', json: configFor(rule, credits, !!only) })
await attachBrain(agent.id, only)
// Activating approves the production rules as they stand; until then nothing runs.
return request<ExpressAgent>(`${AGENTS}/${agent.id}/activate`, { method: 'POST' })
}
@@ -249,15 +261,17 @@ export async function createSchedule(rule: Rule): Promise<ExpressAgent> {
/** Changing a rule changes what was approved, so the agent is re-activated right after. */
export async function updateTopic(schedule: Schedule, topic: string): Promise<ExpressAgent> {
const rule = { ...schedule.rule, topic: topic.trim() }
const { name, goal, instructions } = configFor(rule)
const chosen = usesChosen(schedule.agent)
const { name, goal, instructions } = configFor(rule, 0, chosen)
await request(`${AGENTS}/${schedule.agent.id}`, { method: 'PATCH', json: { name, goal, instructions } })
await attachBrain(schedule.agent.id)
if (!chosen) await attachBrain(schedule.agent.id)
return request<ExpressAgent>(`${AGENTS}/${schedule.agent.id}/${schedule.active ? 'activate' : 'pause'}`, { method: 'POST' })
}
export const pauseSchedule = (id: number) => request<ExpressAgent>(`${AGENTS}/${id}/pause`, { method: 'POST' })
export async function resumeSchedule(id: number) {
await attachBrain(id)
export async function resumeSchedule(schedule: Schedule) {
const id = schedule.agent.id
if (!usesChosen(schedule.agent)) await attachBrain(id)
return request<ExpressAgent>(`${AGENTS}/${id}/activate`, { method: 'POST' })
}
export const archiveSchedule = (id: number) => request<ExpressAgent>(`${AGENTS}/${id}/archive`, { method: 'POST' })