Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 19 additions & 6 deletions src/agents/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,7 @@ function createAgentBuilderWithGadgets(
.withRetry(getRetryConfig(llmistLogger))
.withCompaction(getCompactionConfig(agentType))
.withTrailingMessage(getIterationTrailingMessage(agentType))
.withTextOnlyHandler('acknowledge')
.withHooks({
observers: createObserverHooks({
model: ctx.model,
Expand Down Expand Up @@ -349,7 +350,12 @@ async function injectSyntheticCalls(
// Call the actual gadget to generate output (respects .gitignore by default)
// Use maxDepth=5 to give agents better visibility into nested structures
const listDirGadget = new ListDirectory();
const listDirParams = { directoryPath: '.', maxDepth: 5, includeGitIgnored: false };
const listDirParams = {
comment: 'Pre-fetching codebase structure for context',
directoryPath: '.',
maxDepth: 5,
includeGitIgnored: false,
};
const listDirResult = listDirGadget.execute(listDirParams);
recordSyntheticInvocationId(trackingContext, 'gc_dir');
builder = builder.withSyntheticGadgetCall(
Expand Down Expand Up @@ -377,32 +383,39 @@ async function injectSyntheticCalls(
recordSyntheticInvocationId(trackingContext, invocationId);
builder = builder.withSyntheticGadgetCall(
'ReadFile',
{ filePath: file.path },
{ comment: `Pre-fetching ${file.path} for project context`, filePath: file.path },
file.content,
invocationId,
);
}

// Inject AU understanding if enabled (gives agent immediate codebase context)
if (auEnabled) {
const auListResult = (await auList.execute({ path: '.', maxDepth: 5 })) as string;
const auListResult = (await auList.execute({
comment: 'Pre-fetching AU entries for context',
path: '.',
maxDepth: 5,
})) as string;
// Only inject if there's actual content
if (auListResult && !auListResult.includes('No AU entries found')) {
recordSyntheticInvocationId(trackingContext, 'gc_au_list');
builder = builder.withSyntheticGadgetCall(
'AUList',
{ path: '.', maxDepth: 5 },
{ comment: 'Pre-fetching AU entries for context', path: '.', maxDepth: 5 },
auListResult,
'gc_au_list',
);

// Also inject root-level understanding for high-level context
const auReadResult = (await auRead.execute({ paths: '.' })) as string;
const auReadResult = (await auRead.execute({
comment: 'Pre-fetching root-level understanding',
paths: '.',
})) as string;
if (auReadResult && !auReadResult.includes('No understanding exists yet')) {
recordSyntheticInvocationId(trackingContext, 'gc_au_read');
builder = builder.withSyntheticGadgetCall(
'AURead',
{ paths: '.' },
{ comment: 'Pre-fetching root-level understanding', paths: '.' },
auReadResult,
'gc_au_read',
);
Expand Down
29 changes: 20 additions & 9 deletions src/agents/respond-to-review.ts
Original file line number Diff line number Diff line change
Expand Up @@ -308,7 +308,12 @@ async function injectReviewSyntheticCalls(
// Inject directory listing as synthetic ListDirectory call (first for codebase orientation)
// Call the actual gadget to generate output (respects .gitignore by default)
const listDirGadget = new ListDirectory();
const listDirParams = { directoryPath: '.', maxDepth: 3, includeGitIgnored: false };
const listDirParams = {
comment: 'Pre-fetching codebase structure for context',
directoryPath: '.',
maxDepth: 3,
includeGitIgnored: false,
};
const listDirResult = listDirGadget.execute(listDirParams);
recordSyntheticInvocationId(trackingContext, 'gc_dir');
builder = builder.withSyntheticGadgetCall(
Expand All @@ -322,7 +327,7 @@ async function injectReviewSyntheticCalls(
recordSyntheticInvocationId(trackingContext, 'gc_pr_details');
builder = builder.withSyntheticGadgetCall(
'GetPRDetails',
{ owner, repo, prNumber },
{ comment: 'Pre-fetching PR details for context', owner, repo, prNumber },
ctx.prDetailsFormatted,
'gc_pr_details',
);
Expand All @@ -331,7 +336,7 @@ async function injectReviewSyntheticCalls(
recordSyntheticInvocationId(trackingContext, 'gc_pr_comments');
builder = builder.withSyntheticGadgetCall(
'GetPRComments',
{ owner, repo, prNumber },
{ comment: 'Pre-fetching review comments to address', owner, repo, prNumber },
ctx.commentsFormatted,
'gc_pr_comments',
);
Expand All @@ -340,7 +345,7 @@ async function injectReviewSyntheticCalls(
recordSyntheticInvocationId(trackingContext, 'gc_pr_diff');
builder = builder.withSyntheticGadgetCall(
'GetPRDiff',
{ owner, repo, prNumber },
{ comment: 'Pre-fetching PR diff for context', owner, repo, prNumber },
ctx.diffFormatted,
'gc_pr_diff',
);
Expand All @@ -352,32 +357,38 @@ async function injectReviewSyntheticCalls(
recordSyntheticInvocationId(trackingContext, invocationId);
builder = builder.withSyntheticGadgetCall(
'ReadFile',
{ filePath: file.path },
{ comment: `Pre-fetching ${file.path} for project context`, filePath: file.path },
file.content,
invocationId,
);
}

// Inject AU understanding if enabled (gives agent immediate codebase context)
if (auEnabled) {
const auListResult = (await auList.execute({ path: '.' })) as string;
const auListResult = (await auList.execute({
comment: 'Pre-fetching AU entries for context',
path: '.',
})) as string;
// Only inject if there's actual content
if (auListResult && !auListResult.includes('No AU entries found')) {
recordSyntheticInvocationId(trackingContext, 'gc_au_list');
builder = builder.withSyntheticGadgetCall(
'AUList',
{ path: '.' },
{ comment: 'Pre-fetching AU entries for context', path: '.' },
auListResult,
'gc_au_list',
);

// Also inject root-level understanding for high-level context
const auReadResult = (await auRead.execute({ paths: '.' })) as string;
const auReadResult = (await auRead.execute({
comment: 'Pre-fetching root-level understanding',
paths: '.',
})) as string;
if (auReadResult && !auReadResult.includes('No understanding exists yet')) {
recordSyntheticInvocationId(trackingContext, 'gc_au_read');
builder = builder.withSyntheticGadgetCall(
'AURead',
{ paths: '.' },
{ comment: 'Pre-fetching root-level understanding', paths: '.' },
auReadResult,
'gc_au_read',
);
Expand Down
24 changes: 15 additions & 9 deletions src/agents/review.ts
Original file line number Diff line number Diff line change
Expand Up @@ -299,7 +299,7 @@ async function injectReviewSyntheticCalls(
recordSyntheticInvocationId(trackingContext, 'gc_pr_details');
builder = builder.withSyntheticGadgetCall(
'GetPRDetails',
{ owner, repo, prNumber },
{ comment: 'Pre-fetching PR details for review context', owner, repo, prNumber },
ctx.prDetailsFormatted,
'gc_pr_details',
);
Expand All @@ -308,7 +308,7 @@ async function injectReviewSyntheticCalls(
recordSyntheticInvocationId(trackingContext, 'gc_pr_diff');
builder = builder.withSyntheticGadgetCall(
'GetPRDiff',
{ owner, repo, prNumber },
{ comment: 'Pre-fetching PR diff for code review', owner, repo, prNumber },
ctx.diffFormatted,
'gc_pr_diff',
);
Expand All @@ -317,7 +317,7 @@ async function injectReviewSyntheticCalls(
recordSyntheticInvocationId(trackingContext, 'gc_pr_checks');
builder = builder.withSyntheticGadgetCall(
'GetPRChecks',
{ owner, repo, prNumber },
{ comment: 'Pre-fetching CI check status for review', owner, repo, prNumber },
ctx.checkStatusFormatted,
'gc_pr_checks',
);
Expand All @@ -329,7 +329,7 @@ async function injectReviewSyntheticCalls(
recordSyntheticInvocationId(trackingContext, invocationId);
builder = builder.withSyntheticGadgetCall(
'ReadFile',
{ filePath: file.path },
{ comment: `Pre-fetching ${file.path} for project context`, filePath: file.path },
file.content,
invocationId,
);
Expand All @@ -342,32 +342,38 @@ async function injectReviewSyntheticCalls(
recordSyntheticInvocationId(trackingContext, invocationId);
builder = builder.withSyntheticGadgetCall(
'ReadFile',
{ filePath: file.path },
{ comment: `Pre-fetching ${file.path} for review`, filePath: file.path },
`path=${file.path}\n\n${file.content}`,
invocationId,
);
}

// Inject AU understanding if enabled (gives agent immediate codebase context)
if (auEnabled) {
const auListResult = (await auList.execute({ path: '.' })) as string;
const auListResult = (await auList.execute({
comment: 'Pre-fetching AU entries for context',
path: '.',
})) as string;
// Only inject if there's actual content
if (auListResult && !auListResult.includes('No AU entries found')) {
recordSyntheticInvocationId(trackingContext, 'gc_au_list');
builder = builder.withSyntheticGadgetCall(
'AUList',
{ path: '.' },
{ comment: 'Pre-fetching AU entries for context', path: '.' },
auListResult,
'gc_au_list',
);

// Also inject root-level understanding for high-level context
const auReadResult = (await auRead.execute({ paths: '.' })) as string;
const auReadResult = (await auRead.execute({
comment: 'Pre-fetching root-level understanding',
paths: '.',
})) as string;
if (auReadResult && !auReadResult.includes('No understanding exists yet')) {
recordSyntheticInvocationId(trackingContext, 'gc_au_read');
builder = builder.withSyntheticGadgetCall(
'AURead',
{ paths: '.' },
{ comment: 'Pre-fetching root-level understanding', paths: '.' },
auReadResult,
'gc_au_read',
);
Expand Down
5 changes: 5 additions & 0 deletions src/agents/utils/agentLoop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,11 @@ async function handleGadgetCallEvent(
logContext.isSynthetic = true;
}

// Include the comment field (unabbreviated) for observability
if (parameters?.comment) {
logContext.comment = parameters.comment;
}

addGadgetSpecificLogContext(logContext, gadgetName, parameters);
log.info('[Gadget]', logContext);
}
Expand Down
42 changes: 33 additions & 9 deletions src/config/hintConfig.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { TrailingMessage } from 'llmist';
import { formatTodoList, loadTodos } from '../gadgets/todo/storage.js';

/**
* Agent-specific batch hints.
Expand Down Expand Up @@ -32,13 +33,36 @@ function getAgentHint(agentType?: string): string {
return AGENT_HINTS.default;
}

/**
* Format the iteration status line with appropriate urgency indicator.
*/
function formatIterationStatus(
iteration: number,
maxIterations: number,
batchHint: string,
): string {
const remaining = maxIterations - iteration;
const percent = Math.round((iteration / maxIterations) * 100);

if (percent >= 80) {
return `🚨 Iteration ${iteration}/${maxIterations} (${percent}% used, ${remaining} remaining) - ${batchHint}`;
}

if (percent >= 50) {
return `⚠️ Iteration ${iteration}/${maxIterations} (${percent}% used, ${remaining} remaining) - ${batchHint}`;
}

return `Iteration ${iteration}/${maxIterations} (${percent}% used, ${remaining} remaining) - ${batchHint}`;
}

/**
* Get trailing message function for iteration tracking.
*
* Injects iteration budget awareness into each LLM call:
* - Always shows current iteration, remaining count, and percentage
* - Adds urgency indicator when running low on iterations
* - Includes agent-specific batch processing hints
* - For implementation agent: includes current todo list for visibility
*
* Trailing messages are ephemeral - they appear in each request but don't
* persist to conversation history, keeping context clean.
Expand All @@ -50,17 +74,17 @@ export function getIterationTrailingMessage(agentType?: string): TrailingMessage
const batchHint = getAgentHint(agentType);

return (ctx) => {
const remaining = ctx.maxIterations - ctx.iteration;
const percent = Math.round((ctx.iteration / ctx.maxIterations) * 100);

if (percent >= 80) {
return `🚨 Iteration ${ctx.iteration}/${ctx.maxIterations} (${percent}% used, ${remaining} remaining) - ${batchHint}`;
}
const iterationStatus = formatIterationStatus(ctx.iteration, ctx.maxIterations, batchHint);

if (percent >= 50) {
return `⚠️ Iteration ${ctx.iteration}/${ctx.maxIterations} (${percent}% used, ${remaining} remaining) - ${batchHint}`;
// For implementation agent, include the current todo list
if (agentType === 'implementation') {
const todos = loadTodos();
if (todos.length > 0) {
const todoListFormatted = formatTodoList(todos);
return `${iterationStatus}\n\n## Current Progress\n\n${todoListFormatted}`;
}
}

return `Iteration ${ctx.iteration}/${ctx.maxIterations} (${percent}% used, ${remaining} remaining) - ${batchHint}`;
return iterationStatus;
};
}
4 changes: 4 additions & 0 deletions src/gadgets/EditFile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,13 +60,15 @@ Each call provides immediate feedback, allowing you to adjust subsequent edits.`
timeoutMs: 30000,
maxConcurrent: 1, // Sequential execution to prevent race conditions on file writes
schema: z.object({
comment: z.string().min(1).describe('Brief rationale for this gadget call'),
filePath: z.string().describe('Path to the file to edit (relative or absolute)'),
search: z.string().describe('The content to search for in the file'),
replace: z.string().describe('The content to replace it with (empty string to delete)'),
}),
examples: [
{
params: {
comment: 'Increasing timeout from 1s to 5s to fix test flakiness',
filePath: 'src/config.ts',
search: 'timeout: 1000',
replace: 'timeout: 5000',
Expand Down Expand Up @@ -103,6 +105,7 @@ No lint issues found.`,
},
{
params: {
comment: 'Updating retry constant per requirements',
filePath: 'src/constants.ts',
search: 'MAX_RETRIES = 3',
replace: 'MAX_RETRIES = 5',
Expand Down Expand Up @@ -150,6 +153,7 @@ No lint issues found.`,
},
{
params: {
comment: 'Enabling feature flag for new functionality',
filePath: 'src/data.json',
search: '"enabled": false',
replace: '"enabled": true',
Expand Down
15 changes: 13 additions & 2 deletions src/gadgets/ListDirectory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,7 @@ Allowed paths:
- Current working directory and subdirectories
- /tmp directory`,
schema: z.object({
comment: z.string().min(1).describe('Brief rationale for this gadget call'),
directoryPath: z.string().default('.').describe('Path to the directory to list'),
maxDepth: z
.number()
Expand All @@ -266,13 +267,23 @@ Allowed paths:
}),
examples: [
{
params: { directoryPath: '.', maxDepth: 1, includeGitIgnored: false },
params: {
comment: 'Getting overview of project structure',
directoryPath: '.',
maxDepth: 1,
includeGitIgnored: false,
},
output:
'path=. maxDepth=1 includeGitIgnored=false\n\n#T|N|S|A\nD|src|0|2h\nD|tests|0|1d\nF|package.json|2841|3h',
comment: 'List current directory (excluding gitignored files)',
},
{
params: { directoryPath: 'src', maxDepth: 2, includeGitIgnored: true },
params: {
comment: 'Exploring src directory to find component files',
directoryPath: 'src',
maxDepth: 2,
includeGitIgnored: true,
},
output:
'path=src maxDepth=2 includeGitIgnored=true\n\n#T|N|S|A\nD|components|0|1d\nF|index.ts|512|1h',
comment: 'List src directory including all files',
Expand Down
Loading