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
37 changes: 37 additions & 0 deletions assistant/src/__tests__/channel-approval.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,43 @@ describe('parseApprovalDecision', () => {
])('returns null for non-matching text: "%s"', (input) => {
expect(parseApprovalDecision(input)).toBeNull();
});

// ── Run-reference tag extraction ────────────────────────────────

test('extracts runId from [ref:...] tag with approve decision', () => {
const result = parseApprovalDecision('yes [ref:run-abc-123]');
expect(result).not.toBeNull();
expect(result!.action).toBe('approve_once');
expect(result!.source).toBe('plain_text');
expect(result!.runId).toBe('run-abc-123');
});

test('extracts runId from [ref:...] tag with reject decision', () => {
const result = parseApprovalDecision('no [ref:run-xyz-456]');
expect(result).not.toBeNull();
expect(result!.action).toBe('reject');
expect(result!.runId).toBe('run-xyz-456');
});

test('extracts runId from [ref:...] tag with always decision', () => {
const result = parseApprovalDecision('always [ref:run-789]');
expect(result).not.toBeNull();
expect(result!.action).toBe('approve_always');
expect(result!.runId).toBe('run-789');
});

test('handles ref tag on separate line', () => {
const result = parseApprovalDecision('yes\n[ref:run-abc-123]');
expect(result).not.toBeNull();
expect(result!.action).toBe('approve_once');
expect(result!.runId).toBe('run-abc-123');
});

test('decision without ref tag has no runId', () => {
const result = parseApprovalDecision('yes');
expect(result).not.toBeNull();
expect(result!.runId).toBeUndefined();
});
});

// ═══════════════════════════════════════════════════════════════════════════
Expand Down
33 changes: 31 additions & 2 deletions assistant/src/runtime/channel-approval-parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,17 +44,46 @@ const PHRASE_MAP = buildPhraseMap();
// Public API
// ---------------------------------------------------------------------------

// ---------------------------------------------------------------------------
// Run-reference tag extraction
// ---------------------------------------------------------------------------

/**
* Pattern matching a `[ref:<runId>]` disambiguation tag appended to
* plain-text approval prompts. Guardians can include this tag in their
* reply so that `handleApprovalInterception` can resolve the correct
* pending approval when multiple approvals target the same chat.
*/
const REF_TAG_RE = /\[ref:([^\]]+)\]/i;

/**
* Extract a run-reference tag from the text and return the cleaned
* decision text plus the extracted runId (if any).
*/
function extractRefTag(text: string): { cleaned: string; runId?: string } {
const match = REF_TAG_RE.exec(text);
if (!match) return { cleaned: text };
const runId = match[1].trim();
const cleaned = text.replace(REF_TAG_RE, '').trim();
return { cleaned, runId: runId || undefined };
}

/**
* Parse a plain-text message into an approval decision.
*
* Returns a structured `ApprovalDecisionResult` if the text matches one
* of the known intent phrases, or `null` if it does not match.
*
* Matching is case-insensitive with leading/trailing whitespace trimmed.
*
* When the text contains a `[ref:<runId>]` tag (appended by the
* plain-text fallback path), the extracted runId is included in the
* result so the caller can disambiguate among multiple pending approvals.
*/
export function parseApprovalDecision(text: string): ApprovalDecisionResult | null {
const normalised = text.trim().toLowerCase();
const { cleaned, runId } = extractRefTag(text);
const normalised = cleaned.trim().toLowerCase();
const action = PHRASE_MAP.get(normalised);
if (!action) return null;
return { action, source: 'plain_text' };
return { action, source: 'plain_text', ...(runId ? { runId } : {}) };
}
11 changes: 9 additions & 2 deletions assistant/src/runtime/routes/channel-routes.ts
Comment thread
noanflaherty marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -201,10 +201,14 @@ async function deliverGeneratedApprovalPrompt(params: DeliverGeneratedApprovalPr
{ fallbackText: prompt.plainTextFallback, requiredKeywords: keywords },
);

// Embed the run reference so plain-text replies can disambiguate when
// multiple approvals are pending for the same guardian chat.
const taggedFallback = `${plainTextFallback}\n[ref:${uiMetadata.runId}]`;

try {
await deliverChannelReply(replyCallbackUrl, {
chatId,
text: plainTextFallback,
text: taggedFallback,
assistantId,
}, bearerToken);
return true;
Expand All @@ -222,10 +226,13 @@ async function deliverGeneratedApprovalPrompt(params: DeliverGeneratedApprovalPr
{ fallbackText: prompt.plainTextFallback, requiredKeywords: keywords },
);

// Embed the run reference for disambiguation in multi-pending scenarios.
const taggedPlainText = `${plainText}\n[ref:${uiMetadata.runId}]`;

try {
await deliverChannelReply(replyCallbackUrl, {
chatId,
text: plainText,
text: taggedPlainText,
assistantId,
}, bearerToken);
return true;
Expand Down
Loading