Skip to content
Open
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
17 changes: 16 additions & 1 deletion apps/server/src/engine/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,15 @@ import type { AgentService } from "./service.ts";
import { tanstackAgent } from "./tanstack-agent.ts";
import type { TaskContext } from "./worker.ts";

export type TaskAnswer = { question?: string; answer: string };

/** Every reply the user gave this task, oldest first, with the question it answered. */
export function taskAnswers(state: Record<string, unknown>): TaskAnswer[] {
if (Array.isArray(state.answers)) return state.answers as TaskAnswer[];
// Tasks answered before replies were kept as a list hold only the latest one.
return typeof state.answer === "string" ? [{ answer: state.answer }] : [];
}

export async function executeModelTask(
service: AgentService,
owner: string,
Expand Down Expand Up @@ -339,7 +348,13 @@ export async function executeModelTask(
role: "user",
content:
task.prompt +
(task.state.answer ? `\nAdditional answer: ${String(task.state.answer)}` : ""),
taskAnswers(task.state)
.map(({ question, answer }) =>
question
? `\nYou asked: ${question}\nAnswer: ${answer}`
: `\nAdditional answer: ${answer}`,
)
.join(""),
},
],
state: {},
Expand Down
10 changes: 8 additions & 2 deletions apps/server/src/engine/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ import { backgroundFailure } from "../log.ts";
import { SearchService } from "../search.ts";
import type { WorkspaceService } from "../workspace.ts";
import { analyzeSpending } from "./finance.ts";
import { executeModelTask } from "./model.ts";
import { executeModelTask, taskAnswers } from "./model.ts";
import {
countPageDiff,
describePageDiff,
Expand Down Expand Up @@ -341,6 +341,12 @@ export class AgentService {
const task = await this.getTask(owner, id);
if (task.status !== "waiting_input")
throw new AppError("This task is not waiting for input", 409);
// Each run starts fresh, so keep every reply with its question, not just the latest.
const { answer: _latest, ...state } = task.state;
const answers = [
...taskAnswers(task.state),
{ ...(task.question ? { question: task.question } : {}), answer },
];
const next = await this.db.compareAndSwap<AgentTask>(
owner,
"tasks",
Expand All @@ -350,7 +356,7 @@ export class AgentService {
status: "queued",
question: null,
input: { ...task.input, ...(fields ? { fields } : {}) },
state: { ...task.state, answer },
state: { ...state, answers },
updatedAt: date(),
},
);
Expand Down
47 changes: 47 additions & 0 deletions tests/model-outcome.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,3 +74,50 @@ for (const scenario of [
}
});
}

test("a model task keeps every earlier answer when it asks again", async (t) => {
const { requests } = await modelFixture(t, (index) =>
index === 0
? { name: "ask_user", arguments: { question: "Which date should I use?" } }
: index === 1
? { name: "ask_user", arguments: { question: "What time works?" } }
: { name: "finish_task", arguments: { summary: "Planned." } },
);
const directory = await mkdtemp(join(tmpdir(), "openmuse-model-answers-"));
const db = await createStore();
const server = await createApp(db, {
mode: "sample",
port: 8787,
host: "127.0.0.1",
publicUrl: "http://localhost:8787",
dataDir: directory,
agentBackend: "model",
model: "openai/fixture",
intelligenceApiKey: "test-project-key-never-sent",
googleRedirectUri: "http://localhost:8787/api/google/callback",
allowedOrigins: [],
});
try {
const task = await server.agent.createTask("owner", { prompt: "Plan a sample walk" });
await server.agent.worker.tick();
await server.agent.answer("owner", task.id, "Friday");
await server.agent.worker.tick();
await server.agent.answer("owner", task.id, "3pm");
await server.agent.worker.tick();
assert.equal((await server.agent.getTask("owner", task.id)).status, "succeeded");
assert.equal(requests.length, 3);
const userMessage = (body: string) =>
JSON.stringify(
(JSON.parse(body) as { input: { role?: string; content: unknown }[] }).input.find(
(item) => item.role === "user",
)?.content,
);
const last = userMessage(requests[2].body);
for (const text of ["Which date should I use?", "Friday", "What time works?", "3pm"])
assert.ok(last.includes(text), `the third run's request is missing "${text}": ${last}`);
} finally {
await server.agent.stop();
await db.close();
await rm(directory, { recursive: true, force: true });
}
});