mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-23 00:53:38 +02:00
Reject prompt templates for new bundled agents
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
b779e43272
commit
484d37feb9
16 files changed
+85
-87
No files matched your search
@@ -66,8 +66,6 @@ export function buildClaudeLocalConfig(v: CreateConfigValues): Record<string, un
|
||||
const ac: Record<string, unknown> = {};
|
||||
if (v.cwd) ac.cwd = v.cwd;
|
||||
if (v.instructionsFilePath) ac.instructionsFilePath = v.instructionsFilePath;
|
||||
if (v.promptTemplate) ac.promptTemplate = v.promptTemplate;
|
||||
if (v.bootstrapPrompt) ac.bootstrapPromptTemplate = v.bootstrapPrompt;
|
||||
if (v.model) ac.model = v.model;
|
||||
if (v.thinkingEffort) ac.effort = v.thinkingEffort;
|
||||
if (v.chrome) ac.chrome = true;
|
||||
|
||||
@@ -70,8 +70,6 @@ export function buildCodexLocalConfig(v: CreateConfigValues): Record<string, unk
|
||||
const ac: Record<string, unknown> = {};
|
||||
if (v.cwd) ac.cwd = v.cwd;
|
||||
if (v.instructionsFilePath) ac.instructionsFilePath = v.instructionsFilePath;
|
||||
if (v.promptTemplate) ac.promptTemplate = v.promptTemplate;
|
||||
if (v.bootstrapPrompt) ac.bootstrapPromptTemplate = v.bootstrapPrompt;
|
||||
ac.model = v.model || DEFAULT_CODEX_LOCAL_MODEL;
|
||||
if (v.thinkingEffort) ac.modelReasoningEffort = v.thinkingEffort;
|
||||
ac.timeoutSec = 0;
|
||||
|
||||
@@ -61,8 +61,6 @@ export function buildCursorLocalConfig(v: CreateConfigValues): Record<string, un
|
||||
const ac: Record<string, unknown> = {};
|
||||
if (v.cwd) ac.cwd = v.cwd;
|
||||
if (v.instructionsFilePath) ac.instructionsFilePath = v.instructionsFilePath;
|
||||
if (v.promptTemplate) ac.promptTemplate = v.promptTemplate;
|
||||
if (v.bootstrapPrompt) ac.bootstrapPromptTemplate = v.bootstrapPrompt;
|
||||
ac.model = v.model || DEFAULT_CURSOR_LOCAL_MODEL;
|
||||
const mode = normalizeMode(v.thinkingEffort);
|
||||
if (mode) ac.mode = mode;
|
||||
|
||||
@@ -55,8 +55,6 @@ export function buildGeminiLocalConfig(v: CreateConfigValues): Record<string, un
|
||||
const ac: Record<string, unknown> = {};
|
||||
if (v.cwd) ac.cwd = v.cwd;
|
||||
if (v.instructionsFilePath) ac.instructionsFilePath = v.instructionsFilePath;
|
||||
if (v.promptTemplate) ac.promptTemplate = v.promptTemplate;
|
||||
if (v.bootstrapPrompt) ac.bootstrapPromptTemplate = v.bootstrapPrompt;
|
||||
ac.model = v.model || DEFAULT_GEMINI_LOCAL_MODEL;
|
||||
ac.timeoutSec = 0;
|
||||
ac.graceSec = 15;
|
||||
|
||||
@@ -54,8 +54,6 @@ export function buildOpenCodeLocalConfig(v: CreateConfigValues): Record<string,
|
||||
const ac: Record<string, unknown> = {};
|
||||
if (v.cwd) ac.cwd = v.cwd;
|
||||
if (v.instructionsFilePath) ac.instructionsFilePath = v.instructionsFilePath;
|
||||
if (v.promptTemplate) ac.promptTemplate = v.promptTemplate;
|
||||
if (v.bootstrapPrompt) ac.bootstrapPromptTemplate = v.bootstrapPrompt;
|
||||
if (v.model) ac.model = v.model;
|
||||
if (v.thinkingEffort) ac.variant = v.thinkingEffort;
|
||||
ac.dangerouslySkipPermissions = v.dangerouslySkipPermissions;
|
||||
|
||||
@@ -47,8 +47,6 @@ export function buildPiLocalConfig(v: CreateConfigValues): Record<string, unknow
|
||||
const ac: Record<string, unknown> = {};
|
||||
if (v.cwd) ac.cwd = v.cwd;
|
||||
if (v.instructionsFilePath) ac.instructionsFilePath = v.instructionsFilePath;
|
||||
if (v.promptTemplate) ac.promptTemplate = v.promptTemplate;
|
||||
if (v.bootstrapPrompt) ac.bootstrapPromptTemplate = v.bootstrapPrompt;
|
||||
if (v.model) ac.model = v.model;
|
||||
if (v.thinkingEffort) ac.thinking = v.thinkingEffort;
|
||||
|
||||
|
||||
@@ -26,6 +26,20 @@ describe("dynamic adapter type validation schemas", () => {
|
||||
).toThrow();
|
||||
});
|
||||
|
||||
it("accepts an explicit managed instructions bundle for new agents", () => {
|
||||
expect(
|
||||
createAgentSchema.parse({
|
||||
name: "Bundle Agent",
|
||||
adapterType: "codex_local",
|
||||
instructionsBundle: {
|
||||
files: {
|
||||
"AGENTS.md": "Use AGENTS.md.",
|
||||
},
|
||||
},
|
||||
}).instructionsBundle?.files["AGENTS.md"],
|
||||
).toBe("Use AGENTS.md.");
|
||||
});
|
||||
|
||||
it("accepts external adapter types in invite acceptance schema", () => {
|
||||
expect(
|
||||
acceptInviteSchema.parse({
|
||||
|
||||
@@ -44,6 +44,13 @@ const adapterConfigSchema = z.record(z.unknown()).superRefine((value, ctx) => {
|
||||
}
|
||||
});
|
||||
|
||||
export const createAgentInstructionsBundleSchema = z.object({
|
||||
entryFile: z.string().trim().min(1).optional(),
|
||||
files: z.record(z.string()).refine((files) => Object.keys(files).length > 0, {
|
||||
message: "instructionsBundle.files must contain at least one file",
|
||||
}),
|
||||
});
|
||||
|
||||
export const createAgentSchema = z.object({
|
||||
name: z.string().min(1),
|
||||
role: z.enum(AGENT_ROLES).optional().default("general"),
|
||||
@@ -54,6 +61,7 @@ export const createAgentSchema = z.object({
|
||||
desiredSkills: z.array(z.string().min(1)).optional(),
|
||||
adapterType: agentAdapterTypeSchema,
|
||||
adapterConfig: adapterConfigSchema.optional().default({}),
|
||||
instructionsBundle: createAgentInstructionsBundleSchema.optional(),
|
||||
runtimeConfig: z.record(z.unknown()).optional().default({}),
|
||||
defaultEnvironmentId: z.string().uuid().optional().nullable(),
|
||||
budgetMonthlyCents: z.number().int().nonnegative().optional().default(0),
|
||||
|
||||
@@ -481,8 +481,11 @@ describe.sequential("agent skill routes", () => {
|
||||
name: "QA Agent",
|
||||
role: "engineer",
|
||||
adapterType: "claude_local",
|
||||
adapterConfig: {
|
||||
promptTemplate: "You are QA.",
|
||||
adapterConfig: {},
|
||||
instructionsBundle: {
|
||||
files: {
|
||||
"AGENTS.md": "You are QA.",
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -504,7 +507,7 @@ describe.sequential("agent skill routes", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("clears legacy prompt templates when directly created local agents already provide instructions", async () => {
|
||||
it("rejects legacy prompt templates for directly created local agents", async () => {
|
||||
const res = await requestApp(await createApp(), (baseUrl) => request(baseUrl)
|
||||
.post("/api/companies/company-1/agents")
|
||||
.send({
|
||||
@@ -518,21 +521,10 @@ describe.sequential("agent skill routes", () => {
|
||||
},
|
||||
}));
|
||||
|
||||
expect([200, 201], JSON.stringify(res.body)).toContain(res.status);
|
||||
expect(res.status, JSON.stringify(res.body)).toBe(422);
|
||||
expect(res.body.error).toContain("New agents must use instructionsBundle/AGENTS.md");
|
||||
expect(mockAgentService.create).not.toHaveBeenCalled();
|
||||
expect(mockAgentInstructionsService.materializeManagedBundle).not.toHaveBeenCalled();
|
||||
expect(mockAgentService.update).toHaveBeenCalledWith(
|
||||
"11111111-1111-4111-8111-111111111111",
|
||||
expect.objectContaining({
|
||||
adapterConfig: expect.objectContaining({
|
||||
instructionsFilePath: "/tmp/existing/AGENTS.md",
|
||||
}),
|
||||
}),
|
||||
);
|
||||
const updateInput = mockAgentService.update.mock.calls.at(-1)?.[1] as
|
||||
| { adapterConfig?: Record<string, unknown> }
|
||||
| undefined;
|
||||
expect(updateInput?.adapterConfig?.promptTemplate).toBeUndefined();
|
||||
expect(updateInput?.adapterConfig?.bootstrapPromptTemplate).toBeUndefined();
|
||||
});
|
||||
|
||||
it("materializes the bundled CEO instruction set for default CEO agents", async () => {
|
||||
@@ -683,8 +675,11 @@ describe.sequential("agent skill routes", () => {
|
||||
name: "QA Agent",
|
||||
role: "engineer",
|
||||
adapterType: "claude_local",
|
||||
adapterConfig: {
|
||||
promptTemplate: "You are QA.",
|
||||
adapterConfig: {},
|
||||
instructionsBundle: {
|
||||
files: {
|
||||
"AGENTS.md": "You are QA.",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -707,7 +702,7 @@ describe.sequential("agent skill routes", () => {
|
||||
expect(approvalInput?.payload?.adapterConfig?.promptTemplate).toBeUndefined();
|
||||
});
|
||||
|
||||
it("clears legacy prompt templates from hire approval payloads that already provide instructions", async () => {
|
||||
it("rejects legacy prompt templates for hire approval payloads", async () => {
|
||||
const res = await request(await createApp(createDb(true)))
|
||||
.post("/api/companies/company-1/agent-hires")
|
||||
.send({
|
||||
@@ -721,22 +716,9 @@ describe.sequential("agent skill routes", () => {
|
||||
},
|
||||
});
|
||||
|
||||
expect(res.status, JSON.stringify(res.body)).toBe(201);
|
||||
expect(res.status, JSON.stringify(res.body)).toBe(422);
|
||||
expect(res.body.error).toContain("New agents must use instructionsBundle/AGENTS.md");
|
||||
expect(mockAgentService.create).not.toHaveBeenCalled();
|
||||
expect(mockAgentInstructionsService.materializeManagedBundle).not.toHaveBeenCalled();
|
||||
expect(mockApprovalService.create).toHaveBeenCalledWith(
|
||||
"company-1",
|
||||
expect.objectContaining({
|
||||
payload: expect.objectContaining({
|
||||
adapterConfig: expect.objectContaining({
|
||||
instructionsFilePath: "/tmp/existing/AGENTS.md",
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
const approvalInput = mockApprovalService.create.mock.calls.at(-1)?.[1] as
|
||||
| { payload?: { adapterConfig?: Record<string, unknown> } }
|
||||
| undefined;
|
||||
expect(approvalInput?.payload?.adapterConfig?.promptTemplate).toBeUndefined();
|
||||
expect(approvalInput?.payload?.adapterConfig?.bootstrapPromptTemplate).toBeUndefined();
|
||||
});
|
||||
});
|
||||
+32
-10
@@ -690,7 +690,10 @@ export function agentRoutes(
|
||||
role: string;
|
||||
adapterType: string;
|
||||
adapterConfig: unknown;
|
||||
}>(agent: T): Promise<T> {
|
||||
}>(
|
||||
agent: T,
|
||||
input?: { files: Record<string, string>; entryFile?: string },
|
||||
): Promise<T> {
|
||||
if (!adapterSupportsInstructionsBundle(agent.adapterType)) {
|
||||
return agent;
|
||||
}
|
||||
@@ -715,24 +718,33 @@ export function agentRoutes(
|
||||
return (updated as T | null) ?? { ...agent, adapterConfig: nextAdapterConfig };
|
||||
}
|
||||
|
||||
const promptTemplate = typeof adapterConfig.promptTemplate === "string"
|
||||
? adapterConfig.promptTemplate
|
||||
: "";
|
||||
const files = promptTemplate.trim().length === 0
|
||||
? await loadDefaultAgentInstructionsBundle(resolveDefaultAgentInstructionsBundleRole(agent.role))
|
||||
: { "AGENTS.md": promptTemplate };
|
||||
const files = input?.files
|
||||
?? await loadDefaultAgentInstructionsBundle(resolveDefaultAgentInstructionsBundleRole(agent.role));
|
||||
const materialized = await instructions.materializeManagedBundle(
|
||||
agent,
|
||||
files,
|
||||
{ entryFile: "AGENTS.md", replaceExisting: false },
|
||||
{ entryFile: input?.entryFile ?? "AGENTS.md", replaceExisting: false },
|
||||
);
|
||||
const nextAdapterConfig = { ...materialized.adapterConfig };
|
||||
delete nextAdapterConfig.promptTemplate;
|
||||
delete nextAdapterConfig.bootstrapPromptTemplate;
|
||||
|
||||
const updated = await svc.update(agent.id, { adapterConfig: nextAdapterConfig });
|
||||
return (updated as T | null) ?? { ...agent, adapterConfig: nextAdapterConfig };
|
||||
}
|
||||
|
||||
function assertNoNewAgentLegacyPromptTemplate(adapterType: string, adapterConfig: Record<string, unknown>) {
|
||||
if (!adapterSupportsInstructionsBundle(adapterType)) return;
|
||||
if (
|
||||
Object.prototype.hasOwnProperty.call(adapterConfig, "promptTemplate")
|
||||
|| Object.prototype.hasOwnProperty.call(adapterConfig, "bootstrapPromptTemplate")
|
||||
) {
|
||||
throw unprocessable(
|
||||
"New agents must use instructionsBundle/AGENTS.md instead of adapterConfig.promptTemplate or bootstrapPromptTemplate",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function assertCanManageInstructionsPath(req: Request, targetAgent: { id: string; companyId: string }) {
|
||||
assertCompanyAccess(req, targetAgent.companyId);
|
||||
if (req.actor.type !== "board") {
|
||||
@@ -1474,11 +1486,16 @@ export function agentRoutes(
|
||||
const sourceIssueIds = parseSourceIssueIds(req.body);
|
||||
const {
|
||||
desiredSkills: requestedDesiredSkills,
|
||||
instructionsBundle,
|
||||
sourceIssueId: _sourceIssueId,
|
||||
sourceIssueIds: _sourceIssueIds,
|
||||
...hireInput
|
||||
} = req.body;
|
||||
hireInput.adapterType = assertKnownAdapterType(hireInput.adapterType);
|
||||
assertNoNewAgentLegacyPromptTemplate(
|
||||
hireInput.adapterType,
|
||||
(hireInput.adapterConfig ?? {}) as Record<string, unknown>,
|
||||
);
|
||||
assertNoAgentHostWorkspaceCommandMutation(
|
||||
req,
|
||||
collectAgentAdapterWorkspaceCommandPaths(hireInput.adapterConfig),
|
||||
@@ -1531,7 +1548,7 @@ export function agentRoutes(
|
||||
spentMonthlyCents: 0,
|
||||
lastHeartbeatAt: null,
|
||||
});
|
||||
const agent = await materializeDefaultInstructionsBundleForNewAgent(createdAgent);
|
||||
const agent = await materializeDefaultInstructionsBundleForNewAgent(createdAgent, instructionsBundle);
|
||||
|
||||
let approval: Awaited<ReturnType<typeof approvalsSvc.getById>> | null = null;
|
||||
const actor = getActorInfo(req);
|
||||
@@ -1661,9 +1678,14 @@ export function agentRoutes(
|
||||
|
||||
const {
|
||||
desiredSkills: requestedDesiredSkills,
|
||||
instructionsBundle,
|
||||
...createInput
|
||||
} = req.body;
|
||||
createInput.adapterType = assertKnownAdapterType(createInput.adapterType);
|
||||
assertNoNewAgentLegacyPromptTemplate(
|
||||
createInput.adapterType,
|
||||
(createInput.adapterConfig ?? {}) as Record<string, unknown>,
|
||||
);
|
||||
assertNoAgentHostWorkspaceCommandMutation(
|
||||
req,
|
||||
collectAgentAdapterWorkspaceCommandPaths(createInput.adapterConfig),
|
||||
@@ -1706,7 +1728,7 @@ export function agentRoutes(
|
||||
spentMonthlyCents: 0,
|
||||
lastHeartbeatAt: null,
|
||||
});
|
||||
const agent = await materializeDefaultInstructionsBundleForNewAgent(createdAgent);
|
||||
const agent = await materializeDefaultInstructionsBundleForNewAgent(createdAgent, instructionsBundle);
|
||||
|
||||
const actor = getActorInfo(req);
|
||||
await logActivity(db, {
|
||||
|
||||
@@ -85,7 +85,7 @@ curl -sS "$PAPERCLIP_API_URL/llms/agent-icons.txt" \
|
||||
- capabilities
|
||||
- managed instructions bundle (`AGENTS.md`) for adapters that support it; avoid durable `promptTemplate` config
|
||||
- for coding or execution agents, include the Paperclip execution contract: start actionable work in the same heartbeat; do not stop at a plan unless planning was requested; leave durable progress with a clear next action; use child issues for long or parallel delegated work instead of polling; mark blocked work with owner/action; respect budget, pause/cancel, approval gates, and company boundaries
|
||||
- instruction text such as `AGENTS.md` built from step 4; for local managed-bundle adapters, agent-authenticated hire requests may send the adapted `AGENTS.md` in `adapterConfig.promptTemplate` only as a creation-time transport because the server materializes it into managed `AGENTS.md` and removes the legacy field. Do not set `promptTemplate` when you are already setting an instructions bundle/path.
|
||||
- instruction text such as `AGENTS.md` built from step 4; for local managed-bundle adapters, send this as top-level `instructionsBundle.files["AGENTS.md"]`. Do not set `adapterConfig.promptTemplate` or `bootstrapPromptTemplate` for new agents.
|
||||
- source issue linkage (`sourceIssueId` or `sourceIssueIds`) when this hire came from an issue
|
||||
|
||||
### 7. Review the draft against the quality checklist
|
||||
@@ -109,6 +109,7 @@ curl -sS -X POST "$PAPERCLIP_API_URL/api/companies/$PAPERCLIP_COMPANY_ID/agent-h
|
||||
"desiredSkills": ["vercel-labs/agent-browser/agent-browser"],
|
||||
"adapterType": "codex_local",
|
||||
"adapterConfig": {"cwd": "/abs/path/to/repo", "model": "o4-mini"},
|
||||
"instructionsBundle": {"files": {"AGENTS.md": "You are the CTO..."}},
|
||||
"runtimeConfig": {"heartbeat": {"enabled": false, "wakeOnDemand": true}},
|
||||
"sourceIssueId": "<issue-id>"
|
||||
}'
|
||||
|
||||
@@ -41,7 +41,7 @@ If you are hiring a role that is not in this index, do not force a fit. Use the
|
||||
## How to apply an exact template
|
||||
|
||||
1. Open the matching reference in `references/agents/`.
|
||||
2. Copy that template into the new agent's instruction bundle (usually `AGENTS.md`). For agent-authenticated hire requests using local managed-bundle adapters, `adapterConfig.promptTemplate` is only a creation-time transport for the adapted template; Paperclip materializes it into managed `AGENTS.md` and removes the legacy field. Do not combine `promptTemplate` with an explicit instructions bundle/path.
|
||||
2. Copy that template into the new agent's instruction bundle (usually `AGENTS.md`). For hire requests using local managed-bundle adapters, send the adapted template as top-level `instructionsBundle.files["AGENTS.md"]`. Do not put new-agent instructions in `adapterConfig.promptTemplate`.
|
||||
3. Replace placeholders like `{{companyName}}`, `{{managerTitle}}`, `{{issuePrefix}}`, and URLs.
|
||||
4. Remove tools or workflows the target adapter cannot use.
|
||||
5. Keep the Paperclip heartbeat requirement and the task-comment requirement.
|
||||
|
||||
@@ -42,8 +42,13 @@ Request body matches agent create shape:
|
||||
"adapterType": "claude_local",
|
||||
"adapterConfig": {
|
||||
"cwd": "/absolute/path",
|
||||
"model": "claude-sonnet-4-5-20250929",
|
||||
"promptTemplate": "You are CTO..."
|
||||
"model": "claude-sonnet-4-5-20250929"
|
||||
},
|
||||
"instructionsBundle": {
|
||||
"entryFile": "AGENTS.md",
|
||||
"files": {
|
||||
"AGENTS.md": "You are CTO..."
|
||||
}
|
||||
},
|
||||
"runtimeConfig": {
|
||||
"heartbeat": {
|
||||
|
||||
@@ -118,7 +118,7 @@ How the agent verifies its own work before marking an issue done or handing it t
|
||||
- **Over-generic prompts.** "Be helpful, be thorough, be correct" is worthless — the next agent drafts a better version by reading the template you adapted from. Write role-specific guidance only.
|
||||
- **Lens dumping.** Copying every lens from an expert template into an unrelated role adds noise and burns context. Five well-chosen lenses beat fifteen irrelevant ones.
|
||||
- **Permission sprawl.** Do not grant write access, admin endpoints, or broad skill sets "just in case." Grant exactly what the role needs.
|
||||
- **Secrets in adapter config.** Do not embed long-lived tokens, API keys, or private URLs in `adapterConfig` or `promptTemplate` when environment injection or a scoped skill can carry the capability instead.
|
||||
- **Secrets in agent config.** Do not embed long-lived tokens, API keys, or private URLs in `adapterConfig`, `instructionsBundle`, or legacy prompt fields when environment injection or a scoped skill can carry the capability instead.
|
||||
- **Silent timer heartbeats.** A timer heartbeat burns budget every interval. If the role has no scheduled work, leave it off.
|
||||
- **Bypassing governance.** Never skip `sourceIssueId`, reporting line, icon, or approval flow to ship faster. Hires without these are hard to audit and hard to hand off.
|
||||
- **Copying another company's prompt verbatim.** Placeholders like `{{companyName}}`, `{{managerTitle}}`, and `{{issuePrefix}}` must be replaced with this company's values before submitting the hire.
|
||||
|
||||
@@ -57,13 +57,13 @@ Use it for every path: exact template, adjacent template, or generic fallback.
|
||||
- [ ] `sourceIssueId` (or `sourceIssueIds`) is set when the hire was triggered by an issue
|
||||
- [ ] `desiredSkills` lists only skills that already exist in the company library, or will be installed first via the company-skills workflow
|
||||
- [ ] Adapter config matches this Paperclip instance (cwd, model, credentials) per `/llms/agent-configuration/<adapter>.txt`
|
||||
- [ ] Local managed-bundle adapters end up with managed `AGENTS.md` config, not durable `adapterConfig.promptTemplate`; use `promptTemplate` only as the creation-time transport when the API caller cannot set bundle files directly
|
||||
- [ ] Local managed-bundle adapters send custom instructions through top-level `instructionsBundle.files["AGENTS.md"]` and do not set `adapterConfig.promptTemplate` or `bootstrapPromptTemplate`
|
||||
- [ ] Placeholders like `{{companyName}}`, `{{managerTitle}}`, `{{issuePrefix}}`, and any URL stubs are replaced with real values
|
||||
|
||||
## H. Safety and permissions (least privilege)
|
||||
|
||||
- [ ] The hire grants only the access the role needs — no "just in case" permissions
|
||||
- [ ] No secrets are embedded in plain text in `adapterConfig`, creation-time instruction text, or any legacy prompt field; prefer environment-injected credentials or scoped skills
|
||||
- [ ] No secrets are embedded in plain text in `adapterConfig`, `instructionsBundle`, or any legacy prompt field; prefer environment-injected credentials or scoped skills
|
||||
- [ ] Any `desiredSkills` or adapter settings that expand external-system access, browser/network reach, filesystem scope, or secret-handling capability are individually justified in the hire comment
|
||||
- [ ] `runtimeConfig.heartbeat.enabled` is `false` unless the role genuinely needs scheduled recurring work AND `intervalSec` is justified in the hire comment
|
||||
- [ ] `AGENTS.md` explicitly names anything the role must never do (external posts, shared infra changes, destructive ops without approval)
|
||||
|
||||
@@ -724,28 +724,6 @@ export function AgentConfigForm(props: AgentConfigFormProps) {
|
||||
</Field>
|
||||
)}
|
||||
|
||||
{/* Prompt template (create mode only — edit mode shows this in Identity) */}
|
||||
{isLocal && isCreate && (
|
||||
<>
|
||||
<Field label="Prompt Template" hint={help.promptTemplate}>
|
||||
<MarkdownEditor
|
||||
value={val!.promptTemplate}
|
||||
onChange={(v) => set!({ promptTemplate: v })}
|
||||
placeholder="You are agent {{ agent.name }}. Your role is {{ agent.role }}..."
|
||||
contentClassName="min-h-[88px] text-sm font-mono"
|
||||
imageUploadHandler={async (file) => {
|
||||
const namespace = "agents/drafts/prompt-template";
|
||||
const asset = await uploadMarkdownImage.mutateAsync({ file, namespace });
|
||||
return asset.contentPath;
|
||||
}}
|
||||
/>
|
||||
</Field>
|
||||
<div className="rounded-md border border-amber-500/25 bg-amber-500/10 px-3 py-2 text-xs text-amber-100">
|
||||
Prompt template is replayed on every heartbeat. Prefer small task framing and variables like <code>{"{{ context.* }}"}</code> or <code>{"{{ run.* }}"}</code>; avoid repeating stable instructions here.
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Adapter-specific fields are rendered inside Permissions & Configuration */}
|
||||
</div>
|
||||
|
||||
|
||||
Reference in new issue
Block a user