-
Notifications
You must be signed in to change notification settings - Fork 0
feat(overseer): make AGENT_NOTIFY_SUMMARY contract invisible to humans #81
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
base: feat/overseer-readonly-entity
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,5 @@ | ||
| import type { NotifySummary } from './messages' | ||
| import { matchNotifySummaryLine } from './messages' | ||
|
|
||
| export const NOTIFY_SUMMARY_STATUSES = [ | ||
| 'done', | ||
|
|
@@ -21,6 +22,41 @@ export const AGENT_NOTIFY_CONTRACT_INLINE_PREFIX = [ | |
| '' | ||
| ].join('\n') | ||
|
|
||
| /** | ||
| * Strip the machine-only notify contract from text destined for HUMAN eyes. | ||
| * | ||
| * The `AGENT_NOTIFY_SUMMARY` contract rides fully in-band so it works across | ||
| * every agent flavor, but it must never reach the human render. Two removals: | ||
| * 1. The trailing `AGENT_NOTIFY_SUMMARY {...}` line (collapse-normalized, so | ||
| * Cursor's corrupted `SUMARY` variant strips too) plus any blank lines it | ||
| * leaves behind. | ||
| * 2. A leading inline-contract prefix block - only present on historical | ||
| * operator messages stored before input-side decoupling (the hub now | ||
| * injects the prefix into the agent-bound copy only, never the stored one). | ||
| * | ||
| * Overseer event capture and notification builders MUST read the raw text, not | ||
| * this - stripping is render-only so the machine signal survives in the store. | ||
| */ | ||
| export function stripAgentContract(text: string): string { | ||
| if (typeof text !== 'string' || text.length === 0) return text | ||
| let out = text | ||
|
|
||
| if (out.startsWith(AGENT_NOTIFY_CONTRACT_INLINE_PREFIX)) { | ||
| out = out.slice(AGENT_NOTIFY_CONTRACT_INLINE_PREFIX.length) | ||
| } | ||
|
|
||
| const lines = out.split('\n') | ||
| let lastIdx = lines.length - 1 | ||
| while (lastIdx >= 0 && lines[lastIdx].trim() === '') lastIdx -= 1 | ||
| if (lastIdx >= 0 && matchNotifySummaryLine(lines[lastIdx])) { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This only removes a trailing marker when Useful? React with 👍 / 👎. |
||
| const kept = lines.slice(0, lastIdx) | ||
| while (kept.length > 0 && kept[kept.length - 1].trim() === '') kept.pop() | ||
| out = kept.join('\n') | ||
| } | ||
|
|
||
| return out | ||
| } | ||
|
|
||
| export const HAPI_EVENTS_BEGIN = '<!--HAPI_EVENTS_BEGIN-->' | ||
| export const HAPI_EVENTS_END = '<!--HAPI_EVENTS_END-->' | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,7 +4,7 @@ import type { AppendMessage, AttachmentAdapter, ThreadMessageLike } from '@assis | |
| import { useExternalMessageConverter, useExternalStoreRuntime } from '@assistant-ui/react' | ||
| import type { PendingSchedule } from '@/components/AssistantChat/ScheduleTimePicker' | ||
| import { resolvePendingSchedule } from '@/components/AssistantChat/ScheduleTimePicker' | ||
| import { safeStringify } from '@hapi/protocol' | ||
| import { safeStringify, stripAgentContract } from '@hapi/protocol' | ||
| import { renderEventLabel } from '@/chat/presentation' | ||
| import type { ChatBlock, CliOutputBlock, CodexReview, UsageData } from '@/chat/types' | ||
| import type { AgentEvent, ToolCallBlock } from '@/chat/types' | ||
|
|
@@ -324,7 +324,11 @@ function toThreadMessageLike(block: VisibleChatBlock, threadMessageId: string): | |
| role: 'user', | ||
| id: threadMessageId, | ||
| createdAt: new Date(block.createdAt), | ||
| content: [{ type: 'text', text: block.text }], | ||
| // Strip the machine-only notify contract from the human render. On | ||
| // non-Cursor flavors the hub prepends an inline contract prefix to | ||
| // the stored operator message (#20); stripAgentContract removes that | ||
| // leading block. No-op when absent. | ||
| content: [{ type: 'text', text: stripAgentContract(block.text) }], | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Stripping the stored user text only inside Useful? React with 👍 / 👎. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Using the same Useful? React with 👍 / 👎. |
||
| metadata: { | ||
| custom: { | ||
| kind: 'user', | ||
|
|
@@ -343,7 +347,12 @@ function toThreadMessageLike(block: VisibleChatBlock, threadMessageId: string): | |
| role: 'assistant', | ||
| id: threadMessageId, | ||
| createdAt: new Date(block.createdAt), | ||
| content: [{ type: 'text', text: block.text }], | ||
| // Strip the trailing AGENT_NOTIFY_SUMMARY line (collapse-normalized, | ||
| // so Cursor's corrupted SUMARY variant strips too) so the human never | ||
| // sees the machine contract. The raw text stays in the store for the | ||
| // overseer event/inbox pipeline. copyText derives from this content, | ||
| // so the clipboard is clean too. | ||
| content: [{ type: 'text', text: stripAgentContract(block.text) }], | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This only strips blocks that are converted into top-level assistant-ui messages. Subagent/Task traces are reduced into Useful? React with 👍 / 👎. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This only cleans the assistant-ui render/copy path; I checked the voice path and Useful? React with 👍 / 👎. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When an agent response contains only the notify line, this still emits an assistant message whose sole text part is Useful? React with 👍 / 👎. |
||
| metadata: { | ||
| custom: { | ||
| kind: 'assistant', | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This helper is only wired into the assistant-ui conversion path, but the session export path (
web/src/lib/sessionExport/markdown.ts) normalizes raw stored messages and writesmessage.content.text/agent text blocks directly into the downloaded markdown. Exporting a session with a trailingAGENT_NOTIFY_SUMMARYreply or a historical inline-prefix user message still exposes the machine contract to the human, so the export formatter should runstripAgentContracton user and assistant text before serializing.Useful? React with 👍 / 👎.