Skip to content

Commit 1eca742

Browse files
implement anchor query parsing and matching logic
1 parent 6aa7e87 commit 1eca742

File tree

1 file changed

+137
-0
lines changed

1 file changed

+137
-0
lines changed

lib/tools/utils.ts

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,12 @@ interface MatchResult {
1919
matchType: "exact" | "fuzzy"
2020
}
2121

22+
interface AnchorQuery {
23+
type: "muid" | "uid"
24+
value: string
25+
id: string
26+
}
27+
2228
function summarizeMatches(
2329
matches: MatchResult[],
2430
limit = 8,
@@ -94,6 +100,91 @@ function extractMessageContent(msg: WithParts): string {
94100
return content
95101
}
96102

103+
function parseAnchorQuery(searchString: string): AnchorQuery | undefined {
104+
const muid = searchString.match(/^\[(muid)_(\d+)\]$/)
105+
if (muid) {
106+
return {
107+
type: "muid",
108+
value: searchString,
109+
id: muid[2],
110+
}
111+
}
112+
113+
const uid = searchString.match(/^\[(uid)_(\d+)\]$/)
114+
if (uid) {
115+
return {
116+
type: "uid",
117+
value: searchString,
118+
id: uid[2],
119+
}
120+
}
121+
122+
return undefined
123+
}
124+
125+
function findAnchorMatches(messages: WithParts[], anchor: AnchorQuery): MatchResult[] {
126+
if (anchor.type === "muid") {
127+
const matches: MatchResult[] = []
128+
for (let i = 0; i < messages.length; i++) {
129+
const msg = messages[i]
130+
if (msg.info.role !== "user") {
131+
continue
132+
}
133+
const parts = Array.isArray(msg.parts) ? msg.parts : []
134+
const found = parts.some(
135+
(part) =>
136+
part.type === "text" &&
137+
typeof part.text === "string" &&
138+
part.text.startsWith(anchor.value),
139+
)
140+
if (!found) {
141+
continue
142+
}
143+
matches.push({
144+
messageId: msg.info.id,
145+
messageIndex: i,
146+
score: 100,
147+
matchType: "exact",
148+
})
149+
}
150+
return matches
151+
}
152+
153+
const matches: MatchResult[] = []
154+
const uidPrefix = new RegExp(`^\\[uid_${anchor.id}(?:\\]|,)`)
155+
for (let i = 0; i < messages.length; i++) {
156+
const msg = messages[i]
157+
const parts = Array.isArray(msg.parts) ? msg.parts : []
158+
const found = parts.some((part) => {
159+
if (part.type !== "tool") {
160+
return false
161+
}
162+
163+
const output = part.state?.status === "completed" ? part.state.output : undefined
164+
if (typeof output === "string" && uidPrefix.test(output)) {
165+
return true
166+
}
167+
168+
const error = part.state?.status === "error" ? part.state.error : undefined
169+
if (typeof error === "string" && uidPrefix.test(error)) {
170+
return true
171+
}
172+
173+
return false
174+
})
175+
if (!found) {
176+
continue
177+
}
178+
matches.push({
179+
messageId: msg.info.id,
180+
messageIndex: i,
181+
score: 100,
182+
matchType: "exact",
183+
})
184+
}
185+
return matches
186+
}
187+
97188
function findExactMatches(messages: WithParts[], searchString: string): MatchResult[] {
98189
const matches: MatchResult[] = []
99190

@@ -152,6 +243,52 @@ export function findStringInMessages(
152243

153244
const searchableMessages = messages.length > 1 ? messages.slice(0, -1) : messages
154245
const lastMessage = messages.length > 0 ? messages[messages.length - 1] : undefined
246+
const anchor = parseAnchorQuery(searchString)
247+
248+
if (anchor) {
249+
const anchorMatches = findAnchorMatches(searchableMessages, anchor)
250+
const anchorSummary = summarizeMatches(anchorMatches)
251+
clog.info(C.BOUNDARY, `${stringType}: anchor match results`, {
252+
type: anchor.type,
253+
count: anchorSummary.total,
254+
sample: anchorSummary.sample,
255+
omitted: anchorSummary.omitted,
256+
})
257+
258+
if (anchorMatches.length === 1) {
259+
clog.info(C.BOUNDARY, `${stringType}: single anchor match found`, {
260+
messageId: anchorMatches[0].messageId,
261+
messageIndex: anchorMatches[0].messageIndex,
262+
})
263+
return {
264+
messageId: anchorMatches[0].messageId,
265+
messageIndex: anchorMatches[0].messageIndex,
266+
}
267+
}
268+
269+
if (anchorMatches.length > 1) {
270+
clog.error(C.BOUNDARY, `${stringType}: MULTIPLE anchor matches - ambiguous`, {
271+
type: anchor.type,
272+
count: anchorMatches.length,
273+
matches: anchorMatches.map((m) => ({ msgId: m.messageId, idx: m.messageIndex })),
274+
searchString: searchString.substring(0, 150),
275+
})
276+
throw new Error(
277+
`Found multiple matches for ${stringType}. ` +
278+
`Use a different [muid_x] or [uid_x] anchor that appears only once.`,
279+
)
280+
}
281+
282+
clog.error(C.BOUNDARY, `${stringType}: anchor NOT FOUND`, {
283+
type: anchor.type,
284+
searchString: searchString.substring(0, 150),
285+
messageCount: searchableMessages.length,
286+
})
287+
throw new Error(
288+
`${stringType} anchor not found in conversation. ` +
289+
`Use an existing [muid_x] or [uid_x] marker from the current context.`,
290+
)
291+
}
155292

156293
clog.debug(
157294
C.BOUNDARY,

0 commit comments

Comments
 (0)