Skip to content

Conversation

@daesunp
Copy link
Contributor

@daesunp daesunp commented Jan 5, 2026

Description

This PR adds a dsl for organizing the prompt for tree-agent.

@daesunp daesunp marked this pull request as ready for review January 5, 2026 19:30
Copilot AI review requested due to automatic review settings January 5, 2026 19:30
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces a Domain Specific Language (DSL) for organizing and building prompts in the tree-agent package. The new DSL provides a structured, programmatic approach to constructing prompts instead of using string concatenation and template literals.

Key Changes

  • Added promptBuilder.ts with three builder classes: PromptBuilder for basic prompt construction, ClassPromptBuilder for documenting methods, and InterfaceBuilder for generating TypeScript interface definitions
  • Refactored prompt.ts to use the new DSL, replacing string template literals with builder pattern calls
  • Updated snapshot file to reflect the formatted output from the new DSL

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 8 comments.

File Description
packages/framework/tree-agent/src/promptBuilder.ts New file introducing PromptBuilder, ClassPromptBuilder, and InterfaceBuilder classes for structured prompt construction
packages/framework/tree-agent/src/prompt.ts Refactored to use the new DSL builders instead of template literals for prompt generation, particularly in getPrompt, getTreeArrayNodeDocumentation, and getTreeMapNodeDocumentation functions
packages/framework/tree-agent/src/test/snapshots/prompt.md Updated snapshot reflecting the output format changes from the DSL, including reformatted JSDoc comments and method signatures

Comment on lines 306 to 312
const param = parameters[i];
if (param !== undefined) {
const { name: paramName, type, isSpread: isSpread, isOptional } = param;
const paramSignature = `${isSpread === true ? "..." : ""}${paramName}${isOptional === true ? "?" : ""}: ${type}`;
const isLastParam = i === parameters.length - 1;
lines.push(` ${paramSignature}${isLastParam ? "" : ","}`);
}
Copy link

Copilot AI Jan 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The check if (param !== undefined) at line 307 is unnecessary since the parameters array is being iterated with a standard for loop. Array elements cannot be undefined unless explicitly set to undefined, and the loop is using the index to access elements. This check should be removed or the code should be refactored to use a for-of loop if the concern is about sparse arrays.

Suggested change
const param = parameters[i];
if (param !== undefined) {
const { name: paramName, type, isSpread: isSpread, isOptional } = param;
const paramSignature = `${isSpread === true ? "..." : ""}${paramName}${isOptional === true ? "?" : ""}: ${type}`;
const isLastParam = i === parameters.length - 1;
lines.push(` ${paramSignature}${isLastParam ? "" : ","}`);
}
const { name: paramName, type, isSpread: isSpread, isOptional } = parameters[i];
const paramSignature = `${isSpread === true ? "..." : ""}${paramName}${isOptional === true ? "?" : ""}: ${type}`;
const isLastParam = i === parameters.length - 1;
lines.push(` ${paramSignature}${isLastParam ? "" : ","}`);

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed class

Comment on lines 9 to 332
export class PromptBuilder {
private readonly sections: string[] = [];

/**
* Adds a heading (with # prefix) to the prompt.
*/
public addHeading(level: 1 | 2 | 3 | 4 | 5 | 6, text: string): this {
const prefix = "#".repeat(level);
this.sections.push(`${prefix} ${text}`);
return this;
}

/**
* Adds a paragraph of text to the prompt.
*/
public addParagraph(text: string): this {
this.sections.push(text);
return this;
}

/**
* Adds multiple paragraphs separated by blank lines.
*/
public addParagraphs(...texts: string[]): this {
for (const text of texts) this.addParagraph(text);
return this;
}

/**
* Adds a code block with the specified language.
*/
public addCodeBlock(language: string, code: string): this {
this.sections.push(`\`\`\`${language}\n${code}\n\`\`\``);
return this;
}

/**
* Adds a TypeScript code block.
*/
public addTypeScriptBlock(code: string): this {
return this.addCodeBlock("typescript", code);
}

/**
* Adds a JavaScript code block.
*/
public addJavaScriptBlock(code: string): this {
return this.addCodeBlock("javascript", code);
}

/**
* Adds a JSON code block.
*/
public addJsonBlock(code: string): this {
return this.addCodeBlock("json", code);
}

/**
* Adds a blank line to the prompt.
*/
public addBlank(): this {
this.sections.push("");
return this;
}

/**
* Adds raw content without any modification.
*/
public addRaw(content: string): this {
this.sections.push(content);
return this;
}

/**
* Builds the prompt string from all added sections.
*/
public build(): string {
return this.sections.join("\n");
}
}

/**
* Parameter documentation for a method.
*/
export interface MethodParameter {
name: string;
type: string;
description?: string;
isSpread?: boolean;
isOptional?: boolean;
}

/**
* Configuration for documenting a method.
*/
export interface MethodDocConfig {
name: string;
description: string;
parameters: MethodParameter[];
returnType?: string;
returnDescription?: string;
remarks?: string;
examples?: string[];
}

/**
* A prompt builder for documenting class interfaces and their methods.
*/
export class ClassPromptBuilder extends PromptBuilder {
public addMethod(config: MethodDocConfig): this {
const { name, description, parameters, returnType, returnDescription, remarks, examples } =
config;

const docLines: string[] = ["/**"];

docLines.push(` * ${description}`);

if (parameters.length > 0) {
docLines.push(` * @remarks`);
for (const { name: paramName, type, description: paramDesc } of parameters) {
const paramDoc = paramDesc === undefined ? "" : ` - ${paramDesc}`;
docLines.push(` * @param ${paramName}: \`${type}\`${paramDoc}`);
}
}

if (returnType !== undefined) {
const returnDoc = returnDescription === undefined ? "" : ` - ${returnDescription}`;
docLines.push(` * @returns \`${returnType}\`${returnDoc}`);
}

if (remarks !== undefined && remarks.length > 0) {
docLines.push(` *`);
docLines.push(` * @remarks`);
for (const line of remarks.split("\n")) {
docLines.push(` * ${line}`);
}
}

if (examples !== undefined && examples.length > 0) {
docLines.push(` *`);
for (const example of examples) {
docLines.push(` * ${example}`);
}
}

docLines.push(` */`);

const docString = docLines.join("\n");
this.addRaw(docString);

const paramList = parameters
.map(({ name: paramName, type }) => `${paramName}: ${type}`)
.join(", ");
const returnTypeStr = returnType ?? "void";
this.addRaw(`${name}(${paramList}): ${returnTypeStr};`);

return this;
}
}

/**
* Configuration for documenting an interface method.
*/
export interface InterfaceMethodConfig {
name: string;
description: string;
parameters: MethodParameter[];
returnType?: string;
returnDescription?: string;
throws?: string[];
remarks?: string;
examples?: string[];
}

/**
* A prompt builder for constructing TypeScript interface definitions with method documentation.
*/
export class InterfaceBuilder {
private readonly methods: InterfaceMethodConfig[] = [];
private readonly typeParameters: string[];
private readonly extendsClause: string | undefined;
private multiLineSignatures: boolean = false;

/**
* Creates a new InterfaceBuilder.
*/
public constructor(
private readonly name: string,
private readonly description: string,
typeParamNames?: string[],
baseInterface?: string,
) {
this.typeParameters = typeParamNames ?? [];
this.extendsClause = baseInterface;
}

/**
* Enables multi-line formatting for method signatures.
* When enabled, parameters will be placed on separate lines for better readability.
*/
public enableMultiLineSignatures(): this {
this.multiLineSignatures = true;
return this;
}

/**
* Adds a method to the interface.
*/
public addMethod(config: InterfaceMethodConfig): this {
this.methods.push(config);
return this;
}

/**
* Builds the TypeScript interface definition as a string.
*/
public build(): string {
const lines: string[] = [];

// Add the JSDoc comment for the interface
lines.push("/**");
lines.push(` * ${this.description}`);
lines.push(" */");

// Add the interface declaration
const typeParamStr =
this.typeParameters.length > 0 ? `<${this.typeParameters.join(", ")}>` : "";
const extendsStr =
this.extendsClause === undefined ? "" : ` extends ${this.extendsClause}`;
lines.push(`export interface ${this.name}${typeParamStr}${extendsStr} {`);

// Add methods
for (const [index, method] of this.methods.entries()) {
if (index > 0) {
lines.push("");
}

const {
name: methodName,
description: methodDesc,
parameters,
returnType,
returnDescription,
throws,
remarks,
examples,
} = method;

const docLines: string[] = ["/**"];
docLines.push(` * ${methodDesc}`);

if (parameters.length > 0) {
for (const { name: paramName, description: paramDesc } of parameters) {
const paramDoc = paramDesc === undefined ? "" : ` - ${paramDesc}`;
docLines.push(` * @param ${paramName}${paramDoc}`);
}
}

if (returnType !== undefined) {
const returnDoc = returnDescription === undefined ? "" : ` - ${returnDescription}`;
docLines.push(` * @returns \`${returnType}\`${returnDoc}`);
}

if (throws !== undefined && throws.length > 0) {
for (const throwsDesc of throws) {
const throwsLines = throwsDesc.split("\n");
docLines.push(` * @throws ${throwsLines[0]}`);
for (let i = 1; i < throwsLines.length; i++) {
docLines.push(` * ${throwsLines[i]}`);
}
}
}

if (remarks !== undefined && remarks.length > 0) {
docLines.push(` *`);
docLines.push(` * @remarks`);
for (const line of remarks.split("\n")) {
docLines.push(` * ${line}`);
}
}

if (examples !== undefined && examples.length > 0) {
docLines.push(` *`);
for (const example of examples) {
docLines.push(` * ${example}`);
}
}

docLines.push(` */`);

for (const line of docLines) {
lines.push(` ${line}`);
}

if (this.multiLineSignatures && parameters.length > 0) {
lines.push(` ${methodName}(`);
for (let i = 0; i < parameters.length; i++) {
const param = parameters[i];
if (param !== undefined) {
const { name: paramName, type, isSpread: isSpread, isOptional } = param;
const paramSignature = `${isSpread === true ? "..." : ""}${paramName}${isOptional === true ? "?" : ""}: ${type}`;
const isLastParam = i === parameters.length - 1;
lines.push(` ${paramSignature}${isLastParam ? "" : ","}`);
}
}
const returnTypeStr = returnType ?? "void";
lines.push(` ): ${returnTypeStr};`);
} else {
const paramList = parameters
.map(
({ name: paramName, type, isSpread: isSpread, isOptional }) =>
`${isSpread === true ? "..." : ""}${paramName}${isOptional === true ? "?" : ""}: ${type}`,
)
.join(", ");
const returnTypeStr = returnType ?? "void";
lines.push(` ${methodName}(${paramList}): ${returnTypeStr};`);
}
}

lines.push("}");

return lines.join("\n");
}
}
Copy link

Copilot AI Jan 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The newly introduced PromptBuilder, ClassPromptBuilder, and InterfaceBuilder classes lack dedicated unit tests. While the snapshot test in prompt.spec.ts indirectly tests some of the functionality, there should be dedicated tests for these builder classes to ensure they correctly handle edge cases (empty parameters, missing optional fields, multi-line remarks, etc.).

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed class

}

/**
* A prompt builder for documenting class interfaces and their methods.
Copy link

Copilot AI Jan 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The class is described as "A prompt builder for documenting class interfaces and their methods" but the naming is ambiguous - it's unclear if this is for documenting classes or interfaces. The generated output appears to be for standalone methods (not within a class or interface structure), which differs from InterfaceBuilder that generates complete interface declarations. Consider renaming to MethodDocBuilder or clarifying the documentation.

Suggested change
* A prompt builder for documenting class interfaces and their methods.
* A prompt builder for generating documentation comments and signatures for standalone methods.
*
* This builder focuses on method-level JSDoc and TypeScript-style signatures, and does not
* generate enclosing class or interface declarations.

Copilot uses AI. Check for mistakes.
Comment on lines 117 to 167
export class ClassPromptBuilder extends PromptBuilder {
public addMethod(config: MethodDocConfig): this {
const { name, description, parameters, returnType, returnDescription, remarks, examples } =
config;

const docLines: string[] = ["/**"];

docLines.push(` * ${description}`);

if (parameters.length > 0) {
docLines.push(` * @remarks`);
for (const { name: paramName, type, description: paramDesc } of parameters) {
const paramDoc = paramDesc === undefined ? "" : ` - ${paramDesc}`;
docLines.push(` * @param ${paramName}: \`${type}\`${paramDoc}`);
}
}

if (returnType !== undefined) {
const returnDoc = returnDescription === undefined ? "" : ` - ${returnDescription}`;
docLines.push(` * @returns \`${returnType}\`${returnDoc}`);
}

if (remarks !== undefined && remarks.length > 0) {
docLines.push(` *`);
docLines.push(` * @remarks`);
for (const line of remarks.split("\n")) {
docLines.push(` * ${line}`);
}
}

if (examples !== undefined && examples.length > 0) {
docLines.push(` *`);
for (const example of examples) {
docLines.push(` * ${example}`);
}
}

docLines.push(` */`);

const docString = docLines.join("\n");
this.addRaw(docString);

const paramList = parameters
.map(({ name: paramName, type }) => `${paramName}: ${type}`)
.join(", ");
const returnTypeStr = returnType ?? "void";
this.addRaw(`${name}(${paramList}): ${returnTypeStr};`);

return this;
}
}
Copy link

Copilot AI Jan 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ClassPromptBuilder class is defined but never used anywhere in the codebase. If it's intended for future use, consider marking it with a TODO comment. Otherwise, it should be removed to reduce code maintenance burden. Only PromptBuilder and InterfaceBuilder are actually used in prompt.ts.

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed class

Comment on lines 102 to 118
* Configuration for documenting a method.
*/
export interface MethodDocConfig {
name: string;
description: string;
parameters: MethodParameter[];
returnType?: string;
returnDescription?: string;
remarks?: string;
examples?: string[];
}

/**
* A prompt builder for documenting class interfaces and their methods.
*/
export class ClassPromptBuilder extends PromptBuilder {
public addMethod(config: MethodDocConfig): this {
Copy link

Copilot AI Jan 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The MethodDocConfig interface is only used by the unused ClassPromptBuilder class. Since ClassPromptBuilder is not used anywhere in the codebase, this interface is also unused and should be removed unless there are plans for future use.

Suggested change
* Configuration for documenting a method.
*/
export interface MethodDocConfig {
name: string;
description: string;
parameters: MethodParameter[];
returnType?: string;
returnDescription?: string;
remarks?: string;
examples?: string[];
}
/**
* A prompt builder for documenting class interfaces and their methods.
*/
export class ClassPromptBuilder extends PromptBuilder {
public addMethod(config: MethodDocConfig): this {
* A prompt builder for documenting class interfaces and their methods.
*/
export class ClassPromptBuilder extends PromptBuilder {
public addMethod(config: {
name: string;
description: string;
parameters: MethodParameter[];
returnType?: string;
returnDescription?: string;
remarks?: string;
examples?: string[];
}): this {

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed class and interface

Comment on lines 126 to 132
if (parameters.length > 0) {
docLines.push(` * @remarks`);
for (const { name: paramName, type, description: paramDesc } of parameters) {
const paramDoc = paramDesc === undefined ? "" : ` - ${paramDesc}`;
docLines.push(` * @param ${paramName}: \`${type}\`${paramDoc}`);
}
}
Copy link

Copilot AI Jan 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The @remarks tag is incorrectly placed before the @param tags when parameters exist. According to TSDoc conventions, @param tags should come before @remarks. The current implementation adds @remarks before parameters (line 127), which produces non-standard documentation format.

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed class

Comment on lines 126 to 145
if (parameters.length > 0) {
docLines.push(` * @remarks`);
for (const { name: paramName, type, description: paramDesc } of parameters) {
const paramDoc = paramDesc === undefined ? "" : ` - ${paramDesc}`;
docLines.push(` * @param ${paramName}: \`${type}\`${paramDoc}`);
}
}

if (returnType !== undefined) {
const returnDoc = returnDescription === undefined ? "" : ` - ${returnDescription}`;
docLines.push(` * @returns \`${returnType}\`${returnDoc}`);
}

if (remarks !== undefined && remarks.length > 0) {
docLines.push(` *`);
docLines.push(` * @remarks`);
for (const line of remarks.split("\n")) {
docLines.push(` * ${line}`);
}
}
Copy link

Copilot AI Jan 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ClassPromptBuilder generates two @remarks sections when both parameters exist and a remarks string is provided: one at line 127 (before params) and another at line 141. This creates duplicate @remarks tags in the generated documentation, which is invalid TSDoc.

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed class

this.addRaw(docString);

const paramList = parameters
.map(({ name: paramName, type }) => `${paramName}: ${type}`)
Copy link

Copilot AI Jan 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The method signature generation in ClassPromptBuilder ignores the isSpread and isOptional properties of MethodParameter. This means spread parameters (like ...value) and optional parameters (like value?) won't be correctly formatted in the generated method signature. The InterfaceBuilder correctly handles these properties (lines 308-309, 319-320), but ClassPromptBuilder does not.

Suggested change
.map(({ name: paramName, type }) => `${paramName}: ${type}`)
.map(({ name: paramName, type, isSpread, isOptional }) => {
const spreadPrefix = isSpread ? "..." : "";
const optionalSuffix = isOptional ? "?" : "";
return `${spreadPrefix}${paramName}${optionalSuffix}: ${type}`;
})

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed class


const prompt = `You are a helpful assistant collaborating with the user on a document. The document state is a JSON tree, and you are able to analyze and edit it.
The JSON tree adheres to the following Typescript schema:
const builder = new PromptBuilder();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is cool, nice idea! I like how it is panning out below. Are you planning on also using it to handle the rest of the prompt generation above? The section below that has been updated is definitely nicer now, but it also wasn't particularly complicated to begin with. More of the "yucky" complicated logic is above. I'm curious to see how much clear it gets with this pattern - that's where the most benefit will be.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated more places with prompt builder :)

/**
* Adds a paragraph of text to the prompt.
*/
public addParagraph(text: string): this {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like we don't need this method if we already have addParagraphs

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed method

/**
* Adds a TypeScript code block.
*/
public addTypeScriptBlock(code: string): this {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you think this method, and the javascript one, and the json one, are necessary? We could just as easily supply the language string at the call site using addCodeBlock

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed methods

/**
* A prompt builder for documenting class interfaces and their methods.
*/
export class ClassPromptBuilder extends PromptBuilder {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both this class and the interface one are pretty complicated. I don't know if they're actually helping to solve the problem here. In fact - ClassPromptBuilder isn't even used? Regardless, InterfaceBuilder is used, but it's making the array and map interface printing more complicated, not less, IMO. Before, they were basically just big long string literals with a couple of small replacements - perfectly readable. Now, they are far less readable and they also have to be parsed by this complicated helper class before being printed. I think there may be some promise in the general builder approach employed elsewhere, but the interface builder is moving us into higher complexity and lower readability - we want the opposite.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My thought was that one advantage of the interface builder would be more consistent spacing and formatting, but definitely agree that it seems unnecessarily complex and not very readable. Removed ClassBuilder and InterfaceBuilder!

@github-actions
Copy link
Contributor

github-actions bot commented Jan 7, 2026

🔗 No broken links found! ✅

Your attention to detail is admirable.

linkcheck output


> fluid-framework-docs-site@0.0.0 ci:check-links /home/runner/work/FluidFramework/FluidFramework/docs
> start-server-and-test "npm run serve -- --no-open" 3000 check-links

1: starting server using command "npm run serve -- --no-open"
and when url "[ 'http://127.0.0.1:3000' ]" is responding with HTTP status code 200
running tests using command "npm run check-links"


> fluid-framework-docs-site@0.0.0 serve
> docusaurus serve --no-open

[SUCCESS] Serving "build" directory at: http://localhost:3000/

> fluid-framework-docs-site@0.0.0 check-links
> linkcheck http://localhost:3000 --skip-file skipped-urls.txt

Crawling...

Stats:
  245432 links
    1786 destination URLs
    2025 URLs ignored
       0 warnings
       0 errors


Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants