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
13 changes: 13 additions & 0 deletions packages/junior/src/chat/agent/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -501,6 +501,19 @@ async function executeAgentRunInPrivacyContext(
const handoffThinkingLevel = toAgentThinkingLevel(
thinkingSelection!.thinkingLevel,
);
void (async () => {
await observers.onStatus?.({ text: "Switching models" });
})().catch((error) => {
logWarn(
"assistant_status_observer_failed",
{},
{
"exception.message":
error instanceof Error ? error.message : String(error),
},
"Failed to report assistant status",
);
});
const handoffMessages = await compactContextForHandoff(
{
conversationContext: input.conversationContext,
Expand Down
9 changes: 9 additions & 0 deletions packages/junior/src/chat/runtime/agent-continue-runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ import {
retainRuntimeTurnContext,
stripRuntimeTurnContext,
} from "@/chat/pi/transcript";
import { latestReportedProgress } from "@/chat/runtime/report-progress";

const AGENT_CONTINUE_LOCK_RETRY_DELAYS_MS = [250, 1_000, 2_000] as const;

Expand Down Expand Up @@ -395,9 +396,17 @@ export async function continueSlackAgentRun(
return false;
}

const turnMessages =
activeSessionRecord.turnStartMessageIndex === undefined
? []
: activeSessionRecord.piMessages.slice(
activeSessionRecord.turnStartMessageIndex,
);

return {
messageText: userMessage.text,
messageTs: getTurnUserSlackMessageTs(userMessage),
initialStatus: latestReportedProgress(turnMessages),
replyContext: {
input: {
conversationContext,
Expand Down
6 changes: 3 additions & 3 deletions packages/junior/src/chat/runtime/reply-executor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1018,7 +1018,7 @@ export function createReplyToThread(deps: ReplyExecutorDeps) {
channelId,
runId,
},
onCompactionStart: () => status.start(compactingStatus),
onCompactionStart: () => status.update(compactingStatus),
piMessages,
});
if (compaction.compacted) {
Expand Down Expand Up @@ -1077,7 +1077,7 @@ export function createReplyToThread(deps: ReplyExecutorDeps) {
}
}

status.start();
status.update();
const assistantTitleTask = maybeUpdateAssistantTitle({
assistantThreadContext,
assistantUserName: botConfig.userName,
Expand Down Expand Up @@ -1726,7 +1726,7 @@ export function createReplyToThread(deps: ReplyExecutorDeps) {
);
}
}
await status.stop();
await status.clear();
}
},
);
Expand Down
43 changes: 43 additions & 0 deletions packages/junior/src/chat/runtime/report-progress.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { PiMessage } from "@/chat/pi/messages";
import type { AssistantStatusSpec } from "@/chat/slack/assistant-thread/status-render";

/** Convert a `reportProgress` tool payload into assistant status text. */
Expand All @@ -20,3 +21,45 @@ export function buildReportedProgressStatus(

return { text };
}

/** Recover the latest explicit progress update from a resumable Pi transcript. */
export function latestReportedProgress(
messages: readonly PiMessage[],
): AssistantStatusSpec | undefined {
for (
let messageIndex = messages.length - 1;
messageIndex >= 0;
messageIndex -= 1
) {
const message = messages[messageIndex] as {
role?: unknown;
content?: unknown;
};
if (message.role !== "assistant" || !Array.isArray(message.content)) {
continue;
}
for (
let partIndex = message.content.length - 1;
partIndex >= 0;
partIndex -= 1
) {
const part = message.content[partIndex];
if (!part || typeof part !== "object") {
continue;
}
const toolCall = part as {
type?: unknown;
name?: unknown;
arguments?: unknown;
};
if (toolCall.type !== "toolCall" || toolCall.name !== "reportProgress") {
continue;
}
const status = buildReportedProgressStatus(toolCall.arguments);
if (status) {
return status;
}
}
}
return undefined;
}
10 changes: 6 additions & 4 deletions packages/junior/src/chat/runtime/slack-resume.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { persistThreadStateById } from "@/chat/runtime/thread-state";
import {
createSlackWebApiAssistantStatusSession,
type AssistantStatusSession,
type AssistantStatusSpec,
} from "@/chat/slack/assistant-thread/status";
import {
buildSlackReplyFooter,
Expand Down Expand Up @@ -136,6 +137,7 @@ interface ResumeSlackTurnArgs {
replyContext?: ResumeReplyContext;
lockKey?: string;
initialText?: string;
initialStatus?: AssistantStatusSpec;
agentRunner: AgentRunner;
scheduleSessionCompletedPluginTasks?: (params: {
conversationId: string;
Expand Down Expand Up @@ -406,7 +408,7 @@ export async function resumeSlackTurn(
runArgs.initialText,
);
}
status.start();
status.update(runArgs.initialStatus);

const replyContext = createResumeReplyContext(runArgs, status);
const replyPromise = runArgs.agentRunner.run(replyContext);
Expand All @@ -431,7 +433,7 @@ export async function resumeSlackTurn(
if (outcome.status !== "completed") {
// Expected pauses defer their handlers until the lock is released,
// mirroring the failure path below.
await status.stop();
await status.clear();
const onAuthPause = runArgs.onAuthPause;
const onTimeoutPause = runArgs.onTimeoutPause;
if (outcome.status === "awaiting_auth" && onAuthPause) {
Expand Down Expand Up @@ -471,7 +473,7 @@ export async function resumeSlackTurn(
context: getResumeLogContext(runArgs, lockKey),
});

await status.stop();
await status.clear();
const footer = buildSlackReplyFooter({
conversationId: getResumeConversationId(runArgs, lockKey),
});
Expand Down Expand Up @@ -541,7 +543,7 @@ export async function resumeSlackTurn(
}
}
} catch (error) {
await status.stop();
await status.clear();

if (finalReplyDelivered) {
postDeliveryCommitError = error;
Expand Down
37 changes: 21 additions & 16 deletions packages/junior/src/chat/slack/assistant-thread/status-scheduler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,10 @@ const STATUS_ROTATION_INTERVAL_MS = 30_000;
export type TimerHandle = ReturnType<typeof setTimeout>;

export interface AssistantStatusSession {
start: (status?: AssistantStatusSpec) => void;
stop: () => Promise<void>;
update: (status: AssistantStatusSpec) => void;
/** Activate or replace the loading state; omit status to show generic loading. */
update: (status?: AssistantStatusSpec) => void;
/** Stop refresh scheduling and clear the loading state. */
clear: () => Promise<void>;
}

/**
Expand Down Expand Up @@ -59,7 +60,7 @@ export function createAssistantStatusScheduler(args: {

const enqueueStatusUpdate = (task: () => Promise<void>): Promise<void> => {
// Status writes are best effort, but they still need strict ordering so a
// slow "thinking" write cannot land after stop() already cleared the UI.
// slow "thinking" write cannot land after clear() already cleared the UI.
const request = inflightStatusUpdate
.catch(() => undefined)
.then(async () => {
Expand Down Expand Up @@ -172,17 +173,7 @@ export function createAssistantStatusScheduler(args: {
};

return {
start(status?: AssistantStatusSpec) {
active = true;
clearPending();
if (status) {
void postRenderedStatus(status);
return;
}
currentKey = "initial";
void postStatus(getInitialStatusText(), loadingMessages);
},
async stop() {
async clear() {
active = false;
clearPending();
if (rotationTimer) {
Expand All @@ -192,8 +183,22 @@ export function createAssistantStatusScheduler(args: {
currentKey = "";
await postStatus("");
},
update(status: AssistantStatusSpec) {
update(status?: AssistantStatusSpec) {
if (!active) {
active = true;
clearPending();
if (status) {
void postRenderedStatus(status);
return;
}
currentKey = "initial";
void postStatus(getInitialStatusText(), loadingMessages);
return;
}
if (!status) {
clearPending();
currentKey = "initial";
void postStatus(getInitialStatusText(), loadingMessages);
return;
}
const presentation = renderAssistantStatus({
Expand Down
Loading
Loading