feat(agents): custom context for Tier 2 + periodic system prompt re-injection
- SettingsTab: Custom Context textarea for Tier 2 project cards - AgentSession passes systemPrompt for ALL projects (Tier 1 gets full generated prompt, Tier 2 gets custom context) - Periodic re-injection: 1-hour timer checks if agent is idle, then auto-sends context refresh prompt with role/tools reminder - AgentPane: autoPrompt prop consumed when session is done/error, resumes session with fresh system prompt
This commit is contained in:
parent
14808a97e9
commit
0c28f204c7
3 changed files with 79 additions and 9 deletions
|
|
@ -58,10 +58,14 @@
|
|||
agentSystemPrompt?: string;
|
||||
/** Extra env vars injected into agent process (e.g. BTMSG_AGENT_ID) */
|
||||
extraEnv?: Record<string, string>;
|
||||
/** Auto-triggered prompt (e.g. periodic context refresh). Picked up when agent is idle. */
|
||||
autoPrompt?: string;
|
||||
/** Called when autoPrompt has been consumed */
|
||||
onautopromptconsumed?: () => void;
|
||||
onExit?: () => void;
|
||||
}
|
||||
|
||||
let { sessionId, projectId, prompt: initialPrompt = '', cwd: initialCwd, profile: profileName, provider: providerId = 'claude', capabilities = DEFAULT_CAPABILITIES, useWorktrees = false, agentSystemPrompt, extraEnv, onExit }: Props = $props();
|
||||
let { sessionId, projectId, prompt: initialPrompt = '', cwd: initialCwd, profile: profileName, provider: providerId = 'claude', capabilities = DEFAULT_CAPABILITIES, useWorktrees = false, agentSystemPrompt, extraEnv, autoPrompt, onautopromptconsumed, onExit }: Props = $props();
|
||||
|
||||
let session = $derived(getAgentSession(sessionId));
|
||||
let inputPrompt = $state(initialPrompt);
|
||||
|
|
@ -145,6 +149,16 @@
|
|||
// NOTE: Do NOT stop agents in onDestroy — it fires on layout changes/remounts,
|
||||
// not just explicit close. Stop-on-close is handled by workspace teardown.
|
||||
|
||||
// Auto-prompt: pick up externally triggered prompts (e.g. periodic context refresh)
|
||||
$effect(() => {
|
||||
if (!autoPrompt || isRunning) return;
|
||||
// Only trigger if session exists and is idle (done/error)
|
||||
if (!session || (session.status !== 'done' && session.status !== 'error')) return;
|
||||
const prompt = autoPrompt;
|
||||
onautopromptconsumed?.();
|
||||
startQuery(prompt, true); // resume session with context refresh
|
||||
});
|
||||
|
||||
let promptRef = $state<HTMLTextAreaElement | undefined>();
|
||||
|
||||
async function startQuery(text: string, resume = false) {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
<script lang="ts">
|
||||
import { onDestroy } from 'svelte';
|
||||
import type { ProjectConfig, GroupAgentRole } from '../../types/groups';
|
||||
import { generateAgentPrompt } from '../../utils/agent-prompts';
|
||||
import { getActiveGroup } from '../../stores/workspace.svelte';
|
||||
|
|
@ -24,6 +25,9 @@
|
|||
import { SessionId, ProjectId } from '../../types/ids';
|
||||
import AgentPane from '../Agent/AgentPane.svelte';
|
||||
|
||||
/** How often to re-inject the system prompt (default 1 hour) */
|
||||
const REINJECTION_INTERVAL_MS = 60 * 60 * 1000;
|
||||
|
||||
interface Props {
|
||||
project: ProjectConfig;
|
||||
onsessionid?: (id: string) => void;
|
||||
|
|
@ -34,8 +38,9 @@
|
|||
let providerId = $derived(project.provider ?? getDefaultProviderId());
|
||||
let providerMeta = $derived(getProvider(providerId));
|
||||
let group = $derived(getActiveGroup());
|
||||
// Build system prompt: full agent prompt for Tier 1, custom context for Tier 2
|
||||
let agentPrompt = $derived.by(() => {
|
||||
if (!project.isAgent || !project.agentRole || !group) return undefined;
|
||||
if (project.isAgent && project.agentRole && group) {
|
||||
return generateAgentPrompt({
|
||||
role: project.agentRole as GroupAgentRole,
|
||||
agentId: project.id,
|
||||
|
|
@ -43,6 +48,9 @@
|
|||
group,
|
||||
customPrompt: project.systemPrompt,
|
||||
});
|
||||
}
|
||||
// Tier 2: pass custom context directly (if set)
|
||||
return project.systemPrompt || undefined;
|
||||
});
|
||||
|
||||
// Inject BTMSG_AGENT_ID for agent projects so they can use btmsg/bttask CLIs
|
||||
|
|
@ -51,6 +59,36 @@
|
|||
return { BTMSG_AGENT_ID: project.id };
|
||||
});
|
||||
|
||||
// Periodic context re-injection timer
|
||||
let lastPromptTime = $state(Date.now());
|
||||
let contextRefreshPrompt = $state<string | undefined>(undefined);
|
||||
let reinjectionTimer: ReturnType<typeof setInterval> | null = null;
|
||||
|
||||
function startReinjectionTimer() {
|
||||
if (reinjectionTimer) clearInterval(reinjectionTimer);
|
||||
lastPromptTime = Date.now();
|
||||
reinjectionTimer = setInterval(() => {
|
||||
const elapsed = Date.now() - lastPromptTime;
|
||||
if (elapsed >= REINJECTION_INTERVAL_MS && !contextRefreshPrompt) {
|
||||
const refreshMsg = project.isAgent
|
||||
? '[Context Refresh] Review your role and available tools above. Check your inbox with `btmsg inbox` and review the task board with `bttask board`.'
|
||||
: '[Context Refresh] Review the instructions above and continue your work.';
|
||||
contextRefreshPrompt = refreshMsg;
|
||||
}
|
||||
}, 60_000); // Check every minute
|
||||
}
|
||||
|
||||
function handleAutoPromptConsumed() {
|
||||
contextRefreshPrompt = undefined;
|
||||
lastPromptTime = Date.now();
|
||||
}
|
||||
|
||||
// Start timer and clean up
|
||||
startReinjectionTimer();
|
||||
onDestroy(() => {
|
||||
if (reinjectionTimer) clearInterval(reinjectionTimer);
|
||||
});
|
||||
|
||||
let sessionId = $state(SessionId(crypto.randomUUID()));
|
||||
let lastState = $state<ProjectAgentState | null>(null);
|
||||
let loading = $state(true);
|
||||
|
|
@ -60,6 +98,8 @@
|
|||
sessionId = SessionId(crypto.randomUUID());
|
||||
hasRestoredHistory = false;
|
||||
lastState = null;
|
||||
lastPromptTime = Date.now();
|
||||
contextRefreshPrompt = undefined;
|
||||
registerSessionProject(sessionId, ProjectId(project.id), providerId);
|
||||
trackProject(ProjectId(project.id), sessionId);
|
||||
onsessionid?.(sessionId);
|
||||
|
|
@ -153,6 +193,8 @@
|
|||
useWorktrees={project.useWorktrees ?? false}
|
||||
agentSystemPrompt={agentPrompt}
|
||||
extraEnv={agentEnv}
|
||||
autoPrompt={contextRefreshPrompt}
|
||||
onautopromptconsumed={handleAutoPromptConsumed}
|
||||
onExit={handleNewSession}
|
||||
/>
|
||||
{/if}
|
||||
|
|
|
|||
|
|
@ -941,6 +941,20 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-field">
|
||||
<span class="card-field-label">
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/></svg>
|
||||
Custom Context
|
||||
</span>
|
||||
<textarea
|
||||
class="agent-prompt-input"
|
||||
value={project.systemPrompt ?? ''}
|
||||
placeholder="Additional instructions injected into this project's agent session"
|
||||
rows="3"
|
||||
onchange={e => updateProject(activeGroupId, project.id, { systemPrompt: (e.target as HTMLTextAreaElement).value || undefined })}
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<div class="card-footer">
|
||||
<button class="btn-remove" onclick={() => removeProject(activeGroupId, project.id)}>
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M8 6V4h8v2"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"/></svg>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue