feat(ai): support Claude Fable 5 (upgrade Claude Agent SDK to 0.3.173) (#354)

This commit is contained in:
ezl-keygraph
2026-06-12 14:50:27 +05:30
committed by GitHub
parent ac6db3b52e
commit 3d1a3c75f8
10 changed files with 119 additions and 44 deletions
+7
View File
@@ -14,6 +14,7 @@ export interface AuditLogger {
logToolStart(toolName: string, parameters: unknown): Promise<void>;
logToolEnd(result: unknown): Promise<void>;
logError(error: Error, duration: number, turns: number): Promise<void>;
logNote(category: string, message: string): Promise<void>;
}
class RealAuditLogger implements AuditLogger {
@@ -56,6 +57,10 @@ class RealAuditLogger implements AuditLogger {
timestamp: formatTimestamp(),
});
}
async logNote(category: string, message: string): Promise<void> {
await this.auditSession.logWorkflowNote(category, message);
}
}
/** Null Object implementation - all methods are safe no-ops */
@@ -67,6 +72,8 @@ class NullAuditLogger implements AuditLogger {
async logToolEnd(_result: unknown): Promise<void> {}
async logError(_error: Error, _duration: number, _turns: number): Promise<void> {}
async logNote(_category: string, _message: string): Promise<void> {}
}
// Returns no-op when auditSession is null
+10
View File
@@ -25,6 +25,7 @@ import type {
AssistantResult,
ContentBlock,
ExecutionContext,
ModelRefusalFallbackMessage,
ResultData,
ResultMessage,
SystemInitMessage,
@@ -343,6 +344,15 @@ export async function dispatchMessage(
}
return { type: 'continue', model: initMsg.model };
}
if (message.subtype === 'model_refusal_fallback') {
const fallback = message as ModelRefusalFallbackMessage;
const category = fallback.api_refusal_category ?? 'policy';
await auditLogger.logNote(
'model-fallback',
`Model refused (${category}); fell back ${fallback.original_model}${fallback.fallback_model}`,
);
return { type: 'continue' };
}
return { type: 'continue' };
}
+9
View File
@@ -40,3 +40,12 @@ export function resolveModel(tier: ModelTier = 'medium'): string {
export function supportsAdaptiveThinking(model: string): boolean {
return /opus-4-[678]/.test(model);
}
/**
* Whether a model is in the Fable family. Fable's safety classifiers flag
* cybersecurity tasks and route them to Opus 4.8, so a security scan on Fable
* largely runs on Opus 4.8 anyway.
*/
export function isFableModel(model: string): boolean {
return /fable/i.test(model);
}
+9
View File
@@ -98,6 +98,15 @@ export interface SystemInitMessage {
permissionMode?: string;
}
/** Emitted when a model refuses a request and the SDK falls back to another model (e.g. Fable 5 routing cybersecurity tasks to Opus 4.8). */
export interface ModelRefusalFallbackMessage {
type: 'system';
subtype: 'model_refusal_fallback';
original_model: string;
fallback_model: string;
api_refusal_category?: string | null;
}
export interface UserMessage {
type: 'user';
}
+8
View File
@@ -158,6 +158,14 @@ export class AuditSession {
}
}
/**
* Write a human-readable note to the unified workflow log (e.g. a model
* refusal fallback). Independent of agent event logging.
*/
async logWorkflowNote(category: string, message: string): Promise<void> {
await this.workflowLogger.logEvent(category, message);
}
/**
* End agent execution (mutex-protected)
*/
+19 -5
View File
@@ -12,6 +12,7 @@
*/
import fs from 'node:fs/promises';
import { isFableModel, resolveModel } from '../ai/models.js';
import { formatDuration, formatTimestamp } from '../utils/formatting.js';
import { LogStream } from './log-stream.js';
import { generateWorkflowLogPath, type SessionMetadata } from './utils.js';
@@ -77,18 +78,31 @@ export class WorkflowLogger {
* Write header to log file
*/
private async writeHeader(): Promise<void> {
const header = [
const lines = [
`================================================================================`,
`Shannon Pentest - Workflow Log`,
`================================================================================`,
`Workflow ID: ${this.workflowId ?? this.sessionMetadata.id}`,
`Target URL: ${this.sessionMetadata.webUrl}`,
`Started: ${formatTimestamp()}`,
`================================================================================`,
``,
].join('\n');
];
return this.logStream.write(header);
// Surface Fable usage: its safety classifiers route cybersecurity tasks to
// Opus 4.8, so those phases run on Opus 4.8 regardless of the tier setting.
const fableTiers = (['small', 'medium', 'large'] as const)
.map((tier) => ({ tier, model: resolveModel(tier) }))
.filter(({ model }) => isFableModel(model));
if (fableTiers.length > 0) {
const tierList = fableTiers.map(({ tier, model }) => `${tier} (${model})`).join(', ');
lines.push(
`Note: ${tierList} set to a Fable model. Fable's safety classifiers`,
` route cybersecurity tasks to Opus 4.8, so those phases run on Opus 4.8.`,
);
}
lines.push(`================================================================================`, ``);
return this.logStream.write(lines.join('\n'));
}
/**