@rlynjb/aptkit-core 0.2.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.js +1 -0
- package/node_modules/@aptkit/evals/dist/src/index.d.ts +1 -0
- package/node_modules/@aptkit/evals/dist/src/index.js +1 -0
- package/node_modules/@aptkit/evals/dist/src/rubric-judge.d.ts +67 -0
- package/node_modules/@aptkit/evals/dist/src/rubric-judge.js +154 -0
- package/node_modules/@aptkit/evals/package.json +3 -0
- package/node_modules/@aptkit/runtime/dist/src/index.d.ts +1 -0
- package/node_modules/@aptkit/runtime/dist/src/index.js +1 -0
- package/node_modules/@aptkit/runtime/dist/src/structured-generation.d.ts +43 -0
- package/node_modules/@aptkit/runtime/dist/src/structured-generation.js +109 -0
- package/node_modules/@aptkit/workflows/README.md +6 -0
- package/node_modules/@aptkit/workflows/dist/src/content-generation-workflow.d.ts +58 -0
- package/node_modules/@aptkit/workflows/dist/src/content-generation-workflow.js +93 -0
- package/node_modules/@aptkit/workflows/dist/src/index.d.ts +2 -0
- package/node_modules/@aptkit/workflows/dist/src/index.js +2 -0
- package/node_modules/@aptkit/workflows/dist/src/markdown-sections.d.ts +9 -0
- package/node_modules/@aptkit/workflows/dist/src/markdown-sections.js +32 -0
- package/node_modules/@aptkit/workflows/package.json +27 -0
- package/package.json +6 -4
package/dist/src/index.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ export * from '@aptkit/tools';
|
|
|
3
3
|
export * from '@aptkit/context';
|
|
4
4
|
export * from '@aptkit/prompts';
|
|
5
5
|
export * from '@aptkit/evals';
|
|
6
|
+
export * from '@aptkit/workflows';
|
|
6
7
|
export * from '@aptkit/agent-recommendation';
|
|
7
8
|
export { ANOMALY_MONITORING_CAPABILITY_ID, AnomalyMonitoringAgent, ECOMMERCE_ANOMALY_CATEGORIES, anomalyMonitoringToolPolicy, coverageReport, formatCategoryChecklist, runnableCategories, schemaCapabilities, tryParseAnomalies, validateAnomalies, } from '@aptkit/agent-anomaly-monitoring';
|
|
8
9
|
export type { Anomaly as MonitoringAnomaly, AnomalyCategory as MonitoringAnomalyCategory, } from '@aptkit/agent-anomaly-monitoring';
|
package/dist/src/index.js
CHANGED
|
@@ -3,6 +3,7 @@ export * from '@aptkit/tools';
|
|
|
3
3
|
export * from '@aptkit/context';
|
|
4
4
|
export * from '@aptkit/prompts';
|
|
5
5
|
export * from '@aptkit/evals';
|
|
6
|
+
export * from '@aptkit/workflows';
|
|
6
7
|
export * from '@aptkit/agent-recommendation';
|
|
7
8
|
export { ANOMALY_MONITORING_CAPABILITY_ID, AnomalyMonitoringAgent, ECOMMERCE_ANOMALY_CATEGORIES, anomalyMonitoringToolPolicy, coverageReport, formatCategoryChecklist, runnableCategories, schemaCapabilities, tryParseAnomalies, validateAnomalies, } from '@aptkit/agent-anomaly-monitoring';
|
|
8
9
|
export { DIAGNOSTIC_INVESTIGATION_CAPABILITY_ID, DiagnosticInvestigationAgent, diagnosticInvestigationToolPolicy, diagnosisConfidence, tryParseDiagnosis, validateDiagnosis, } from '@aptkit/agent-diagnostic-investigation';
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { type CapabilityTraceSink, type JsonValidation, type ModelProvider, type StructuredGenerationResult } from '@aptkit/runtime';
|
|
2
|
+
export type RubricScoreLevel = {
|
|
3
|
+
score: number;
|
|
4
|
+
description: string;
|
|
5
|
+
};
|
|
6
|
+
export type RubricDimension = {
|
|
7
|
+
id: string;
|
|
8
|
+
label: string;
|
|
9
|
+
description: string;
|
|
10
|
+
scale: readonly RubricScoreLevel[];
|
|
11
|
+
};
|
|
12
|
+
export type RubricVerdictRule = {
|
|
13
|
+
verdict: string;
|
|
14
|
+
description: string;
|
|
15
|
+
};
|
|
16
|
+
export type RubricCalibrationExample = {
|
|
17
|
+
input: string;
|
|
18
|
+
expected: string;
|
|
19
|
+
};
|
|
20
|
+
export type RubricDefinition = {
|
|
21
|
+
id: string;
|
|
22
|
+
title: string;
|
|
23
|
+
task: string;
|
|
24
|
+
dimensions: readonly RubricDimension[];
|
|
25
|
+
verdicts: readonly RubricVerdictRule[];
|
|
26
|
+
checks?: readonly string[];
|
|
27
|
+
calibrationExamples?: readonly RubricCalibrationExample[];
|
|
28
|
+
};
|
|
29
|
+
export type RubricDimensionScore = {
|
|
30
|
+
score: number;
|
|
31
|
+
reason: string;
|
|
32
|
+
};
|
|
33
|
+
export type RubricJudgment = {
|
|
34
|
+
dimensions: Record<string, RubricDimensionScore>;
|
|
35
|
+
checks?: Record<string, boolean>;
|
|
36
|
+
verdict: string;
|
|
37
|
+
fix: string;
|
|
38
|
+
reasoning?: string;
|
|
39
|
+
};
|
|
40
|
+
export type RubricJudgeInput = {
|
|
41
|
+
subject: string;
|
|
42
|
+
context?: Record<string, string>;
|
|
43
|
+
};
|
|
44
|
+
export type RubricJudgeOptions = {
|
|
45
|
+
model: ModelProvider;
|
|
46
|
+
rubric: RubricDefinition;
|
|
47
|
+
capabilityId?: string;
|
|
48
|
+
maxTokens?: number;
|
|
49
|
+
temperature?: number;
|
|
50
|
+
trace?: CapabilityTraceSink;
|
|
51
|
+
};
|
|
52
|
+
export type RubricJudgeRunOptions = {
|
|
53
|
+
signal?: AbortSignal;
|
|
54
|
+
};
|
|
55
|
+
export declare class RubricJudge {
|
|
56
|
+
private readonly model;
|
|
57
|
+
private readonly rubric;
|
|
58
|
+
private readonly capabilityId;
|
|
59
|
+
private readonly maxTokens;
|
|
60
|
+
private readonly temperature?;
|
|
61
|
+
private readonly trace?;
|
|
62
|
+
constructor(options: RubricJudgeOptions);
|
|
63
|
+
judge(input: RubricJudgeInput, options?: RubricJudgeRunOptions): Promise<StructuredGenerationResult<RubricJudgment>>;
|
|
64
|
+
}
|
|
65
|
+
export declare function buildRubricJudgeSystemPrompt(rubric: RubricDefinition): string;
|
|
66
|
+
export declare function buildRubricJudgeUserPrompt(input: RubricJudgeInput): string;
|
|
67
|
+
export declare function createRubricJudgmentValidator(rubric: RubricDefinition): (value: unknown) => JsonValidation<RubricJudgment>;
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { generateStructured, } from '@aptkit/runtime';
|
|
2
|
+
export class RubricJudge {
|
|
3
|
+
model;
|
|
4
|
+
rubric;
|
|
5
|
+
capabilityId;
|
|
6
|
+
maxTokens;
|
|
7
|
+
temperature;
|
|
8
|
+
trace;
|
|
9
|
+
constructor(options) {
|
|
10
|
+
this.model = options.model;
|
|
11
|
+
this.rubric = options.rubric;
|
|
12
|
+
this.capabilityId = options.capabilityId ?? 'rubric-judge';
|
|
13
|
+
this.maxTokens = options.maxTokens ?? 1200;
|
|
14
|
+
this.temperature = options.temperature;
|
|
15
|
+
this.trace = options.trace;
|
|
16
|
+
}
|
|
17
|
+
judge(input, options = {}) {
|
|
18
|
+
return generateStructured({
|
|
19
|
+
capabilityId: this.capabilityId,
|
|
20
|
+
model: this.model,
|
|
21
|
+
system: buildRubricJudgeSystemPrompt(this.rubric),
|
|
22
|
+
userPrompt: buildRubricJudgeUserPrompt(input),
|
|
23
|
+
validate: createRubricJudgmentValidator(this.rubric),
|
|
24
|
+
maxTokens: this.maxTokens,
|
|
25
|
+
temperature: this.temperature,
|
|
26
|
+
trace: this.trace,
|
|
27
|
+
signal: options.signal,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
export function buildRubricJudgeSystemPrompt(rubric) {
|
|
32
|
+
const dimensions = rubric.dimensions
|
|
33
|
+
.map((dimension) => {
|
|
34
|
+
const scale = dimension.scale
|
|
35
|
+
.map((level) => ` ${level.score} = ${level.description}`)
|
|
36
|
+
.join('\n');
|
|
37
|
+
return `${dimension.id} ${dimension.label}: ${dimension.description}\n${scale}`;
|
|
38
|
+
})
|
|
39
|
+
.join('\n\n');
|
|
40
|
+
const verdicts = rubric.verdicts
|
|
41
|
+
.map((rule) => `- ${rule.verdict}: ${rule.description}`)
|
|
42
|
+
.join('\n');
|
|
43
|
+
const checks = rubric.checks?.length
|
|
44
|
+
? `\nChecks to return as booleans:\n${rubric.checks.map((check) => `- ${check}`).join('\n')}\n`
|
|
45
|
+
: '';
|
|
46
|
+
const examples = rubric.calibrationExamples?.length
|
|
47
|
+
? `\nCalibration examples. Use these only to anchor the scoring scale; do not repeat them.\n${rubric.calibrationExamples
|
|
48
|
+
.map((example) => `Input:\n${example.input}\nExpected:\n${example.expected}`)
|
|
49
|
+
.join('\n\n')}\n`
|
|
50
|
+
: '';
|
|
51
|
+
const dimensionShape = Object.fromEntries(rubric.dimensions.map((dimension) => [dimension.id, { score: 0, reason: '' }]));
|
|
52
|
+
const checkShape = Object.fromEntries((rubric.checks ?? []).map((check) => [check, true]));
|
|
53
|
+
const outputShape = {
|
|
54
|
+
dimensions: dimensionShape,
|
|
55
|
+
...(rubric.checks?.length ? { checks: checkShape } : {}),
|
|
56
|
+
verdict: rubric.verdicts[0]?.verdict ?? 'pass',
|
|
57
|
+
fix: '',
|
|
58
|
+
reasoning: '',
|
|
59
|
+
};
|
|
60
|
+
return [
|
|
61
|
+
`You are a rubric judge for: ${rubric.title}.`,
|
|
62
|
+
rubric.task,
|
|
63
|
+
'',
|
|
64
|
+
'Score the subject against the rubric. Score meaning and evidence, not style preferences unless the rubric asks for style.',
|
|
65
|
+
'Never rewrite the subject. Return one highest-leverage fix, not a list.',
|
|
66
|
+
'',
|
|
67
|
+
'Rubric dimensions:',
|
|
68
|
+
dimensions,
|
|
69
|
+
'',
|
|
70
|
+
'Allowed verdicts:',
|
|
71
|
+
verdicts,
|
|
72
|
+
checks.trimEnd(),
|
|
73
|
+
examples.trimEnd(),
|
|
74
|
+
'',
|
|
75
|
+
'Output JSON only. No prose. No markdown fences. Use exactly this shape:',
|
|
76
|
+
JSON.stringify(outputShape),
|
|
77
|
+
].filter(Boolean).join('\n');
|
|
78
|
+
}
|
|
79
|
+
export function buildRubricJudgeUserPrompt(input) {
|
|
80
|
+
const context = input.context && Object.keys(input.context).length > 0
|
|
81
|
+
? `Context:\n${Object.entries(input.context).map(([key, value]) => `${key}: ${value}`).join('\n')}\n\n`
|
|
82
|
+
: '';
|
|
83
|
+
return `${context}Subject:\n${input.subject}`;
|
|
84
|
+
}
|
|
85
|
+
export function createRubricJudgmentValidator(rubric) {
|
|
86
|
+
const dimensionIds = new Set(rubric.dimensions.map((dimension) => dimension.id));
|
|
87
|
+
const verdicts = new Set(rubric.verdicts.map((rule) => rule.verdict));
|
|
88
|
+
const scoreRanges = new Map(rubric.dimensions.map((dimension) => [
|
|
89
|
+
dimension.id,
|
|
90
|
+
{
|
|
91
|
+
min: Math.min(...dimension.scale.map((level) => level.score)),
|
|
92
|
+
max: Math.max(...dimension.scale.map((level) => level.score)),
|
|
93
|
+
},
|
|
94
|
+
]));
|
|
95
|
+
return (value) => {
|
|
96
|
+
if (!isRecord(value))
|
|
97
|
+
return { ok: false, error: 'judgment must be an object' };
|
|
98
|
+
if (!isRecord(value.dimensions))
|
|
99
|
+
return { ok: false, error: 'judgment.dimensions must be an object' };
|
|
100
|
+
const dimensions = {};
|
|
101
|
+
for (const id of dimensionIds) {
|
|
102
|
+
const score = value.dimensions[id];
|
|
103
|
+
if (!isRecord(score))
|
|
104
|
+
return { ok: false, error: `dimensions.${id} must be an object` };
|
|
105
|
+
if (typeof score.score !== 'number')
|
|
106
|
+
return { ok: false, error: `dimensions.${id}.score must be a number` };
|
|
107
|
+
if (typeof score.reason !== 'string')
|
|
108
|
+
return { ok: false, error: `dimensions.${id}.reason must be a string` };
|
|
109
|
+
const range = scoreRanges.get(id);
|
|
110
|
+
if (range && (score.score < range.min || score.score > range.max)) {
|
|
111
|
+
return { ok: false, error: `dimensions.${id}.score must be between ${range.min} and ${range.max}` };
|
|
112
|
+
}
|
|
113
|
+
dimensions[id] = { score: score.score, reason: score.reason.trim() };
|
|
114
|
+
}
|
|
115
|
+
if (typeof value.verdict !== 'string' || !verdicts.has(value.verdict)) {
|
|
116
|
+
return { ok: false, error: 'judgment.verdict is not allowed by the rubric' };
|
|
117
|
+
}
|
|
118
|
+
if (typeof value.fix !== 'string')
|
|
119
|
+
return { ok: false, error: 'judgment.fix must be a string' };
|
|
120
|
+
if (value.reasoning !== undefined && typeof value.reasoning !== 'string') {
|
|
121
|
+
return { ok: false, error: 'judgment.reasoning must be a string when present' };
|
|
122
|
+
}
|
|
123
|
+
const checks = validateChecks(value.checks, rubric.checks);
|
|
124
|
+
if (!checks.ok)
|
|
125
|
+
return checks;
|
|
126
|
+
return {
|
|
127
|
+
ok: true,
|
|
128
|
+
value: {
|
|
129
|
+
dimensions,
|
|
130
|
+
...(checks.value ? { checks: checks.value } : {}),
|
|
131
|
+
verdict: value.verdict,
|
|
132
|
+
fix: value.fix.trim(),
|
|
133
|
+
...(value.reasoning ? { reasoning: value.reasoning.trim() } : {}),
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
function validateChecks(value, expectedChecks) {
|
|
139
|
+
if (!expectedChecks?.length)
|
|
140
|
+
return { ok: true, value: undefined };
|
|
141
|
+
if (!isRecord(value))
|
|
142
|
+
return { ok: false, error: 'judgment.checks must be an object' };
|
|
143
|
+
const checks = {};
|
|
144
|
+
for (const check of expectedChecks) {
|
|
145
|
+
if (typeof value[check] !== 'boolean') {
|
|
146
|
+
return { ok: false, error: `checks.${check} must be a boolean` };
|
|
147
|
+
}
|
|
148
|
+
checks[check] = value[check];
|
|
149
|
+
}
|
|
150
|
+
return { ok: true, value: checks };
|
|
151
|
+
}
|
|
152
|
+
function isRecord(value) {
|
|
153
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
154
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { type CapabilityTraceSink } from './events.js';
|
|
2
|
+
import { type JsonValidator } from './json-output.js';
|
|
3
|
+
import type { ModelMessage, ModelProvider } from './model-provider.js';
|
|
4
|
+
export type StructuredGenerationAttempt = {
|
|
5
|
+
attempt: number;
|
|
6
|
+
rawText?: string;
|
|
7
|
+
error?: string;
|
|
8
|
+
};
|
|
9
|
+
export type StructuredGenerationSuccess<T> = {
|
|
10
|
+
ok: true;
|
|
11
|
+
value: T;
|
|
12
|
+
rawText: string;
|
|
13
|
+
attempts: StructuredGenerationAttempt[];
|
|
14
|
+
};
|
|
15
|
+
export type StructuredGenerationFailure = {
|
|
16
|
+
ok: false;
|
|
17
|
+
error: string;
|
|
18
|
+
attempts: StructuredGenerationAttempt[];
|
|
19
|
+
};
|
|
20
|
+
export type StructuredGenerationResult<T> = StructuredGenerationSuccess<T> | StructuredGenerationFailure;
|
|
21
|
+
export type StructuredGenerationRetryOptions = {
|
|
22
|
+
maxAttempts?: number;
|
|
23
|
+
strictSuffix?: string;
|
|
24
|
+
};
|
|
25
|
+
export type GenerateStructuredOptions<T> = {
|
|
26
|
+
capabilityId: string;
|
|
27
|
+
model: ModelProvider;
|
|
28
|
+
validate: JsonValidator<T>;
|
|
29
|
+
system?: string;
|
|
30
|
+
messages?: ModelMessage[];
|
|
31
|
+
userPrompt?: string;
|
|
32
|
+
maxTokens?: number;
|
|
33
|
+
temperature?: number;
|
|
34
|
+
retry?: StructuredGenerationRetryOptions;
|
|
35
|
+
trace?: CapabilityTraceSink;
|
|
36
|
+
signal?: AbortSignal;
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* Runs a JSON-producing model prompt with bounded parse/validation retries.
|
|
40
|
+
* This is the provider-neutral version of Dryrun's on-device JSON pipeline:
|
|
41
|
+
* generate, extract JSON, validate, retry once with a strict JSON-only nudge.
|
|
42
|
+
*/
|
|
43
|
+
export declare function generateStructured<T>(options: GenerateStructuredOptions<T>): Promise<StructuredGenerationResult<T>>;
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { timestamp } from './events.js';
|
|
2
|
+
import { parseValidatedJson } from './json-output.js';
|
|
3
|
+
const DEFAULT_STRICT_SUFFIX = '\n\nReturn ONLY valid JSON - no prose, no markdown fences.';
|
|
4
|
+
/**
|
|
5
|
+
* Runs a JSON-producing model prompt with bounded parse/validation retries.
|
|
6
|
+
* This is the provider-neutral version of Dryrun's on-device JSON pipeline:
|
|
7
|
+
* generate, extract JSON, validate, retry once with a strict JSON-only nudge.
|
|
8
|
+
*/
|
|
9
|
+
export async function generateStructured(options) {
|
|
10
|
+
const maxAttempts = Math.max(1, options.retry?.maxAttempts ?? 2);
|
|
11
|
+
const strictSuffix = options.retry?.strictSuffix ?? DEFAULT_STRICT_SUFFIX;
|
|
12
|
+
const baseMessages = normalizeMessages(options);
|
|
13
|
+
const attempts = [];
|
|
14
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
15
|
+
options.signal?.throwIfAborted();
|
|
16
|
+
const messages = attempt === 1 ? baseMessages : appendStrictSuffix(baseMessages, strictSuffix);
|
|
17
|
+
let response;
|
|
18
|
+
try {
|
|
19
|
+
response = await options.model.complete({
|
|
20
|
+
system: options.system,
|
|
21
|
+
messages,
|
|
22
|
+
maxTokens: options.maxTokens,
|
|
23
|
+
temperature: options.temperature,
|
|
24
|
+
signal: options.signal,
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
if (isAbortError(error) || options.signal?.aborted)
|
|
29
|
+
throw error;
|
|
30
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
31
|
+
attempts.push({ attempt, error: message });
|
|
32
|
+
emitWarning(options, `structured generation model call failed on attempt ${attempt}: ${message}`);
|
|
33
|
+
return { ok: false, error: message, attempts };
|
|
34
|
+
}
|
|
35
|
+
emitUsage(options, response);
|
|
36
|
+
const rawText = textFromResponse(response);
|
|
37
|
+
const parsed = parseValidatedJson(rawText, options.validate);
|
|
38
|
+
if (parsed.ok) {
|
|
39
|
+
attempts.push({ attempt, rawText });
|
|
40
|
+
return { ok: true, value: parsed.value, rawText, attempts };
|
|
41
|
+
}
|
|
42
|
+
attempts.push({ attempt, rawText, error: parsed.error });
|
|
43
|
+
if (attempt < maxAttempts) {
|
|
44
|
+
emitWarning(options, `structured generation validation failed on attempt ${attempt}: ${parsed.error}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
const error = attempts[attempts.length - 1]?.error ?? 'structured generation failed';
|
|
48
|
+
emitError(options, `structured generation failed after ${maxAttempts} attempt${maxAttempts === 1 ? '' : 's'}: ${error}`);
|
|
49
|
+
return { ok: false, error, attempts };
|
|
50
|
+
}
|
|
51
|
+
function normalizeMessages(options) {
|
|
52
|
+
if (options.messages?.length)
|
|
53
|
+
return [...options.messages];
|
|
54
|
+
if (options.userPrompt !== undefined)
|
|
55
|
+
return [{ role: 'user', content: options.userPrompt }];
|
|
56
|
+
throw new Error('generateStructured requires messages or userPrompt');
|
|
57
|
+
}
|
|
58
|
+
function appendStrictSuffix(messages, strictSuffix) {
|
|
59
|
+
const next = [...messages];
|
|
60
|
+
for (let index = next.length - 1; index >= 0; index -= 1) {
|
|
61
|
+
const message = next[index];
|
|
62
|
+
if (message.role === 'user' && typeof message.content === 'string') {
|
|
63
|
+
next[index] = { ...message, content: `${message.content}${strictSuffix}` };
|
|
64
|
+
return next;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
next.push({ role: 'user', content: strictSuffix.trim() });
|
|
68
|
+
return next;
|
|
69
|
+
}
|
|
70
|
+
function textFromResponse(response) {
|
|
71
|
+
return response.content
|
|
72
|
+
.filter((block) => block.type === 'text')
|
|
73
|
+
.map((block) => block.text)
|
|
74
|
+
.join('');
|
|
75
|
+
}
|
|
76
|
+
function emitUsage(options, response) {
|
|
77
|
+
if (!response.usage)
|
|
78
|
+
return;
|
|
79
|
+
options.trace?.emit({
|
|
80
|
+
type: 'model_usage',
|
|
81
|
+
capabilityId: options.capabilityId,
|
|
82
|
+
provider: options.model.id,
|
|
83
|
+
model: response.model ?? options.model.defaultModel ?? 'unknown',
|
|
84
|
+
inputTokens: response.usage.inputTokens,
|
|
85
|
+
outputTokens: response.usage.outputTokens,
|
|
86
|
+
estimated: response.usage.estimated,
|
|
87
|
+
timestamp: timestamp(),
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
function emitWarning(options, message) {
|
|
91
|
+
options.trace?.emit({
|
|
92
|
+
type: 'warning',
|
|
93
|
+
capabilityId: options.capabilityId,
|
|
94
|
+
message,
|
|
95
|
+
timestamp: timestamp(),
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
function emitError(options, message) {
|
|
99
|
+
options.trace?.emit({
|
|
100
|
+
type: 'error',
|
|
101
|
+
capabilityId: options.capabilityId,
|
|
102
|
+
message,
|
|
103
|
+
timestamp: timestamp(),
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
function isAbortError(error) {
|
|
107
|
+
return error instanceof DOMException && error.name === 'AbortError'
|
|
108
|
+
|| error instanceof Error && error.name === 'AbortError';
|
|
109
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { type CapabilityTraceSink } from '@aptkit/runtime';
|
|
2
|
+
import { type MarkdownSection } from './markdown-sections.js';
|
|
3
|
+
export type ContentAngle = {
|
|
4
|
+
id: string;
|
|
5
|
+
label: string;
|
|
6
|
+
};
|
|
7
|
+
export type ExistingContentVariant = {
|
|
8
|
+
sourceHash: string;
|
|
9
|
+
variantIndex: number;
|
|
10
|
+
};
|
|
11
|
+
export type ContentVariantPlan = {
|
|
12
|
+
sourceHash: string;
|
|
13
|
+
variantIndex: number;
|
|
14
|
+
sectionIndex: number;
|
|
15
|
+
totalSections: number;
|
|
16
|
+
section: MarkdownSection;
|
|
17
|
+
angle: ContentAngle;
|
|
18
|
+
};
|
|
19
|
+
export type GeneratedContentVariant<T> = ContentVariantPlan & {
|
|
20
|
+
item: T;
|
|
21
|
+
};
|
|
22
|
+
export type ContentGenerator<T> = (plan: ContentVariantPlan, options?: {
|
|
23
|
+
signal?: AbortSignal;
|
|
24
|
+
trace?: CapabilityTraceSink;
|
|
25
|
+
}) => Promise<T | null>;
|
|
26
|
+
export type EnsureGeneratedContentOptions<TExisting extends ExistingContentVariant, TGenerated> = {
|
|
27
|
+
capabilityId?: string;
|
|
28
|
+
sourceMarkdown: string;
|
|
29
|
+
sourceHash: string;
|
|
30
|
+
existing: readonly TExisting[];
|
|
31
|
+
targetCount?: number;
|
|
32
|
+
angles: readonly ContentAngle[];
|
|
33
|
+
maxSkips?: number;
|
|
34
|
+
generator: ContentGenerator<TGenerated>;
|
|
35
|
+
trace?: CapabilityTraceSink;
|
|
36
|
+
signal?: AbortSignal;
|
|
37
|
+
};
|
|
38
|
+
export type EnsureGeneratedContentResult<TExisting extends ExistingContentVariant, TGenerated> = {
|
|
39
|
+
freshExisting: TExisting[];
|
|
40
|
+
staleExisting: TExisting[];
|
|
41
|
+
generated: GeneratedContentVariant<TGenerated>[];
|
|
42
|
+
items: Array<TExisting | GeneratedContentVariant<TGenerated>>;
|
|
43
|
+
attempted: ContentVariantPlan[];
|
|
44
|
+
skipped: ContentVariantPlan[];
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* Ensures a source document has enough generated variants for the current hash.
|
|
48
|
+
*
|
|
49
|
+
* The host owns persistence. This helper returns fresh existing variants,
|
|
50
|
+
* stale variants to invalidate, and newly generated variants to save.
|
|
51
|
+
*/
|
|
52
|
+
export declare function ensureGeneratedContent<TExisting extends ExistingContentVariant, TGenerated>(options: EnsureGeneratedContentOptions<TExisting, TGenerated>): Promise<EnsureGeneratedContentResult<TExisting, TGenerated>>;
|
|
53
|
+
export declare function planContentVariant(options: {
|
|
54
|
+
sourceHash: string;
|
|
55
|
+
variantIndex: number;
|
|
56
|
+
sections: readonly MarkdownSection[];
|
|
57
|
+
angles: readonly ContentAngle[];
|
|
58
|
+
}): ContentVariantPlan;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { timestamp } from '@aptkit/runtime';
|
|
2
|
+
import { splitMarkdownSections } from './markdown-sections.js';
|
|
3
|
+
const DEFAULT_TARGET_COUNT = 4;
|
|
4
|
+
const DEFAULT_MAX_SKIPS = 3;
|
|
5
|
+
/**
|
|
6
|
+
* Ensures a source document has enough generated variants for the current hash.
|
|
7
|
+
*
|
|
8
|
+
* The host owns persistence. This helper returns fresh existing variants,
|
|
9
|
+
* stale variants to invalidate, and newly generated variants to save.
|
|
10
|
+
*/
|
|
11
|
+
export async function ensureGeneratedContent(options) {
|
|
12
|
+
options.signal?.throwIfAborted();
|
|
13
|
+
if (options.angles.length === 0)
|
|
14
|
+
throw new Error('ensureGeneratedContent requires at least one angle');
|
|
15
|
+
const capabilityId = options.capabilityId ?? 'content-generation-workflow';
|
|
16
|
+
const targetCount = Math.max(0, options.targetCount ?? DEFAULT_TARGET_COUNT);
|
|
17
|
+
const maxSkips = Math.max(0, options.maxSkips ?? DEFAULT_MAX_SKIPS);
|
|
18
|
+
const sections = splitMarkdownSections(options.sourceMarkdown);
|
|
19
|
+
const freshExisting = options.existing
|
|
20
|
+
.filter((item) => item.sourceHash === options.sourceHash)
|
|
21
|
+
.sort((a, b) => a.variantIndex - b.variantIndex);
|
|
22
|
+
const staleExisting = options.existing
|
|
23
|
+
.filter((item) => item.sourceHash !== options.sourceHash)
|
|
24
|
+
.sort((a, b) => a.variantIndex - b.variantIndex);
|
|
25
|
+
if (targetCount === 0 || sections.length === 0 || freshExisting.length >= targetCount) {
|
|
26
|
+
return {
|
|
27
|
+
freshExisting,
|
|
28
|
+
staleExisting,
|
|
29
|
+
generated: [],
|
|
30
|
+
items: freshExisting,
|
|
31
|
+
attempted: [],
|
|
32
|
+
skipped: [],
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
const needed = targetCount - freshExisting.length;
|
|
36
|
+
const baseIndex = (freshExisting.at(-1)?.variantIndex ?? -1) + 1;
|
|
37
|
+
const lastIndex = baseIndex + needed + maxSkips;
|
|
38
|
+
const generated = [];
|
|
39
|
+
const attempted = [];
|
|
40
|
+
const skipped = [];
|
|
41
|
+
for (let variantIndex = baseIndex; generated.length < needed && variantIndex < lastIndex; variantIndex += 1) {
|
|
42
|
+
options.signal?.throwIfAborted();
|
|
43
|
+
const plan = planContentVariant({
|
|
44
|
+
sourceHash: options.sourceHash,
|
|
45
|
+
variantIndex,
|
|
46
|
+
sections,
|
|
47
|
+
angles: options.angles,
|
|
48
|
+
});
|
|
49
|
+
attempted.push(plan);
|
|
50
|
+
options.trace?.emit({
|
|
51
|
+
type: 'step',
|
|
52
|
+
capabilityId,
|
|
53
|
+
role: 'workflow',
|
|
54
|
+
content: `generating ${plan.angle.label} for section ${plan.sectionIndex + 1} of ${plan.totalSections}`,
|
|
55
|
+
timestamp: timestamp(),
|
|
56
|
+
});
|
|
57
|
+
const item = await options.generator(plan, { signal: options.signal, trace: options.trace });
|
|
58
|
+
if (item === null) {
|
|
59
|
+
skipped.push(plan);
|
|
60
|
+
options.trace?.emit({
|
|
61
|
+
type: 'warning',
|
|
62
|
+
capabilityId,
|
|
63
|
+
message: `content variant ${variantIndex} produced no usable output; trying next variant`,
|
|
64
|
+
timestamp: timestamp(),
|
|
65
|
+
});
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
generated.push({ ...plan, item });
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
freshExisting,
|
|
72
|
+
staleExisting,
|
|
73
|
+
generated,
|
|
74
|
+
items: [...freshExisting, ...generated],
|
|
75
|
+
attempted,
|
|
76
|
+
skipped,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
export function planContentVariant(options) {
|
|
80
|
+
if (options.sections.length === 0)
|
|
81
|
+
throw new Error('planContentVariant requires at least one section');
|
|
82
|
+
if (options.angles.length === 0)
|
|
83
|
+
throw new Error('planContentVariant requires at least one angle');
|
|
84
|
+
const sectionIndex = options.variantIndex % options.sections.length;
|
|
85
|
+
return {
|
|
86
|
+
sourceHash: options.sourceHash,
|
|
87
|
+
variantIndex: options.variantIndex,
|
|
88
|
+
sectionIndex,
|
|
89
|
+
totalSections: options.sections.length,
|
|
90
|
+
section: options.sections[sectionIndex],
|
|
91
|
+
angle: options.angles[options.variantIndex % options.angles.length],
|
|
92
|
+
};
|
|
93
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export type MarkdownSection = {
|
|
2
|
+
heading?: string;
|
|
3
|
+
content: string;
|
|
4
|
+
};
|
|
5
|
+
/**
|
|
6
|
+
* Splits markdown into ordered sections by h2 (`##`) headings. H3+ headings
|
|
7
|
+
* stay inside the current section, matching Dryrun's source-document workflow.
|
|
8
|
+
*/
|
|
9
|
+
export declare function splitMarkdownSections(markdown: string): MarkdownSection[];
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Splits markdown into ordered sections by h2 (`##`) headings. H3+ headings
|
|
3
|
+
* stay inside the current section, matching Dryrun's source-document workflow.
|
|
4
|
+
*/
|
|
5
|
+
export function splitMarkdownSections(markdown) {
|
|
6
|
+
if (markdown.trim().length === 0)
|
|
7
|
+
return [];
|
|
8
|
+
const sections = [];
|
|
9
|
+
let heading;
|
|
10
|
+
let body = [];
|
|
11
|
+
function flush() {
|
|
12
|
+
const content = body.join('\n').trim();
|
|
13
|
+
if (content.length > 0 || heading !== undefined) {
|
|
14
|
+
sections.push(heading === undefined ? { content } : { heading, content });
|
|
15
|
+
}
|
|
16
|
+
body = [];
|
|
17
|
+
}
|
|
18
|
+
for (const line of markdown.split(/\r?\n/)) {
|
|
19
|
+
const trimmed = line.trimStart();
|
|
20
|
+
if (trimmed.startsWith('## ') && !trimmed.startsWith('### ')) {
|
|
21
|
+
flush();
|
|
22
|
+
heading = trimmed.slice(3).trim();
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
body.push(line);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
flush();
|
|
29
|
+
if (sections.length === 0)
|
|
30
|
+
return [{ content: markdown.trim() }];
|
|
31
|
+
return sections;
|
|
32
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@aptkit/workflows",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "./dist/src/index.js",
|
|
6
|
+
"types": "./dist/src/index.d.ts",
|
|
7
|
+
"files": [
|
|
8
|
+
"README.md",
|
|
9
|
+
"dist/src"
|
|
10
|
+
],
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"types": "./dist/src/index.d.ts",
|
|
14
|
+
"import": "./dist/src/index.js"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"scripts": {
|
|
18
|
+
"build": "tsc -p tsconfig.json",
|
|
19
|
+
"test": "npm run build && node --test dist/test/*.test.js"
|
|
20
|
+
},
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"@aptkit/runtime": "0.0.0"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@types/node": "^20.0.0"
|
|
26
|
+
}
|
|
27
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rlynjb/aptkit-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Standalone AptKit core bundle for extracted agent capabilities.",
|
|
6
6
|
"main": "./dist/src/index.js",
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
}
|
|
26
26
|
},
|
|
27
27
|
"scripts": {
|
|
28
|
-
"build": "tsc -
|
|
28
|
+
"build": "tsc -b tsconfig.json",
|
|
29
29
|
"test": "npm run build && node --test dist/test/*.test.js"
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
@@ -37,7 +37,8 @@
|
|
|
37
37
|
"@aptkit/evals": "0.0.0",
|
|
38
38
|
"@aptkit/prompts": "0.0.0",
|
|
39
39
|
"@aptkit/runtime": "0.0.0",
|
|
40
|
-
"@aptkit/tools": "0.0.0"
|
|
40
|
+
"@aptkit/tools": "0.0.0",
|
|
41
|
+
"@aptkit/workflows": "0.0.0"
|
|
41
42
|
},
|
|
42
43
|
"bundledDependencies": [
|
|
43
44
|
"@aptkit/agent-anomaly-monitoring",
|
|
@@ -48,6 +49,7 @@
|
|
|
48
49
|
"@aptkit/evals",
|
|
49
50
|
"@aptkit/prompts",
|
|
50
51
|
"@aptkit/runtime",
|
|
51
|
-
"@aptkit/tools"
|
|
52
|
+
"@aptkit/tools",
|
|
53
|
+
"@aptkit/workflows"
|
|
52
54
|
]
|
|
53
55
|
}
|