fix(prompt-gate): ignore internal user tails in tool waits
This commit is contained in:
@@ -356,6 +356,141 @@ describe("dispatchInternalPrompt shared gate behavior", () => {
|
|||||||
expect(promptCalls).toBe(0)
|
expect(promptCalls).toBe(0)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("#given internal user tail follows an assistant waiting on tools #when an internal promptAsync is requested #then no prompt is sent", async () => {
|
||||||
|
// given
|
||||||
|
let promptCalls = 0
|
||||||
|
const client = {
|
||||||
|
session: {
|
||||||
|
status: async () => ({ data: { ses_internal_tail_tools: { type: "idle" } } }),
|
||||||
|
messages: async () => ({
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
info: { id: "msg_user", role: "user" },
|
||||||
|
parts: [{ type: "text", text: "run work" }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
info: { id: "msg_assistant", role: "assistant", finish: "tool-calls" },
|
||||||
|
parts: [{ type: "tool_use", id: "toolu_pending", state: { status: "running" } }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
info: { id: "msg_internal_user", role: "user" },
|
||||||
|
parts: [{ type: "text", text: "wake\n<!-- OMO_INTERNAL_INITIATOR -->" }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
promptAsync: async () => {
|
||||||
|
promptCalls += 1
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// when
|
||||||
|
const result = await dispatchInternalPrompt({
|
||||||
|
mode: "async",
|
||||||
|
client,
|
||||||
|
sessionID: "ses_internal_tail_tools",
|
||||||
|
input: { path: { id: "ses_internal_tail_tools" }, body: { parts: [] } },
|
||||||
|
source: "test:internal-tail-tools",
|
||||||
|
settleMs: 0,
|
||||||
|
postDispatchHoldMs: 0,
|
||||||
|
})
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(result.status).toBe("active")
|
||||||
|
expect(promptCalls).toBe(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("#given synthetic user tail follows an assistant waiting on tools #when an internal promptAsync is requested #then no prompt is sent", async () => {
|
||||||
|
// given
|
||||||
|
let promptCalls = 0
|
||||||
|
const client = {
|
||||||
|
session: {
|
||||||
|
status: async () => ({ data: { ses_synthetic_tail_tools: { type: "idle" } } }),
|
||||||
|
messages: async () => ({
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
info: { id: "msg_user", role: "user" },
|
||||||
|
parts: [{ type: "text", text: "run work" }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
info: { id: "msg_assistant", role: "assistant", finish: "tool-calls" },
|
||||||
|
parts: [{ type: "tool_use", id: "toolu_pending", state: { status: "running" } }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
info: { id: "msg_synthetic_user", role: "user" },
|
||||||
|
parts: [{ type: "text", text: "continue", synthetic: true }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
promptAsync: async () => {
|
||||||
|
promptCalls += 1
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// when
|
||||||
|
const result = await dispatchInternalPrompt({
|
||||||
|
mode: "async",
|
||||||
|
client,
|
||||||
|
sessionID: "ses_synthetic_tail_tools",
|
||||||
|
input: { path: { id: "ses_synthetic_tail_tools" }, body: { parts: [] } },
|
||||||
|
source: "test:synthetic-tail-tools",
|
||||||
|
settleMs: 0,
|
||||||
|
postDispatchHoldMs: 0,
|
||||||
|
})
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(result.status).toBe("active")
|
||||||
|
expect(promptCalls).toBe(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
test("#given mixed real user tail follows an assistant waiting on tools #when an internal promptAsync is requested #then promptAsync is sent", async () => {
|
||||||
|
// given
|
||||||
|
let promptCalls = 0
|
||||||
|
const client = {
|
||||||
|
session: {
|
||||||
|
status: async () => ({ data: { ses_mixed_tail_tools: { type: "idle" } } }),
|
||||||
|
messages: async () => ({
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
info: { id: "msg_user", role: "user" },
|
||||||
|
parts: [{ type: "text", text: "run work" }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
info: { id: "msg_assistant", role: "assistant", finish: "tool-calls" },
|
||||||
|
parts: [{ type: "tool_use", id: "toolu_pending", state: { status: "running" } }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
info: { id: "msg_mixed_user", role: "user" },
|
||||||
|
parts: [
|
||||||
|
{ type: "text", text: "wake\n<!-- OMO_INTERNAL_INITIATOR -->" },
|
||||||
|
{ type: "text", text: "real user follow-up" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
promptAsync: async () => {
|
||||||
|
promptCalls += 1
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// when
|
||||||
|
const result = await dispatchInternalPrompt({
|
||||||
|
mode: "async",
|
||||||
|
client,
|
||||||
|
sessionID: "ses_mixed_tail_tools",
|
||||||
|
input: { path: { id: "ses_mixed_tail_tools" }, body: { parts: [] } },
|
||||||
|
source: "test:mixed-tail-tools",
|
||||||
|
settleMs: 0,
|
||||||
|
postDispatchHoldMs: 0,
|
||||||
|
})
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(result.status).toBe("dispatched")
|
||||||
|
expect(promptCalls).toBe(1)
|
||||||
|
})
|
||||||
|
|
||||||
test("#given latest assistant turn is waiting on tools #when tool-state check is disabled #then promptAsync is sent", async () => {
|
test("#given latest assistant turn is waiting on tools #when tool-state check is disabled #then promptAsync is sent", async () => {
|
||||||
// given
|
// given
|
||||||
let promptCalls = 0
|
let promptCalls = 0
|
||||||
|
|||||||
@@ -1,4 +1,9 @@
|
|||||||
import { log } from "./logger"
|
import { log } from "./logger"
|
||||||
|
import {
|
||||||
|
isSyntheticOrInternalUserMessage,
|
||||||
|
type InternalInitiatorMessageLike,
|
||||||
|
type InternalInitiatorTextPartLike,
|
||||||
|
} from "./internal-initiator-marker"
|
||||||
import {
|
import {
|
||||||
DEFAULT_SESSION_IDLE_SETTLE_MS,
|
DEFAULT_SESSION_IDLE_SETTLE_MS,
|
||||||
isSessionActive,
|
isSessionActive,
|
||||||
@@ -198,6 +203,48 @@ function messageRole(message: unknown): string | undefined {
|
|||||||
return typeof message.role === "string" ? message.role : undefined
|
return typeof message.role === "string" ? message.role : undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toInternalInitiatorTextPartLike(part: unknown): InternalInitiatorTextPartLike {
|
||||||
|
const result: InternalInitiatorTextPartLike = {}
|
||||||
|
if (!isRecord(part)) {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof part.type === "string") {
|
||||||
|
result.type = part.type
|
||||||
|
}
|
||||||
|
if (typeof part.text === "string") {
|
||||||
|
result.text = part.text
|
||||||
|
}
|
||||||
|
if (typeof part.synthetic === "boolean") {
|
||||||
|
result.synthetic = part.synthetic
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
function toInternalInitiatorMessageLike(message: unknown): InternalInitiatorMessageLike | undefined {
|
||||||
|
if (!isRecord(message)) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
const result: InternalInitiatorMessageLike = {}
|
||||||
|
const info = message.info
|
||||||
|
if (isRecord(info) && typeof info.role === "string") {
|
||||||
|
result.info = { role: info.role }
|
||||||
|
}
|
||||||
|
if (typeof message.role === "string") {
|
||||||
|
result.role = message.role
|
||||||
|
}
|
||||||
|
if (Array.isArray(message.parts)) {
|
||||||
|
result.parts = message.parts.map(toInternalInitiatorTextPartLike)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
function messageIsSyntheticOrInternalUser(message: unknown): boolean {
|
||||||
|
const initiatorMessage = toInternalInitiatorMessageLike(message)
|
||||||
|
return initiatorMessage !== undefined && isSyntheticOrInternalUserMessage(initiatorMessage)
|
||||||
|
}
|
||||||
|
|
||||||
function partIsWaitingOnTool(part: unknown): boolean {
|
function partIsWaitingOnTool(part: unknown): boolean {
|
||||||
if (!isRecord(part)) {
|
if (!isRecord(part)) {
|
||||||
return false
|
return false
|
||||||
@@ -224,6 +271,9 @@ function latestAssistantTurnIsWaitingOnTools(messages: unknown[]): boolean {
|
|||||||
return message.parts.some(partIsWaitingOnTool)
|
return message.parts.some(partIsWaitingOnTool)
|
||||||
}
|
}
|
||||||
if (role === "user") {
|
if (role === "user") {
|
||||||
|
if (messageIsSyntheticOrInternalUser(message)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user