旧仕様削除
This commit is contained in:
parent
b5e9d1fcbe
commit
1df353148e
@ -292,7 +292,7 @@ describe('stances', () => {
|
|||||||
expect(config.movements[0]!.stanceContents).toBeUndefined();
|
expect(config.movements[0]!.stanceContents).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should leave stanceContents undefined for unknown stance names', () => {
|
it('should treat unknown stance names as inline content', () => {
|
||||||
const raw = {
|
const raw = {
|
||||||
name: 'test-piece',
|
name: 'test-piece',
|
||||||
stances: {
|
stances: {
|
||||||
@ -309,7 +309,7 @@ describe('stances', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const config = normalizePieceConfig(raw, testDir);
|
const config = normalizePieceConfig(raw, testDir);
|
||||||
expect(config.movements[0]!.stanceContents).toBeUndefined();
|
expect(config.movements[0]!.stanceContents).toEqual(['nonexistent']);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should resolve stances in parallel sub-movements', () => {
|
it('should resolve stances in parallel sub-movements', () => {
|
||||||
|
|||||||
@ -71,7 +71,7 @@ export async function ejectBuiltin(name?: string, options: EjectOptions = {}): P
|
|||||||
success(`Ejected piece: ${pieceDest}`);
|
success(`Ejected piece: ${pieceDest}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Copy related resource files (agents, personas, stances, instructions, report-formats)
|
// Copy related resource files (personas, stances, instructions, report-formats)
|
||||||
const resourceRefs = extractResourceRelativePaths(builtinPath);
|
const resourceRefs = extractResourceRelativePaths(builtinPath);
|
||||||
let copiedCount = 0;
|
let copiedCount = 0;
|
||||||
|
|
||||||
|
|||||||
@ -1,10 +1,9 @@
|
|||||||
/**
|
/**
|
||||||
* Agent and persona configuration loader
|
* Persona configuration loader
|
||||||
*
|
*
|
||||||
* Loads persona prompts with user → builtin fallback:
|
* Loads persona prompts with user → builtin fallback:
|
||||||
* 1. User personas: ~/.takt/personas/*.md (preferred)
|
* 1. User personas: ~/.takt/personas/*.md
|
||||||
* 2. User agents (legacy): ~/.takt/agents/*.md (backward compat)
|
* 2. Builtin personas: resources/global/{lang}/personas/*.md
|
||||||
* 3. Builtin personas: resources/global/{lang}/personas/*.md
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { readFileSync, existsSync, readdirSync } from 'node:fs';
|
import { readFileSync, existsSync, readdirSync } from 'node:fs';
|
||||||
@ -12,7 +11,6 @@ import { join, basename } from 'node:path';
|
|||||||
import type { CustomAgentConfig } from '../../../core/models/index.js';
|
import type { CustomAgentConfig } from '../../../core/models/index.js';
|
||||||
import {
|
import {
|
||||||
getGlobalPersonasDir,
|
getGlobalPersonasDir,
|
||||||
getGlobalAgentsDir,
|
|
||||||
getGlobalPiecesDir,
|
getGlobalPiecesDir,
|
||||||
getBuiltinPersonasDir,
|
getBuiltinPersonasDir,
|
||||||
getBuiltinPiecesDir,
|
getBuiltinPiecesDir,
|
||||||
@ -20,12 +18,11 @@ import {
|
|||||||
} from '../paths.js';
|
} from '../paths.js';
|
||||||
import { getLanguage } from '../global/globalConfig.js';
|
import { getLanguage } from '../global/globalConfig.js';
|
||||||
|
|
||||||
/** Get all allowed base directories for persona/agent prompt files */
|
/** Get all allowed base directories for persona prompt files */
|
||||||
function getAllowedPromptBases(): string[] {
|
function getAllowedPromptBases(): string[] {
|
||||||
const lang = getLanguage();
|
const lang = getLanguage();
|
||||||
return [
|
return [
|
||||||
getGlobalPersonasDir(),
|
getGlobalPersonasDir(),
|
||||||
getGlobalAgentsDir(),
|
|
||||||
getGlobalPiecesDir(),
|
getGlobalPiecesDir(),
|
||||||
getBuiltinPersonasDir(lang),
|
getBuiltinPersonasDir(lang),
|
||||||
getBuiltinPiecesDir(lang),
|
getBuiltinPiecesDir(lang),
|
||||||
@ -51,20 +48,12 @@ export function loadAgentsFromDir(dirPath: string): CustomAgentConfig[] {
|
|||||||
return agents;
|
return agents;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Load all custom agents from global directories (~/.takt/personas/, ~/.takt/agents/) */
|
/** Load all custom agents from ~/.takt/personas/ */
|
||||||
export function loadCustomAgents(): Map<string, CustomAgentConfig> {
|
export function loadCustomAgents(): Map<string, CustomAgentConfig> {
|
||||||
const agents = new Map<string, CustomAgentConfig>();
|
const agents = new Map<string, CustomAgentConfig>();
|
||||||
|
|
||||||
// Legacy: ~/.takt/agents/*.md (loaded first, overwritten by personas/)
|
|
||||||
for (const agent of loadAgentsFromDir(getGlobalAgentsDir())) {
|
|
||||||
agents.set(agent.name, agent);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Preferred: ~/.takt/personas/*.md (takes priority)
|
|
||||||
for (const agent of loadAgentsFromDir(getGlobalPersonasDir())) {
|
for (const agent of loadAgentsFromDir(getGlobalPersonasDir())) {
|
||||||
agents.set(agent.name, agent);
|
agents.set(agent.name, agent);
|
||||||
}
|
}
|
||||||
|
|
||||||
return agents;
|
return agents;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -73,13 +62,7 @@ export function listCustomAgents(): string[] {
|
|||||||
return Array.from(loadCustomAgents().keys()).sort();
|
return Array.from(loadCustomAgents().keys()).sort();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Load agent prompt content. */
|
||||||
* Load agent prompt content.
|
|
||||||
* Prompts can be loaded from:
|
|
||||||
* - ~/.takt/personas/*.md (preferred)
|
|
||||||
* - ~/.takt/agents/*.md (legacy)
|
|
||||||
* - ~/.takt/pieces/{piece}/*.md (piece-specific)
|
|
||||||
*/
|
|
||||||
export function loadAgentPrompt(agent: CustomAgentConfig): string {
|
export function loadAgentPrompt(agent: CustomAgentConfig): string {
|
||||||
if (agent.prompt) {
|
if (agent.prompt) {
|
||||||
return agent.prompt;
|
return agent.prompt;
|
||||||
@ -102,10 +85,7 @@ export function loadAgentPrompt(agent: CustomAgentConfig): string {
|
|||||||
throw new Error(`Agent ${agent.name} has no prompt defined`);
|
throw new Error(`Agent ${agent.name} has no prompt defined`);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Load persona prompt from a resolved path. */
|
||||||
* Load persona prompt from a resolved path.
|
|
||||||
* Used by piece engine when personaPath is already resolved.
|
|
||||||
*/
|
|
||||||
export function loadPersonaPromptFromPath(personaPath: string): string {
|
export function loadPersonaPromptFromPath(personaPath: string): string {
|
||||||
const isValid = getAllowedPromptBases().some((base) => isPathSafe(base, personaPath));
|
const isValid = getAllowedPromptBases().some((base) => isPathSafe(base, personaPath));
|
||||||
if (!isValid) {
|
if (!isValid) {
|
||||||
|
|||||||
@ -6,87 +6,110 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { readFileSync, existsSync } from 'node:fs';
|
import { readFileSync, existsSync } from 'node:fs';
|
||||||
|
import { homedir } from 'node:os';
|
||||||
import { join, dirname, basename } from 'node:path';
|
import { join, dirname, basename } from 'node:path';
|
||||||
import { parse as parseYaml } from 'yaml';
|
import { parse as parseYaml } from 'yaml';
|
||||||
import type { z } from 'zod';
|
import type { z } from 'zod';
|
||||||
import { PieceConfigRawSchema, PieceMovementRawSchema } from '../../../core/models/index.js';
|
import { PieceConfigRawSchema, PieceMovementRawSchema } from '../../../core/models/index.js';
|
||||||
import type { PieceConfig, PieceMovement, PieceRule, ReportConfig, ReportObjectConfig, LoopMonitorConfig, LoopMonitorJudge } from '../../../core/models/index.js';
|
import type { PieceConfig, PieceMovement, PieceRule, ReportConfig, ReportObjectConfig, LoopMonitorConfig, LoopMonitorJudge } from '../../../core/models/index.js';
|
||||||
|
|
||||||
/** Parsed movement type from Zod schema (replaces `any`) */
|
|
||||||
type RawStep = z.output<typeof PieceMovementRawSchema>;
|
type RawStep = z.output<typeof PieceMovementRawSchema>;
|
||||||
|
|
||||||
/**
|
/** Resolve a resource spec to an absolute file path. */
|
||||||
* Resolve persona path from piece specification.
|
function resolveResourcePath(spec: string, pieceDir: string): string {
|
||||||
* - Relative path (./persona.md): relative to piece directory
|
if (spec.startsWith('./')) return join(pieceDir, spec.slice(2));
|
||||||
* - Absolute path (/path/to/persona.md or ~/...): use as-is
|
if (spec.startsWith('~')) return join(homedir(), spec.slice(1));
|
||||||
*/
|
if (spec.startsWith('/')) return spec;
|
||||||
function resolvePersonaPathForPiece(personaSpec: string, pieceDir: string): string {
|
return join(pieceDir, spec);
|
||||||
if (personaSpec.startsWith('./')) {
|
|
||||||
return join(pieceDir, personaSpec.slice(2));
|
|
||||||
}
|
|
||||||
if (personaSpec.startsWith('~')) {
|
|
||||||
const homedir = process.env.HOME || process.env.USERPROFILE || '';
|
|
||||||
return join(homedir, personaSpec.slice(1));
|
|
||||||
}
|
|
||||||
if (personaSpec.startsWith('/')) {
|
|
||||||
return personaSpec;
|
|
||||||
}
|
|
||||||
return join(pieceDir, personaSpec);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extract display name from persona path.
|
* Resolve a resource spec to its file content.
|
||||||
* e.g., "~/.takt/personas/coder.md" -> "coder"
|
* If the spec ends with .md and the file exists, returns file content.
|
||||||
|
* Otherwise returns the spec as-is (treated as inline content).
|
||||||
*/
|
*/
|
||||||
|
function resolveResourceContent(spec: string | undefined, pieceDir: string): string | undefined {
|
||||||
|
if (spec == null) return undefined;
|
||||||
|
if (spec.endsWith('.md')) {
|
||||||
|
const resolved = resolveResourcePath(spec, pieceDir);
|
||||||
|
if (existsSync(resolved)) return readFileSync(resolved, 'utf-8');
|
||||||
|
}
|
||||||
|
return spec;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve a section reference to content.
|
||||||
|
* Looks up ref in resolvedMap first, then falls back to resolveResourceContent.
|
||||||
|
*/
|
||||||
|
function resolveRefToContent(
|
||||||
|
ref: string,
|
||||||
|
resolvedMap: Record<string, string> | undefined,
|
||||||
|
pieceDir: string,
|
||||||
|
): string | undefined {
|
||||||
|
const mapped = resolvedMap?.[ref];
|
||||||
|
if (mapped) return mapped;
|
||||||
|
return resolveResourceContent(ref, pieceDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Resolve multiple references to content strings (for fields that accept string | string[]). */
|
||||||
|
function resolveRefList(
|
||||||
|
refs: string | string[] | undefined,
|
||||||
|
resolvedMap: Record<string, string> | undefined,
|
||||||
|
pieceDir: string,
|
||||||
|
): string[] | undefined {
|
||||||
|
if (refs == null) return undefined;
|
||||||
|
const list = Array.isArray(refs) ? refs : [refs];
|
||||||
|
const contents: string[] = [];
|
||||||
|
for (const ref of list) {
|
||||||
|
const content = resolveRefToContent(ref, resolvedMap, pieceDir);
|
||||||
|
if (content) contents.push(content);
|
||||||
|
}
|
||||||
|
return contents.length > 0 ? contents : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Resolve a piece-level section map (each value resolved to file content or inline). */
|
||||||
|
function resolveSectionMap(
|
||||||
|
raw: Record<string, string> | undefined,
|
||||||
|
pieceDir: string,
|
||||||
|
): Record<string, string> | undefined {
|
||||||
|
if (!raw) return undefined;
|
||||||
|
const resolved: Record<string, string> = {};
|
||||||
|
for (const [name, value] of Object.entries(raw)) {
|
||||||
|
const content = resolveResourceContent(value, pieceDir);
|
||||||
|
if (content) resolved[name] = content;
|
||||||
|
}
|
||||||
|
return Object.keys(resolved).length > 0 ? resolved : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Extract display name from persona path (e.g., "coder.md" → "coder"). */
|
||||||
function extractPersonaDisplayName(personaPath: string): string {
|
function extractPersonaDisplayName(personaPath: string): string {
|
||||||
return basename(personaPath, '.md');
|
return basename(personaPath, '.md');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Resolve persona from YAML field to spec + absolute path. */
|
||||||
* Resolve a string value that may be a file path.
|
function resolvePersona(
|
||||||
* If the value ends with .md and the file exists (resolved relative to pieceDir),
|
rawPersona: string | undefined,
|
||||||
* read and return the file contents. Otherwise return the value as-is.
|
sections: PieceSections,
|
||||||
*/
|
pieceDir: string,
|
||||||
function resolveContentPath(value: string | undefined, pieceDir: string): string | undefined {
|
): { personaSpec?: string; personaPath?: string } {
|
||||||
if (value == null) return undefined;
|
if (!rawPersona) return {};
|
||||||
if (value.endsWith('.md')) {
|
const personaSpec = sections.personas?.[rawPersona] ?? rawPersona;
|
||||||
let resolvedPath = value;
|
|
||||||
if (value.startsWith('./')) {
|
const resolved = resolveResourcePath(personaSpec, pieceDir);
|
||||||
resolvedPath = join(pieceDir, value.slice(2));
|
const personaPath = existsSync(resolved) ? resolved : undefined;
|
||||||
} else if (value.startsWith('~')) {
|
return { personaSpec, personaPath };
|
||||||
const homedir = process.env.HOME || process.env.USERPROFILE || '';
|
|
||||||
resolvedPath = join(homedir, value.slice(1));
|
|
||||||
} else if (!value.startsWith('/')) {
|
|
||||||
resolvedPath = join(pieceDir, value);
|
|
||||||
}
|
|
||||||
if (existsSync(resolvedPath)) {
|
|
||||||
return readFileSync(resolvedPath, 'utf-8');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Pre-resolved section maps passed to movement normalization. */
|
||||||
* Resolve a value from a section map by key lookup.
|
|
||||||
* If the value matches a key in sectionMap, return the mapped value.
|
|
||||||
* Otherwise return the value as-is (treated as file path or inline content).
|
|
||||||
*/
|
|
||||||
function resolveSectionReference(
|
|
||||||
value: string,
|
|
||||||
sectionMap: Record<string, string> | undefined,
|
|
||||||
): string {
|
|
||||||
const resolved = sectionMap?.[value];
|
|
||||||
return resolved ?? value;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Section maps parsed from piece YAML for section reference expansion */
|
|
||||||
interface PieceSections {
|
interface PieceSections {
|
||||||
|
/** Persona name → file path (raw, not content-resolved) */
|
||||||
personas?: Record<string, string>;
|
personas?: Record<string, string>;
|
||||||
stances?: Record<string, string>;
|
/** Stance name → resolved content */
|
||||||
/** Stances resolved to file content (for backward-compat plain name lookup) */
|
|
||||||
resolvedStances?: Record<string, string>;
|
resolvedStances?: Record<string, string>;
|
||||||
instructions?: Record<string, string>;
|
/** Instruction name → resolved content */
|
||||||
reportFormats?: Record<string, string>;
|
resolvedInstructions?: Record<string, string>;
|
||||||
|
/** Report format name → resolved content */
|
||||||
|
resolvedReportFormats?: Record<string, string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Check if a raw report value is the object form (has 'name' property). */
|
/** Check if a raw report value is the object form (has 'name' property). */
|
||||||
@ -94,24 +117,19 @@ function isReportObject(raw: unknown): raw is { name: string; order?: string; fo
|
|||||||
return typeof raw === 'object' && raw !== null && !Array.isArray(raw) && 'name' in raw;
|
return typeof raw === 'object' && raw !== null && !Array.isArray(raw) && 'name' in raw;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Normalize the raw report field from YAML into internal format. */
|
||||||
* Normalize the raw report field from YAML into internal format.
|
|
||||||
* Supports section references for format/order fields via rawReportFormats section.
|
|
||||||
*/
|
|
||||||
function normalizeReport(
|
function normalizeReport(
|
||||||
raw: string | Record<string, string>[] | { name: string; order?: string; format?: string } | undefined,
|
raw: string | Record<string, string>[] | { name: string; order?: string; format?: string } | undefined,
|
||||||
pieceDir: string,
|
pieceDir: string,
|
||||||
rawReportFormats?: Record<string, string>,
|
resolvedReportFormats?: Record<string, string>,
|
||||||
): string | ReportConfig[] | ReportObjectConfig | undefined {
|
): string | ReportConfig[] | ReportObjectConfig | undefined {
|
||||||
if (raw == null) return undefined;
|
if (raw == null) return undefined;
|
||||||
if (typeof raw === 'string') return raw;
|
if (typeof raw === 'string') return raw;
|
||||||
if (isReportObject(raw)) {
|
if (isReportObject(raw)) {
|
||||||
const expandedFormat = raw.format ? resolveSectionReference(raw.format, rawReportFormats) : undefined;
|
|
||||||
const expandedOrder = raw.order ? resolveSectionReference(raw.order, rawReportFormats) : undefined;
|
|
||||||
return {
|
return {
|
||||||
name: raw.name,
|
name: raw.name,
|
||||||
order: resolveContentPath(expandedOrder, pieceDir),
|
order: raw.order ? resolveRefToContent(raw.order, resolvedReportFormats, pieceDir) : undefined,
|
||||||
format: resolveContentPath(expandedFormat, pieceDir),
|
format: raw.format ? resolveRefToContent(raw.format, resolvedReportFormats, pieceDir) : undefined,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return (raw as Record<string, string>[]).flatMap((entry) =>
|
return (raw as Record<string, string>[]).flatMap((entry) =>
|
||||||
@ -197,34 +215,6 @@ function normalizeRule(r: {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Resolve stance references for a movement.
|
|
||||||
*
|
|
||||||
* Resolution priority:
|
|
||||||
* 1. Section key → look up in resolvedStances (pre-resolved content)
|
|
||||||
* 2. File path (`./path`, `../path`, `*.md`) → resolve file directly
|
|
||||||
* 3. Unknown names are silently ignored
|
|
||||||
*/
|
|
||||||
function resolveStanceContents(
|
|
||||||
stanceRef: string | string[] | undefined,
|
|
||||||
sections: PieceSections,
|
|
||||||
pieceDir: string,
|
|
||||||
): string[] | undefined {
|
|
||||||
if (stanceRef == null) return undefined;
|
|
||||||
const refs = Array.isArray(stanceRef) ? stanceRef : [stanceRef];
|
|
||||||
const contents: string[] = [];
|
|
||||||
for (const ref of refs) {
|
|
||||||
const sectionContent = sections.resolvedStances?.[ref];
|
|
||||||
if (sectionContent) {
|
|
||||||
contents.push(sectionContent);
|
|
||||||
} else if (ref.endsWith('.md') || ref.startsWith('./') || ref.startsWith('../')) {
|
|
||||||
const content = resolveContentPath(ref, pieceDir);
|
|
||||||
if (content) contents.push(content);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return contents.length > 0 ? contents : undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Normalize a raw step into internal PieceMovement format. */
|
/** Normalize a raw step into internal PieceMovement format. */
|
||||||
function normalizeStepFromRaw(
|
function normalizeStepFromRaw(
|
||||||
step: RawStep,
|
step: RawStep,
|
||||||
@ -233,31 +223,17 @@ function normalizeStepFromRaw(
|
|||||||
): PieceMovement {
|
): PieceMovement {
|
||||||
const rules: PieceRule[] | undefined = step.rules?.map(normalizeRule);
|
const rules: PieceRule[] | undefined = step.rules?.map(normalizeRule);
|
||||||
|
|
||||||
// Resolve persona via section reference expansion
|
|
||||||
const rawPersona = (step as Record<string, unknown>).persona as string | undefined;
|
const rawPersona = (step as Record<string, unknown>).persona as string | undefined;
|
||||||
const expandedPersona = rawPersona ? resolveSectionReference(rawPersona, sections.personas) : undefined;
|
const { personaSpec, personaPath } = resolvePersona(rawPersona, sections, pieceDir);
|
||||||
const personaSpec: string | undefined = expandedPersona || undefined;
|
|
||||||
|
|
||||||
// Resolve persona path: if the resolved path exists on disk, use it; otherwise leave personaPath undefined
|
|
||||||
// so that the runner treats personaSpec as an inline system prompt string.
|
|
||||||
let personaPath: string | undefined;
|
|
||||||
if (personaSpec) {
|
|
||||||
const resolved = resolvePersonaPathForPiece(personaSpec, pieceDir);
|
|
||||||
if (existsSync(resolved)) {
|
|
||||||
personaPath = resolved;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const displayName: string | undefined = (step as Record<string, unknown>).persona_name as string
|
const displayName: string | undefined = (step as Record<string, unknown>).persona_name as string
|
||||||
|| undefined;
|
|| undefined;
|
||||||
|
|
||||||
// Resolve stance references (supports section key, file paths)
|
|
||||||
const stanceRef = (step as Record<string, unknown>).stance as string | string[] | undefined;
|
const stanceRef = (step as Record<string, unknown>).stance as string | string[] | undefined;
|
||||||
const stanceContents = resolveStanceContents(stanceRef, sections, pieceDir);
|
const stanceContents = resolveRefList(stanceRef, sections.resolvedStances, pieceDir);
|
||||||
|
|
||||||
// Resolve instruction: instruction_template > instruction (with section reference expansion) > default
|
|
||||||
const expandedInstruction = step.instruction
|
const expandedInstruction = step.instruction
|
||||||
? resolveContentPath(resolveSectionReference(step.instruction, sections.instructions), pieceDir)
|
? resolveRefToContent(step.instruction, sections.resolvedInstructions, pieceDir)
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
const result: PieceMovement = {
|
const result: PieceMovement = {
|
||||||
@ -272,9 +248,9 @@ function normalizeStepFromRaw(
|
|||||||
model: step.model,
|
model: step.model,
|
||||||
permissionMode: step.permission_mode,
|
permissionMode: step.permission_mode,
|
||||||
edit: step.edit,
|
edit: step.edit,
|
||||||
instructionTemplate: resolveContentPath(step.instruction_template, pieceDir) || expandedInstruction || '{task}',
|
instructionTemplate: resolveResourceContent(step.instruction_template, pieceDir) || expandedInstruction || '{task}',
|
||||||
rules,
|
rules,
|
||||||
report: normalizeReport(step.report, pieceDir, sections.reportFormats),
|
report: normalizeReport(step.report, pieceDir, sections.resolvedReportFormats),
|
||||||
passPreviousResponse: step.pass_previous_response ?? true,
|
passPreviousResponse: step.pass_previous_response ?? true,
|
||||||
stanceContents,
|
stanceContents,
|
||||||
};
|
};
|
||||||
@ -286,31 +262,18 @@ function normalizeStepFromRaw(
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Normalize a raw loop monitor judge from YAML into internal format. */
|
||||||
* Normalize a raw loop monitor judge from YAML into internal format.
|
|
||||||
* Resolves persona paths and instruction_template content paths.
|
|
||||||
*/
|
|
||||||
function normalizeLoopMonitorJudge(
|
function normalizeLoopMonitorJudge(
|
||||||
raw: { persona?: string; instruction_template?: string; rules: Array<{ condition: string; next: string }> },
|
raw: { persona?: string; instruction_template?: string; rules: Array<{ condition: string; next: string }> },
|
||||||
pieceDir: string,
|
pieceDir: string,
|
||||||
sections: PieceSections,
|
sections: PieceSections,
|
||||||
): LoopMonitorJudge {
|
): LoopMonitorJudge {
|
||||||
const rawPersona = raw.persona || undefined;
|
const { personaSpec, personaPath } = resolvePersona(raw.persona, sections, pieceDir);
|
||||||
const expandedPersona = rawPersona ? resolveSectionReference(rawPersona, sections.personas) : undefined;
|
|
||||||
const personaSpec = expandedPersona || undefined;
|
|
||||||
|
|
||||||
let personaPath: string | undefined;
|
|
||||||
if (personaSpec) {
|
|
||||||
const resolved = resolvePersonaPathForPiece(personaSpec, pieceDir);
|
|
||||||
if (existsSync(resolved)) {
|
|
||||||
personaPath = resolved;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
persona: personaSpec,
|
persona: personaSpec,
|
||||||
personaPath,
|
personaPath,
|
||||||
instructionTemplate: resolveContentPath(raw.instruction_template, pieceDir),
|
instructionTemplate: resolveResourceContent(raw.instruction_template, pieceDir),
|
||||||
rules: raw.rules.map((r) => ({ condition: r.condition, next: r.next })),
|
rules: raw.rules.map((r) => ({ condition: r.condition, next: r.next })),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -331,52 +294,27 @@ function normalizeLoopMonitors(
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Convert raw YAML piece config to internal format. */
|
||||||
* Resolve a piece-level section map.
|
|
||||||
* Each value is resolved via resolveContentPath (supports .md file references).
|
|
||||||
* Used for stances, instructions, and report_formats.
|
|
||||||
*/
|
|
||||||
function resolveSectionMap(
|
|
||||||
raw: Record<string, string> | undefined,
|
|
||||||
pieceDir: string,
|
|
||||||
): Record<string, string> | undefined {
|
|
||||||
if (!raw) return undefined;
|
|
||||||
const resolved: Record<string, string> = {};
|
|
||||||
for (const [name, value] of Object.entries(raw)) {
|
|
||||||
const content = resolveContentPath(value, pieceDir);
|
|
||||||
if (content) {
|
|
||||||
resolved[name] = content;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return Object.keys(resolved).length > 0 ? resolved : undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Convert raw YAML piece config to internal format.
|
|
||||||
* Agent paths are resolved relative to the piece directory.
|
|
||||||
*/
|
|
||||||
export function normalizePieceConfig(raw: unknown, pieceDir: string): PieceConfig {
|
export function normalizePieceConfig(raw: unknown, pieceDir: string): PieceConfig {
|
||||||
const parsed = PieceConfigRawSchema.parse(raw);
|
const parsed = PieceConfigRawSchema.parse(raw);
|
||||||
|
|
||||||
// Resolve piece-level section maps
|
|
||||||
const resolvedStances = resolveSectionMap(parsed.stances, pieceDir);
|
const resolvedStances = resolveSectionMap(parsed.stances, pieceDir);
|
||||||
const resolvedInstructions = resolveSectionMap(parsed.instructions, pieceDir);
|
const resolvedInstructions = resolveSectionMap(parsed.instructions, pieceDir);
|
||||||
const resolvedReportFormats = resolveSectionMap(parsed.report_formats, pieceDir);
|
const resolvedReportFormats = resolveSectionMap(parsed.report_formats, pieceDir);
|
||||||
|
|
||||||
// Build sections for section reference expansion in movements
|
|
||||||
const sections: PieceSections = {
|
const sections: PieceSections = {
|
||||||
personas: parsed.personas,
|
personas: parsed.personas,
|
||||||
stances: parsed.stances,
|
|
||||||
resolvedStances,
|
resolvedStances,
|
||||||
instructions: parsed.instructions,
|
resolvedInstructions,
|
||||||
reportFormats: parsed.report_formats,
|
resolvedReportFormats,
|
||||||
};
|
};
|
||||||
|
|
||||||
const movements: PieceMovement[] = parsed.movements.map((step) =>
|
const movements: PieceMovement[] = parsed.movements.map((step) =>
|
||||||
normalizeStepFromRaw(step, pieceDir, sections),
|
normalizeStepFromRaw(step, pieceDir, sections),
|
||||||
);
|
);
|
||||||
|
|
||||||
const initialMovement = parsed.initial_movement ?? movements[0]?.name ?? '';
|
// Schema guarantees movements.min(1)
|
||||||
|
const initialMovement = parsed.initial_movement ?? movements[0]!.name;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name: parsed.name,
|
name: parsed.name,
|
||||||
|
|||||||
@ -21,11 +21,6 @@ export function getGlobalPersonasDir(): string {
|
|||||||
return join(getGlobalConfigDir(), 'personas');
|
return join(getGlobalConfigDir(), 'personas');
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @deprecated Use getGlobalPersonasDir(). Kept for backward compat with ~/.takt/agents/ */
|
|
||||||
export function getGlobalAgentsDir(): string {
|
|
||||||
return join(getGlobalConfigDir(), 'agents');
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Get takt global pieces directory (~/.takt/pieces) */
|
/** Get takt global pieces directory (~/.takt/pieces) */
|
||||||
export function getGlobalPiecesDir(): string {
|
export function getGlobalPiecesDir(): string {
|
||||||
return join(getGlobalConfigDir(), 'pieces');
|
return join(getGlobalConfigDir(), 'pieces');
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user