Skip to content
Closed
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: 25 additions & 0 deletions apps/hub/src/artifacts/activity.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { expect, test } from "bun:test";
import { createSidecarEmitter } from "@intx/hub-sessions";
import { createAgentActivity } from "./activity.ts";

const runId = "run_04ba732b8686649f77a2797393bafbc5";

test("a turn resumed after a restart, with no message run, ends on its reply", () => {
const events = createSidecarEmitter();
const activity = createAgentActivity(events);
const send = (event: unknown) =>
events.emit("agent.event", {
agentAddress: `${runId}@sunny-harbor-panda.localhost`,
sessionId: "session",
event,
});
send({
type: "tool.start",
seq: 1,
data: { call: { id: "call-1", name: "write_artifact", arguments: {} } },
});
expect(activity.of(runId).working).toBe(true);
send({ type: "connector.reply", seq: 2, data: { content: "Done." } });
expect(activity.of(runId).working).toBe(false);
activity.dispose();
});
5 changes: 4 additions & 1 deletion apps/hub/src/artifacts/activity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,10 @@ export function createAgentActivity(events: Pick<SidecarEventEmitter, "on">) {
delete run.said;
return;
}
case "message.run.ended": {
// The reactor ends a message run right after it replies, but a turn resumed from an approval
// restored after a restart has no message run, so its reply is the only end it sends.
case "message.run.ended":
case "connector.reply": {
const run = entry(local);
run.working = false;
run.since = now;
Expand Down
8 changes: 7 additions & 1 deletion apps/web/src/pages/workspace/live-run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,13 @@ export function foldLiveRun(
reload,
};
}
if (event.type === "message.run.ended" || event.type === "reactor.done") {
// The reactor closes the message run right after it replies, but a turn resumed from an approval
// restored after a restart has no message run to close, so its reply is the only end it sends.
if (
event.type === "message.run.ended" ||
event.type === "connector.reply" ||
event.type === "reactor.done"
) {
return {
run: { ...run, ended: true, running: false, endedAt: Date.now() },
reload,
Expand Down
12 changes: 12 additions & 0 deletions apps/web/src/pages/workspace/transcript.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,18 @@ describe("conversation transcript", () => {
expect(awaitingCommit(done, [])).toBe(true);
});

test("a turn resumed from an approval after a restart ends on its reply", () => {
const resumed = [
{ type: "message.correlated" },
{ type: "inference.start" },
{ type: "inference.done" },
].reduce((run, event) => foldLiveRun(run, event, null).run, emptyLiveRun);
expect(resumed.running).toBe(true);
const replied = foldLiveRun(resumed, { type: "connector.reply" }, null).run;
expect(replied.running).toBe(false);
expect(replied.ended).toBe(true);
});

test("a specialist's report joins its hand-off as one message and a long run of lone ones folds", () => {
const summary =
"Wrote the limits for your weather app, covering offline use and units.";
Expand Down