Prioritize ready actions and scheduled agent prompts (#1316)
* Sort sessions by last activity * Prioritize shipping actions in git controls * Fix scheduled new agent titles
This commit is contained in:
@@ -216,6 +216,212 @@ describe("ScheduleService", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("titles scheduled new agents from the schedule prompt", async () => {
|
||||||
|
const manager = new AgentManager({
|
||||||
|
logger: createTestLogger(),
|
||||||
|
clients: createTestAgentClients(),
|
||||||
|
registry: agentStorage,
|
||||||
|
});
|
||||||
|
const service = new ScheduleService({
|
||||||
|
paseoHome: tempDir,
|
||||||
|
logger: createTestLogger(),
|
||||||
|
agentManager: manager,
|
||||||
|
agentStorage,
|
||||||
|
providerSnapshotManager: NO_UNATTENDED_SCHEDULE_POLICY,
|
||||||
|
now: () => now,
|
||||||
|
});
|
||||||
|
|
||||||
|
const created = await service.create({
|
||||||
|
prompt: "Audit flaky checkout flow\n\nReport only blockers.",
|
||||||
|
cadence: { type: "every", everyMs: 60_000 },
|
||||||
|
target: {
|
||||||
|
type: "new-agent",
|
||||||
|
config: {
|
||||||
|
provider: "claude",
|
||||||
|
cwd: tempDir,
|
||||||
|
approvalPolicy: "never",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
maxRuns: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
now = new Date("2026-01-01T00:01:00.000Z");
|
||||||
|
await service.tick();
|
||||||
|
|
||||||
|
const inspected = await service.inspect(created.id);
|
||||||
|
const agentId = inspected.runs[0]?.agentId;
|
||||||
|
expect(agentId).toMatch(/^[0-9a-f-]{36}$/);
|
||||||
|
const storedAgent = await agentStorage.get(agentId!);
|
||||||
|
expect(storedAgent?.title).toBe("Audit flaky checkout flow");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("shows scheduled new-agent prompts as normal user turns", async () => {
|
||||||
|
class PromptEchoScheduleSession implements AgentSession {
|
||||||
|
readonly provider = "claude";
|
||||||
|
readonly capabilities = SCHEDULE_TEST_CAPABILITIES;
|
||||||
|
readonly id = "scheduled-prompt-echo-session";
|
||||||
|
private turnCount = 0;
|
||||||
|
private readonly subscribers = new Set<(event: AgentStreamEvent) => void>();
|
||||||
|
|
||||||
|
async run(_prompt: AgentPromptInput, _options?: AgentRunOptions): Promise<AgentRunResult> {
|
||||||
|
return {
|
||||||
|
sessionId: this.id,
|
||||||
|
finalText: "done",
|
||||||
|
timeline: [{ type: "assistant_message", text: "done" }],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async startTurn(prompt: AgentPromptInput): Promise<{ turnId: string }> {
|
||||||
|
const turnId = `turn-${++this.turnCount}`;
|
||||||
|
const textPrompt = typeof prompt === "string" ? prompt : JSON.stringify(prompt);
|
||||||
|
setImmediate(() => {
|
||||||
|
this.emit({ type: "turn_started", provider: this.provider, turnId });
|
||||||
|
this.emit({
|
||||||
|
type: "timeline",
|
||||||
|
provider: this.provider,
|
||||||
|
turnId,
|
||||||
|
item: { type: "user_message", text: textPrompt },
|
||||||
|
});
|
||||||
|
this.emit({
|
||||||
|
type: "timeline",
|
||||||
|
provider: this.provider,
|
||||||
|
turnId,
|
||||||
|
item: { type: "assistant_message", text: "done" },
|
||||||
|
});
|
||||||
|
this.emit({
|
||||||
|
type: "turn_completed",
|
||||||
|
provider: this.provider,
|
||||||
|
turnId,
|
||||||
|
usage: { inputTokens: 1, outputTokens: 1 },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return { turnId };
|
||||||
|
}
|
||||||
|
|
||||||
|
subscribe(callback: (event: AgentStreamEvent) => void): () => void {
|
||||||
|
this.subscribers.add(callback);
|
||||||
|
return () => {
|
||||||
|
this.subscribers.delete(callback);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async *streamHistory(): AsyncGenerator<AgentStreamEvent> {}
|
||||||
|
|
||||||
|
async getRuntimeInfo() {
|
||||||
|
return {
|
||||||
|
provider: this.provider,
|
||||||
|
sessionId: this.id,
|
||||||
|
model: null,
|
||||||
|
modeId: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAvailableModes(): Promise<AgentMode[]> {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
async getCurrentMode(): Promise<string | null> {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async setMode(_modeId: string): Promise<void> {}
|
||||||
|
|
||||||
|
getPendingPermissions(): AgentPermissionRequest[] {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
async respondToPermission(
|
||||||
|
_requestId: string,
|
||||||
|
_response: AgentPermissionResponse,
|
||||||
|
): Promise<void> {}
|
||||||
|
|
||||||
|
describePersistence(): AgentPersistenceHandle {
|
||||||
|
return {
|
||||||
|
provider: this.provider,
|
||||||
|
sessionId: this.id,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async interrupt(): Promise<void> {}
|
||||||
|
|
||||||
|
async close(): Promise<void> {}
|
||||||
|
|
||||||
|
private emit(event: AgentStreamEvent): void {
|
||||||
|
for (const subscriber of this.subscribers) {
|
||||||
|
subscriber(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class PromptEchoScheduleClient implements AgentClient {
|
||||||
|
readonly provider = "claude";
|
||||||
|
readonly capabilities = SCHEDULE_TEST_CAPABILITIES;
|
||||||
|
|
||||||
|
async createSession(_config: AgentSessionConfig): Promise<AgentSession> {
|
||||||
|
return new PromptEchoScheduleSession();
|
||||||
|
}
|
||||||
|
|
||||||
|
async resumeSession(_handle: AgentPersistenceHandle): Promise<AgentSession> {
|
||||||
|
return new PromptEchoScheduleSession();
|
||||||
|
}
|
||||||
|
|
||||||
|
async listModels(_options: ListModelsOptions): Promise<AgentModelDefinition[]> {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
async isAvailable(): Promise<boolean> {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const manager = new AgentManager({
|
||||||
|
logger: createTestLogger(),
|
||||||
|
clients: { claude: new PromptEchoScheduleClient() },
|
||||||
|
registry: agentStorage,
|
||||||
|
});
|
||||||
|
const service = new ScheduleService({
|
||||||
|
paseoHome: tempDir,
|
||||||
|
logger: createTestLogger(),
|
||||||
|
agentManager: manager,
|
||||||
|
agentStorage,
|
||||||
|
providerSnapshotManager: NO_UNATTENDED_SCHEDULE_POLICY,
|
||||||
|
now: () => now,
|
||||||
|
});
|
||||||
|
const observedUserMessages: string[] = [];
|
||||||
|
const unsubscribe = manager.subscribe((event) => {
|
||||||
|
if (event.type !== "agent_stream" || event.event.type !== "timeline") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (event.event.item.type === "user_message") {
|
||||||
|
observedUserMessages.push(event.event.item.text);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const created = await service.create({
|
||||||
|
prompt: "Audit nightly run",
|
||||||
|
cadence: { type: "every", everyMs: 60_000 },
|
||||||
|
target: {
|
||||||
|
type: "new-agent",
|
||||||
|
config: {
|
||||||
|
provider: "claude",
|
||||||
|
cwd: tempDir,
|
||||||
|
approvalPolicy: "never",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
maxRuns: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
now = new Date("2026-01-01T00:01:00.000Z");
|
||||||
|
try {
|
||||||
|
await service.tick();
|
||||||
|
} finally {
|
||||||
|
unsubscribe();
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(observedUserMessages).toEqual(["Audit nightly run"]);
|
||||||
|
expect((await service.inspect(created.id)).runs[0]?.status).toBe("succeeded");
|
||||||
|
});
|
||||||
|
|
||||||
test("archives new-agent schedule sessions after the run finishes", async () => {
|
test("archives new-agent schedule sessions after the run finishes", async () => {
|
||||||
class CountingScheduleSession implements AgentSession {
|
class CountingScheduleSession implements AgentSession {
|
||||||
readonly provider = "claude";
|
readonly provider = "claude";
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import type { AgentSessionConfig } from "../agent/agent-sdk-types.js";
|
|||||||
import { curateAgentActivity } from "../agent/activity-curator.js";
|
import { curateAgentActivity } from "../agent/activity-curator.js";
|
||||||
import { ensureAgentLoaded } from "../agent/agent-loading.js";
|
import { ensureAgentLoaded } from "../agent/agent-loading.js";
|
||||||
import { formatSystemNotificationPrompt } from "../agent/agent-prompt.js";
|
import { formatSystemNotificationPrompt } from "../agent/agent-prompt.js";
|
||||||
|
import { resolveCreateAgentTitles } from "../agent/create-agent-title.js";
|
||||||
import { ScheduleStore } from "./store.js";
|
import { ScheduleStore } from "./store.js";
|
||||||
import { computeNextRunAt, validateScheduleCadence } from "./cron.js";
|
import { computeNextRunAt, validateScheduleCadence } from "./cron.js";
|
||||||
import type {
|
import type {
|
||||||
@@ -528,9 +529,8 @@ export class ScheduleService {
|
|||||||
schedule: StoredSchedule,
|
schedule: StoredSchedule,
|
||||||
runId: string,
|
runId: string,
|
||||||
): Promise<ScheduleExecutionResult> {
|
): Promise<ScheduleExecutionResult> {
|
||||||
const wrappedPrompt = formatSystemNotificationPrompt(buildScheduleFireBody(schedule, runId));
|
|
||||||
|
|
||||||
if (schedule.target.type === "agent") {
|
if (schedule.target.type === "agent") {
|
||||||
|
const wrappedPrompt = formatSystemNotificationPrompt(buildScheduleFireBody(schedule, runId));
|
||||||
const record = await this.agentStorage.get(schedule.target.agentId);
|
const record = await this.agentStorage.get(schedule.target.agentId);
|
||||||
if (record?.archivedAt) {
|
if (record?.archivedAt) {
|
||||||
throw new Error(`Agent ${schedule.target.agentId} is archived`);
|
throw new Error(`Agent ${schedule.target.agentId} is archived`);
|
||||||
@@ -583,14 +583,22 @@ export class ScheduleService {
|
|||||||
systemPrompt: targetConfig.systemPrompt,
|
systemPrompt: targetConfig.systemPrompt,
|
||||||
mcpServers: targetConfig.mcpServers as AgentSessionConfig["mcpServers"],
|
mcpServers: targetConfig.mcpServers as AgentSessionConfig["mcpServers"],
|
||||||
};
|
};
|
||||||
|
const { provisionalTitle } = resolveCreateAgentTitles({
|
||||||
|
configTitle: config.title,
|
||||||
|
initialPrompt: schedule.prompt,
|
||||||
|
});
|
||||||
const labels = {
|
const labels = {
|
||||||
"paseo.schedule-id": schedule.id,
|
"paseo.schedule-id": schedule.id,
|
||||||
"paseo.schedule-run": runId,
|
"paseo.schedule-run": runId,
|
||||||
};
|
};
|
||||||
const agent = await this.agentManager.createAgent(config, undefined, { labels });
|
const agent = await this.agentManager.createAgent(config, undefined, {
|
||||||
|
labels,
|
||||||
|
initialPrompt: schedule.prompt,
|
||||||
|
initialTitle: provisionalTitle,
|
||||||
|
});
|
||||||
let result;
|
let result;
|
||||||
try {
|
try {
|
||||||
result = await this.agentManager.runAgent(agent.id, wrappedPrompt);
|
result = await this.agentManager.runAgent(agent.id, schedule.prompt);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
try {
|
try {
|
||||||
await this.agentManager.archiveAgent(agent.id);
|
await this.agentManager.archiveAgent(agent.id);
|
||||||
|
|||||||
Reference in New Issue
Block a user