feat(agents): manage AGENTS.md from the agent editor + clarify context-file roles

Add a friendly AGENTS.md section editor inside the edit-agent modal's
"高级" (Advanced) collapsible, alongside the existing additional-instructions
field. New AgentGuideEditor reuses MemorySection cards, parses the file into
## sections, and saves whole-file via the workspace API (no backend change).
Empty files offer a "create" scaffold flow.

Reword the collapsible to a generic "高级" and add a role-clarifying note that
fixes the boundary: factory identity goes in the form fields, evolving
memory/rules go in the context files. Also fix the additional-instructions
textarea so it fills the form width.

Closes matevip/mateclaw#290
This commit is contained in:
倪程伟 2026-06-08 10:45:47 +08:00 committed by matevip
parent c7ae406dcc
commit 4f11a444c1
4 changed files with 191 additions and 3 deletions

View File

@ -1221,7 +1221,8 @@ export default {
taglinePreview: 'Card preview:',
backstory: 'Backstory',
backstoryHint: 'Invisible to users, felt in every conversation. Their experience, beliefs, and working principles.',
extraInstructions: 'Advanced: Additional Instructions',
advanced: 'Advanced',
extraInstructions: 'Additional Instructions',
extraInstructionsHint: 'Optional. Use for output format, process checklists, or boundary rules.',
maxIterations: 'Max Iterations',
defaultThinkingLevel: 'Default Thinking Level',
@ -1235,6 +1236,16 @@ export default {
tagUndo: 'Undo',
enabled: 'Enabled',
},
guide: {
summary: 'Behavior Guide (AGENTS.md)',
desc: "This is the employee's behavior handbook (AGENTS.md): when to write memories, what goes in each file, and which memory tools are available. The Role/Goal/Backstory above are the employee's factory identity; evolving memories like PROFILE/MEMORY live in the Memory menu.",
empty: 'This employee has no behavior guide (AGENTS.md) yet.',
create: 'Create Behavior Guide',
saved: 'Saved',
addSection: 'Add Section',
addSectionPlaceholder: 'Section title',
scaffold: '## When to Write Memories\n\n(Describe when this employee should record to MEMORY.md / PROFILE.md.)\n\n## File Responsibilities\n\n(Describe what belongs in SOUL.md / PROFILE.md / MEMORY.md.)\n',
},
thinkingLevels: {
auto: 'Follow Model',
off: 'Off',

View File

@ -1113,7 +1113,8 @@ export default {
taglinePreview: '卡片预览:',
backstory: '背景Backstory',
backstoryHint: '用户看不见,但每一次对话都能感觉到。写他的经验、信念、做事原则。',
extraInstructions: '高级:补充指令',
advanced: '高级',
extraInstructions: '补充指令',
extraInstructionsHint: '可选。用于细化输出格式、流程清单或边界规则。',
maxIterations: '最大迭代次数',
defaultThinkingLevel: '默认思考深度',
@ -1127,6 +1128,16 @@ export default {
tagUndo: '撤销',
enabled: '启用',
},
guide: {
summary: '行为指南AGENTS.md',
desc: '这里是该员工的行为手册AGENTS.md规定何时写记忆、各文件放什么、可用的记忆工具。上方的岗位目标背景是员工的出厂身份PROFILEMEMORY 等可演化的记忆在「记忆」菜单管理。',
empty: '该员工还没有行为指南AGENTS.md。',
create: '创建行为指南',
saved: '已保存',
addSection: '新增小节',
addSectionPlaceholder: '小节标题',
scaffold: '## 何时写记忆\n\n说明这个员工在什么情况下应当记录到 MEMORY.md / PROFILE.md。\n\n## 文件职责\n\n说明 SOUL.md / PROFILE.md / MEMORY.md 各自该放什么内容。)\n',
},
thinkingLevels: {
auto: '跟随模型',
off: '关闭',

View File

@ -341,10 +341,16 @@
<div class="form-group full-width">
<details class="advanced-prompt">
<summary class="advanced-prompt__summary">
{{ t('agents.fields.extraInstructions') }}
{{ t('agents.fields.advanced') }}
</summary>
<label class="form-label">{{ t('agents.fields.extraInstructions') }}</label>
<textarea v-model="profileForm.extra" class="form-textarea" rows="4" :placeholder="t('agents.placeholders.extraInstructions')"></textarea>
<p class="form-hint">{{ t('agents.fields.extraInstructionsHint') }}</p>
<div v-if="editingAgent" class="advanced-guide">
<label class="form-label">{{ t('agents.guide.summary') }}</label>
<p class="form-hint">{{ t('agents.guide.desc') }}</p>
<AgentGuideEditor :agent-id="editingAgent.id" />
</div>
</details>
</div>
<div class="form-group">
@ -669,6 +675,7 @@ import type { Agent } from '@/types/index'
import SkillIcon from '@/components/common/SkillIcon.vue'
import SkillIconPicker from '@/components/common/SkillIconPicker.vue'
import LivePanel from '@/components/live/LivePanel.vue'
import AgentGuideEditor from './Agents/components/AgentGuideEditor.vue'
import {
emptyProfile,
parsePrompt,
@ -1957,6 +1964,8 @@ html.dark .seg-count.warn {
}
.advanced-prompt[open] > .advanced-prompt__summary { padding: 0 0 8px; border-bottom: 1px solid var(--mc-border-light); margin-bottom: 8px; }
.advanced-prompt[open] > .advanced-prompt__summary::before { transform: rotate(90deg); }
.advanced-prompt .form-textarea { width: 100%; box-sizing: border-box; }
.advanced-guide { margin-top: 16px; padding-top: 14px; border-top: 1px solid var(--mc-border-light); }
.modal-footer { display: flex; justify-content: flex-end; gap: 10px; padding: 16px 24px; border-top: 1px solid var(--mc-border-light); }
@media (max-width: 900px) {

View File

@ -0,0 +1,157 @@
<template>
<div class="guide-editor">
<div v-if="loading" class="guide-state"></div>
<!-- Empty state: AGENTS.md not present or blank -->
<div v-else-if="sections.length === 0" class="guide-empty">
<p class="guide-empty__text">{{ t('agents.guide.empty') }}</p>
<button class="guide-create-btn" :disabled="saving" @click="createGuide">
{{ t('agents.guide.create') }}
</button>
</div>
<!-- Section cards -->
<div v-else class="guide-sections">
<MemorySection
v-for="(sec, idx) in sections"
:key="idx"
:section="sec"
:save-handler="(body: string) => saveSection(idx, body)"
/>
<div class="guide-add">
<input
v-model="newHeading"
class="guide-add__input"
:placeholder="t('agents.guide.addSectionPlaceholder')"
@keyup.enter="addSection"
/>
<button class="guide-add__btn" :disabled="!newHeading.trim() || saving" @click="addSection">
+ {{ t('agents.guide.addSection') }}
</button>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useI18n } from 'vue-i18n'
import { agentContextApi } from '@/api/index'
import { mcToast } from '@/composables/useMcToast'
import MemorySection from '@/views/Memory/components/MemorySection.vue'
import type { MemorySectionData } from '@/views/Memory/components/types'
const props = defineProps<{ agentId: string | number }>()
const { t } = useI18n()
const FILE = 'AGENTS.md'
const loading = ref(true)
const saving = ref(false)
const sections = ref<MemorySectionData[]>([])
const newHeading = ref('')
onMounted(load)
async function load() {
loading.value = true
try {
const res: any = await agentContextApi.getFile(props.agentId, FILE)
sections.value = parseSections(res?.data?.content || '')
} catch {
// File may not exist yet treat as empty so the create flow shows.
sections.value = []
} finally {
loading.value = false
}
}
/** Split into `## ` sections; content before the first heading is a synthetic preamble. */
function parseSections(content: string): MemorySectionData[] {
if (!content.trim()) return []
const result: MemorySectionData[] = []
for (const part of content.split(/(?=^## )/m)) {
const match = part.match(/^## (.+)\n([\s\S]*)/)
if (match) {
result.push({ heading: match[1].trim(), body: match[2].trim(), userEdited: false })
} else if (part.trim() && result.length === 0) {
result.push({ heading: t('memory.memoryBrowser.header'), body: part.trim(), userEdited: false, synthetic: true })
}
}
return result
}
/** Re-serialize the local sections back into a whole AGENTS.md document. */
function serialize(secs: MemorySectionData[]): string {
return secs
.map(s => (s.synthetic ? s.body.trim() : `## ${s.heading}\n${s.body.trim()}`))
.filter(Boolean)
.join('\n\n') + '\n'
}
async function saveSection(idx: number, body: string) {
const next = sections.value.map((s, i) => (i === idx ? { ...s, body } : s))
await agentContextApi.saveFile(props.agentId, FILE, serialize(next))
mcToast.success(t('agents.guide.saved'))
await load()
}
async function createGuide() {
saving.value = true
try {
await agentContextApi.saveFile(props.agentId, FILE, t('agents.guide.scaffold'))
mcToast.success(t('agents.guide.saved'))
await load()
} catch (e: any) {
mcToast.error(e?.message || 'Save failed')
} finally {
saving.value = false
}
}
async function addSection() {
const heading = newHeading.value.trim()
if (!heading) return
saving.value = true
try {
const next = [...sections.value, { heading, body: '', userEdited: false }]
await agentContextApi.saveFile(props.agentId, FILE, serialize(next))
newHeading.value = ''
await load()
} catch (e: any) {
mcToast.error(e?.message || 'Save failed')
} finally {
saving.value = false
}
}
</script>
<style scoped>
.guide-editor { margin-top: 8px; }
.guide-state { font-size: 13px; color: var(--mc-text-tertiary); padding: 8px 0; }
.guide-empty {
display: flex; flex-direction: column; align-items: flex-start; gap: 10px;
padding: 14px 16px; border-radius: 12px; background: var(--mc-bg-sunken);
}
.guide-empty__text { margin: 0; font-size: 13px; color: var(--mc-text-secondary); }
.guide-create-btn {
padding: 6px 14px; border-radius: 8px; font-size: 12px; font-weight: 500;
background: var(--mc-primary); color: #fff; border: 1px solid var(--mc-primary);
cursor: pointer; transition: all 0.12s;
}
.guide-create-btn:disabled { opacity: 0.4; cursor: not-allowed; }
.guide-sections { display: flex; flex-direction: column; gap: 10px; }
.guide-add { display: flex; gap: 8px; align-items: center; }
.guide-add__input {
flex: 1; padding: 8px 12px; border: 1px solid var(--mc-border); border-radius: 8px;
background: var(--mc-bg-elevated); font-size: 13px; color: var(--mc-text-primary); outline: none;
}
.guide-add__input:focus { border-color: var(--mc-primary); }
.guide-add__btn {
padding: 8px 14px; border-radius: 8px; font-size: 12px; font-weight: 500;
background: transparent; color: var(--mc-text-secondary); border: 1px solid var(--mc-border);
cursor: pointer; white-space: nowrap; transition: all 0.12s;
}
.guide-add__btn:disabled { opacity: 0.4; cursor: not-allowed; }
.guide-add__btn:not(:disabled):hover { border-color: var(--mc-primary); color: var(--mc-primary); }
</style>