-
Notifications
You must be signed in to change notification settings - Fork 6
ref(chat): Consolidate assistant status handling #164
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
9259729
ref(chat): Consolidate assistant status handling
dcramer 6f30bf7
ref(chat): Expand assistant status prefix pool
dcramer 81ca20c
ref(chat): Use explicit assistant status specs
dcramer b2445b0
fix(chat): Align sandbox status follow-ups
dcramer 57f6392
ref(chat): Reuse normalized progress statuses
dcramer 9e054e8
ref(chat): Require typed assistant statuses
dcramer 6cc6c0b
fix(chat): Remove unused secondPlayfulStatus test constant
dcramer b80fa5d
fix(chat): Use ellipsis as default thinking status context
dcramer File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,194 @@ | ||
| import type { SlackAdapter } from "@chat-adapter/slack"; | ||
| import { logWarn } from "@/chat/logging"; | ||
| import { getSlackClient } from "@/chat/slack/client"; | ||
| import { truncateStatusText } from "@/chat/runtime/status-format"; | ||
|
|
||
| const STATUS_PATTERNS = { | ||
| thinking: { | ||
| defaultContext: "…", | ||
| variants: ["Thinking", "Reasoning", "Considering", "Working through"], | ||
| }, | ||
| searching: { | ||
| defaultContext: "sources", | ||
| variants: ["Searching", "Scanning", "Probing", "Trawling"], | ||
| }, | ||
| reading: { | ||
| defaultContext: "task", | ||
| variants: ["Reading", "Inspecting", "Parsing", "Skimming"], | ||
| }, | ||
| reviewing: { | ||
| defaultContext: "results", | ||
| variants: ["Reviewing", "Checking", "Inspecting", "Auditing"], | ||
| }, | ||
| loading: { | ||
| defaultContext: "task", | ||
| variants: ["Loading", "Priming", "Booting", "Spinning up"], | ||
| }, | ||
| updating: { | ||
| defaultContext: "state", | ||
| variants: ["Updating", "Patching", "Refreshing", "Adjusting"], | ||
| }, | ||
| fetching: { | ||
| defaultContext: "sources", | ||
| variants: ["Fetching", "Pulling", "Retrieving", "Loading"], | ||
| }, | ||
| creating: { | ||
| defaultContext: "draft", | ||
| variants: ["Creating", "Building", "Assembling", "Generating"], | ||
| }, | ||
| listing: { | ||
| defaultContext: "items", | ||
| variants: ["Listing", "Gathering", "Collecting", "Enumerating"], | ||
| }, | ||
| posting: { | ||
| defaultContext: "reply", | ||
| variants: ["Posting", "Sending", "Delivering", "Dispatching"], | ||
| }, | ||
| adding: { | ||
| defaultContext: "details", | ||
| variants: ["Adding", "Applying", "Attaching", "Dropping in"], | ||
| }, | ||
| running: { | ||
| defaultContext: "tasks", | ||
| variants: ["Running", "Executing", "Launching", "Processing"], | ||
| }, | ||
| } as const; | ||
|
|
||
| export type AssistantStatusKind = keyof typeof STATUS_PATTERNS; | ||
|
|
||
| export interface AssistantStatusSpec { | ||
| kind: AssistantStatusKind; | ||
| context?: string; | ||
| } | ||
|
|
||
| /** | ||
| * Slack assistant status transport contract. | ||
| * | ||
| * Slack's `assistant.threads.setStatus` API auto-clears after roughly two | ||
| * minutes if no message is sent, so callers must refresh non-empty statuses | ||
| * periodically during long-running work and clear them explicitly with an | ||
| * empty status when the turn ends. | ||
| */ | ||
| export interface AssistantStatusTransport { | ||
| /** Best-effort update for the visible assistant status in a Slack thread. */ | ||
| setStatus: ( | ||
| channelId: string, | ||
| threadTs: string, | ||
| status: string, | ||
| suggestions?: string[], | ||
| ) => Promise<void>; | ||
| } | ||
|
|
||
| /** | ||
| * Rendered Slack assistant status payload. | ||
| * | ||
| * Statuses are explicit specs (`kind + context`). Specs use one consistent | ||
| * `Verb target` pattern and may rotate verbs within the same kind. | ||
| */ | ||
| export interface AssistantStatusPresentation { | ||
| key: string; | ||
| hint: string; | ||
| visible: string; | ||
| suggestions?: string[]; | ||
| } | ||
|
|
||
| /** Build a typed assistant status from a stable kind and optional context. */ | ||
| export function makeAssistantStatus( | ||
| kind: AssistantStatusKind, | ||
| context?: string, | ||
| ): AssistantStatusSpec { | ||
| return { kind, ...(context ? { context } : {}) }; | ||
| } | ||
|
|
||
| /** Normalize a typed assistant status context before handing it to Slack. */ | ||
| export function normalizeAssistantStatusText(text: string): string { | ||
| const trimmed = text.trim(); | ||
| if (!trimmed) { | ||
| return ""; | ||
| } | ||
| return truncateStatusText(trimmed.replace(/(?:\.\s*)+$/, "").trim()); | ||
| } | ||
|
|
||
| /** | ||
| * Render a Slack assistant status from a typed spec. | ||
| * | ||
| * Typed specs follow a consistent `Verb target` shape and rotate only within | ||
| * their declared kind. | ||
| */ | ||
| export function buildAssistantStatusPresentation(args: { | ||
| status: AssistantStatusSpec; | ||
| random?: () => number; | ||
| }): AssistantStatusPresentation { | ||
| const random = args.random ?? Math.random; | ||
| const pattern = STATUS_PATTERNS[args.status.kind]; | ||
| const context = | ||
| normalizeAssistantStatusText(args.status.context ?? "") || | ||
| pattern.defaultContext; | ||
| const index = Math.floor(random() * pattern.variants.length); | ||
| const verb = pattern.variants[index] ?? pattern.variants[0]; | ||
| const visible = truncateStatusText(`${verb} ${context}`); | ||
| const hint = truncateStatusText(`${pattern.variants[0]} ${context}`); | ||
|
|
||
| return { | ||
| key: `${args.status.kind}:${context}`, | ||
| hint, | ||
| visible, | ||
| suggestions: Array.from(new Set([visible, hint])), | ||
| }; | ||
| } | ||
|
|
||
| /** Create a best-effort Slack adapter transport for assistant status updates. */ | ||
| export function createSlackAdapterAssistantStatusTransport(args: { | ||
| getSlackAdapter: () => Pick<SlackAdapter, "setAssistantStatus">; | ||
| }): AssistantStatusTransport { | ||
| return { | ||
| async setStatus(channelId, threadTs, status, suggestions) { | ||
| try { | ||
| await args | ||
| .getSlackAdapter() | ||
| .setAssistantStatus(channelId, threadTs, status, suggestions); | ||
| } catch (error) { | ||
| logAssistantStatusFailure(status, error); | ||
| } | ||
| }, | ||
| }; | ||
| } | ||
|
|
||
| /** | ||
| * Create a best-effort Web API transport for assistant status updates. | ||
| * | ||
| * This is used by flows that do not have a chat adapter instance handy, such | ||
| * as OAuth resume handlers, but it still follows the same status semantics and | ||
| * `loading_messages` payload shape as the adapter-backed runtime path. | ||
| */ | ||
| export function createSlackWebApiAssistantStatusTransport(args?: { | ||
| getSlackClient?: typeof getSlackClient; | ||
| }): AssistantStatusTransport { | ||
| const getClient = args?.getSlackClient ?? getSlackClient; | ||
| return { | ||
| async setStatus(channelId, threadTs, status, suggestions) { | ||
| try { | ||
| await getClient().assistant.threads.setStatus({ | ||
| channel_id: channelId, | ||
| thread_ts: threadTs, | ||
| status, | ||
| ...(suggestions ? { loading_messages: suggestions } : {}), | ||
| }); | ||
| } catch (error) { | ||
| logAssistantStatusFailure(status, error); | ||
| } | ||
| }, | ||
| }; | ||
| } | ||
|
|
||
| function logAssistantStatusFailure(status: string, error: unknown): void { | ||
| logWarn( | ||
| "assistant_status_update_failed", | ||
| {}, | ||
| { | ||
| "app.slack.status_text": status || "(clear)", | ||
| "error.message": error instanceof Error ? error.message : String(error), | ||
| }, | ||
| "Failed to update assistant status", | ||
| ); | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.