Skip to content
Open
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
20 changes: 20 additions & 0 deletions server/api/chat/agentPromptCreation.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export type AgentPromptSections = {
toolCoordination: string
knowledgeBaseWorkflow: string
publicAgentDiscipline: string
agentQueryCrafting: string
generalExecution: string
Expand All @@ -14,6 +15,23 @@ export const agentPromptSections: AgentPromptSections = {
- Pair every plan update with concrete tool execution; never emit a planning-only turn.
- Example: run \`searchGlobal\` to list escalation IDs, then next turn call \`getSlackRelatedMessages\` per ID.
`.trim(),
knowledgeBaseWorkflow: `
### Knowledge Base Workflow
- Decide first whether the ask actually needs knowledge-base evidence; skip both \`ls\` and \`searchKnowledgeBase\` when other tools or existing context already cover the answer.
- Treat \`ls\` as the structure and metadata tool for KB: use it when the user asks what exists, where something lives, which files match a constraint such as PDF, or when a quick browse will make the next search materially sharper.
- Use \`ls\` alone when the question is about inventory, hierarchy, paths, or metadata rather than document contents.
- Use \`searchKnowledgeBase\` directly when the relevant collection, folder, file, or path is already known from the user query, agent prompt, prior tool output, or previously discovered IDs.
- Use \`ls\` before \`searchKnowledgeBase\` when you need to discover accessible collections, confirm a canonical path, inspect folder/file layout, collect file or folder IDs, or narrow the search to a metadata-defined subset such as PDFs inside a folder.
- \`ls\` and \`searchKnowledgeBase\` are complementary, not a mandatory pair; chain them only when browsing will materially improve the next search.
- Feel free to call \`ls\` in between turns or anytime if you think it would help sharpen scope, confirm structure, or avoid wasted KB searching.
- Keep \`ls\` cheap by default: start with \`depth: 1\` and \`metadata: false\`; increase depth or enable metadata only when the task needs deeper traversal or row details such as \`mime_type\`, timestamps, descriptions, or collection metadata.
- Put structural scoping in \`filters.targets\`, not inside the free-text query. \`targets\` can union multiple relevant KB locations inside the current allowed scope, including exact file IDs discovered from \`ls\`.
- Examples:
- Structure-only ask: answer "what is inside \`/Policies\`?" with \`ls({ target: { type: "path", collectionId: "kb-1", path: "/Policies" }, depth: 1, metadata: false })\`; do not call \`searchKnowledgeBase\` if the user only needs the listing.
- Filtered content ask: for "answer only from PDF files in Security policies", first call \`ls({ target: { type: "path", collectionId: "kb-1", path: "/Policies/Security" }, depth: 2, metadata: true })\`, keep only rows whose \`mime_type\` is PDF, then call \`searchKnowledgeBase({ query: "exception approval workflow", filters: { targets: [{ type: "file", fileId: "file-pdf-1" }, { type: "file", fileId: "file-pdf-2" }] }, limit: 5 })\`.
- Known scope ask: if the ask already names the exact KB location, call \`searchKnowledgeBase({ query: "contractor onboarding steps", filters: { targets: [{ type: "path", collectionId: "kb-1", path: "/HR/Onboarding/Checklist.md" }] }, limit: 5 })\`; skip \`ls\`.
- \`ls\` not useful: if the ask is not about KB, or the exact KB scope is already known and browsing will not improve precision, skip \`ls\`.
`.trim(),
publicAgentDiscipline: `
### Public Agent Discipline
- Call \`list_custom_agents\` before delegating; log the evaluation and expect it to return \`null\` when nobody qualifies.
Expand All @@ -35,6 +53,7 @@ export const agentPromptSections: AgentPromptSections = {
- Show proactive drive: after each subtask plan, immediately schedule and run the necessary tools.
- Combine sequential and parallel tool usage when safe; default to running more than one independent tool per turn.
- Track dependencies explicitly so you never trigger a child tool before its parent results are analyzed.
- Prefer the shortest correct tool path; do not insert browsing or discovery steps when a precise search can answer directly.
`.trim(),
chainOfThought: `
### Chain-of-Thought Commitment
Expand All @@ -50,6 +69,7 @@ export const agentPromptSections: AgentPromptSections = {
export function buildAgentPromptAddendum(): string {
return [
agentPromptSections.toolCoordination,
agentPromptSections.knowledgeBaseWorkflow,
agentPromptSections.publicAgentDiscipline,
agentPromptSections.agentQueryCrafting,
agentPromptSections.generalExecution,
Expand Down
17 changes: 10 additions & 7 deletions server/api/chat/jaf-adapter.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
import { z, type ZodType } from "zod"
import type { Tool } from "@xynehq/jaf"
import { ToolResponse } from "@xynehq/jaf"
import type { MinimalAgentFragment, Citation } from "./types"
import type { AgentRunContext } from "./agent-schemas"
import { answerContextMapFromFragments } from "@/ai/context"
import { getLogger } from "@/logger"
import { Subsystem } from "@/types"
import { Apps } from "@xyne/vespa-ts/types"
import type { Tool } from "@xynehq/jaf"
import { ToolResponse } from "@xynehq/jaf"
import { type ZodType, z } from "zod"
import type { AgentRunContext } from "./agent-schemas"
import type { Citation, MinimalAgentFragment } from "./types"

const Logger = getLogger(Subsystem.Chat).child({ module: "jaf-adapter" })

type ToolSchemaParameters = Tool<unknown, AgentRunContext>["schema"]["parameters"]
type ToolSchemaParameters = Tool<
unknown,
AgentRunContext
>["schema"]["parameters"]

const toToolSchemaParameters = (schema: ZodType): ToolSchemaParameters =>
schema as unknown as ToolSchemaParameters
Expand Down Expand Up @@ -84,7 +87,7 @@ export function buildMCPJAFTools(
)
}
}
Logger.info(
Logger.debug(
{ connectorId, toolName, descLen: (toolDescription || "").length },
"[MCP] Registering tool for JAF agent",
)
Expand Down
297 changes: 297 additions & 0 deletions server/api/chat/jaf-logging.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,297 @@
import { getLoggerWithChild } from "@/logger"
import { Subsystem } from "@/types"
import { getErrorMessage } from "@/utils"
import { type TraceEvent, getTextContent } from "@xynehq/jaf"

const loggerWithChild = getLoggerWithChild(Subsystem.Chat, {
module: "jaf-logging",
})

export type JAFTraceLoggingContext = {
chatId: string
email: string
flow: "MessageAgents" | "DelegatedAgenticRun"
runId: string
}

function truncateValue(value: string, maxLength = 160): string {
if (value.length <= maxLength) return value
return `${value.slice(0, maxLength - 1)}…`
}

function summarizeToolResultPayload(result: any): string {
if (!result) {
return "No result returned."
}
const summaryCandidates: Array<unknown> = [
result?.data?.summary,
result?.data?.result,
]
for (const candidate of summaryCandidates) {
if (typeof candidate === "string" && candidate.trim().length > 0) {
return truncateValue(candidate.trim(), 200)
}
}
if (typeof result?.data === "string") {
return truncateValue(result.data, 200)
}
try {
return truncateValue(JSON.stringify(result?.data ?? result), 200)
} catch {
return "Result unavailable."
}
}

function formatToolArgumentsForLogging(args: Record<string, unknown>): string {
if (!args || typeof args !== "object") {
return "{}"
}
const entries = Object.entries(args)
if (entries.length === 0) {
return "{}"
}
const parts = entries.map(([key, value]) => {
let serialized: string
if (typeof value === "string") {
serialized = `"${truncateValue(value, 80)}"`
} else if (
typeof value === "number" ||
typeof value === "boolean" ||
value === null
) {
serialized = String(value)
} else {
try {
serialized = truncateValue(JSON.stringify(value), 80)
} catch {
serialized = "[unserializable]"
}
}
return `${key}: ${serialized}`
})
const combined = parts.join(", ")
return truncateValue(combined, 400)
}

export function logJAFTraceEvent(
context: JAFTraceLoggingContext,
event: TraceEvent,
): void {
if (event.type === "before_tool_execution") {
return
}

const logger = loggerWithChild({ email: context.email })
const baseLog = {
chatId: context.chatId,
eventType: event.type,
flow: context.flow,
runId: context.runId,
}

switch (event.type) {
case "run_start":
logger.info(baseLog, "[JAF] Run started")
return

case "run_end": {
const outcome = (
event.data as { outcome?: { status?: string; error?: any } }
)?.outcome
const runEndLog = {
...baseLog,
errorDetail:
outcome?.status === "error"
? getErrorMessage(outcome?.error)
: undefined,
errorTag:
outcome?.status === "error" &&
outcome?.error &&
typeof outcome.error === "object" &&
"_tag" in outcome.error
? String((outcome.error as { _tag?: string })._tag)
: undefined,
outcomeStatus: outcome?.status ?? "unknown",
}
if (outcome?.status === "error") {
logger.error(runEndLog, "[JAF] Run ended with error")
} else {
logger.info(runEndLog, "[JAF] Run completed")
}
return
}

case "guardrail_violation":
logger.warn(
{
...baseLog,
reason: event.data.reason,
stage: event.data.stage,
},
"[JAF] Guardrail violation",
)
return

case "handoff_denied":
logger.warn(
{
...baseLog,
from: event.data.from,
reason: event.data.reason,
to: event.data.to,
},
"[JAF] Handoff denied",
)
return

case "decode_error":
logger.error(
{
...baseLog,
errors: event.data.errors,
},
"[JAF] Decode error",
)
return

case "turn_start":
logger.debug(
{
...baseLog,
agentName: event.data.agentName,
turn: event.data.turn,
},
"[JAF] Turn started",
)
return

case "turn_end":
logger.debug(
{
...baseLog,
turn: event.data.turn,
},
"[JAF] Turn ended",
)
return

case "tool_requests":
logger.debug(
{
...baseLog,
toolCount: event.data.toolCalls.length,
toolNames: event.data.toolCalls.map((toolCall) => toolCall.name),
},
"[JAF] Tool requests planned",
)
return

case "tool_call_start":
logger.debug(
{
...baseLog,
args: formatToolArgumentsForLogging(
(event.data.args ?? {}) as Record<string, unknown>,
),
toolName: event.data.toolName,
},
"[JAF] Tool call started",
)
return

case "tool_call_end":
if (event.data.error) {
logger.error(
{
...baseLog,
error: event.data.error,
executionTimeMs: event.data.executionTime,
resultPreview: summarizeToolResultPayload(event.data.result),
status: event.data.status ?? "error",
toolName: event.data.toolName,
},
"[JAF] Tool call failed",
)
} else {
logger.debug(
{
...baseLog,
executionTimeMs: event.data.executionTime,
resultPreview: summarizeToolResultPayload(event.data.result),
status: event.data.status ?? "completed",
toolName: event.data.toolName,
},
"[JAF] Tool call completed",
)
}
return

case "assistant_message": {
const content = getTextContent(event.data.message.content) || ""
logger.debug(
{
...baseLog,
contentLength: content.length,
contentPreview: truncateValue(content, 200),
hasToolCalls:
Array.isArray(event.data.message?.tool_calls) &&
(event.data.message.tool_calls?.length ?? 0) > 0,
},
"[JAF] Assistant message received",
)
return
}

case "token_usage":
logger.debug(
{
...baseLog,
completionTokens: event.data.completion ?? 0,
promptTokens: event.data.prompt ?? 0,
totalTokens: event.data.total ?? 0,
},
"[JAF] Token usage recorded",
)
return

case "clarification_requested":
logger.debug(
{
...baseLog,
clarificationId: event.data.clarificationId,
optionsCount: event.data.options.length,
question: truncateValue(event.data.question, 200),
},
"[JAF] Clarification requested",
)
return

case "clarification_provided":
logger.debug(
{
...baseLog,
clarificationId: event.data.clarificationId,
selectedId: event.data.selectedId,
},
"[JAF] Clarification provided",
)
return

case "final_output":
logger.debug(
{
...baseLog,
outputLength:
typeof event.data.output === "string"
? event.data.output.length
: 0,
},
"[JAF] Final output emitted",
)
return

default:
logger.debug(baseLog, "[JAF] Trace event received")
return
}
}
Loading