release: v2.0.4 - Advanced Business Orchestration & UI Polishing
This commit is contained in:
@@ -20,7 +20,7 @@
|
||||
*/
|
||||
import * as vscode from 'vscode';
|
||||
import { COMPANY_AGENTS, DEFAULT_ACTIVE_AGENTS, getCompanyAgent } from './agents';
|
||||
import { CompanyState, COMPANY_STATE_KEY } from './types';
|
||||
import { AgentPromptOverride, CompanyState, COMPANY_STATE_KEY } from './types';
|
||||
|
||||
/** Default state for a brand-new user. CEO is always on. */
|
||||
function _defaultState(): CompanyState {
|
||||
@@ -29,6 +29,7 @@ function _defaultState(): CompanyState {
|
||||
companyName: '1인 기업',
|
||||
activeAgentIds: DEFAULT_ACTIVE_AGENTS.slice(),
|
||||
modelOverrides: {},
|
||||
promptOverrides: {},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -57,7 +58,29 @@ function _normalize(raw: Partial<CompanyState> | undefined): CompanyState {
|
||||
}
|
||||
}
|
||||
}
|
||||
return { enabled, companyName, activeAgentIds: withoutCeo, modelOverrides: overrides };
|
||||
// Prompt overrides — drop anything for an unknown agent, and prune empty
|
||||
// sub-fields so we never persist `{persona: ""}` (resolveAgentPrompt
|
||||
// treats empty same as missing, but cleaner state stays portable).
|
||||
const promptOverrides: Record<string, AgentPromptOverride> = {};
|
||||
if (raw.promptOverrides && typeof raw.promptOverrides === 'object') {
|
||||
for (const [agentId, v] of Object.entries(raw.promptOverrides as Record<string, unknown>)) {
|
||||
if (!getCompanyAgent(agentId) || !v || typeof v !== 'object') continue;
|
||||
const ov = v as Record<string, unknown>;
|
||||
const cleaned: AgentPromptOverride = {};
|
||||
if (typeof ov.persona === 'string' && ov.persona.trim()) cleaned.persona = ov.persona.trim();
|
||||
if (typeof ov.specialty === 'string' && ov.specialty.trim()) cleaned.specialty = ov.specialty.trim();
|
||||
if (typeof ov.tagline === 'string' && ov.tagline.trim()) cleaned.tagline = ov.tagline.trim();
|
||||
if (cleaned.persona || cleaned.specialty || cleaned.tagline) {
|
||||
promptOverrides[agentId] = cleaned;
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
enabled, companyName,
|
||||
activeAgentIds: withoutCeo,
|
||||
modelOverrides: overrides,
|
||||
promptOverrides,
|
||||
};
|
||||
}
|
||||
|
||||
/** Read the current company state. Always returns a fully-populated object. */
|
||||
@@ -134,6 +157,48 @@ export async function setAgentModelOverride(
|
||||
return next;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set / clear a per-agent prompt override. Pass `null` (or an empty
|
||||
* `AgentPromptOverride`) to drop the override entirely; pass a partial
|
||||
* object to update just the named fields while keeping the others.
|
||||
*
|
||||
* Field semantics:
|
||||
* - non-empty string → save as the new override
|
||||
* - empty string / null / undefined → *clear* that sub-field
|
||||
* This makes "save just persona" and "reset just specialty" symmetric: send
|
||||
* the new persona text and an empty string for specialty.
|
||||
*/
|
||||
export async function setAgentPromptOverride(
|
||||
context: vscode.ExtensionContext,
|
||||
agentId: string,
|
||||
override: AgentPromptOverride | null,
|
||||
): Promise<CompanyState> {
|
||||
const cur = readCompanyState(context);
|
||||
const overrides = { ...cur.promptOverrides };
|
||||
if (!override) {
|
||||
delete overrides[agentId];
|
||||
} else {
|
||||
const existing: AgentPromptOverride = { ...(overrides[agentId] ?? {}) };
|
||||
for (const key of ['persona', 'specialty', 'tagline'] as const) {
|
||||
const v = override[key];
|
||||
if (v === undefined) continue; // not specified → leave as-is
|
||||
if (typeof v === 'string' && v.trim()) {
|
||||
existing[key] = v.trim();
|
||||
} else {
|
||||
delete existing[key];
|
||||
}
|
||||
}
|
||||
if (existing.persona || existing.specialty || existing.tagline) {
|
||||
overrides[agentId] = existing;
|
||||
} else {
|
||||
delete overrides[agentId];
|
||||
}
|
||||
}
|
||||
const next: CompanyState = { ...cur, promptOverrides: overrides };
|
||||
await writeCompanyState(context, next);
|
||||
return next;
|
||||
}
|
||||
|
||||
// ── Derived helpers (no I/O) ────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
@@ -177,6 +242,29 @@ export function summarizeForChip(state: CompanyState): string {
|
||||
return `${state.companyName} · ${count} agents`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the *effective* prompt fields for an agent — merge the static
|
||||
* default from `agents.ts` with any user-saved override. Returns plain
|
||||
* strings so the prompt builder doesn't have to worry about which source
|
||||
* each field came from.
|
||||
*/
|
||||
export function resolveAgentPrompt(state: CompanyState, agentId: string): {
|
||||
persona: string;
|
||||
specialty: string;
|
||||
tagline: string;
|
||||
/** Whether *any* field is currently overridden — useful for UI hints. */
|
||||
hasOverride: boolean;
|
||||
} {
|
||||
const def = getCompanyAgent(agentId);
|
||||
const ov = state.promptOverrides?.[agentId];
|
||||
return {
|
||||
persona: (ov?.persona ?? def?.persona ?? '').toString(),
|
||||
specialty: (ov?.specialty ?? def?.specialty ?? '').toString(),
|
||||
tagline: (ov?.tagline ?? def?.tagline ?? '').toString(),
|
||||
hasOverride: !!(ov && (ov.persona || ov.specialty || ov.tagline)),
|
||||
};
|
||||
}
|
||||
|
||||
// Re-export the static catalogue so callers only have to import from one
|
||||
// module to get the full picture.
|
||||
export { COMPANY_AGENTS, getCompanyAgent };
|
||||
|
||||
@@ -20,6 +20,8 @@ export {
|
||||
setCompanyName,
|
||||
setActiveAgents,
|
||||
setAgentModelOverride,
|
||||
setAgentPromptOverride,
|
||||
resolveAgentPrompt,
|
||||
activeAgentIds,
|
||||
isAgentActive,
|
||||
modelForAgent,
|
||||
@@ -31,6 +33,7 @@ export type {
|
||||
CompanyState,
|
||||
CompanyTaskPlan,
|
||||
AgentTurnOutput,
|
||||
AgentPromptOverride,
|
||||
SessionResult,
|
||||
} from './types';
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
* memory/decisions and passes them in), which keeps it trivial to test.
|
||||
*/
|
||||
import { COMPANY_AGENTS, getCompanyAgent } from './agents';
|
||||
import { resolveAgentPrompt } from './companyConfig';
|
||||
import { CompanyState } from './types';
|
||||
|
||||
export interface SpecialistPromptInputs {
|
||||
@@ -50,16 +51,20 @@ export function buildSpecialistPrompt(inputs: SpecialistPromptInputs): string {
|
||||
return `You are an agent named "${inputs.agentId}". Respond in Korean.`;
|
||||
}
|
||||
const company = inputs.state.companyName || '1인 기업';
|
||||
// Honour any per-agent prompt overrides the user saved in the manage
|
||||
// panel. `resolveAgentPrompt` falls back to the static defaults from
|
||||
// `agents.ts` for fields the user hasn't edited.
|
||||
const resolved = resolveAgentPrompt(inputs.state, inputs.agentId);
|
||||
const parts: string[] = [];
|
||||
|
||||
// ── Identity ──
|
||||
parts.push(`# ${agent.emoji} ${agent.name} — ${agent.role}`);
|
||||
parts.push(`당신은 ${company}의 ${agent.role}입니다.`);
|
||||
parts.push(`전문 분야: ${agent.specialty}`);
|
||||
if (agent.persona) {
|
||||
parts.push(`전문 분야: ${resolved.specialty}`);
|
||||
if (resolved.persona) {
|
||||
parts.push('');
|
||||
parts.push('## 페르소나');
|
||||
parts.push(agent.persona);
|
||||
parts.push(resolved.persona);
|
||||
}
|
||||
|
||||
// ── Output contract ──
|
||||
|
||||
@@ -34,6 +34,22 @@ export interface CompanyAgentDef {
|
||||
alwaysOn?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* User edits to an agent's *prompt* surface. Each field is optional: empty
|
||||
* or missing means "use the static default from `agents.ts`". Storing this
|
||||
* in state (rather than rewriting `agents.ts`) means the user can re-tune
|
||||
* any agent's voice / specialty without touching code, and a `Reset` button
|
||||
* can wipe the override to recover the shipped default.
|
||||
*/
|
||||
export interface AgentPromptOverride {
|
||||
/** Replaces the persona block. Multi-line markdown allowed. */
|
||||
persona?: string;
|
||||
/** Replaces the specialty list shown to the CEO planner + specialist. */
|
||||
specialty?: string;
|
||||
/** Replaces the short tagline shown in the manage UI. */
|
||||
tagline?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Persisted runtime state for the company mode. Stored in VS Code's
|
||||
* `globalState` plus a small JSON file under `.astra/company/_shared/`.
|
||||
@@ -53,6 +69,13 @@ export interface CompanyState {
|
||||
* time, by design.
|
||||
*/
|
||||
modelOverrides: Record<string, string>;
|
||||
/**
|
||||
* Optional per-agent prompt edits (persona / specialty / tagline).
|
||||
* Empty record = every agent uses its shipped default. The dispatcher
|
||||
* resolves the effective values via `resolveAgentPrompt` at build time
|
||||
* so changes apply on the very next turn — no restart required.
|
||||
*/
|
||||
promptOverrides: Record<string, AgentPromptOverride>;
|
||||
}
|
||||
|
||||
/** Output of the CEO planner LLM call after JSON parsing. */
|
||||
|
||||
@@ -198,6 +198,26 @@ export async function handleChatMessage(provider: SidebarChatProvider, data: any
|
||||
}
|
||||
return true;
|
||||
}
|
||||
case 'setCompanyAgentPrompt': {
|
||||
// Patch one agent's persona / specialty / tagline. Each field is
|
||||
// optional in the payload; passing an *empty string* explicitly
|
||||
// clears that field (back to the default from `agents.ts`).
|
||||
// Sending `null` for the whole override resets every field at once.
|
||||
const { setAgentPromptOverride } = await import('../features/company');
|
||||
const agentId = typeof data.agentId === 'string' ? data.agentId : '';
|
||||
if (!agentId) return true;
|
||||
const v = data.override;
|
||||
const override = v === null
|
||||
? null
|
||||
: {
|
||||
persona: typeof v?.persona === 'string' ? v.persona : undefined,
|
||||
specialty: typeof v?.specialty === 'string' ? v.specialty : undefined,
|
||||
tagline: typeof v?.tagline === 'string' ? v.tagline : undefined,
|
||||
};
|
||||
await setAgentPromptOverride(provider._context, agentId, override);
|
||||
await provider._sendCompanyAgents();
|
||||
return true;
|
||||
}
|
||||
case 'proactiveTrigger':
|
||||
await provider._handleProactiveSuggestion(data.context);
|
||||
return true;
|
||||
|
||||
+19
-4
@@ -1215,24 +1215,39 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
|
||||
});
|
||||
}
|
||||
|
||||
/** Push the full agent catalogue when the manage panel opens. */
|
||||
/**
|
||||
* Push the full agent catalogue when the manage panel opens. Each entry
|
||||
* carries both the *default* (from `agents.ts`) and *override* (from
|
||||
* globalState) fields so the UI can show the user what they've edited,
|
||||
* gray out unchanged fields, and offer a Reset button per agent.
|
||||
*/
|
||||
async _sendCompanyAgents(): Promise<void> {
|
||||
if (!this._view) return;
|
||||
const state = readCompanyState(this._context);
|
||||
const agents = COMPANY_AGENT_ORDER.map((id) => {
|
||||
const def = COMPANY_AGENTS[id];
|
||||
const override = state.promptOverrides[id] || {};
|
||||
return {
|
||||
id,
|
||||
name: def.name,
|
||||
role: def.role,
|
||||
emoji: def.emoji,
|
||||
color: def.color,
|
||||
tagline: def.tagline,
|
||||
specialty: def.specialty,
|
||||
hasPersona: !!def.persona,
|
||||
alwaysOn: !!def.alwaysOn,
|
||||
active: id === 'ceo' || state.activeAgentIds.includes(id),
|
||||
modelOverride: state.modelOverrides[id] || '',
|
||||
// Defaults — never change at runtime.
|
||||
defaultTagline: def.tagline,
|
||||
defaultSpecialty: def.specialty,
|
||||
defaultPersona: def.persona || '',
|
||||
// Current effective values (default + override merged).
|
||||
tagline: override.tagline || def.tagline,
|
||||
specialty: override.specialty || def.specialty,
|
||||
persona: override.persona || def.persona || '',
|
||||
// Per-field override flags for the UI.
|
||||
personaOverridden: !!override.persona,
|
||||
specialtyOverridden: !!override.specialty,
|
||||
taglineOverridden: !!override.tagline,
|
||||
};
|
||||
});
|
||||
this._view.webview.postMessage({
|
||||
|
||||
Reference in New Issue
Block a user