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
5 changes: 5 additions & 0 deletions .changeset/slim-developer-session-prompts.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@aim-ai/api": patch
---

Slim AIM Developer session prompts to avoid preloading task pool and baseline context.
111 changes: 8 additions & 103 deletions modules/api/src/developer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,10 @@ import type { Task } from "@aim-ai/contract";
import { AIM_SESSION_SETTLEMENT_PROTOCOL } from "./aim-session-settlement-protocol.js";
import type { ApiLogger } from "./api-logger.js";
import { cancelableSleep } from "./cancelable-sleep.js";
import { execGh, execGit } from "./exec-file.js";
import { execGh } from "./exec-file.js";
import type { OpenCodeSessionManager } from "./opencode-session-manager.js";
import type { OptimizerLaneEventInput } from "./optimizer-lane-events.js";
import { ensureProjectWorkspace } from "./project-workspace.js";
import {
type BaselineFacts,
buildTaskSessionPrompt,
} from "./task-continue-prompt.js";

type BaselineRepository = {
getLatestBaselineFacts(projectDirectory: string): Promise<BaselineFacts>;
};
import { buildTaskSessionPrompt } from "./task-continue-prompt.js";

type DeveloperSessionManager = Pick<OpenCodeSessionManager, "createSession">;

Expand Down Expand Up @@ -49,7 +41,6 @@ type DeveloperTaskRepository = {
sessionId: string,
): Promise<null | Task>;
getTaskById?(taskId: string): Promise<null | Task> | null | Task;
listRejectedTasksByProject(projectId: string): Promise<Task[]>;
listUnfinishedTasks(): Promise<Task[]>;
updateTask?(
taskId: string,
Expand All @@ -58,7 +49,6 @@ type DeveloperTaskRepository = {
};

type CreateDeveloperOptions = {
baselineRepository?: BaselineRepository;
canStartTask?: (
task: Task,
) =>
Expand All @@ -75,9 +65,6 @@ type CreateDeveloperOptions = {

const heartbeatMs = 1000;

const git = async (projectDirectory: string, args: string[]) =>
(await execGit(projectDirectory, args, { target: projectDirectory })).trim();

const getPullRequestFollowupOutput = (pullRequestUrl: string) =>
execGh(
[
Expand Down Expand Up @@ -172,23 +159,6 @@ const categorizePullRequest = (pullRequest: PullRequestFollowupView) => {
return "ready_to_merge";
};

const defaultBaselineRepository: BaselineRepository = {
async getLatestBaselineFacts(projectDirectory) {
await git(projectDirectory, ["fetch", "origin", "main"]);

return {
commitSha: await git(projectDirectory, ["rev-parse", "origin/main"]),
fetchedAt: new Date().toISOString(),
summary: await git(projectDirectory, [
"log",
"-1",
"--format=%s",
"origin/main",
]),
};
},
};

const defaultPullRequestStatusProvider: PullRequestStatusProvider = {
async getTaskPullRequestStatus(task) {
if (!task.pull_request_url) {
Expand All @@ -212,23 +182,9 @@ const defaultPullRequestStatusProvider: PullRequestStatusProvider = {
const summarizeError = (error: unknown) =>
error instanceof Error ? error.message : String(error);

const withExplicitSourceBaselineFreshness = (task: Task): Task =>
task.source_baseline_freshness
? task
: {
...task,
source_baseline_freshness: {
current_commit: null,
source_commit: null,
status: "unknown",
summary: "not set",
},
};

const buildMergedPullRequestSettlementPrompt = (
task: Task,
context: Parameters<typeof buildTaskSessionPrompt>[1],
) => `${buildTaskSessionPrompt(task, context)}
) => `${buildTaskSessionPrompt(task)}

Merged PR settlement objective:
- This is settlement-only work for a PR-backed AIM Task whose pull_request_status is merged_but_not_resolved.
Expand All @@ -240,7 +196,6 @@ Merged PR settlement objective:
`;

export const createDeveloper = ({
baselineRepository = defaultBaselineRepository,
canStartTask = () => ({ ok: true }),
logger,
onLaneEvent,
Expand Down Expand Up @@ -316,57 +271,8 @@ export const createDeveloper = ({
return unmetDependencyIds;
};

const buildSettlementPromptContext = async (
task: Task,
unfinishedTasks: Task[],
) => {
const directory = await ensureProjectWorkspace(task);
const [baselineFacts, rejectedTasks] = await Promise.all([
baselineRepository.getLatestBaselineFacts(directory),
taskRepository.listRejectedTasksByProject(task.project_id),
]);
const activeTasks = unfinishedTasks
.filter((activeTask) => activeTask.project_id === task.project_id)
.map(withExplicitSourceBaselineFreshness);

return {
directory,
prompt: buildMergedPullRequestSettlementPrompt(task, {
activeTasks,
baselineFacts,
rejectedTasks,
}),
};
};

const buildContinuationPrompt = async (
task: Task,
unfinishedTasks: Task[],
) => {
const directory = await ensureProjectWorkspace(task);
const [baselineFacts, rejectedTasks] = await Promise.all([
baselineRepository.getLatestBaselineFacts(directory),
taskRepository.listRejectedTasksByProject(task.project_id),
]);
const activeTasks = unfinishedTasks
.filter((activeTask) => activeTask.project_id === task.project_id)
.map(withExplicitSourceBaselineFreshness);

return buildTaskSessionPrompt(withExplicitSourceBaselineFreshness(task), {
activeTasks,
baselineFacts,
rejectedTasks,
});
};

const continueMergedPullRequestSettlement = async (
task: Task,
unfinishedTasks: Task[],
) => {
const { prompt } = await buildSettlementPromptContext(
task,
unfinishedTasks,
);
const continueMergedPullRequestSettlement = async (task: Task) => {
const prompt = buildMergedPullRequestSettlementPrompt(task);

if (task.session_id) {
onLaneEvent?.({
Expand Down Expand Up @@ -412,7 +318,6 @@ export const createDeveloper = ({
const continueAssignedPendingSession = async (
task: Task,
tasksById: Map<string, Task>,
unfinishedTasks: Task[],
) => {
if (!task.session_id) {
return;
Expand Down Expand Up @@ -559,7 +464,7 @@ export const createDeveloper = ({
}

await rebindAssignedTaskToNewSession({
prompt: await buildContinuationPrompt(task, unfinishedTasks),
prompt: buildTaskSessionPrompt(task),
successSummary: (sessionId) =>
`Developer lane rebound assigned pending task ${task.task_id} from session ${task.session_id} to session ${sessionId}.`,
title: `AIM Developer: ${task.title}`,
Expand Down Expand Up @@ -596,7 +501,7 @@ export const createDeveloper = ({
}

try {
await continueMergedPullRequestSettlement(task, tasks);
await continueMergedPullRequestSettlement(task);
} catch (error) {
logger?.error(
{
Expand Down Expand Up @@ -699,7 +604,7 @@ export const createDeveloper = ({
}

try {
await continueAssignedPendingSession(task, tasksById, tasks);
await continueAssignedPendingSession(task, tasksById);
} catch (error) {
logger?.error(
{
Expand Down
72 changes: 1 addition & 71 deletions modules/api/src/task-continue-prompt.ts
Original file line number Diff line number Diff line change
@@ -1,75 +1,6 @@
import type { Task } from "@aim-ai/contract";

export type BaselineFacts = {
commitSha: string;
fetchedAt: string;
summary: string;
};

export type TaskSessionPromptContext = {
activeTasks: Task[];
baselineFacts: BaselineFacts;
rejectedTasks: Task[];
};

const formatTaskSummary = (task: Task) => {
const freshness = task.source_baseline_freshness;

return [
`- task_id: ${task.task_id}`,
` title: ${task.title}`,
` status: ${task.status}`,
` done: ${task.done}`,
` session_id: ${task.session_id ?? "not recorded"}`,
` worktree_path: ${task.worktree_path ?? "not recorded"}`,
` pull_request_url: ${task.pull_request_url ?? "not recorded"}`,
` source_baseline_freshness: ${freshness?.status ?? "unknown"}; ${freshness?.summary ?? "not set"}`,
].join("\n");
};

const formatRejectedTaskSummary = (task: Task) =>
[
formatTaskSummary(task),
` rejected_feedback: ${task.result || "not recorded"}`,
].join("\n");

const buildContextPromptSection = (context?: TaskSessionPromptContext) => {
if (!context) {
return "";
}

const activeTaskLines = context.activeTasks.length
? context.activeTasks.map(formatTaskSummary).join("\n")
: "- none";
const rejectedTaskLines = context.rejectedTasks.length
? context.rejectedTasks.map(formatRejectedTaskSummary).join("\n")
: "- none";

return `

Current baseline facts:
- origin/main commit: ${context.baselineFacts.commitSha}
- fetched_at: ${context.baselineFacts.fetchedAt}
- latest_commit_summary: ${context.baselineFacts.summary}

Current Active Task Pool:
${activeTaskLines}

Rejected Task feedback for this project:
${rejectedTaskLines}

source_baseline_freshness guardrails:
- Treat stale or unknown source_baseline_freshness as a collision warning, not permission to proceed blindly.
- Before editing, compare the task source baseline against the latest origin/main facts above.
- Avoid duplicating or conflicting with same-project unfinished Tasks in the Active Task Pool.
- Account for rejected Task feedback before continuing implementation.
`;
};

export const buildTaskSessionPrompt = (
task: Task,
context?: TaskSessionPromptContext,
) => `
export const buildTaskSessionPrompt = (task: Task) => `
You are the AIM developer.

FOLLOW the aim-developer-guide SKILL and finish the task assigned to you by AIM Coordinator.
Expand All @@ -89,7 +20,6 @@ High-cost lifecycle warnings:
- If worktree_path is recorded, do not recreate the worktree; continue the existing lifecycle.
- If pull_request_url is recorded, do not recreate the PR; continue the existing lifecycle.
- If a lifecycle fact is not recorded, verify current task facts before creating anything.
${buildContextPromptSection(context)}

Final status reporting is concise: resolved only after the GitHub PR is merged, the worktree is cleaned up, and the local main branch is refreshed to origin/main; rejected only when Task Spec verification fails.

Expand Down
Loading
Loading