@gmickel/gno 1.19.0 → 1.21.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/README.md +28 -8
- package/assets/skill/SKILL.md +73 -27
- package/assets/skill/mcp-reference.md +7 -2
- package/assets/skill/recipes/citation-and-provenance.md +32 -9
- package/package.json +1 -1
- package/spec/cli.md +142 -17
- package/spec/db/schema.sql +170 -0
- package/spec/evals-agentic.md +87 -5
- package/spec/mcp.md +75 -3
- package/spec/output-schemas/ask.schema.json +198 -0
- package/spec/output-schemas/capsule-reverified-event.schema.json +47 -0
- package/spec/output-schemas/changes.schema.json +280 -0
- package/spec/output-schemas/claim-verification.schema.json +291 -0
- package/spec/output-schemas/context-capsule-v1.schema.json +36 -1
- package/spec/output-schemas/document-diff.schema.json +185 -0
- package/spec/output-schemas/impact.schema.json +122 -0
- package/spec/output-schemas/saved-capsule-list.schema.json +16 -0
- package/spec/output-schemas/saved-capsule-registration.schema.json +172 -0
- package/spec/output-schemas/saved-capsule-reverification.schema.json +59 -0
- package/spec/output-schemas/saved-capsule-unwatch.schema.json +16 -0
- package/spec/output-schemas/saved-capsule-watch.schema.json +17 -0
- package/src/app/context-runtime-contract.ts +10 -5
- package/src/app/context-runtime-input.ts +29 -1
- package/src/app/context-runtime-types.ts +4 -0
- package/src/app/context-runtime.ts +5 -1
- package/src/app/context-surface.ts +4 -0
- package/src/app/verified-ask.ts +291 -0
- package/src/cli/commands/ask-format.ts +255 -0
- package/src/cli/commands/ask.ts +40 -149
- package/src/cli/commands/changes.ts +160 -0
- package/src/cli/commands/context-saved.ts +189 -0
- package/src/cli/options.ts +8 -0
- package/src/cli/program.ts +227 -1
- package/src/core/capsule-registry.ts +279 -0
- package/src/core/capsule-reverification-scheduler.ts +218 -0
- package/src/core/capsule-reverification.ts +289 -0
- package/src/core/change-diff.ts +182 -0
- package/src/core/change-journal.ts +228 -0
- package/src/core/context-budget.ts +6 -0
- package/src/core/context-capsule-retrieval-schema.ts +4 -0
- package/src/core/context-capsule-schema.ts +17 -0
- package/src/core/context-capsule-validation.ts +3 -2
- package/src/core/context-capsule.ts +18 -0
- package/src/core/context-compiler.ts +33 -21
- package/src/core/context-evidence.ts +6 -0
- package/src/core/knowledge-delta.ts +395 -0
- package/src/core/knowledge-impact.ts +202 -0
- package/src/core/retrieval-trace-evidence-origin.ts +3 -0
- package/src/core/retrieval-trace-session.ts +15 -2
- package/src/ingestion/sync.ts +214 -165
- package/src/llm/errors.ts +10 -1
- package/src/llm/httpGeneration.ts +11 -1
- package/src/llm/nodeLlamaCpp/generation.ts +54 -10
- package/src/llm/types.ts +6 -0
- package/src/mcp/tools/ask.ts +228 -0
- package/src/mcp/tools/changes.ts +80 -0
- package/src/mcp/tools/context.ts +28 -7
- package/src/mcp/tools/index.ts +38 -0
- package/src/pipeline/claim-verification-schema.ts +235 -0
- package/src/pipeline/claim-verification.ts +487 -0
- package/src/pipeline/claim-verifier.ts +474 -0
- package/src/pipeline/types.ts +25 -0
- package/src/sdk/client.ts +77 -2
- package/src/sdk/index.ts +7 -0
- package/src/sdk/types.ts +22 -0
- package/src/serve/doc-events.ts +12 -1
- package/src/serve/public/components/AskVerificationPanel.tsx +189 -0
- package/src/serve/public/globals.built.css +1 -1
- package/src/serve/public/pages/Ask.tsx +42 -4
- package/src/serve/resident-runtime.ts +22 -0
- package/src/serve/routes/api.ts +162 -3
- package/src/serve/routes/changes.ts +102 -0
- package/src/serve/server.ts +34 -0
- package/src/serve/watch-service.ts +9 -0
- package/src/store/index.ts +21 -0
- package/src/store/migrations/015-document-change-journal.ts +85 -0
- package/src/store/migrations/016-saved-capsules.ts +131 -0
- package/src/store/migrations/017-document-change-retention-counters.ts +33 -0
- package/src/store/migrations/018-saved-capsule-registration-epoch.ts +24 -0
- package/src/store/migrations/019-saved-capsule-registration-generation.ts +53 -0
- package/src/store/migrations/index.ts +10 -0
- package/src/store/sqlite/adapter.ts +291 -7
- package/src/store/sqlite/capsule-registry-store.ts +534 -0
- package/src/store/sqlite/change-journal-store.ts +473 -0
- package/src/store/types.ts +262 -0
package/src/ingestion/sync.ts
CHANGED
|
@@ -40,6 +40,13 @@ import {
|
|
|
40
40
|
getDefaultPipeline,
|
|
41
41
|
} from "../converters/pipeline";
|
|
42
42
|
import { DEFAULT_LIMITS } from "../converters/types";
|
|
43
|
+
import {
|
|
44
|
+
diffDocumentStructure,
|
|
45
|
+
extractDocumentStructure,
|
|
46
|
+
isRelationMap,
|
|
47
|
+
normalizeRelationEdgeType,
|
|
48
|
+
normalizeRelationTarget,
|
|
49
|
+
} from "../core/change-diff";
|
|
43
50
|
import {
|
|
44
51
|
normalizeMarkdownPath,
|
|
45
52
|
normalizeWikiName,
|
|
@@ -83,35 +90,6 @@ const NON_RETRYABLE_CONVERSION_ERROR_CODES = new Set([
|
|
|
83
90
|
"UNSUPPORTED",
|
|
84
91
|
]);
|
|
85
92
|
|
|
86
|
-
type RelationMap = Record<string, string[]>;
|
|
87
|
-
|
|
88
|
-
function isRelationMap(value: unknown): value is RelationMap {
|
|
89
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
90
|
-
return false;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
return Object.values(value).every(
|
|
94
|
-
(targets) =>
|
|
95
|
-
Array.isArray(targets) &&
|
|
96
|
-
targets.every((target) => typeof target === "string")
|
|
97
|
-
);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
function normalizeRelationTarget(raw: string): string {
|
|
101
|
-
const trimmed = raw.trim();
|
|
102
|
-
if (trimmed.startsWith("[[") && trimmed.endsWith("]]")) {
|
|
103
|
-
return trimmed.slice(2, -2).split("|")[0]?.trim() ?? "";
|
|
104
|
-
}
|
|
105
|
-
return trimmed;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
function normalizeRelationEdgeType(raw: string): string {
|
|
109
|
-
return raw
|
|
110
|
-
.trim()
|
|
111
|
-
.toLowerCase()
|
|
112
|
-
.replace(/[-\s]+/g, "_");
|
|
113
|
-
}
|
|
114
|
-
|
|
115
93
|
function findDocByWikiRef(
|
|
116
94
|
docs: DocumentRow[],
|
|
117
95
|
targetRef: string,
|
|
@@ -747,7 +725,24 @@ export class SyncService {
|
|
|
747
725
|
// Log but continue - error recording is best-effort
|
|
748
726
|
}
|
|
749
727
|
|
|
750
|
-
|
|
728
|
+
const hadRetrievableEvidence = Boolean(existing?.mirrorHash);
|
|
729
|
+
const structureDelta = hadRetrievableEvidence
|
|
730
|
+
? diffDocumentStructure(
|
|
731
|
+
await this.readPreviousStructure(store, existing),
|
|
732
|
+
{
|
|
733
|
+
headings: [],
|
|
734
|
+
links: [],
|
|
735
|
+
typedEdges: [],
|
|
736
|
+
dates: {},
|
|
737
|
+
}
|
|
738
|
+
).delta
|
|
739
|
+
: undefined;
|
|
740
|
+
|
|
741
|
+
// Upsert document with error info, explicitly clear mirrorHash. This
|
|
742
|
+
// transition is journaled only when retrievable evidence previously
|
|
743
|
+
// existed; initial/repeated error placeholders are not evidence
|
|
744
|
+
// creates. Real evidence disappearance stays in the same transaction
|
|
745
|
+
// so saved Capsule freshness checks cannot miss it.
|
|
751
746
|
const upsertResult = await store.upsertDocument({
|
|
752
747
|
collection: collection.name,
|
|
753
748
|
relPath: entry.relPath,
|
|
@@ -761,6 +756,7 @@ export class SyncService {
|
|
|
761
756
|
lastErrorMessage: convertResult.error.message,
|
|
762
757
|
ingestVersion: INGEST_VERSION,
|
|
763
758
|
contentTypeRulesFingerprint,
|
|
759
|
+
changeJournal: structureDelta ? { structureDelta } : false,
|
|
764
760
|
// mirrorHash intentionally omitted (will be null)
|
|
765
761
|
});
|
|
766
762
|
|
|
@@ -788,155 +784,186 @@ export class SyncService {
|
|
|
788
784
|
mime.ext,
|
|
789
785
|
contentTypeRules
|
|
790
786
|
);
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
collection: collection.name,
|
|
795
|
-
relPath: entry.relPath,
|
|
796
|
-
sourceHash,
|
|
797
|
-
sourceMime: mime.mime,
|
|
798
|
-
sourceExt: mime.ext,
|
|
799
|
-
sourceSize,
|
|
800
|
-
sourceMtime,
|
|
801
|
-
sourceCtime,
|
|
802
|
-
title: artifact.title,
|
|
803
|
-
mirrorHash: artifact.mirrorHash,
|
|
804
|
-
converterId: artifact.meta.converterId,
|
|
805
|
-
converterVersion: artifact.meta.converterVersion,
|
|
806
|
-
languageHint: artifact.languageHint ?? collection.languageHint,
|
|
807
|
-
contentType: extractedMetadata.contentType,
|
|
808
|
-
contentTypeSource: extractedMetadata.contentTypeSource,
|
|
809
|
-
categories: extractedMetadata.categories,
|
|
810
|
-
author: extractedMetadata.author,
|
|
811
|
-
frontmatterDate: extractedMetadata.frontmatterDate,
|
|
812
|
-
dateFields: extractedMetadata.dateFields,
|
|
813
|
-
contentTypeRulesFingerprint,
|
|
814
|
-
// Clear error fields on success (requires store to handle undefined → null)
|
|
815
|
-
lastErrorCode: undefined,
|
|
816
|
-
lastErrorMessage: undefined,
|
|
817
|
-
ingestVersion: INGEST_VERSION,
|
|
818
|
-
});
|
|
819
|
-
|
|
820
|
-
const { id: docId, docid } = mustOk(docidResult, "upsertDocument", {
|
|
821
|
-
collection: collection.name,
|
|
822
|
-
relPath: entry.relPath,
|
|
823
|
-
});
|
|
824
|
-
|
|
825
|
-
// 8. Upsert content (content-addressed dedupe) - CHECKED
|
|
826
|
-
const contentResult = await store.upsertContent(
|
|
827
|
-
artifact.mirrorHash,
|
|
828
|
-
artifact.markdown
|
|
787
|
+
const previousStructure = await this.readPreviousStructure(
|
|
788
|
+
store,
|
|
789
|
+
existing
|
|
829
790
|
);
|
|
830
|
-
|
|
831
|
-
mirrorHash: artifact.mirrorHash,
|
|
832
|
-
});
|
|
833
|
-
|
|
834
|
-
// 9. Chunk content
|
|
835
|
-
const chunks = this.chunker.chunk(
|
|
791
|
+
const nextStructure = extractDocumentStructure(
|
|
836
792
|
artifact.markdown,
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
entry.relPath
|
|
793
|
+
entry.relPath,
|
|
794
|
+
extractedMetadata.dateFields
|
|
840
795
|
);
|
|
796
|
+
const structureDelta = diffDocumentStructure(
|
|
797
|
+
previousStructure,
|
|
798
|
+
nextStructure
|
|
799
|
+
).delta;
|
|
800
|
+
|
|
801
|
+
const persistSuccessfulFile = async (): Promise<FileSyncResult> => {
|
|
802
|
+
// 7. Upsert document - EXPLICITLY clear error fields on success
|
|
803
|
+
const docidResult = await store.upsertDocument({
|
|
804
|
+
collection: collection.name,
|
|
805
|
+
relPath: entry.relPath,
|
|
806
|
+
sourceHash,
|
|
807
|
+
sourceMime: mime.mime,
|
|
808
|
+
sourceExt: mime.ext,
|
|
809
|
+
sourceSize,
|
|
810
|
+
sourceMtime,
|
|
811
|
+
sourceCtime,
|
|
812
|
+
title: artifact.title,
|
|
813
|
+
mirrorHash: artifact.mirrorHash,
|
|
814
|
+
converterId: artifact.meta.converterId,
|
|
815
|
+
converterVersion: artifact.meta.converterVersion,
|
|
816
|
+
languageHint: artifact.languageHint ?? collection.languageHint,
|
|
817
|
+
contentType: extractedMetadata.contentType,
|
|
818
|
+
contentTypeSource: extractedMetadata.contentTypeSource,
|
|
819
|
+
categories: extractedMetadata.categories,
|
|
820
|
+
author: extractedMetadata.author,
|
|
821
|
+
frontmatterDate: extractedMetadata.frontmatterDate,
|
|
822
|
+
dateFields: extractedMetadata.dateFields,
|
|
823
|
+
contentTypeRulesFingerprint,
|
|
824
|
+
// Clear error fields on success (requires store to handle undefined → null)
|
|
825
|
+
lastErrorCode: undefined,
|
|
826
|
+
lastErrorMessage: undefined,
|
|
827
|
+
ingestVersion: INGEST_VERSION,
|
|
828
|
+
changeJournal: { structureDelta },
|
|
829
|
+
});
|
|
841
830
|
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
text: c.text,
|
|
847
|
-
startLine: c.startLine,
|
|
848
|
-
endLine: c.endLine,
|
|
849
|
-
language: c.language ?? undefined,
|
|
850
|
-
tokenCount: c.tokenCount ?? undefined,
|
|
851
|
-
}));
|
|
852
|
-
|
|
853
|
-
// 11. Upsert chunks - CHECKED
|
|
854
|
-
const chunksResult = await store.upsertChunks(
|
|
855
|
-
artifact.mirrorHash,
|
|
856
|
-
chunkInputs
|
|
857
|
-
);
|
|
858
|
-
mustOk(chunksResult, "upsertChunks", {
|
|
859
|
-
mirrorHash: artifact.mirrorHash,
|
|
860
|
-
chunkCount: chunkInputs.length,
|
|
861
|
-
});
|
|
831
|
+
const { id: docId, docid } = mustOk(docidResult, "upsertDocument", {
|
|
832
|
+
collection: collection.name,
|
|
833
|
+
relPath: entry.relPath,
|
|
834
|
+
});
|
|
862
835
|
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
836
|
+
// 8. Upsert content (content-addressed dedupe) - CHECKED
|
|
837
|
+
const contentResult = await store.upsertContent(
|
|
838
|
+
artifact.mirrorHash,
|
|
839
|
+
artifact.markdown
|
|
840
|
+
);
|
|
841
|
+
mustOk(contentResult, "upsertContent", {
|
|
842
|
+
mirrorHash: artifact.mirrorHash,
|
|
843
|
+
});
|
|
868
844
|
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
);
|
|
877
|
-
mustOk(tagsResult, "setDocTags", {
|
|
878
|
-
docId,
|
|
879
|
-
tagCount: extractedTags.length,
|
|
880
|
-
});
|
|
845
|
+
// 9. Chunk content
|
|
846
|
+
const chunks = this.chunker.chunk(
|
|
847
|
+
artifact.markdown,
|
|
848
|
+
DEFAULT_CHUNK_PARAMS,
|
|
849
|
+
artifact.languageHint ?? collection.languageHint,
|
|
850
|
+
entry.relPath
|
|
851
|
+
);
|
|
881
852
|
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
853
|
+
// 10. Convert to ChunkInput for store
|
|
854
|
+
const chunkInputs: ChunkInput[] = chunks.map((c) => ({
|
|
855
|
+
seq: c.seq,
|
|
856
|
+
pos: c.pos,
|
|
857
|
+
text: c.text,
|
|
858
|
+
startLine: c.startLine,
|
|
859
|
+
endLine: c.endLine,
|
|
860
|
+
language: c.language ?? undefined,
|
|
861
|
+
tokenCount: c.tokenCount ?? undefined,
|
|
862
|
+
}));
|
|
863
|
+
|
|
864
|
+
// 11. Upsert chunks - CHECKED
|
|
865
|
+
const chunksResult = await store.upsertChunks(
|
|
866
|
+
artifact.mirrorHash,
|
|
867
|
+
chunkInputs
|
|
868
|
+
);
|
|
869
|
+
mustOk(chunksResult, "upsertChunks", {
|
|
870
|
+
mirrorHash: artifact.mirrorHash,
|
|
871
|
+
chunkCount: chunkInputs.length,
|
|
872
|
+
});
|
|
890
873
|
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
874
|
+
// 12. Rebuild FTS for this hash - CHECKED
|
|
875
|
+
const ftsResult = await store.rebuildFtsForHash(artifact.mirrorHash);
|
|
876
|
+
mustOk(ftsResult, "rebuildFtsForHash", {
|
|
877
|
+
mirrorHash: artifact.mirrorHash,
|
|
878
|
+
});
|
|
879
|
+
|
|
880
|
+
// 13. Extract and store tags from frontmatter and body hashtags
|
|
881
|
+
// Always call setDocTags to clear removed tags on re-sync
|
|
882
|
+
const extractedTags = extractTags(artifact.markdown);
|
|
883
|
+
const tagsResult = await store.setDocTags(
|
|
884
|
+
docId,
|
|
885
|
+
extractedTags,
|
|
886
|
+
"frontmatter"
|
|
887
|
+
);
|
|
888
|
+
mustOk(tagsResult, "setDocTags", {
|
|
889
|
+
docId,
|
|
890
|
+
tagCount: extractedTags.length,
|
|
891
|
+
});
|
|
892
|
+
|
|
893
|
+
// 14. Extract and store links (wiki and markdown links)
|
|
894
|
+
const excludedRanges = getExcludedRanges(artifact.markdown);
|
|
895
|
+
const lineOffsets = buildLineOffsets(artifact.markdown);
|
|
896
|
+
const parsedLinks = parseLinks(
|
|
897
|
+
artifact.markdown,
|
|
898
|
+
lineOffsets,
|
|
899
|
+
excludedRanges
|
|
900
|
+
);
|
|
901
|
+
|
|
902
|
+
const linkInputs: DocLinkInput[] = [];
|
|
903
|
+
for (const link of parsedLinks) {
|
|
904
|
+
// Compute target_ref_norm based on link type
|
|
905
|
+
let targetRefNorm: string;
|
|
906
|
+
if (link.kind === "wiki") {
|
|
907
|
+
targetRefNorm = normalizeWikiName(link.targetRef);
|
|
908
|
+
} else {
|
|
909
|
+
// Markdown links with collection prefix are not supported
|
|
910
|
+
// (use wiki links for cross-collection references)
|
|
911
|
+
if (link.targetCollection) {
|
|
912
|
+
continue;
|
|
913
|
+
}
|
|
914
|
+
const resolved = normalizeMarkdownPath(
|
|
915
|
+
link.targetRef,
|
|
916
|
+
entry.relPath
|
|
917
|
+
);
|
|
918
|
+
if (!resolved) {
|
|
919
|
+
// Link escapes collection root - skip silently
|
|
920
|
+
continue;
|
|
921
|
+
}
|
|
922
|
+
targetRefNorm = resolved;
|
|
907
923
|
}
|
|
908
|
-
|
|
924
|
+
|
|
925
|
+
linkInputs.push({
|
|
926
|
+
targetRef: link.targetRef,
|
|
927
|
+
targetRefNorm,
|
|
928
|
+
targetAnchor: link.targetAnchor,
|
|
929
|
+
targetCollection: link.targetCollection,
|
|
930
|
+
linkType: link.kind,
|
|
931
|
+
linkText: link.displayText,
|
|
932
|
+
startLine: link.startLine,
|
|
933
|
+
startCol: link.startCol,
|
|
934
|
+
endLine: link.endLine,
|
|
935
|
+
endCol: link.endCol,
|
|
936
|
+
});
|
|
909
937
|
}
|
|
910
938
|
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
startCol: link.startCol,
|
|
920
|
-
endLine: link.endLine,
|
|
921
|
-
endCol: link.endCol,
|
|
939
|
+
const linksResult = await store.setDocLinks(
|
|
940
|
+
docId,
|
|
941
|
+
linkInputs,
|
|
942
|
+
"parsed"
|
|
943
|
+
);
|
|
944
|
+
mustOk(linksResult, "setDocLinks", {
|
|
945
|
+
docId,
|
|
946
|
+
linkCount: linkInputs.length,
|
|
922
947
|
});
|
|
923
|
-
}
|
|
924
948
|
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
status,
|
|
935
|
-
docid,
|
|
936
|
-
mirrorHash: artifact.mirrorHash,
|
|
937
|
-
contentType: extractedMetadata.contentType,
|
|
938
|
-
contentTypeSource: extractedMetadata.contentTypeSource,
|
|
949
|
+
const status = existing ? "updated" : "added";
|
|
950
|
+
return {
|
|
951
|
+
relPath: entry.relPath,
|
|
952
|
+
status,
|
|
953
|
+
docid,
|
|
954
|
+
mirrorHash: artifact.mirrorHash,
|
|
955
|
+
contentType: extractedMetadata.contentType,
|
|
956
|
+
contentTypeSource: extractedMetadata.contentTypeSource,
|
|
957
|
+
};
|
|
939
958
|
};
|
|
959
|
+
if (!store.withTransaction) {
|
|
960
|
+
return await persistSuccessfulFile();
|
|
961
|
+
}
|
|
962
|
+
const persisted = await store.withTransaction(persistSuccessfulFile);
|
|
963
|
+
return mustOk(persisted, "persistSuccessfulFile", {
|
|
964
|
+
collection: collection.name,
|
|
965
|
+
relPath: entry.relPath,
|
|
966
|
+
});
|
|
940
967
|
} catch (error) {
|
|
941
968
|
const message = error instanceof Error ? error.message : "Unknown error";
|
|
942
969
|
// Distinguish store errors from other internal errors
|
|
@@ -977,6 +1004,7 @@ export class SyncService {
|
|
|
977
1004
|
existingResult.value.sourceMtime,
|
|
978
1005
|
lastErrorCode: code,
|
|
979
1006
|
lastErrorMessage: message,
|
|
1007
|
+
changeJournal: false,
|
|
980
1008
|
});
|
|
981
1009
|
}
|
|
982
1010
|
} catch {
|
|
@@ -992,6 +1020,27 @@ export class SyncService {
|
|
|
992
1020
|
}
|
|
993
1021
|
}
|
|
994
1022
|
|
|
1023
|
+
private async readPreviousStructure(
|
|
1024
|
+
store: StorePort,
|
|
1025
|
+
existing: DocumentRow | null
|
|
1026
|
+
): Promise<ReturnType<typeof extractDocumentStructure> | null | undefined> {
|
|
1027
|
+
if (!existing) return null;
|
|
1028
|
+
if (!existing.mirrorHash) return undefined;
|
|
1029
|
+
|
|
1030
|
+
const content = await store.getContent(existing.mirrorHash);
|
|
1031
|
+
if (!content.ok) {
|
|
1032
|
+
throw new Error(`Store operation failed: ${content.error.message}`, {
|
|
1033
|
+
cause: content.error,
|
|
1034
|
+
});
|
|
1035
|
+
}
|
|
1036
|
+
if (content.value === null) return undefined;
|
|
1037
|
+
return extractDocumentStructure(
|
|
1038
|
+
content.value,
|
|
1039
|
+
existing.relPath,
|
|
1040
|
+
existing.dateFields
|
|
1041
|
+
);
|
|
1042
|
+
}
|
|
1043
|
+
|
|
995
1044
|
/**
|
|
996
1045
|
* Sync a specific set of files within a collection.
|
|
997
1046
|
*/
|
package/src/llm/errors.ts
CHANGED
|
@@ -22,7 +22,8 @@ export type LlmErrorCode =
|
|
|
22
22
|
| "OUT_OF_MEMORY"
|
|
23
23
|
| "INVALID_URI"
|
|
24
24
|
| "LOCK_FAILED"
|
|
25
|
-
| "AUTO_DOWNLOAD_DISABLED"
|
|
25
|
+
| "AUTO_DOWNLOAD_DISABLED"
|
|
26
|
+
| "STRUCTURED_OUTPUT_UNAVAILABLE";
|
|
26
27
|
|
|
27
28
|
export interface LlmError {
|
|
28
29
|
code: LlmErrorCode;
|
|
@@ -248,3 +249,11 @@ export function autoDownloadDisabledError(uri: string): LlmError {
|
|
|
248
249
|
suggestion: "Run 'gno models pull' to download models manually.",
|
|
249
250
|
});
|
|
250
251
|
}
|
|
252
|
+
|
|
253
|
+
export function structuredOutputUnavailableError(uri: string): LlmError {
|
|
254
|
+
return llmError("STRUCTURED_OUTPUT_UNAVAILABLE", {
|
|
255
|
+
message: `JSON Schema constrained generation is unavailable for model: ${uri}`,
|
|
256
|
+
modelUri: uri,
|
|
257
|
+
retryable: false,
|
|
258
|
+
});
|
|
259
|
+
}
|
|
@@ -7,7 +7,10 @@
|
|
|
7
7
|
|
|
8
8
|
import type { GenerationPort, GenParams, LlmResult } from "./types";
|
|
9
9
|
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
inferenceFailedError,
|
|
12
|
+
structuredOutputUnavailableError,
|
|
13
|
+
} from "./errors";
|
|
11
14
|
|
|
12
15
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
13
16
|
// Types
|
|
@@ -42,6 +45,7 @@ export class HttpGeneration implements GenerationPort {
|
|
|
42
45
|
private readonly apiUrl: string;
|
|
43
46
|
private readonly modelName: string;
|
|
44
47
|
readonly modelUri: string;
|
|
48
|
+
readonly structuredOutput = "none" as const;
|
|
45
49
|
|
|
46
50
|
constructor(modelUri: string) {
|
|
47
51
|
this.modelUri = modelUri;
|
|
@@ -63,6 +67,12 @@ export class HttpGeneration implements GenerationPort {
|
|
|
63
67
|
prompt: string,
|
|
64
68
|
params?: GenParams
|
|
65
69
|
): Promise<LlmResult<string>> {
|
|
70
|
+
if (params?.jsonSchema) {
|
|
71
|
+
return {
|
|
72
|
+
ok: false,
|
|
73
|
+
error: structuredOutputUnavailableError(this.modelUri),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
66
76
|
try {
|
|
67
77
|
const response = await fetch(this.apiUrl, {
|
|
68
78
|
method: "POST",
|
|
@@ -18,6 +18,39 @@ type LlamaModel = Awaited<
|
|
|
18
18
|
Awaited<ReturnType<typeof import("node-llama-cpp").getLlama>>["loadModel"]
|
|
19
19
|
>
|
|
20
20
|
>;
|
|
21
|
+
type Llama = Awaited<ReturnType<typeof import("node-llama-cpp").getLlama>>;
|
|
22
|
+
type JsonGrammarSchema = Parameters<Llama["createGrammarForJsonSchema"]>[0];
|
|
23
|
+
|
|
24
|
+
export interface JsonSchemaGrammarLike {
|
|
25
|
+
parse(response: string): unknown;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface StructuredPromptSession {
|
|
29
|
+
prompt(
|
|
30
|
+
prompt: string,
|
|
31
|
+
options: {
|
|
32
|
+
temperature: number;
|
|
33
|
+
seed: number;
|
|
34
|
+
maxTokens: number;
|
|
35
|
+
grammar?: JsonSchemaGrammarLike;
|
|
36
|
+
}
|
|
37
|
+
): Promise<string>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export const promptWithJsonSchemaGrammar = async (
|
|
41
|
+
session: StructuredPromptSession,
|
|
42
|
+
prompt: string,
|
|
43
|
+
options: {
|
|
44
|
+
temperature: number;
|
|
45
|
+
seed: number;
|
|
46
|
+
maxTokens: number;
|
|
47
|
+
},
|
|
48
|
+
grammar?: JsonSchemaGrammarLike
|
|
49
|
+
): Promise<string> => {
|
|
50
|
+
const response = await session.prompt(prompt, { ...options, grammar });
|
|
51
|
+
grammar?.parse(response);
|
|
52
|
+
return response;
|
|
53
|
+
};
|
|
21
54
|
|
|
22
55
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
23
56
|
// Default Parameters (for determinism)
|
|
@@ -34,6 +67,7 @@ const DEFAULT_MAX_TOKENS = 256;
|
|
|
34
67
|
export class NodeLlamaCppGeneration implements GenerationPort {
|
|
35
68
|
private readonly manager: ModelManager;
|
|
36
69
|
readonly modelUri: string;
|
|
70
|
+
readonly structuredOutput = "json_schema" as const;
|
|
37
71
|
private readonly modelPath: string;
|
|
38
72
|
|
|
39
73
|
constructor(manager: ModelManager, modelUri: string, modelPath: string) {
|
|
@@ -56,11 +90,16 @@ export class NodeLlamaCppGeneration implements GenerationPort {
|
|
|
56
90
|
}
|
|
57
91
|
|
|
58
92
|
const llamaModel = model.value.model as LlamaModel;
|
|
59
|
-
|
|
60
|
-
params?.contextSize ? { contextSize: params.contextSize } : undefined
|
|
61
|
-
);
|
|
62
|
-
|
|
93
|
+
let context: Awaited<ReturnType<LlamaModel["createContext"]>> | null = null;
|
|
63
94
|
try {
|
|
95
|
+
const grammar = params?.jsonSchema
|
|
96
|
+
? await (
|
|
97
|
+
await this.manager.getLlama()
|
|
98
|
+
).createGrammarForJsonSchema(params.jsonSchema as JsonGrammarSchema)
|
|
99
|
+
: undefined;
|
|
100
|
+
context = await llamaModel.createContext(
|
|
101
|
+
params?.contextSize ? { contextSize: params.contextSize } : undefined
|
|
102
|
+
);
|
|
64
103
|
// Import LlamaChatSession dynamically
|
|
65
104
|
const { LlamaChatSession } = await import("node-llama-cpp");
|
|
66
105
|
const session = new LlamaChatSession({
|
|
@@ -68,17 +107,22 @@ export class NodeLlamaCppGeneration implements GenerationPort {
|
|
|
68
107
|
});
|
|
69
108
|
|
|
70
109
|
// Note: stop sequences not yet supported - requires stopOnTrigger API
|
|
71
|
-
const response = await
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
110
|
+
const response = await promptWithJsonSchemaGrammar(
|
|
111
|
+
session as StructuredPromptSession,
|
|
112
|
+
prompt,
|
|
113
|
+
{
|
|
114
|
+
temperature: params?.temperature ?? DEFAULT_TEMPERATURE,
|
|
115
|
+
seed: params?.seed ?? DEFAULT_SEED,
|
|
116
|
+
maxTokens: params?.maxTokens ?? DEFAULT_MAX_TOKENS,
|
|
117
|
+
},
|
|
118
|
+
grammar
|
|
119
|
+
);
|
|
76
120
|
|
|
77
121
|
return { ok: true, value: response };
|
|
78
122
|
} catch (e) {
|
|
79
123
|
return { ok: false, error: inferenceFailedError(this.modelUri, e) };
|
|
80
124
|
} finally {
|
|
81
|
-
await context
|
|
125
|
+
await context?.dispose().catch(() => {
|
|
82
126
|
// Ignore disposal errors
|
|
83
127
|
});
|
|
84
128
|
}
|
package/src/llm/types.ts
CHANGED
|
@@ -58,8 +58,12 @@ export interface GenParams {
|
|
|
58
58
|
contextSize?: number;
|
|
59
59
|
/** Stop sequences */
|
|
60
60
|
stop?: string[];
|
|
61
|
+
/** Closed JSON Schema enforced by a capable generation backend. */
|
|
62
|
+
jsonSchema?: Readonly<Record<string, unknown>>;
|
|
61
63
|
}
|
|
62
64
|
|
|
65
|
+
export type StructuredOutputCapability = "json_schema" | "none";
|
|
66
|
+
|
|
63
67
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
64
68
|
// Rerank Types
|
|
65
69
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -90,6 +94,8 @@ export interface EmbeddingPort {
|
|
|
90
94
|
|
|
91
95
|
export interface GenerationPort {
|
|
92
96
|
readonly modelUri: string;
|
|
97
|
+
/** Undefined is treated as unsupported for backwards-compatible ports. */
|
|
98
|
+
readonly structuredOutput?: StructuredOutputCapability;
|
|
93
99
|
generate(prompt: string, params?: GenParams): Promise<LlmResult<string>>;
|
|
94
100
|
dispose(): Promise<void>;
|
|
95
101
|
}
|