mirror of
https://github.com/Kaelio/ktx.git
synced 2026-06-10 08:05:14 +02:00
feat: pass backend-neutral tools to agent runners
This commit is contained in:
parent
73d0f91d3c
commit
43e6822996
31 changed files with 204 additions and 180 deletions
|
|
@ -5,7 +5,7 @@ export interface AgentToolCallOptions {
|
|||
toolCallId?: string;
|
||||
}
|
||||
|
||||
export type AgentToolOutput = string | { markdown: string; structured?: unknown };
|
||||
export type AgentToolOutput = string | { markdown: string; structured?: unknown } | Record<string, unknown>;
|
||||
|
||||
export interface AgentToolDefinition<TInputSchema extends ZodObject<ZodRawShape> = ZodObject<ZodRawShape>> {
|
||||
name: string;
|
||||
|
|
@ -35,7 +35,13 @@ export function assertAgentToolSet(toolSet: AgentToolSet): void {
|
|||
|
||||
export function agentToolOutputToText(output: AgentToolOutput): string {
|
||||
if (output && typeof output === 'object' && 'markdown' in output) {
|
||||
return output.markdown;
|
||||
const markdown = output.markdown;
|
||||
if (typeof markdown === 'string') {
|
||||
return markdown;
|
||||
}
|
||||
}
|
||||
if (output && typeof output === 'object') {
|
||||
return JSON.stringify(output);
|
||||
}
|
||||
return String(output);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { tool } from 'ai';
|
||||
import { z } from 'zod';
|
||||
import { createAgentTool } from '../../../agent/index.js';
|
||||
import { historicSqlEvidencePath, serializeHistoricSqlEvidence } from './evidence.js';
|
||||
import { patternOutputSchema, tableUsageOutputSchema } from './skill-schemas.js';
|
||||
|
||||
|
|
@ -91,12 +91,15 @@ function evidenceEnvelope(input: EmitHistoricSqlEvidenceInput, connectionId: str
|
|||
}
|
||||
|
||||
export function createEmitHistoricSqlEvidenceTool(defaultContext?: EmitHistoricSqlEvidenceToolContext) {
|
||||
return tool({
|
||||
return createAgentTool({
|
||||
name: 'emit_historic_sql_evidence',
|
||||
description:
|
||||
'Record typed historic-SQL evidence for deterministic projection. Use this instead of wiki_write, sl_write_source, sl_edit_source, or context_candidate_write during historic-SQL WorkUnits.',
|
||||
inputSchema: emitHistoricSqlEvidenceInputSchema,
|
||||
execute: async (input, options): Promise<string> => {
|
||||
const context = (options.experimental_context as EmitHistoricSqlEvidenceToolContext | undefined) ?? defaultContext;
|
||||
const context =
|
||||
((options as { experimental_context?: EmitHistoricSqlEvidenceToolContext }).experimental_context ??
|
||||
defaultContext);
|
||||
const ingest = context?.session?.ingest;
|
||||
const configService = context?.session?.configService;
|
||||
if (!ingest || ingest.sourceKey !== 'historic-sql' || !configService || !context?.connectionId) {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { tool } from 'ai';
|
||||
import { z } from 'zod';
|
||||
import { createAgentTool } from '../../../../agent/index.js';
|
||||
import type { ToolOutput } from '../../../../tools/index.js';
|
||||
import { type ParsedTargetTable, stagedLookerQuerySchema } from '../types.js';
|
||||
|
||||
|
|
@ -160,7 +160,8 @@ export function buildLookerSlProposal(raw: LookerQueryToSlInput): LookerSlPropos
|
|||
}
|
||||
|
||||
export function createLookerQueryToSlTool() {
|
||||
return tool({
|
||||
return createAgentTool({
|
||||
name: 'looker_query_to_sl',
|
||||
description:
|
||||
'Given one staged Looker query JSON, return a conservative proposal for SL measures, dimensions, reusable filters, and triage priority. The proposal is advisory; verify with SL tools before writing.',
|
||||
inputSchema: lookerQueryToSlInputSchema,
|
||||
|
|
@ -171,13 +172,6 @@ export function createLookerQueryToSlTool() {
|
|||
structured,
|
||||
};
|
||||
},
|
||||
toModelOutput: ({ output }) => {
|
||||
const markdown =
|
||||
output && typeof output === 'object' && 'markdown' in output
|
||||
? String((output as { markdown: unknown }).markdown)
|
||||
: String(output);
|
||||
return { type: 'content', value: [{ type: 'text', text: markdown }] };
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import type { KtxModelRole } from '@ktx/llm';
|
||||
import type { ToolSet } from 'ai';
|
||||
import type { AgentRunnerService } from '../../agent/index.js';
|
||||
import type { AgentRunnerService, AgentToolSet } from '../../agent/index.js';
|
||||
import { type KtxLogger, noopLogger } from '../../core/index.js';
|
||||
import type { MemoryAction } from '../../memory/index.js';
|
||||
import type { ContextCandidateForDedup, CuratorPaginationPort, CuratorPaginationReport } from '../ports.js';
|
||||
|
|
@ -38,7 +37,7 @@ export interface CuratorPaginationInput {
|
|||
modelRole: KtxModelRole;
|
||||
buildSystemPrompt: () => string;
|
||||
buildUserPrompt: (input: CuratorPaginationPromptInput) => string;
|
||||
buildToolSet: (passNumber: number) => ToolSet;
|
||||
buildToolSet: (passNumber: number) => AgentToolSet;
|
||||
getReconciliationActions: () => MemoryAction[];
|
||||
onStepFinish?: (info: { passNumber: number; stepIndex: number; stepBudget: number }) => void;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -200,7 +200,7 @@ const makeDeps = () => {
|
|||
const slValidator = { validateSingleSource: vi.fn().mockResolvedValue({ errors: [], warnings: [] }) };
|
||||
const toolsetFactory = {
|
||||
createIngestWuToolset: vi.fn().mockReturnValue({
|
||||
toAiSdkTools: vi.fn().mockReturnValue({}),
|
||||
toAgentTools: vi.fn().mockReturnValue({}),
|
||||
getAllTools: vi.fn().mockReturnValue([]),
|
||||
getToolNames: vi.fn().mockReturnValue([]),
|
||||
}),
|
||||
|
|
@ -419,7 +419,7 @@ describe('IngestBundleRunner — Stages 1 → 7', () => {
|
|||
deps.toolsetFactory.createIngestWuToolset.mockImplementation((toolSession: any) => {
|
||||
sessions.push(toolSession);
|
||||
return {
|
||||
toAiSdkTools: vi.fn().mockReturnValue({}),
|
||||
toAgentTools: vi.fn().mockReturnValue({}),
|
||||
getAllTools: vi.fn().mockReturnValue([]),
|
||||
getToolNames: vi.fn().mockReturnValue([]),
|
||||
};
|
||||
|
|
@ -591,7 +591,7 @@ describe('IngestBundleRunner — Stages 1 → 7', () => {
|
|||
deps.toolsetFactory.createIngestWuToolset.mockImplementation((toolSession: any) => {
|
||||
currentToolSession = toolSession;
|
||||
return {
|
||||
toAiSdkTools: vi.fn().mockReturnValue({}),
|
||||
toAgentTools: vi.fn().mockReturnValue({}),
|
||||
getAllTools: vi.fn().mockReturnValue([]),
|
||||
getToolNames: vi.fn().mockReturnValue([]),
|
||||
};
|
||||
|
|
@ -663,7 +663,7 @@ describe('IngestBundleRunner — Stages 1 → 7', () => {
|
|||
deps.toolsetFactory.createIngestWuToolset.mockImplementation((toolSession: any) => {
|
||||
currentToolSession = toolSession;
|
||||
return {
|
||||
toAiSdkTools: vi.fn().mockReturnValue({}),
|
||||
toAgentTools: vi.fn().mockReturnValue({}),
|
||||
getAllTools: vi.fn().mockReturnValue([]),
|
||||
getToolNames: vi.fn().mockReturnValue([]),
|
||||
};
|
||||
|
|
@ -834,7 +834,7 @@ describe('IngestBundleRunner — Stages 1 → 7', () => {
|
|||
it('stores memory-flow provenance and transcript summaries in the ingest report body', async () => {
|
||||
const deps = makeDeps();
|
||||
deps.toolsetFactory.createIngestWuToolset.mockReturnValue({
|
||||
toAiSdkTools: vi.fn().mockReturnValue({
|
||||
toAgentTools: vi.fn().mockReturnValue({
|
||||
read_raw_span: {
|
||||
description: 'read a raw span',
|
||||
inputSchema: {},
|
||||
|
|
@ -1376,7 +1376,7 @@ describe('IngestBundleRunner — Stages 1 → 7', () => {
|
|||
deps.toolsetFactory.createIngestWuToolset.mockImplementation((toolSession: any) => {
|
||||
currentToolSession = toolSession;
|
||||
return {
|
||||
toAiSdkTools: vi.fn().mockReturnValue({}),
|
||||
toAgentTools: vi.fn().mockReturnValue({}),
|
||||
getAllTools: vi.fn().mockReturnValue([]),
|
||||
getToolNames: vi.fn().mockReturnValue([]),
|
||||
};
|
||||
|
|
@ -1933,7 +1933,7 @@ describe('IngestBundleRunner — Stages 1 → 7', () => {
|
|||
deps.toolsetFactory.createIngestWuToolset.mockImplementation((toolSession: any) => {
|
||||
currentToolSession = toolSession;
|
||||
return {
|
||||
toAiSdkTools: vi.fn().mockReturnValue({}),
|
||||
toAgentTools: vi.fn().mockReturnValue({}),
|
||||
getAllTools: vi.fn().mockReturnValue([]),
|
||||
getToolNames: vi.fn().mockReturnValue([]),
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import { mkdir, readFile, rm, writeFile } from 'node:fs/promises';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { type Tool, tool } from 'ai';
|
||||
import pLimit from 'p-limit';
|
||||
import { z } from 'zod';
|
||||
import { createAgentTool, type AgentToolSet } from '../agent/index.js';
|
||||
import { type KtxLogger, noopLogger } from '../core/index.js';
|
||||
import type { CaptureSession, MemoryAction } from '../memory/index.js';
|
||||
import type { SemanticLayerService, SemanticLayerSource, SlValidationDeps } from '../sl/index.js';
|
||||
|
|
@ -694,8 +694,9 @@ export class IngestBundleRunner {
|
|||
};
|
||||
|
||||
const skillsLoadedPerWu: string[] = [];
|
||||
const loadSkillTool: Record<string, Tool> = {
|
||||
load_skill: tool({
|
||||
const loadSkillTool: AgentToolSet = {
|
||||
load_skill: createAgentTool({
|
||||
name: 'load_skill',
|
||||
description:
|
||||
'Load a skill to get specialized instructions. Call this when a skill listed in the system prompt matches the current task.',
|
||||
inputSchema: z.object({ name: z.string() }),
|
||||
|
|
@ -765,7 +766,7 @@ export class IngestBundleRunner {
|
|||
wu: wuInner,
|
||||
loadSkillTool,
|
||||
emitUnmappedFallbackTool: wuEmitUnmappedFallbackTool,
|
||||
toolsetTools: wuToolset.toAiSdkTools(wuToolContext),
|
||||
toolsetTools: wuToolset.toAgentTools(wuToolContext),
|
||||
}),
|
||||
join(transcriptDir, `${wuInner.unitKey}.jsonl`),
|
||||
wuInner.unitKey,
|
||||
|
|
@ -921,8 +922,9 @@ export class IngestBundleRunner {
|
|||
ingest: ingestToolMetadata,
|
||||
session: rcToolSession,
|
||||
};
|
||||
const rcLoadSkill: Record<string, Tool> = {
|
||||
load_skill: tool({
|
||||
const rcLoadSkill: AgentToolSet = {
|
||||
load_skill: createAgentTool({
|
||||
name: 'load_skill',
|
||||
description: 'Load a skill.',
|
||||
inputSchema: z.object({ name: z.string() }),
|
||||
execute: async ({ name }) => {
|
||||
|
|
@ -1026,7 +1028,7 @@ export class IngestBundleRunner {
|
|||
emitArtifactResolutionTool: rcEmitArtifactResolutionTool,
|
||||
emitUnmappedFallbackTool: rcEmitUnmappedFallbackTool,
|
||||
readRawSpanTool: rcRawSpanTool,
|
||||
toolsetTools: rcToolset.toAiSdkTools(rcToolContext),
|
||||
toolsetTools: rcToolset.toAgentTools(rcToolContext),
|
||||
}),
|
||||
join(transcriptDir, 'reconcile.jsonl'),
|
||||
'reconcile',
|
||||
|
|
@ -1075,7 +1077,7 @@ export class IngestBundleRunner {
|
|||
emitArtifactResolutionTool: rcEmitArtifactResolutionTool,
|
||||
emitUnmappedFallbackTool: rcEmitUnmappedFallbackTool,
|
||||
readRawSpanTool: rcRawSpanTool,
|
||||
toolsetTools: rcToolset.toAiSdkTools(rcToolContext),
|
||||
toolsetTools: rcToolset.toAgentTools(rcToolContext),
|
||||
}),
|
||||
join(transcriptDir, 'reconcile.jsonl'),
|
||||
'reconcile',
|
||||
|
|
|
|||
|
|
@ -540,7 +540,7 @@ class LocalIngestToolsetFactory implements IngestToolsetFactoryPort {
|
|||
}
|
||||
|
||||
createIngestWuToolset(session: ToolSession, options?: { includeContextEvidenceTools?: boolean }): IngestToolsetLike {
|
||||
const sourceTools: Record<string, Tool> =
|
||||
const sourceTools: AgentToolSet =
|
||||
session.ingest?.sourceKey === 'historic-sql'
|
||||
? {
|
||||
emit_historic_sql_evidence: createEmitHistoricSqlEvidenceTool({
|
||||
|
|
|
|||
|
|
@ -314,7 +314,7 @@ export interface CuratorPaginationPort {
|
|||
items: ReconcileCandidateForPrompt[];
|
||||
runState: ReconcilePromptRunState;
|
||||
}) => string;
|
||||
buildToolSet: (passNumber: number) => ToolSet;
|
||||
buildToolSet: (passNumber: number) => AgentToolSet;
|
||||
getReconciliationActions: () => MemoryAction[];
|
||||
onStepFinish?: (info: { passNumber: number; stepIndex: number; stepBudget: number }) => void;
|
||||
}): Promise<ReconciliationOutcome & { report: CuratorPaginationReport; warnings: string[] }>;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,16 @@
|
|||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { z } from 'zod';
|
||||
import { createAgentTool } from '../../agent/index.js';
|
||||
import { buildReconcileSystemPrompt, buildReconcileToolSet, buildReconcileUserPrompt } from './build-reconcile-context.js';
|
||||
|
||||
const fakeTool = (name: string) =>
|
||||
createAgentTool({
|
||||
name,
|
||||
description: name,
|
||||
inputSchema: z.object({}),
|
||||
execute: async () => `${name} output`,
|
||||
});
|
||||
|
||||
describe('buildReconcileSystemPrompt', () => {
|
||||
it('appends canonical pins when relevant pins are supplied', () => {
|
||||
const prompt = buildReconcileSystemPrompt({
|
||||
|
|
@ -76,26 +86,16 @@ describe('buildReconcileUserPrompt', () => {
|
|||
describe('buildReconcileToolSet', () => {
|
||||
it('includes emit_unmapped_fallback with the reconciliation tools', () => {
|
||||
const toolSet = buildReconcileToolSet({
|
||||
loadSkillTool: { load_skill: { description: 'load', inputSchema: {} as any, execute: vi.fn() } } as any,
|
||||
stageListTool: { stage_list: { description: 'stage list', inputSchema: {} as any, execute: vi.fn() } } as any,
|
||||
stageDiffTool: { stage_diff: { description: 'stage diff', inputSchema: {} as any, execute: vi.fn() } } as any,
|
||||
evictionListTool: {
|
||||
eviction_list: { description: 'eviction list', inputSchema: {} as any, execute: vi.fn() },
|
||||
} as any,
|
||||
emitConflictResolutionTool: {
|
||||
emit_conflict_resolution: { description: 'conflict', inputSchema: {} as any, execute: vi.fn() },
|
||||
} as any,
|
||||
emitEvictionDecisionTool: {
|
||||
emit_eviction_decision: { description: 'eviction', inputSchema: {} as any, execute: vi.fn() },
|
||||
} as any,
|
||||
emitArtifactResolutionTool: {
|
||||
emit_artifact_resolution: { description: 'resolution', inputSchema: {} as any, execute: vi.fn() },
|
||||
} as any,
|
||||
emitUnmappedFallbackTool: {
|
||||
emit_unmapped_fallback: { description: 'fallback', inputSchema: {} as any, execute: vi.fn() },
|
||||
} as any,
|
||||
readRawSpanTool: { read_raw_span: { description: 'raw span', inputSchema: {} as any, execute: vi.fn() } } as any,
|
||||
toolsetTools: { sl_write_source: {} as any, wiki_write: {} as any },
|
||||
loadSkillTool: { load_skill: fakeTool('load_skill') },
|
||||
stageListTool: { stage_list: fakeTool('stage_list') },
|
||||
stageDiffTool: { stage_diff: fakeTool('stage_diff') },
|
||||
evictionListTool: { eviction_list: fakeTool('eviction_list') },
|
||||
emitConflictResolutionTool: { emit_conflict_resolution: fakeTool('emit_conflict_resolution') },
|
||||
emitEvictionDecisionTool: { emit_eviction_decision: fakeTool('emit_eviction_decision') },
|
||||
emitArtifactResolutionTool: { emit_artifact_resolution: fakeTool('emit_artifact_resolution') },
|
||||
emitUnmappedFallbackTool: { emit_unmapped_fallback: fakeTool('emit_unmapped_fallback') },
|
||||
readRawSpanTool: { read_raw_span: fakeTool('read_raw_span') },
|
||||
toolsetTools: { sl_write_source: fakeTool('sl_write_source'), wiki_write: fakeTool('wiki_write') },
|
||||
});
|
||||
|
||||
expect(Object.keys(toolSet).sort()).toEqual(
|
||||
|
|
@ -114,31 +114,30 @@ describe('buildReconcileToolSet', () => {
|
|||
'wiki_write',
|
||||
].sort(),
|
||||
);
|
||||
expect(toolSet.record_verification_ledger.inputSchema).toBeInstanceOf(z.ZodObject);
|
||||
expect(toolSet.emit_conflict_resolution.name).toBe('emit_conflict_resolution');
|
||||
});
|
||||
|
||||
it('requires the verification ledger before reconciliation write tools run', async () => {
|
||||
const slWrite = vi.fn().mockResolvedValue({ markdown: 'written', structured: { success: true } });
|
||||
const toolSet = buildReconcileToolSet({
|
||||
loadSkillTool: { load_skill: { description: 'load', inputSchema: {} as any, execute: vi.fn() } } as any,
|
||||
stageListTool: { stage_list: { description: 'stage list', inputSchema: {} as any, execute: vi.fn() } } as any,
|
||||
stageDiffTool: { stage_diff: { description: 'stage diff', inputSchema: {} as any, execute: vi.fn() } } as any,
|
||||
evictionListTool: {
|
||||
eviction_list: { description: 'eviction list', inputSchema: {} as any, execute: vi.fn() },
|
||||
} as any,
|
||||
emitConflictResolutionTool: {
|
||||
emit_conflict_resolution: { description: 'conflict', inputSchema: {} as any, execute: vi.fn() },
|
||||
} as any,
|
||||
emitEvictionDecisionTool: {
|
||||
emit_eviction_decision: { description: 'eviction', inputSchema: {} as any, execute: vi.fn() },
|
||||
} as any,
|
||||
emitArtifactResolutionTool: {
|
||||
emit_artifact_resolution: { description: 'resolution', inputSchema: {} as any, execute: vi.fn() },
|
||||
} as any,
|
||||
emitUnmappedFallbackTool: {
|
||||
emit_unmapped_fallback: { description: 'fallback', inputSchema: {} as any, execute: vi.fn() },
|
||||
} as any,
|
||||
readRawSpanTool: { read_raw_span: { description: 'raw span', inputSchema: {} as any, execute: vi.fn() } } as any,
|
||||
toolsetTools: { sl_write_source: { description: 'sl write', inputSchema: {} as any, execute: slWrite } as any },
|
||||
loadSkillTool: { load_skill: fakeTool('load_skill') },
|
||||
stageListTool: { stage_list: fakeTool('stage_list') },
|
||||
stageDiffTool: { stage_diff: fakeTool('stage_diff') },
|
||||
evictionListTool: { eviction_list: fakeTool('eviction_list') },
|
||||
emitConflictResolutionTool: { emit_conflict_resolution: fakeTool('emit_conflict_resolution') },
|
||||
emitEvictionDecisionTool: { emit_eviction_decision: fakeTool('emit_eviction_decision') },
|
||||
emitArtifactResolutionTool: { emit_artifact_resolution: fakeTool('emit_artifact_resolution') },
|
||||
emitUnmappedFallbackTool: { emit_unmapped_fallback: fakeTool('emit_unmapped_fallback') },
|
||||
readRawSpanTool: { read_raw_span: fakeTool('read_raw_span') },
|
||||
toolsetTools: {
|
||||
sl_write_source: createAgentTool({
|
||||
name: 'sl_write_source',
|
||||
description: 'sl write',
|
||||
inputSchema: z.object({ connectionId: z.string(), sourceName: z.string() }),
|
||||
execute: slWrite,
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
const correction = await toolSet.sl_write_source.execute?.(
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import type { Tool, ToolSet } from 'ai';
|
||||
import type { AgentToolSet } from '../../agent/index.js';
|
||||
import { buildCanonicalPinsPromptBlock, type CanonicalPin } from '../canonical-pins.js';
|
||||
import {
|
||||
createVerificationLedgerState,
|
||||
|
|
@ -181,19 +181,19 @@ export function buildReconcileUserPrompt(
|
|||
}
|
||||
|
||||
export interface ReconcileToolSetInput {
|
||||
loadSkillTool: Record<string, Tool>;
|
||||
stageListTool: Record<string, Tool>;
|
||||
stageDiffTool: Record<string, Tool>;
|
||||
evictionListTool: Record<string, Tool>;
|
||||
emitConflictResolutionTool: Record<string, Tool>;
|
||||
emitEvictionDecisionTool: Record<string, Tool>;
|
||||
emitArtifactResolutionTool: Record<string, Tool>;
|
||||
emitUnmappedFallbackTool: Record<string, Tool>;
|
||||
readRawSpanTool: Record<string, Tool>;
|
||||
toolsetTools: ToolSet;
|
||||
loadSkillTool: AgentToolSet;
|
||||
stageListTool: AgentToolSet;
|
||||
stageDiffTool: AgentToolSet;
|
||||
evictionListTool: AgentToolSet;
|
||||
emitConflictResolutionTool: AgentToolSet;
|
||||
emitEvictionDecisionTool: AgentToolSet;
|
||||
emitArtifactResolutionTool: AgentToolSet;
|
||||
emitUnmappedFallbackTool: AgentToolSet;
|
||||
readRawSpanTool: AgentToolSet;
|
||||
toolsetTools: AgentToolSet;
|
||||
}
|
||||
|
||||
export function buildReconcileToolSet(input: ReconcileToolSetInput): ToolSet {
|
||||
export function buildReconcileToolSet(input: ReconcileToolSetInput): AgentToolSet {
|
||||
const state = createVerificationLedgerState();
|
||||
return withVerificationLedger(
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,6 +1,16 @@
|
|||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { z } from 'zod';
|
||||
import { createAgentTool } from '../../agent/index.js';
|
||||
import { buildWuSystemPrompt, buildWuToolSet, buildWuUserPrompt } from './build-wu-context.js';
|
||||
|
||||
const fakeTool = (name: string) =>
|
||||
createAgentTool({
|
||||
name,
|
||||
description: name,
|
||||
inputSchema: z.object({}),
|
||||
execute: async () => `${name} output`,
|
||||
});
|
||||
|
||||
describe('buildWuUserPrompt', () => {
|
||||
it('includes rawFiles, dependencyPaths, peerFileIndex, and priorProvenance when present', () => {
|
||||
const prompt = buildWuUserPrompt({
|
||||
|
|
@ -56,11 +66,9 @@ describe('buildWuToolSet', () => {
|
|||
const toolSet = buildWuToolSet({
|
||||
stagedDir: '/tmp/staged',
|
||||
wu: { unitKey: 'u1', rawFiles: ['a.yml'], peerFileIndex: [], dependencyPaths: ['dep.yml'] },
|
||||
loadSkillTool: { load_skill: { description: 'load', inputSchema: {} as any, execute: vi.fn() } } as any,
|
||||
emitUnmappedFallbackTool: {
|
||||
emit_unmapped_fallback: { description: 'fallback', inputSchema: {} as any, execute: vi.fn() },
|
||||
} as any,
|
||||
toolsetTools: { wiki_search: {} as any, sl_write_source: {} as any },
|
||||
loadSkillTool: { load_skill: fakeTool('load_skill') },
|
||||
emitUnmappedFallbackTool: { emit_unmapped_fallback: fakeTool('emit_unmapped_fallback') },
|
||||
toolsetTools: { wiki_write: fakeTool('wiki_write') },
|
||||
});
|
||||
expect(Object.keys(toolSet).sort()).toEqual(
|
||||
[
|
||||
|
|
@ -69,10 +77,11 @@ describe('buildWuToolSet', () => {
|
|||
'read_raw_file',
|
||||
'read_raw_span',
|
||||
'record_verification_ledger',
|
||||
'sl_write_source',
|
||||
'wiki_search',
|
||||
'wiki_write',
|
||||
].sort(),
|
||||
);
|
||||
expect(toolSet.record_verification_ledger.inputSchema).toBeInstanceOf(z.ZodObject);
|
||||
expect(toolSet.wiki_write.name).toBe('wiki_write');
|
||||
});
|
||||
|
||||
it('requires the verification ledger before write-capable tools run', async () => {
|
||||
|
|
@ -80,11 +89,16 @@ describe('buildWuToolSet', () => {
|
|||
const toolSet = buildWuToolSet({
|
||||
stagedDir: '/tmp/staged',
|
||||
wu: { unitKey: 'u1', rawFiles: ['a.yml'], peerFileIndex: [], dependencyPaths: [] },
|
||||
loadSkillTool: { load_skill: { description: 'load', inputSchema: {} as any, execute: vi.fn() } } as any,
|
||||
emitUnmappedFallbackTool: {
|
||||
emit_unmapped_fallback: { description: 'fallback', inputSchema: {} as any, execute: vi.fn() },
|
||||
} as any,
|
||||
toolsetTools: { wiki_write: { description: 'write', inputSchema: {} as any, execute: wikiWrite } as any },
|
||||
loadSkillTool: { load_skill: fakeTool('load_skill') },
|
||||
emitUnmappedFallbackTool: { emit_unmapped_fallback: fakeTool('emit_unmapped_fallback') },
|
||||
toolsetTools: {
|
||||
wiki_write: createAgentTool({
|
||||
name: 'wiki_write',
|
||||
description: 'write',
|
||||
inputSchema: z.object({ key: z.string() }),
|
||||
execute: wikiWrite,
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
const correction = await toolSet.wiki_write.execute?.({ key: 'customer-rules' }, { toolCallId: 't1' } as any);
|
||||
|
|
@ -112,11 +126,9 @@ describe('buildWuToolSet', () => {
|
|||
sourceKey: 'looker',
|
||||
stagedDir: '/tmp/staged',
|
||||
wu: { unitKey: 'looker-look-20', rawFiles: ['looks/20.json'], peerFileIndex: [], dependencyPaths: [] },
|
||||
loadSkillTool: { load_skill: { description: 'load', inputSchema: {} as any, execute: vi.fn() } } as any,
|
||||
emitUnmappedFallbackTool: {
|
||||
emit_unmapped_fallback: { description: 'fallback', inputSchema: {} as any, execute: vi.fn() },
|
||||
} as any,
|
||||
toolsetTools: { wiki_search: {} as any, sl_write_source: {} as any },
|
||||
loadSkillTool: { load_skill: fakeTool('load_skill') },
|
||||
emitUnmappedFallbackTool: { emit_unmapped_fallback: fakeTool('emit_unmapped_fallback') },
|
||||
toolsetTools: { wiki_search: fakeTool('wiki_search'), sl_write_source: fakeTool('sl_write_source') },
|
||||
});
|
||||
|
||||
expect(Object.keys(toolSet).sort()).toEqual(
|
||||
|
|
@ -138,11 +150,9 @@ describe('buildWuToolSet', () => {
|
|||
sourceKey: 'metabase',
|
||||
stagedDir: '/tmp/staged',
|
||||
wu: { unitKey: 'metabase-col-1', rawFiles: ['cards/1.json'], peerFileIndex: [], dependencyPaths: [] },
|
||||
loadSkillTool: { load_skill: { description: 'load', inputSchema: {} as any, execute: vi.fn() } } as any,
|
||||
emitUnmappedFallbackTool: {
|
||||
emit_unmapped_fallback: { description: 'fallback', inputSchema: {} as any, execute: vi.fn() },
|
||||
} as any,
|
||||
toolsetTools: { wiki_search: {} as any, sl_write_source: {} as any },
|
||||
loadSkillTool: { load_skill: fakeTool('load_skill') },
|
||||
emitUnmappedFallbackTool: { emit_unmapped_fallback: fakeTool('emit_unmapped_fallback') },
|
||||
toolsetTools: { wiki_search: fakeTool('wiki_search'), sl_write_source: fakeTool('sl_write_source') },
|
||||
});
|
||||
|
||||
expect(Object.keys(toolSet)).not.toContain('looker_query_to_sl');
|
||||
|
|
@ -160,15 +170,13 @@ describe('buildWuToolSet', () => {
|
|||
slDisallowed: true,
|
||||
slDisallowedReason: 'lookml_connection_mismatch',
|
||||
},
|
||||
loadSkillTool: { load_skill: { description: 'load', inputSchema: {} as any, execute: vi.fn() } } as any,
|
||||
emitUnmappedFallbackTool: {
|
||||
emit_unmapped_fallback: { description: 'fallback', inputSchema: {} as any, execute: vi.fn() },
|
||||
} as any,
|
||||
loadSkillTool: { load_skill: fakeTool('load_skill') },
|
||||
emitUnmappedFallbackTool: { emit_unmapped_fallback: fakeTool('emit_unmapped_fallback') },
|
||||
toolsetTools: {
|
||||
sl_write_source: {} as any,
|
||||
sl_edit_source: {} as any,
|
||||
sl_read_source: {} as any,
|
||||
wiki_search: {} as any,
|
||||
sl_write_source: fakeTool('sl_write_source'),
|
||||
sl_edit_source: fakeTool('sl_edit_source'),
|
||||
sl_read_source: fakeTool('sl_read_source'),
|
||||
wiki_search: fakeTool('wiki_search'),
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import type { Tool, ToolSet } from 'ai';
|
||||
import type { AgentToolSet } from '../../agent/index.js';
|
||||
import { buildCanonicalPinsPromptBlock, type CanonicalPin } from '../canonical-pins.js';
|
||||
import { createLookerQueryToSlTool } from '../adapters/looker/tools/looker-query-to-sl.tool.js';
|
||||
import type { IngestProvenanceRow } from '../ports.js';
|
||||
|
|
@ -88,12 +88,12 @@ export interface BuildWuToolSetInput {
|
|||
sourceKey?: string;
|
||||
stagedDir: string;
|
||||
wu: WorkUnit;
|
||||
loadSkillTool: Record<string, Tool>;
|
||||
emitUnmappedFallbackTool: Record<string, Tool>;
|
||||
toolsetTools: ToolSet;
|
||||
loadSkillTool: AgentToolSet;
|
||||
emitUnmappedFallbackTool: AgentToolSet;
|
||||
toolsetTools: AgentToolSet;
|
||||
}
|
||||
|
||||
function withoutWriteSlTools(toolset: ToolSet, wu: WorkUnit): ToolSet {
|
||||
function withoutWriteSlTools(toolset: AgentToolSet, wu: WorkUnit): AgentToolSet {
|
||||
if (!wu.slDisallowed) {
|
||||
return toolset;
|
||||
}
|
||||
|
|
@ -103,9 +103,10 @@ function withoutWriteSlTools(toolset: ToolSet, wu: WorkUnit): ToolSet {
|
|||
return next;
|
||||
}
|
||||
|
||||
export function buildWuToolSet(input: BuildWuToolSetInput): ToolSet {
|
||||
export function buildWuToolSet(input: BuildWuToolSetInput): AgentToolSet {
|
||||
const allowedPaths = new Set<string>([...input.wu.rawFiles, ...input.wu.dependencyPaths]);
|
||||
const lookerTools: ToolSet = input.sourceKey === 'looker' ? { looker_query_to_sl: createLookerQueryToSlTool() } : {};
|
||||
const lookerTools: AgentToolSet =
|
||||
input.sourceKey === 'looker' ? { looker_query_to_sl: createLookerQueryToSlTool() } : {};
|
||||
const state = createVerificationLedgerState();
|
||||
return withVerificationLedger(
|
||||
withoutWriteSlTools(
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import type { AgentRunnerService } from '@ktx/context/agent';
|
||||
import type { AgentRunnerService, AgentToolSet } from '@ktx/context/agent';
|
||||
import type { KtxModelRole } from '@ktx/llm';
|
||||
import type { Tool } from 'ai';
|
||||
import type { CaptureSession, MemoryAction } from '../../memory/index.js';
|
||||
import { listTouchedSlSources, type TouchedSlSource } from '../../tools/index.js';
|
||||
import type { WorkUnit } from '../types.js';
|
||||
|
|
@ -19,7 +18,7 @@ export interface WorkUnitExecutionDeps {
|
|||
resetHardTo: (targetSha: string) => Promise<void>;
|
||||
buildSystemPrompt: (wu: WorkUnit) => string;
|
||||
buildUserPrompt: (wu: WorkUnit) => string;
|
||||
buildToolSet: (wu: WorkUnit) => Record<string, Tool>;
|
||||
buildToolSet: (wu: WorkUnit) => AgentToolSet;
|
||||
captureSession: CaptureSession;
|
||||
sessionActions: MemoryAction[];
|
||||
modelRole: KtxModelRole;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import type { AgentRunnerService } from '@ktx/context/agent';
|
||||
import type { AgentRunnerService, AgentToolSet } from '@ktx/context/agent';
|
||||
import type { KtxModelRole } from '@ktx/llm';
|
||||
import type { ToolSet } from 'ai';
|
||||
import type { EvictionUnit } from '../types.js';
|
||||
import type { StageIndex } from './stage-index.types.js';
|
||||
|
||||
|
|
@ -10,7 +9,7 @@ export interface ReconciliationContext {
|
|||
agentRunner: AgentRunnerService;
|
||||
buildSystemPrompt: (idx: StageIndex, ev: EvictionUnit | undefined) => string;
|
||||
buildUserPrompt: (idx: StageIndex, ev: EvictionUnit | undefined) => string;
|
||||
buildToolSet: () => ToolSet;
|
||||
buildToolSet: () => AgentToolSet;
|
||||
modelRole: KtxModelRole;
|
||||
stepBudget: number;
|
||||
sourceKey: string;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { tool } from 'ai';
|
||||
import { z } from 'zod';
|
||||
import { createAgentTool } from '../../agent/index.js';
|
||||
import type { ArtifactResolutionRecord, StageIndex } from '../stages/stage-index.types.js';
|
||||
|
||||
interface EmitArtifactResolutionDeps {
|
||||
|
|
@ -17,7 +17,8 @@ function sameArtifactResolution(left: ArtifactResolutionRecord, right: ArtifactR
|
|||
}
|
||||
|
||||
export function createEmitArtifactResolutionTool(deps: EmitArtifactResolutionDeps) {
|
||||
return tool({
|
||||
return createAgentTool({
|
||||
name: 'emit_artifact_resolution',
|
||||
description:
|
||||
'Record one explicit artifact resolution for ingest provenance. Use when reconciliation merges or subsumes an artifact without creating a new wiki or SL write action.',
|
||||
inputSchema: z.object({
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { tool } from 'ai';
|
||||
import { z } from 'zod';
|
||||
import { createAgentTool } from '../../agent/index.js';
|
||||
import type { ConflictResolvedRecord, StageIndex } from '../stages/stage-index.types.js';
|
||||
|
||||
interface EmitConflictResolutionDeps {
|
||||
|
|
@ -7,7 +7,8 @@ interface EmitConflictResolutionDeps {
|
|||
}
|
||||
|
||||
export function createEmitConflictResolutionTool(deps: EmitConflictResolutionDeps) {
|
||||
return tool({
|
||||
return createAgentTool({
|
||||
name: 'emit_conflict_resolution',
|
||||
description:
|
||||
'Record one conflict resolution decision for the final IngestReport. Call after resolving or flagging a cross-WorkUnit conflict.',
|
||||
inputSchema: z.object({
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { tool } from 'ai';
|
||||
import { z } from 'zod';
|
||||
import { createAgentTool } from '../../agent/index.js';
|
||||
import type { EvictionAppliedRecord, StageIndex } from '../stages/stage-index.types.js';
|
||||
|
||||
interface EmitEvictionDecisionDeps {
|
||||
|
|
@ -15,7 +15,8 @@ function sameEvictionArtifact(left: EvictionAppliedRecord, right: EvictionApplie
|
|||
|
||||
export function createEmitEvictionDecisionTool(deps: EmitEvictionDecisionDeps) {
|
||||
const allowedPaths = new Set(deps.deletedRawPaths);
|
||||
return tool({
|
||||
return createAgentTool({
|
||||
name: 'emit_eviction_decision',
|
||||
description:
|
||||
'Record one eviction decision for the final IngestReport. The rawPath must come from the current Eviction Set.',
|
||||
inputSchema: z.object({
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import type { Tool } from 'ai';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import type { AgentToolDefinition } from '../../agent/index.js';
|
||||
import type { StageIndex } from '../stages/stage-index.types.js';
|
||||
import { createEmitArtifactResolutionTool } from './emit-artifact-resolution.tool.js';
|
||||
import { createEmitConflictResolutionTool } from './emit-conflict-resolution.tool.js';
|
||||
|
|
@ -17,11 +17,8 @@ function makeStageIndex(): StageIndex {
|
|||
};
|
||||
}
|
||||
|
||||
async function executeTool<Input>(tool: Tool<Input, string>, input: NoInfer<Input>) {
|
||||
if (!tool.execute) {
|
||||
throw new Error('tool is not executable');
|
||||
}
|
||||
return (await tool.execute(input, { toolCallId: 'tool-call-1', messages: [] })) as string;
|
||||
async function executeTool<Input>(tool: AgentToolDefinition<any>, input: NoInfer<Input>) {
|
||||
return (await tool.execute(input, { toolCallId: 'tool-call-1' })) as string;
|
||||
}
|
||||
|
||||
describe('reconciliation emit tools', () => {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { tool } from 'ai';
|
||||
import { z } from 'zod';
|
||||
import { createAgentTool } from '../../agent/index.js';
|
||||
import type { StageIndex, UnmappedFallbackRecord, UnmappedFallbackReason } from '../stages/stage-index.types.js';
|
||||
|
||||
interface EmitUnmappedFallbackDeps {
|
||||
|
|
@ -61,7 +61,8 @@ function requiresMissingTableValidation(reason: UnmappedFallbackReason): boolean
|
|||
}
|
||||
|
||||
export function createEmitUnmappedFallbackTool(deps: EmitUnmappedFallbackDeps) {
|
||||
return tool({
|
||||
return createAgentTool({
|
||||
name: 'emit_unmapped_fallback',
|
||||
description:
|
||||
'Record one unmapped fallback decision for the final IngestReport. The rawPath must be available to the current ingest stage. The tool generates the canonical detail from the structured reason and optional tableRef; use clarification only to add context that does not contradict the reason code.',
|
||||
inputSchema: z.object({
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { tool } from 'ai';
|
||||
import { z } from 'zod';
|
||||
import { createAgentTool } from '../../agent/index.js';
|
||||
import type { IngestProvenancePort } from '../ports.js';
|
||||
|
||||
export interface EvictionListDeps {
|
||||
|
|
@ -10,7 +10,8 @@ export interface EvictionListDeps {
|
|||
}
|
||||
|
||||
export function createEvictionListTool(deps: EvictionListDeps) {
|
||||
return tool({
|
||||
return createAgentTool({
|
||||
name: 'eviction_list',
|
||||
description:
|
||||
'List every artifact that the most recent completed sync produced from a now-deleted raw file. Remove each listed artifact and record the decision with emit_eviction_decision so the ingest report lists every deleted-source decision.',
|
||||
inputSchema: z.object({}),
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { readFile, stat } from 'node:fs/promises';
|
||||
import { join, normalize, resolve } from 'node:path';
|
||||
import { tool } from 'ai';
|
||||
import { z } from 'zod';
|
||||
import { createAgentTool } from '../../agent/index.js';
|
||||
|
||||
interface ReadRawFileDeps {
|
||||
stagedDir: string;
|
||||
|
|
@ -12,7 +12,8 @@ const MAX_READ_RAW_FILE_BYTES = 120_000;
|
|||
|
||||
export function createReadRawFileTool(deps: ReadRawFileDeps) {
|
||||
const stagedRoot = resolve(deps.stagedDir);
|
||||
return tool({
|
||||
return createAgentTool({
|
||||
name: 'read_raw_file',
|
||||
description:
|
||||
"Read the full text content of a raw source file inside this WorkUnit. `path` must be relative to the staged bundle root (no leading slash, no `..`) and must appear in the WorkUnit's rawFiles or dependencyPaths list.",
|
||||
inputSchema: z.object({
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { readFile } from 'node:fs/promises';
|
||||
import { join, normalize, resolve } from 'node:path';
|
||||
import { tool } from 'ai';
|
||||
import { z } from 'zod';
|
||||
import { createAgentTool } from '../../agent/index.js';
|
||||
|
||||
interface ReadRawSpanDeps {
|
||||
stagedDir: string;
|
||||
|
|
@ -10,7 +10,8 @@ interface ReadRawSpanDeps {
|
|||
|
||||
export function createReadRawSpanTool(deps: ReadRawSpanDeps) {
|
||||
const stagedRoot = resolve(deps.stagedDir);
|
||||
return tool({
|
||||
return createAgentTool({
|
||||
name: 'read_raw_span',
|
||||
description:
|
||||
'Read a 1-based inclusive line range from a raw source file. Use this to resolve a provenance pointer like `file.lkml#L15-28` without loading the whole file into context.',
|
||||
inputSchema: z.object({
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { tool } from 'ai';
|
||||
import { z } from 'zod';
|
||||
import { createAgentTool } from '../../agent/index.js';
|
||||
import { memoryActionIdentity } from '../action-identity.js';
|
||||
import type { StageIndex } from '../stages/stage-index.types.js';
|
||||
|
||||
|
|
@ -8,7 +8,8 @@ export interface StageDiffDeps {
|
|||
}
|
||||
|
||||
export function createStageDiffTool(deps: StageDiffDeps) {
|
||||
return tool({
|
||||
return createAgentTool({
|
||||
name: 'stage_diff',
|
||||
description:
|
||||
'Compare two WorkUnits by their writes. SL writes overlap only when target connection and artifact key both match; same-key SL actions on different target connections are non-overlapping.',
|
||||
inputSchema: z.object({
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { tool } from 'ai';
|
||||
import { z } from 'zod';
|
||||
import { createAgentTool } from '../../agent/index.js';
|
||||
import type { StageIndex } from '../stages/stage-index.types.js';
|
||||
|
||||
export interface StageListDeps {
|
||||
|
|
@ -11,7 +11,8 @@ function formatActionDetail(detail: string): string {
|
|||
}
|
||||
|
||||
export function createStageListTool(deps: StageListDeps) {
|
||||
return tool({
|
||||
return createAgentTool({
|
||||
name: 'stage_list',
|
||||
description:
|
||||
'List every write made by Stage 3 WorkUnits in this job. Each entry has the unitKey, raw files, and the action set (SL sources touched, wiki pages written).',
|
||||
inputSchema: z.object({}),
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { appendFile, mkdir } from 'node:fs/promises';
|
||||
import { dirname } from 'node:path';
|
||||
import type { ToolExecuteFunction, ToolExecutionOptions, ToolSet } from 'ai';
|
||||
import type { AgentToolCallOptions, AgentToolSet } from '../../agent/index.js';
|
||||
|
||||
export interface ToolCallLogEntry {
|
||||
ts: string;
|
||||
|
|
@ -31,7 +31,7 @@ interface ToolCallLoggerOptions {
|
|||
* sequential (`generateText` awaits each tool result), so per-WU files are
|
||||
* effectively single-writer and lines land in call order.
|
||||
*/
|
||||
export function wrapToolsWithLogger<T extends ToolSet>(
|
||||
export function wrapToolsWithLogger<T extends AgentToolSet>(
|
||||
tools: T,
|
||||
logFilePath: string,
|
||||
wuKey: string,
|
||||
|
|
@ -44,13 +44,10 @@ export function wrapToolsWithLogger<T extends ToolSet>(
|
|||
wrapped[name] = original;
|
||||
continue;
|
||||
}
|
||||
const wrappedExecute: ToolExecuteFunction<unknown, unknown> = async (
|
||||
input: unknown,
|
||||
opts: ToolExecutionOptions,
|
||||
) => {
|
||||
const wrappedExecute = async (input: never, opts: AgentToolCallOptions) => {
|
||||
const start = Date.now();
|
||||
try {
|
||||
const output = await (originalExecute as ToolExecuteFunction<unknown, unknown>)(input, opts);
|
||||
const output = await originalExecute(input, opts);
|
||||
const entry: ToolCallLogEntry = {
|
||||
ts: new Date().toISOString(),
|
||||
wuKey,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { tool, type ToolExecuteFunction, type ToolExecutionOptions, type ToolSet } from 'ai';
|
||||
import { z } from 'zod';
|
||||
import { createAgentTool, type AgentToolDefinition, type AgentToolSet } from '../../agent/index.js';
|
||||
|
||||
const verificationLedgerInputSchema = z.object({
|
||||
summary: z.string().min(1).max(2000),
|
||||
|
|
@ -37,31 +37,31 @@ export function createVerificationLedgerState(): VerificationLedgerState {
|
|||
return { entries: [] };
|
||||
}
|
||||
|
||||
export function withVerificationLedger(tools: ToolSet, state: VerificationLedgerState): ToolSet {
|
||||
const wrapped: ToolSet = {};
|
||||
export function withVerificationLedger(tools: AgentToolSet, state: VerificationLedgerState): AgentToolSet {
|
||||
const wrapped: AgentToolSet = {};
|
||||
for (const [name, original] of Object.entries(tools)) {
|
||||
if (!WRITE_TOOL_NAMES.has(name) || typeof original.execute !== 'function') {
|
||||
if (!WRITE_TOOL_NAMES.has(name)) {
|
||||
wrapped[name] = original;
|
||||
continue;
|
||||
}
|
||||
const originalExecute = original.execute;
|
||||
const guardedExecute: ToolExecuteFunction<unknown, unknown> = async (
|
||||
input: unknown,
|
||||
opts: ToolExecutionOptions,
|
||||
) => {
|
||||
if (state.entries.length === 0) {
|
||||
return verificationRequiredOutput(name);
|
||||
}
|
||||
return (originalExecute as ToolExecuteFunction<unknown, unknown>)(input, opts);
|
||||
const guardedTool: AgentToolDefinition<any> = {
|
||||
...original,
|
||||
execute: async (input, options) => {
|
||||
if (state.entries.length === 0) {
|
||||
return verificationRequiredOutput(name);
|
||||
}
|
||||
return original.execute(input, options);
|
||||
},
|
||||
};
|
||||
wrapped[name] = { ...original, execute: guardedExecute };
|
||||
wrapped[name] = guardedTool;
|
||||
}
|
||||
wrapped.record_verification_ledger = createRecordVerificationLedgerTool(state);
|
||||
return wrapped;
|
||||
}
|
||||
|
||||
function createRecordVerificationLedgerTool(state: VerificationLedgerState) {
|
||||
return tool({
|
||||
return createAgentTool({
|
||||
name: 'record_verification_ledger',
|
||||
description:
|
||||
'Record the pre-write verification ledger required by loaded ingest skills. Call this before wiki/SL/fallback writes to state what was verified, which tool calls support it, and what remains intentionally unverified.',
|
||||
inputSchema: verificationLedgerInputSchema,
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ describe('local KTX LLM config', () => {
|
|||
},
|
||||
models: { default: 'env:KTX_MODEL', triage: 'anthropic/claude-haiku-4-5' },
|
||||
promptCaching: { enabled: false },
|
||||
agentRunner: { backend: 'ai-sdk' },
|
||||
};
|
||||
|
||||
expect(
|
||||
|
|
@ -45,6 +46,7 @@ describe('local KTX LLM config', () => {
|
|||
},
|
||||
models: { default: 'env:KTX_MODEL' },
|
||||
promptCaching: { enabled: true, vertexFallbackTo5m: true },
|
||||
agentRunner: { backend: 'ai-sdk' },
|
||||
};
|
||||
|
||||
expect(
|
||||
|
|
@ -69,6 +71,7 @@ describe('local KTX LLM config', () => {
|
|||
vertex: { location: 'env:MISSING_VERTEX_LOCATION' },
|
||||
},
|
||||
models: { default: 'claude-sonnet-4-6' },
|
||||
agentRunner: { backend: 'ai-sdk' },
|
||||
};
|
||||
|
||||
expect(
|
||||
|
|
@ -88,6 +91,7 @@ describe('local KTX LLM config', () => {
|
|||
createLocalKtxLlmProviderFromConfig({
|
||||
provider: { backend: 'none' },
|
||||
models: {},
|
||||
agentRunner: { backend: 'ai-sdk' },
|
||||
}),
|
||||
).toBeNull();
|
||||
});
|
||||
|
|
@ -101,6 +105,7 @@ describe('local KTX LLM config', () => {
|
|||
anthropic: { api_key: 'env:ANTHROPIC_API_KEY' }, // pragma: allowlist secret
|
||||
},
|
||||
models: { default: 'claude-sonnet-4-6' },
|
||||
agentRunner: { backend: 'ai-sdk' },
|
||||
},
|
||||
{ env: { ANTHROPIC_API_KEY: 'sk-ant-test' }, createKtxLlmProvider }, // pragma: allowlist secret
|
||||
);
|
||||
|
|
@ -121,6 +126,7 @@ describe('local KTX LLM config', () => {
|
|||
gateway: { base_url: 'https://gateway.example/v1' },
|
||||
},
|
||||
models: { default: 'anthropic/claude-sonnet-4-6' },
|
||||
agentRunner: { backend: 'ai-sdk' },
|
||||
});
|
||||
|
||||
expect(provider?.promptCachingConfig()).toMatchObject({
|
||||
|
|
|
|||
|
|
@ -617,6 +617,7 @@ describe('createLocalProjectMcpContextPorts', () => {
|
|||
project.config.llm = {
|
||||
provider: { backend: 'none' },
|
||||
models: {},
|
||||
agentRunner: { backend: 'ai-sdk' },
|
||||
};
|
||||
|
||||
const sourceDir = join(tempDir, 'source');
|
||||
|
|
@ -1265,6 +1266,7 @@ describe('createLocalProjectMcpContextPorts', () => {
|
|||
project.config.llm = {
|
||||
provider: { backend: 'none' },
|
||||
models: {},
|
||||
agentRunner: { backend: 'ai-sdk' },
|
||||
};
|
||||
const sourceDir = join(project.projectDir, 'upload');
|
||||
await mkdir(join(sourceDir, 'orders'), { recursive: true });
|
||||
|
|
@ -1442,6 +1444,7 @@ describe('createLocalProjectMcpContextPorts', () => {
|
|||
project.config.llm = {
|
||||
provider: { backend: 'none' },
|
||||
models: {},
|
||||
agentRunner: { backend: 'ai-sdk' },
|
||||
};
|
||||
const agentRunner = new TestAgentRunner();
|
||||
const ports = createLocalProjectMcpContextPorts(project, {
|
||||
|
|
|
|||
|
|
@ -124,11 +124,11 @@ const buildMocks = (overrides: Partial<BuiltMocks> = {}): BuiltMocks => {
|
|||
slValidator: { validateSingleSource: vi.fn().mockResolvedValue({ errors: [], warnings: [] }) },
|
||||
toolsetFactory: {
|
||||
createIngestWuToolset: vi.fn().mockReturnValue({
|
||||
toAiSdkTools: vi.fn().mockReturnValue({}),
|
||||
toAgentTools: vi.fn().mockReturnValue({}),
|
||||
getAllTools: vi.fn().mockReturnValue([]),
|
||||
}),
|
||||
createToolset: vi.fn().mockReturnValue({
|
||||
toAiSdkTools: vi.fn().mockReturnValue({}),
|
||||
toAgentTools: vi.fn().mockReturnValue({}),
|
||||
getAllTools: vi.fn().mockReturnValue([]),
|
||||
}),
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import { createHash } from 'node:crypto';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { join } from 'node:path';
|
||||
import { tool } from 'ai';
|
||||
import * as YAML from 'yaml';
|
||||
import { z } from 'zod';
|
||||
import { createAgentTool } from '../agent/index.js';
|
||||
import { type KtxLogger, noopLogger } from '../core/index.js';
|
||||
import {
|
||||
revertSourceToPreHead,
|
||||
|
|
@ -126,7 +126,8 @@ export class MemoryAgentService {
|
|||
};
|
||||
|
||||
const loadSkillTool = {
|
||||
load_skill: tool({
|
||||
load_skill: createAgentTool({
|
||||
name: 'load_skill',
|
||||
description:
|
||||
'Load a skill to get specialized instructions. Call this when a skill listed in the system prompt matches the current task.',
|
||||
inputSchema: z.object({
|
||||
|
|
@ -212,7 +213,7 @@ export class MemoryAgentService {
|
|||
modelRole: 'candidateExtraction',
|
||||
systemPrompt,
|
||||
userPrompt: prompt,
|
||||
toolSet: { ...toolset.toAiSdkTools(toolContext), ...loadSkillTool },
|
||||
toolSet: { ...toolset.toAgentTools(toolContext), ...loadSkillTool },
|
||||
stepBudget,
|
||||
telemetryTags: {
|
||||
operationName: 'memory-agent-ingest',
|
||||
|
|
|
|||
|
|
@ -820,6 +820,7 @@ describe('local scan enrichment', () => {
|
|||
gateway: {},
|
||||
},
|
||||
models: { default: 'provider/language-model' },
|
||||
agentRunner: { backend: 'ai-sdk' },
|
||||
},
|
||||
{
|
||||
createKtxLlmProvider: createKtxLlmProvider as any,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue