/**
* Analytical Table agent — runtime entry point.
*
* Walks the 10-step runbook (Std 6). Steps 1 (validate input), 8
* (validate output), 9 (score confidence), and 10 (handoff) are
* deterministic. Steps 2-7 are delegated to the LLM through its
* methodology-library tool-use loop in llm.ts.
*
* Std 12: if the LLM is unavailable or returns an invalid response
* the agent fails cleanly with a typed FailureObject — never fakes
* the run.
*/
import {
type AgentResult,
type HITLEscalation,
LOW_CONFIDENCE_THRESHOLD,
makeConfidence,
} from '../../../standards.js';
import type {
ExecutionContext,
FailureObject,
Handoff,
JobRequest,
Lineage,
UnresolvedIssue,
} from '../../../types.js';
import { nowIso } from '../../../types.js';
import {
AGENT_NAME,
AGENT_VERSION,
analyticalTableContract,
} from './matrix.js';
import {
type AnalyticalTableInput,
type AnalyticalTableOutput,
analyticalTableInputSchema,
} from './schema.js';
import {
buildAnalyticalTable,
MODEL_NAME,
TOOL_COUNT,
TOOL_NAMES,
type ToolCallTrace,
} from './llm.js';
export { analyticalTableContract } from './matrix.js';
export type { AnalyticalTableOutput } from './schema.js';
export interface AnalyticalTableSideContext {
readonly jobRequest: JobRequest;
readonly upstreamLineage: Lineage;
}
function trace(ctx: ExecutionContext, standard: number, step: string, detail: string): void {
ctx.trace.push({ agent: AGENT_NAME, standard, step, detail, at: nowIso() });
// eslint-disable-next-line no-console
console.log(` [${AGENT_NAME}][Std ${standard}] ${step} — ${detail}`);
}
function failure(
ctx: ExecutionContext,
category: FailureObject['category'],
reason: string,
context: Record<string, unknown>,
lineage: Lineage,
): FailureObject {
return {
agent: AGENT_NAME,
agentVersion: AGENT_VERSION,
category,
reason,
context,
lineage,
attempts: ctx.retries,
recursionDepth: ctx.recursionDepth,
occurredAt: nowIso(),
};
}
export async function runAnalyticalTable(
rawInput: unknown,
side: AnalyticalTableSideContext,
ctx: ExecutionContext,
): Promise<AgentResult<AnalyticalTableOutput>> {
/* Step 1 (Std 2): receive-pillar1-output — validate. */
const parsed = analyticalTableInputSchema.safeParse(rawInput);
if (!parsed.success) {
return {
ok: false,
escalations: [],
failure: failure(
ctx,
'invalid-input',
'Analytical Table input failed schema validation.',
{ issues: parsed.error.issues },
side.upstreamLineage,
),
};
}
const input: AnalyticalTableInput = parsed.data;
trace(ctx, 2, analyticalTableContract.runbook[0]!.name,
`received ${input.records.length} Pillar 1 record(s); upstream lineage refs=${side.upstreamLineage.upstream.length}`);
const unresolved: UnresolvedIssue[] = [];
const escalations: HITLEscalation[] = [];
if (input.records.length === 0) {
return {
ok: false,
escalations,
failure: failure(
ctx,
'analytical-table-incomplete',
'No Pillar 1 records were supplied — cannot construct an analytical table.',
{ input },
side.upstreamLineage,
),
};
}
/* Steps 2-7 (Std 3 + 5): delegate to LLM with methodology tools. */
trace(ctx, 5, analyticalTableContract.runbook[1]!.name,
`delegating structuring to LLM (${MODEL_NAME}) with ${TOOL_COUNT} methodology tool(s) available: [${TOOL_NAMES.join(', ')}]`);
const onToolCall = (t: ToolCallTrace): void => {
const args = Object.entries(t.input).map(([k, v]) => `${k}=${JSON.stringify(v)}`).join(', ');
ctx.trace.push({
agent: AGENT_NAME,
standard: 5,
step: 'tool-call',
detail: `${t.toolName}(${args}) → ${t.ok ? t.resultSummary : `ERROR: ${t.errorMessage ?? 'unknown'}`}`,
at: t.at,
});
// eslint-disable-next-line no-console
console.log(` [${AGENT_NAME}][Std 5] tool-call — ${t.toolName}(${args}) → ${t.ok ? t.resultSummary : `ERROR: ${t.errorMessage}`}`);
};
const llm = await buildAnalyticalTable(input, side.jobRequest, onToolCall);
if (!llm.ok) {
const isKey = llm.failure.category === 'needs-api-key';
escalations.push({
agent: AGENT_NAME,
reason: 'critical-validation-failure',
failureContext: llm.failure.reason,
lineage: side.upstreamLineage,
validation: {
status: 'review',
confidence: makeConfidence(0, 'LLM unavailable or invalid response'),
checks: [{ name: 'llm-available', passed: false, detail: llm.failure.hint }],
},
recommendedReviewer: isKey ? 'engineer' : 'domain-expert',
raisedAt: nowIso(),
});
return {
ok: false,
escalations,
failure: failure(
ctx,
isKey ? 'tool-unavailable' : 'analytical-table-incomplete',
llm.failure.reason,
{ llmFailure: llm.failure, input },
side.upstreamLineage,
),
};
}
const table = llm.value.table;
trace(ctx, 7, analyticalTableContract.runbook[5]!.name,
`${table.cells.length} cell(s) populated, ${table.missingCells.length} missing-cell(s) flagged across ${table.entities.length} entity(ies) × ${table.metrics.length} metric(s) × ${table.periods.length} period(s)`);
/* Step 8 (Std 7): validate-table. */
const requested = table.entities.length * table.metrics.length * table.periods.length;
const populated = table.cells.filter(c => c.value !== null).length;
const coverage = requested === 0 ? 0 : populated / requested;
const avgCellConfidence =
table.cells.length === 0
? 0
: table.cells.reduce((s, c) => s + c.confidence, 0) / table.cells.length;
/* Std 8: missing-cell triggers. */
for (const mc of table.missingCells) {
unresolved.push({
category: 'missing-data',
detail: `no cell for ${mc.entity}/${mc.metric}/${mc.period}: ${mc.reason}`,
blocking: false,
context: { cell: mc },
});
}
/* Std 8: mixed-unit triggers per metric. */
const unitsByMetric = new Map<string, Set<string>>();
for (const c of table.cells) {
if (!c.unit) continue;
if (!unitsByMetric.has(c.metric)) unitsByMetric.set(c.metric, new Set());
unitsByMetric.get(c.metric)!.add(c.unit);
}
for (const [metric, units] of unitsByMetric) {
if (units.size > 1) {
unresolved.push({
category: 'ontology-conflict',
detail: `metric "${metric}" appears in mixed units: ${[...units].join(', ')}`,
blocking: false,
});
}
}
/* Step 9 (Std 7): score-confidence. */
const blocking = unresolved.filter(u => u.blocking).length;
const confidence = makeConfidence(
Math.max(0, 0.5 * coverage + 0.5 * avgCellConfidence - 0.1 * blocking),
`coverage ${(coverage * 100).toFixed(0)}% × avg-cell-confidence ${avgCellConfidence.toFixed(2)} with ${blocking} blocking issue(s)`,
);
trace(ctx, 7, analyticalTableContract.runbook[7]!.name,
`validation: coverage=${(coverage * 100).toFixed(0)}% avgCellConf=${avgCellConfidence.toFixed(2)} confidence=${confidence.tier}`);
/* Step 10 (Std 11): handoff. */
const lineage: Lineage = {
sourceUrl: side.upstreamLineage.sourceUrl,
capturedAt: nowIso(),
effectiveAs: side.upstreamLineage.effectiveAs,
agentVersion: AGENT_VERSION,
upstream: Array.from(new Set([
...side.upstreamLineage.upstream,
...table.cells.flatMap(c => c.sourceLineage),
])),
};
const validationStatus =
blocking > 0 ? 'review' : confidence.value < LOW_CONFIDENCE_THRESHOLD ? 'flagged' : 'passed';
const handoff: Handoff<AnalyticalTableOutput> = {
fromAgent: AGENT_NAME,
fromAgentVersion: AGENT_VERSION,
toAgent: 'intelligence.performance-metrics',
payload: table,
metadata: {
analysisId: ctx.analysisId,
capabilities: analyticalTableContract.capabilities,
appliedMethodologies: table.appliedMethodologies,
toolCallCount: llm.value.toolCalls.length,
toolCalls: llm.value.toolCalls.map(t => ({
toolName: t.toolName,
ok: t.ok,
input: t.input,
resultSummary: t.resultSummary,
errorMessage: t.errorMessage,
at: t.at,
})),
},
confidence,
validation: {
status: validationStatus,
checks: [
{ name: 'cells-populated', passed: populated > 0, detail: `${populated}/${requested}` },
{ name: 'no-blocking-issues', passed: blocking === 0, detail: `${blocking} blocking` },
{ name: 'cell-lineage-stamped', passed: table.cells.every(c => c.sourceLineage.length > 0) },
],
},
unresolvedIssues: unresolved,
lineage,
timestamp: nowIso(),
};
trace(ctx, 11, analyticalTableContract.runbook[9]!.name,
`handoff → ${handoff.toAgent} (validation=${validationStatus} confidence=${confidence.tier})`);
return { ok: true, handoff, escalations };
}