@geotechcli/core 0.4.21 → 0.4.23

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.
Files changed (154) hide show
  1. package/dist/agents/brain.d.ts +1 -5
  2. package/dist/agents/brain.d.ts.map +1 -1
  3. package/dist/agents/brain.js +4 -120
  4. package/dist/agents/brain.js.map +1 -1
  5. package/dist/agents/data-tools.js +759 -0
  6. package/dist/agents/data-tools.js.map +1 -1
  7. package/dist/agents/runtime-bootstrap.d.ts +6 -0
  8. package/dist/agents/runtime-bootstrap.d.ts.map +1 -0
  9. package/dist/agents/runtime-bootstrap.js +8 -0
  10. package/dist/agents/runtime-bootstrap.js.map +1 -0
  11. package/dist/agents/runtime-fallbacks.d.ts +7 -0
  12. package/dist/agents/runtime-fallbacks.d.ts.map +1 -0
  13. package/dist/agents/runtime-fallbacks.js +87 -0
  14. package/dist/agents/runtime-fallbacks.js.map +1 -0
  15. package/dist/agents/swarm.d.ts +1 -4
  16. package/dist/agents/swarm.d.ts.map +1 -1
  17. package/dist/agents/swarm.js +74 -8
  18. package/dist/agents/swarm.js.map +1 -1
  19. package/dist/agents/tool-runtime.d.ts +7 -0
  20. package/dist/agents/tool-runtime.d.ts.map +1 -0
  21. package/dist/agents/tool-runtime.js +9 -0
  22. package/dist/agents/tool-runtime.js.map +1 -0
  23. package/dist/config/index.d.ts +4 -4
  24. package/dist/config/index.js +1 -1
  25. package/dist/config/index.js.map +1 -1
  26. package/dist/geo/coordinates.d.ts +40 -0
  27. package/dist/geo/coordinates.d.ts.map +1 -0
  28. package/dist/geo/coordinates.js +461 -0
  29. package/dist/geo/coordinates.js.map +1 -0
  30. package/dist/geo/index.d.ts +1 -0
  31. package/dist/geo/index.d.ts.map +1 -1
  32. package/dist/geo/index.js +1 -0
  33. package/dist/geo/index.js.map +1 -1
  34. package/dist/index.d.ts +3 -2
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +3 -2
  37. package/dist/index.js.map +1 -1
  38. package/dist/ingest/ags.d.ts +3 -0
  39. package/dist/ingest/ags.d.ts.map +1 -1
  40. package/dist/ingest/ags.js +98 -9
  41. package/dist/ingest/ags.js.map +1 -1
  42. package/dist/ingest/cpt.d.ts +4 -0
  43. package/dist/ingest/cpt.d.ts.map +1 -1
  44. package/dist/ingest/cpt.js +87 -25
  45. package/dist/ingest/cpt.js.map +1 -1
  46. package/dist/ingest/document-inputs.d.ts +37 -0
  47. package/dist/ingest/document-inputs.d.ts.map +1 -0
  48. package/dist/ingest/document-inputs.js +197 -0
  49. package/dist/ingest/document-inputs.js.map +1 -0
  50. package/dist/ingest/geotech-document.d.ts +118 -0
  51. package/dist/ingest/geotech-document.d.ts.map +1 -0
  52. package/dist/ingest/geotech-document.js +1006 -0
  53. package/dist/ingest/geotech-document.js.map +1 -0
  54. package/dist/ingest/geotech-extract.d.ts +86 -0
  55. package/dist/ingest/geotech-extract.d.ts.map +1 -0
  56. package/dist/ingest/geotech-extract.js +652 -0
  57. package/dist/ingest/geotech-extract.js.map +1 -0
  58. package/dist/ingest/geotech-schemas.d.ts +248 -0
  59. package/dist/ingest/geotech-schemas.d.ts.map +1 -0
  60. package/dist/ingest/geotech-schemas.js +150 -0
  61. package/dist/ingest/geotech-schemas.js.map +1 -0
  62. package/dist/ingest/index.d.ts +8 -0
  63. package/dist/ingest/index.d.ts.map +1 -1
  64. package/dist/ingest/index.js +8 -0
  65. package/dist/ingest/index.js.map +1 -1
  66. package/dist/ingest/ingest-job-child.d.ts +2 -0
  67. package/dist/ingest/ingest-job-child.d.ts.map +1 -0
  68. package/dist/ingest/ingest-job-child.js +45 -0
  69. package/dist/ingest/ingest-job-child.js.map +1 -0
  70. package/dist/ingest/job-store.d.ts +117 -0
  71. package/dist/ingest/job-store.d.ts.map +1 -0
  72. package/dist/ingest/job-store.js +541 -0
  73. package/dist/ingest/job-store.js.map +1 -0
  74. package/dist/ingest/job-worker.d.ts +24 -0
  75. package/dist/ingest/job-worker.d.ts.map +1 -0
  76. package/dist/ingest/job-worker.js +1129 -0
  77. package/dist/ingest/job-worker.js.map +1 -0
  78. package/dist/ingest/pdf.d.ts +102 -0
  79. package/dist/ingest/pdf.d.ts.map +1 -0
  80. package/dist/ingest/pdf.js +1544 -0
  81. package/dist/ingest/pdf.js.map +1 -0
  82. package/dist/ingest/review-store.d.ts +215 -0
  83. package/dist/ingest/review-store.d.ts.map +1 -0
  84. package/dist/ingest/review-store.js +1995 -0
  85. package/dist/ingest/review-store.js.map +1 -0
  86. package/dist/llm/capabilities.d.ts +8 -0
  87. package/dist/llm/capabilities.d.ts.map +1 -0
  88. package/dist/llm/capabilities.js +73 -0
  89. package/dist/llm/capabilities.js.map +1 -0
  90. package/dist/llm/index.d.ts +3 -2
  91. package/dist/llm/index.d.ts.map +1 -1
  92. package/dist/llm/index.js +2 -1
  93. package/dist/llm/index.js.map +1 -1
  94. package/dist/llm/providers/anthropic.d.ts +6 -0
  95. package/dist/llm/providers/anthropic.d.ts.map +1 -1
  96. package/dist/llm/providers/anthropic.js +10 -1
  97. package/dist/llm/providers/anthropic.js.map +1 -1
  98. package/dist/llm/providers/hosted-beta.d.ts +6 -0
  99. package/dist/llm/providers/hosted-beta.d.ts.map +1 -1
  100. package/dist/llm/providers/hosted-beta.js +40 -10
  101. package/dist/llm/providers/hosted-beta.js.map +1 -1
  102. package/dist/llm/providers/huggingface.d.ts +6 -0
  103. package/dist/llm/providers/huggingface.d.ts.map +1 -1
  104. package/dist/llm/providers/huggingface.js +21 -1
  105. package/dist/llm/providers/huggingface.js.map +1 -1
  106. package/dist/llm/providers/openai-compatible.d.ts +6 -0
  107. package/dist/llm/providers/openai-compatible.d.ts.map +1 -1
  108. package/dist/llm/providers/openai-compatible.js +21 -1
  109. package/dist/llm/providers/openai-compatible.js.map +1 -1
  110. package/dist/llm/providers/zhipu.d.ts +6 -0
  111. package/dist/llm/providers/zhipu.d.ts.map +1 -1
  112. package/dist/llm/providers/zhipu.js +15 -1
  113. package/dist/llm/providers/zhipu.js.map +1 -1
  114. package/dist/llm/router.d.ts +7 -0
  115. package/dist/llm/router.d.ts.map +1 -1
  116. package/dist/llm/router.js +33 -13
  117. package/dist/llm/router.js.map +1 -1
  118. package/dist/llm/types.d.ts +22 -4
  119. package/dist/llm/types.d.ts.map +1 -1
  120. package/dist/llm/types.js.map +1 -1
  121. package/dist/meta/metadata.json +1 -1
  122. package/dist/report/html.d.ts +3 -0
  123. package/dist/report/html.d.ts.map +1 -0
  124. package/dist/report/html.js +626 -0
  125. package/dist/report/html.js.map +1 -0
  126. package/dist/report/index.d.ts +2 -0
  127. package/dist/report/index.d.ts.map +1 -1
  128. package/dist/report/index.js +2 -0
  129. package/dist/report/index.js.map +1 -1
  130. package/dist/report/ingest-dossier.d.ts +81 -0
  131. package/dist/report/ingest-dossier.d.ts.map +1 -0
  132. package/dist/report/ingest-dossier.js +324 -0
  133. package/dist/report/ingest-dossier.js.map +1 -0
  134. package/dist/storage/index.d.ts +5 -0
  135. package/dist/storage/index.d.ts.map +1 -1
  136. package/dist/storage/index.js +12 -6
  137. package/dist/storage/index.js.map +1 -1
  138. package/dist/vision/geotech-document.d.ts +46 -0
  139. package/dist/vision/geotech-document.d.ts.map +1 -0
  140. package/dist/vision/geotech-document.js +576 -0
  141. package/dist/vision/geotech-document.js.map +1 -0
  142. package/dist/vision/index.d.ts +31 -0
  143. package/dist/vision/index.d.ts.map +1 -1
  144. package/dist/vision/index.js +659 -27
  145. package/dist/vision/index.js.map +1 -1
  146. package/dist/vision/ocr.d.ts +29 -0
  147. package/dist/vision/ocr.d.ts.map +1 -0
  148. package/dist/vision/ocr.js +287 -0
  149. package/dist/vision/ocr.js.map +1 -0
  150. package/dist/vision/preprocess.d.ts +26 -0
  151. package/dist/vision/preprocess.d.ts.map +1 -0
  152. package/dist/vision/preprocess.js +194 -0
  153. package/dist/vision/preprocess.js.map +1 -0
  154. package/package.json +5 -1
@@ -1,10 +1,97 @@
1
1
  import { toolRegistry } from './tools.js';
2
2
  import { parseAGS } from '../ingest/ags.js';
3
3
  import { parseCPT } from '../ingest/cpt.js';
4
+ import { approvePersistedBoreholeIngestReview, countDocumentPdfPages, getGeotechIngestJob, ingestBoreholeLogDocument, ingestGeotechDocument, inspectPdfDocument, listGeotechIngestJobs, listPersistedBoreholeIngestReviewApprovals, loadLatestPersistedBoreholeIngestReviewApproval, loadGeotechIngestJobResult, loadPersistedBoreholeIngestReviewApproval, listPersistedBoreholeIngestReviews, loadLatestPersistedBoreholeIngestReview, loadPersistedBoreholeIngestReview, persistBoreholeIngestReview, promotePersistedBoreholeIngestReview, readDocumentPdfPageInputs, readDocumentVisionInput, startGeotechIngestJob, summarizePersistedBoreholeIngestReviewApproval, waitGeotechIngestJob, } from '../ingest/index.js';
4
5
  import { queryStandards } from '../standards/index.js';
6
+ import { buildLLMConfig } from '../config/index.js';
5
7
  import { createProject, loadProject, listProjects, addSimulationResult, saveNamedDataset, saveDerivedParameter, addAssumption, addArtifact, } from '../storage/index.js';
6
8
  import { validateReadPath } from './sandbox.js';
9
+ import { getToolRuntimeContext } from './tool-runtime.js';
7
10
  import { existsSync } from 'node:fs';
11
+ import { basename } from 'node:path';
12
+ function resolveActiveLLMConfig() {
13
+ return getToolRuntimeContext()?.config ?? buildLLMConfig();
14
+ }
15
+ function buildDocumentSource(filePath, kind) {
16
+ return {
17
+ filePath,
18
+ fileName: basename(filePath),
19
+ inputKind: kind,
20
+ };
21
+ }
22
+ const MAX_SYNC_INGEST_PDF_PAGES = 5;
23
+ const MAX_SYNC_INGEST_FILE_BYTES = 8 * 1024 * 1024;
24
+ function reviewSourceLabel(record) {
25
+ return record.result.source.fileName ?? record.result.source.filePath ?? record.result.documentType;
26
+ }
27
+ function summarizePersistedReviewRecord(record) {
28
+ return {
29
+ reviewId: record.reviewId,
30
+ datasetName: record.datasetName,
31
+ projectId: record.projectId,
32
+ createdAt: record.createdAt,
33
+ title: record.title,
34
+ documentType: record.result.documentType,
35
+ source: reviewSourceLabel(record),
36
+ summary: record.summary,
37
+ approval: record.approval
38
+ ? {
39
+ datasetName: record.approval.datasetName,
40
+ approvedAt: record.approval.approvedAt,
41
+ approvedBy: record.approval.approvedBy,
42
+ rationale: record.approval.rationale,
43
+ }
44
+ : undefined,
45
+ };
46
+ }
47
+ function shouldRequireAsyncIngestJob(file, inspection) {
48
+ return file.kind === 'pdf'
49
+ && !!inspection
50
+ && (inspection.totalPages > MAX_SYNC_INGEST_PDF_PAGES
51
+ || (typeof file.fileBytes === 'number' && file.fileBytes > MAX_SYNC_INGEST_FILE_BYTES));
52
+ }
53
+ function summarizeIngestJobRecord(record) {
54
+ return {
55
+ jobId: record.jobId,
56
+ datasetName: record.datasetName,
57
+ projectId: record.projectId,
58
+ documentType: record.documentType,
59
+ title: record.title,
60
+ status: record.status,
61
+ createdAt: record.createdAt,
62
+ updatedAt: record.updatedAt,
63
+ completedAt: record.completedAt,
64
+ source: record.source.fileName ?? record.source.filePath,
65
+ totalPages: record.source.totalPages,
66
+ persistReview: record.request.persistReview,
67
+ persistedReviewDatasetName: record.persistedReview?.datasetName,
68
+ sourceFingerprint: record.sourceStamps.sourceFingerprint,
69
+ parserVersion: record.sourceStamps.parserVersion,
70
+ normalizedResultHash: record.sourceStamps.normalizedResultHash,
71
+ resultSummary: record.resultSummary,
72
+ error: record.error,
73
+ };
74
+ }
75
+ function summarizePromotionResult(result) {
76
+ if (result.documentType === 'geotech-document') {
77
+ return `Promoted persisted ingest review into ${result.promotedDatasetNames.length} document dataset(s)${result.approvalDatasetName ? ` using recorded approval ${result.approvalDatasetName}` : ''}.`;
78
+ }
79
+ return `Promoted persisted ingest review into ${result.promotedDatasetNames.length} dataset(s) for ${result.promotedBoreholeIds.length} borehole(s)${result.approvalDatasetName ? ` using recorded approval ${result.approvalDatasetName}` : ''}.`;
80
+ }
81
+ function getSelectedPersistedReview(projectId, datasetName) {
82
+ return datasetName
83
+ ? loadPersistedBoreholeIngestReview(projectId, datasetName)
84
+ : loadLatestPersistedBoreholeIngestReview(projectId);
85
+ }
86
+ function getSelectedPersistedReviewApproval(projectId, reviewDatasetName, approvalDatasetName) {
87
+ if (approvalDatasetName) {
88
+ return loadPersistedBoreholeIngestReviewApproval(projectId, approvalDatasetName);
89
+ }
90
+ if (reviewDatasetName) {
91
+ return loadLatestPersistedBoreholeIngestReviewApproval(projectId, reviewDatasetName);
92
+ }
93
+ return null;
94
+ }
8
95
  // ---------------------------------------------------------------------------
9
96
  // AGS Borehole Data Parser
10
97
  // ---------------------------------------------------------------------------
@@ -86,6 +173,678 @@ toolRegistry.register({
86
173
  }
87
174
  });
88
175
  // ---------------------------------------------------------------------------
176
+ // Geotechnical Document Ingest
177
+ // ---------------------------------------------------------------------------
178
+ toolRegistry.register({
179
+ name: 'ingest_geotech_document',
180
+ description: 'Ingest a geotechnical PDF or image into structured document understanding. Supports borehole logs and broader geotech/geology documents. Returns structured findings, confidence, review signals, and optional persisted review metadata. Large PDFs return async-job guidance instead of blocking the synchronous agent path.',
181
+ parameters: {
182
+ type: 'object',
183
+ required: ['path'],
184
+ properties: {
185
+ path: { type: 'string', description: 'Path to a PDF or image file to ingest' },
186
+ type: {
187
+ type: 'string',
188
+ enum: ['borehole-log', 'geotech-document'],
189
+ description: 'Document ingest mode',
190
+ default: 'geotech-document',
191
+ },
192
+ boreholeId: {
193
+ type: 'string',
194
+ description: 'Optional override borehole ID for borehole-log ingest',
195
+ },
196
+ projectId: {
197
+ type: 'string',
198
+ description: 'Project ID used when persisting an ingest review and when the result should be promoted later',
199
+ },
200
+ persistReview: {
201
+ type: 'boolean',
202
+ description: 'Persist the ingest result as a project review record for later approval or promotion',
203
+ default: false,
204
+ },
205
+ reviewTitle: {
206
+ type: 'string',
207
+ description: 'Optional title when persisting an ingest review',
208
+ },
209
+ },
210
+ },
211
+ }, async (args) => {
212
+ const pathCheck = validateReadPath(String(args.path));
213
+ if (!pathCheck.safe) {
214
+ return { success: false, data: null, summary: '', error: pathCheck.error };
215
+ }
216
+ const filePath = pathCheck.resolved;
217
+ if (!existsSync(filePath)) {
218
+ return { success: false, data: null, summary: '', error: `File not found: ${filePath}` };
219
+ }
220
+ const documentType = args.type === 'borehole-log'
221
+ ? 'borehole-log'
222
+ : 'geotech-document';
223
+ const persistReview = args.persistReview === true;
224
+ const projectId = typeof args.projectId === 'string' && args.projectId.trim()
225
+ ? args.projectId.trim()
226
+ : undefined;
227
+ if (persistReview && !projectId) {
228
+ return {
229
+ success: false,
230
+ data: null,
231
+ summary: '',
232
+ error: 'Persisting an ingest review requires a projectId.',
233
+ };
234
+ }
235
+ try {
236
+ const config = resolveActiveLLMConfig();
237
+ const file = readDocumentVisionInput(filePath);
238
+ if (file.kind === 'unknown') {
239
+ return {
240
+ success: false,
241
+ data: null,
242
+ summary: '',
243
+ error: `Unsupported document type for ingest: ${filePath}`,
244
+ };
245
+ }
246
+ let totalPages = null;
247
+ if (file.kind === 'pdf') {
248
+ try {
249
+ totalPages = await countDocumentPdfPages(filePath);
250
+ }
251
+ catch {
252
+ totalPages = null;
253
+ }
254
+ }
255
+ const shouldShortCircuitAsync = file.kind === 'pdf'
256
+ && ((typeof totalPages === 'number' && totalPages > MAX_SYNC_INGEST_PDF_PAGES)
257
+ || (typeof file.fileBytes === 'number' && file.fileBytes > MAX_SYNC_INGEST_FILE_BYTES));
258
+ const inspection = file.kind === 'pdf' && (!shouldShortCircuitAsync || totalPages == null)
259
+ ? inspectPdfDocument(filePath)
260
+ : null;
261
+ if (shouldShortCircuitAsync || shouldRequireAsyncIngestJob(file, inspection)) {
262
+ return {
263
+ success: true,
264
+ data: {
265
+ documentType,
266
+ requires_async_job: true,
267
+ reason: `PDF ingest was deferred because the document has ${totalPages ?? inspection?.totalPages ?? 0} page(s) and exceeds the synchronous ingest budget.`,
268
+ async_job_recommendation: {
269
+ tool: 'start_geotech_ingest_job',
270
+ projectId,
271
+ path: filePath,
272
+ type: documentType,
273
+ boreholeId: typeof args.boreholeId === 'string' ? args.boreholeId : undefined,
274
+ persistReview,
275
+ reviewTitle: typeof args.reviewTitle === 'string' ? args.reviewTitle : undefined,
276
+ },
277
+ inspection: {
278
+ totalPages: totalPages ?? inspection?.totalPages ?? 0,
279
+ warnings: inspection?.warnings ?? [],
280
+ parserVersion: inspection?.metadata.parser ?? 'deferred-async-inspection',
281
+ },
282
+ },
283
+ summary: `Synchronous ingest was deferred for this ${totalPages ?? inspection?.totalPages ?? 0}-page PDF. Start an async ingest job instead.`,
284
+ };
285
+ }
286
+ if (documentType === 'borehole-log') {
287
+ const result = await ingestBoreholeLogDocument({
288
+ config,
289
+ source: buildDocumentSource(filePath, file.kind === 'pdf' ? 'pdf' : 'image'),
290
+ overrideBoreholeId: args.boreholeId,
291
+ inspection,
292
+ image: file.kind === 'pdf' ? undefined : file,
293
+ pages: file.kind === 'pdf' ? await readDocumentPdfPageInputs(filePath, { inspection }) : undefined,
294
+ });
295
+ const persistedReview = persistReview && projectId
296
+ ? persistBoreholeIngestReview(projectId, result, {
297
+ title: args.reviewTitle,
298
+ })
299
+ : null;
300
+ return {
301
+ success: true,
302
+ data: {
303
+ ...result,
304
+ persistedReview: persistedReview
305
+ ? {
306
+ reviewId: persistedReview.reviewId,
307
+ datasetName: persistedReview.datasetName,
308
+ title: persistedReview.title,
309
+ summary: persistedReview.summary,
310
+ }
311
+ : undefined,
312
+ },
313
+ summary: persistedReview
314
+ ? `Borehole ingest parsed ${result.boreholes.length} boreholes at ${result.confidence}% confidence and saved review ${persistedReview.datasetName}.`
315
+ : `Borehole ingest parsed ${result.boreholes.length} boreholes at ${result.confidence}% confidence (${result.reviewRequired ? 'review required' : 'auto-proceed ready'}).`,
316
+ };
317
+ }
318
+ const result = await ingestGeotechDocument({
319
+ config,
320
+ source: buildDocumentSource(filePath, file.kind === 'pdf' ? 'pdf' : 'image'),
321
+ inspection,
322
+ image: file.kind === 'pdf' ? undefined : file,
323
+ pages: file.kind === 'pdf' ? await readDocumentPdfPageInputs(filePath, { inspection }) : undefined,
324
+ });
325
+ const persistedReview = persistReview && projectId
326
+ ? persistBoreholeIngestReview(projectId, result, {
327
+ title: args.reviewTitle,
328
+ })
329
+ : null;
330
+ return {
331
+ success: true,
332
+ data: {
333
+ ...result,
334
+ persistedReview: persistedReview
335
+ ? {
336
+ reviewId: persistedReview.reviewId,
337
+ datasetName: persistedReview.datasetName,
338
+ title: persistedReview.title,
339
+ summary: persistedReview.summary,
340
+ }
341
+ : undefined,
342
+ },
343
+ summary: persistedReview
344
+ ? `Geotech document ingest found ${result.materials.length} materials, ${result.classifications.length} classifications, and ${result.parameters.length} parameters at ${result.confidence}% confidence and saved review ${persistedReview.datasetName}.`
345
+ : `Geotech document ingest found ${result.materials.length} materials, ${result.classifications.length} classifications, and ${result.parameters.length} parameters at ${result.confidence}% confidence.`,
346
+ };
347
+ }
348
+ catch (err) {
349
+ return {
350
+ success: false,
351
+ data: null,
352
+ summary: '',
353
+ error: `Geotech document ingest failed: ${err instanceof Error ? err.message : String(err)}`,
354
+ };
355
+ }
356
+ });
357
+ toolRegistry.register({
358
+ name: 'start_geotech_ingest_job',
359
+ description: 'Queue a durable geotechnical ingest job inside a project. Use this for large PDFs or when the ingest result should be inspected, persisted, approved, and promoted through the review workflow.',
360
+ parameters: {
361
+ type: 'object',
362
+ required: ['projectId', 'path'],
363
+ properties: {
364
+ projectId: { type: 'string', description: 'Project ID that will own the persisted ingest job' },
365
+ path: { type: 'string', description: 'Path to a PDF or image file to ingest asynchronously' },
366
+ type: {
367
+ type: 'string',
368
+ enum: ['borehole-log', 'geotech-document'],
369
+ description: 'Document ingest mode',
370
+ default: 'geotech-document',
371
+ },
372
+ boreholeId: {
373
+ type: 'string',
374
+ description: 'Optional override borehole ID for borehole-log ingest',
375
+ },
376
+ persistReview: {
377
+ type: 'boolean',
378
+ description: 'Persist the completed ingest result as a project review record',
379
+ default: false,
380
+ },
381
+ reviewTitle: {
382
+ type: 'string',
383
+ description: 'Optional review title to use if persistReview is enabled',
384
+ },
385
+ },
386
+ },
387
+ }, (args) => {
388
+ const pathCheck = validateReadPath(String(args.path));
389
+ if (!pathCheck.safe) {
390
+ return { success: false, data: null, summary: '', error: pathCheck.error };
391
+ }
392
+ const filePath = pathCheck.resolved;
393
+ if (!existsSync(filePath)) {
394
+ return { success: false, data: null, summary: '', error: `File not found: ${filePath}` };
395
+ }
396
+ try {
397
+ const projectId = String(args.projectId);
398
+ const job = startGeotechIngestJob(projectId, {
399
+ path: filePath,
400
+ type: args.type === 'borehole-log' ? 'borehole-log' : 'geotech-document',
401
+ boreholeId: typeof args.boreholeId === 'string' ? args.boreholeId : undefined,
402
+ persistReview: args.persistReview === true,
403
+ reviewTitle: typeof args.reviewTitle === 'string' ? args.reviewTitle : undefined,
404
+ });
405
+ return {
406
+ success: true,
407
+ data: summarizeIngestJobRecord(job),
408
+ summary: `Queued ${job.documentType} ingest job ${job.datasetName} for ${job.source.fileName}.`,
409
+ };
410
+ }
411
+ catch (err) {
412
+ return { success: false, data: null, summary: '', error: err instanceof Error ? err.message : String(err) };
413
+ }
414
+ });
415
+ toolRegistry.register({
416
+ name: 'get_geotech_ingest_job',
417
+ description: 'Load the current status of a durable geotechnical ingest job. If datasetName is omitted, the latest job in the project is returned.',
418
+ parameters: {
419
+ type: 'object',
420
+ required: ['projectId'],
421
+ properties: {
422
+ projectId: { type: 'string', description: 'Project ID containing the persisted ingest job' },
423
+ datasetName: {
424
+ type: 'string',
425
+ description: 'Specific ingest job dataset name; omit to load the latest job',
426
+ },
427
+ },
428
+ },
429
+ }, (args) => {
430
+ try {
431
+ const projectId = String(args.projectId);
432
+ const datasetName = typeof args.datasetName === 'string' && args.datasetName.trim()
433
+ ? args.datasetName.trim()
434
+ : undefined;
435
+ const job = getGeotechIngestJob(projectId, datasetName);
436
+ if (!job) {
437
+ return {
438
+ success: false,
439
+ data: null,
440
+ summary: '',
441
+ error: datasetName
442
+ ? `No persisted ingest job named "${datasetName}" was found in project "${projectId}".`
443
+ : `No persisted ingest jobs were found in project "${projectId}".`,
444
+ };
445
+ }
446
+ return {
447
+ success: true,
448
+ data: summarizeIngestJobRecord(job),
449
+ summary: `Loaded ingest job ${job.datasetName} (${job.status}) for ${job.source.fileName}.`,
450
+ };
451
+ }
452
+ catch (err) {
453
+ return { success: false, data: null, summary: '', error: err instanceof Error ? err.message : String(err) };
454
+ }
455
+ });
456
+ toolRegistry.register({
457
+ name: 'wait_geotech_ingest_job',
458
+ description: 'Execute or wait for a durable geotechnical ingest job until it completes or fails. If datasetName is omitted, waits on the latest job in the project.',
459
+ parameters: {
460
+ type: 'object',
461
+ required: ['projectId'],
462
+ properties: {
463
+ projectId: { type: 'string', description: 'Project ID containing the persisted ingest job' },
464
+ datasetName: {
465
+ type: 'string',
466
+ description: 'Specific ingest job dataset name; omit to use the latest job',
467
+ },
468
+ },
469
+ },
470
+ }, async (args) => {
471
+ try {
472
+ const projectId = String(args.projectId);
473
+ const datasetName = typeof args.datasetName === 'string' && args.datasetName.trim()
474
+ ? args.datasetName.trim()
475
+ : undefined;
476
+ const job = await waitGeotechIngestJob(projectId, datasetName, {
477
+ config: resolveActiveLLMConfig(),
478
+ });
479
+ if (job.status === 'failed') {
480
+ return {
481
+ success: false,
482
+ data: summarizeIngestJobRecord(job),
483
+ summary: '',
484
+ error: job.error ?? `Persisted ingest job "${job.datasetName}" failed.`,
485
+ };
486
+ }
487
+ return {
488
+ success: true,
489
+ data: summarizeIngestJobRecord(job),
490
+ summary: job.persistedReview
491
+ ? `Completed ${job.documentType} ingest job ${job.datasetName} and saved review ${job.persistedReview.datasetName}.`
492
+ : `Completed ${job.documentType} ingest job ${job.datasetName}.`,
493
+ };
494
+ }
495
+ catch (err) {
496
+ return { success: false, data: null, summary: '', error: err instanceof Error ? err.message : String(err) };
497
+ }
498
+ });
499
+ toolRegistry.register({
500
+ name: 'load_geotech_ingest_job_result',
501
+ description: 'Load the completed result payload for a durable geotechnical ingest job. If datasetName is omitted, loads the latest job result in the project.',
502
+ parameters: {
503
+ type: 'object',
504
+ required: ['projectId'],
505
+ properties: {
506
+ projectId: { type: 'string', description: 'Project ID containing the persisted ingest job result' },
507
+ datasetName: {
508
+ type: 'string',
509
+ description: 'Specific ingest job dataset name; omit to use the latest job',
510
+ },
511
+ },
512
+ },
513
+ }, (args) => {
514
+ try {
515
+ const projectId = String(args.projectId);
516
+ const datasetName = typeof args.datasetName === 'string' && args.datasetName.trim()
517
+ ? args.datasetName.trim()
518
+ : undefined;
519
+ const loaded = loadGeotechIngestJobResult(projectId, datasetName);
520
+ return {
521
+ success: true,
522
+ data: loaded,
523
+ summary: loaded.persistedReview
524
+ ? `Loaded completed ${loaded.documentType} ingest result from ${loaded.datasetName} with persisted review ${loaded.persistedReview.datasetName}.`
525
+ : `Loaded completed ${loaded.documentType} ingest result from ${loaded.datasetName}.`,
526
+ };
527
+ }
528
+ catch (err) {
529
+ return { success: false, data: null, summary: '', error: err instanceof Error ? err.message : String(err) };
530
+ }
531
+ });
532
+ toolRegistry.register({
533
+ name: 'list_geotech_ingest_jobs',
534
+ description: 'List durable geotechnical ingest jobs in a project so the agent can inspect queued, completed, and failed ingest work before waiting on or loading a specific job.',
535
+ parameters: {
536
+ type: 'object',
537
+ required: ['projectId'],
538
+ properties: {
539
+ projectId: { type: 'string', description: 'Project ID containing persisted ingest jobs' },
540
+ },
541
+ },
542
+ }, (args) => {
543
+ try {
544
+ const projectId = String(args.projectId);
545
+ const jobs = listGeotechIngestJobs(projectId);
546
+ const items = jobs.map((job) => summarizeIngestJobRecord(job));
547
+ return {
548
+ success: true,
549
+ data: {
550
+ projectId,
551
+ count: items.length,
552
+ jobs: items,
553
+ },
554
+ summary: items.length === 0
555
+ ? `No persisted ingest jobs were found in project ${projectId}.`
556
+ : `${items.length} persisted ingest job${items.length === 1 ? '' : 's'} found in project ${projectId}.`,
557
+ };
558
+ }
559
+ catch (err) {
560
+ return { success: false, data: null, summary: '', error: err instanceof Error ? err.message : String(err) };
561
+ }
562
+ });
563
+ toolRegistry.register({
564
+ name: 'list_persisted_ingest_reviews',
565
+ description: 'List persisted ingest reviews saved inside a project. Use this before loading, approving, or promoting a specific review dataset.',
566
+ parameters: {
567
+ type: 'object',
568
+ required: ['projectId'],
569
+ properties: {
570
+ projectId: { type: 'string', description: 'Project ID containing persisted ingest reviews' },
571
+ },
572
+ },
573
+ }, (args) => {
574
+ try {
575
+ const projectId = String(args.projectId);
576
+ const reviews = listPersistedBoreholeIngestReviews(projectId);
577
+ const items = reviews.map((record) => summarizePersistedReviewRecord(record));
578
+ return {
579
+ success: true,
580
+ data: {
581
+ projectId,
582
+ count: items.length,
583
+ reviews: items,
584
+ },
585
+ summary: items.length === 0
586
+ ? `No persisted ingest reviews were found in project ${projectId}.`
587
+ : `${items.length} persisted ingest review${items.length === 1 ? '' : 's'} found in project ${projectId}: ${items.map((item) => item.datasetName).join(', ')}`,
588
+ };
589
+ }
590
+ catch (err) {
591
+ return { success: false, data: null, summary: '', error: err instanceof Error ? err.message : String(err) };
592
+ }
593
+ });
594
+ toolRegistry.register({
595
+ name: 'load_persisted_ingest_review',
596
+ description: 'Load a persisted ingest review from project memory for inspection. If datasetName is omitted, loads the latest saved review in the project.',
597
+ parameters: {
598
+ type: 'object',
599
+ required: ['projectId'],
600
+ properties: {
601
+ projectId: { type: 'string', description: 'Project ID containing the persisted ingest review' },
602
+ datasetName: {
603
+ type: 'string',
604
+ description: 'Specific persisted ingest review dataset name; omit to load the latest review',
605
+ },
606
+ },
607
+ },
608
+ }, (args) => {
609
+ try {
610
+ const projectId = String(args.projectId);
611
+ const datasetName = typeof args.datasetName === 'string' && args.datasetName.trim()
612
+ ? args.datasetName.trim()
613
+ : undefined;
614
+ const record = getSelectedPersistedReview(projectId, datasetName);
615
+ if (!record) {
616
+ return {
617
+ success: false,
618
+ data: null,
619
+ summary: '',
620
+ error: datasetName
621
+ ? `No persisted ingest review named "${datasetName}" was found in project "${projectId}".`
622
+ : `No persisted ingest reviews were found in project "${projectId}".`,
623
+ };
624
+ }
625
+ return {
626
+ success: true,
627
+ data: record,
628
+ summary: `Loaded persisted ingest review ${record.datasetName} from ${reviewSourceLabel(record)} (${record.summary.confidence}% confidence, ${record.summary.blockingFindings} blocking, ${record.summary.reviewFindings} review, auto-proceed ${record.summary.canAutoProceed ? 'yes' : 'no'}).`,
629
+ };
630
+ }
631
+ catch (err) {
632
+ return { success: false, data: null, summary: '', error: err instanceof Error ? err.message : String(err) };
633
+ }
634
+ });
635
+ toolRegistry.register({
636
+ name: 'promote_persisted_ingest_review',
637
+ description: 'Promote a specific persisted ingest review into durable project datasets. Only use this after loading the review and only when it is already auto-proceed ready or has an explicit recorded approval.',
638
+ parameters: {
639
+ type: 'object',
640
+ required: ['projectId', 'datasetName'],
641
+ properties: {
642
+ projectId: { type: 'string', description: 'Project ID containing the persisted ingest review' },
643
+ datasetName: {
644
+ type: 'string',
645
+ description: 'Specific persisted ingest review dataset name to promote',
646
+ },
647
+ },
648
+ },
649
+ }, (args) => {
650
+ try {
651
+ const projectId = String(args.projectId);
652
+ const datasetName = String(args.datasetName);
653
+ const record = loadPersistedBoreholeIngestReview(projectId, datasetName);
654
+ if (!record) {
655
+ return {
656
+ success: false,
657
+ data: null,
658
+ summary: '',
659
+ error: `No persisted ingest review named "${datasetName}" was found in project "${projectId}".`,
660
+ };
661
+ }
662
+ if ((record.summary.canAutoProceed !== true
663
+ || record.summary.reviewRequired
664
+ || record.summary.blockingFindings > 0)
665
+ && !record.approval) {
666
+ return {
667
+ success: false,
668
+ data: null,
669
+ summary: '',
670
+ error: `Persisted ingest review "${datasetName}" is not safe for autonomous promotion yet (auto-proceed: ${record.summary.canAutoProceed ? 'yes' : 'no'}, blocking findings: ${record.summary.blockingFindings}, review findings: ${record.summary.reviewFindings}). Load the review first, record an explicit approval, or ask the user for manual promotion outside the autonomous agent flow.`,
671
+ };
672
+ }
673
+ const result = promotePersistedBoreholeIngestReview(projectId, datasetName);
674
+ return {
675
+ success: true,
676
+ data: result,
677
+ summary: summarizePromotionResult(result),
678
+ };
679
+ }
680
+ catch (err) {
681
+ return { success: false, data: null, summary: '', error: err instanceof Error ? err.message : String(err) };
682
+ }
683
+ });
684
+ toolRegistry.register({
685
+ name: 'approve_persisted_ingest_review',
686
+ description: 'Record an explicit approval decision for a persisted ingest review so a flagged review can later be promoted with audit trail.',
687
+ parameters: {
688
+ type: 'object',
689
+ required: ['projectId', 'datasetName', 'rationale'],
690
+ properties: {
691
+ projectId: { type: 'string', description: 'Project ID containing the persisted ingest review' },
692
+ datasetName: {
693
+ type: 'string',
694
+ description: 'Specific persisted ingest review dataset name to approve',
695
+ },
696
+ rationale: {
697
+ type: 'string',
698
+ description: 'Why the flagged ingest review is approved for promotion despite requiring review',
699
+ },
700
+ approvedBy: {
701
+ type: 'string',
702
+ description: 'Optional reviewer name, role, or approval source for the audit trail',
703
+ },
704
+ },
705
+ },
706
+ }, (args) => {
707
+ try {
708
+ const projectId = String(args.projectId);
709
+ const datasetName = String(args.datasetName);
710
+ const approval = approvePersistedBoreholeIngestReview(projectId, datasetName, {
711
+ rationale: String(args.rationale),
712
+ approvedBy: typeof args.approvedBy === 'string' ? args.approvedBy : undefined,
713
+ });
714
+ return {
715
+ success: true,
716
+ data: approval,
717
+ summary: `Recorded approval ${approval.datasetName} for persisted ingest review ${datasetName}.`,
718
+ };
719
+ }
720
+ catch (err) {
721
+ return { success: false, data: null, summary: '', error: err instanceof Error ? err.message : String(err) };
722
+ }
723
+ });
724
+ toolRegistry.register({
725
+ name: 'list_persisted_ingest_review_approvals',
726
+ description: 'List approval records for persisted ingest reviews inside a project. Optionally filter to one review dataset to inspect approval history, latest-approval status, and provenance validity.',
727
+ parameters: {
728
+ type: 'object',
729
+ required: ['projectId'],
730
+ properties: {
731
+ projectId: { type: 'string', description: 'Project ID containing persisted ingest review approvals' },
732
+ reviewDatasetName: {
733
+ type: 'string',
734
+ description: 'Optional persisted ingest review dataset name to filter approval history',
735
+ },
736
+ },
737
+ },
738
+ }, (args) => {
739
+ try {
740
+ const projectId = String(args.projectId);
741
+ const reviewDatasetName = typeof args.reviewDatasetName === 'string' && args.reviewDatasetName.trim()
742
+ ? args.reviewDatasetName.trim()
743
+ : undefined;
744
+ const approvals = listPersistedBoreholeIngestReviewApprovals(projectId, reviewDatasetName);
745
+ const latestApprovalByReviewDataset = new Map();
746
+ if (reviewDatasetName) {
747
+ const latestApprovalDatasetName = loadLatestPersistedBoreholeIngestReviewApproval(projectId, reviewDatasetName)?.datasetName;
748
+ if (latestApprovalDatasetName) {
749
+ latestApprovalByReviewDataset.set(reviewDatasetName, latestApprovalDatasetName);
750
+ }
751
+ }
752
+ else {
753
+ for (const approval of approvals) {
754
+ if (!latestApprovalByReviewDataset.has(approval.reviewDatasetName)) {
755
+ const latestApprovalDatasetName = loadLatestPersistedBoreholeIngestReviewApproval(projectId, approval.reviewDatasetName)?.datasetName;
756
+ if (latestApprovalDatasetName) {
757
+ latestApprovalByReviewDataset.set(approval.reviewDatasetName, latestApprovalDatasetName);
758
+ }
759
+ }
760
+ }
761
+ }
762
+ const items = approvals.map((approval) => summarizePersistedBoreholeIngestReviewApproval(approval, {
763
+ latestApprovalDatasetName: latestApprovalByReviewDataset.get(approval.reviewDatasetName),
764
+ }));
765
+ return {
766
+ success: true,
767
+ data: {
768
+ projectId,
769
+ reviewDatasetName,
770
+ count: items.length,
771
+ approvals: items,
772
+ },
773
+ summary: items.length === 0
774
+ ? reviewDatasetName
775
+ ? `No approval records were found for persisted ingest review ${reviewDatasetName} in project ${projectId}.`
776
+ : `No persisted ingest review approvals were found in project ${projectId}.`
777
+ : `${items.length} persisted ingest review approval${items.length === 1 ? '' : 's'} found${reviewDatasetName ? ` for ${reviewDatasetName}` : ''} in project ${projectId}.`,
778
+ };
779
+ }
780
+ catch (err) {
781
+ return { success: false, data: null, summary: '', error: err instanceof Error ? err.message : String(err) };
782
+ }
783
+ });
784
+ toolRegistry.register({
785
+ name: 'load_persisted_ingest_review_approval',
786
+ description: 'Load a specific persisted ingest review approval record, or the latest approval for a given review dataset.',
787
+ parameters: {
788
+ type: 'object',
789
+ required: ['projectId'],
790
+ properties: {
791
+ projectId: { type: 'string', description: 'Project ID containing the approval record' },
792
+ reviewDatasetName: {
793
+ type: 'string',
794
+ description: 'Persisted ingest review dataset name whose latest approval should be loaded when approvalDatasetName is omitted',
795
+ },
796
+ approvalDatasetName: {
797
+ type: 'string',
798
+ description: 'Specific approval dataset name to load',
799
+ },
800
+ },
801
+ },
802
+ }, (args) => {
803
+ try {
804
+ const projectId = String(args.projectId);
805
+ const reviewDatasetName = typeof args.reviewDatasetName === 'string' && args.reviewDatasetName.trim()
806
+ ? args.reviewDatasetName.trim()
807
+ : undefined;
808
+ const approvalDatasetName = typeof args.approvalDatasetName === 'string' && args.approvalDatasetName.trim()
809
+ ? args.approvalDatasetName.trim()
810
+ : undefined;
811
+ if (!reviewDatasetName && !approvalDatasetName) {
812
+ return {
813
+ success: false,
814
+ data: null,
815
+ summary: '',
816
+ error: 'Loading an approval requires either approvalDatasetName or reviewDatasetName.',
817
+ };
818
+ }
819
+ const approval = getSelectedPersistedReviewApproval(projectId, reviewDatasetName, approvalDatasetName);
820
+ if (!approval) {
821
+ return {
822
+ success: false,
823
+ data: null,
824
+ summary: '',
825
+ error: approvalDatasetName
826
+ ? `No persisted ingest review approval named "${approvalDatasetName}" was found in project "${projectId}".`
827
+ : `No persisted ingest review approvals were found for "${reviewDatasetName}" in project "${projectId}".`,
828
+ };
829
+ }
830
+ const latestApprovalDatasetName = loadLatestPersistedBoreholeIngestReviewApproval(projectId, approval.reviewDatasetName)?.datasetName;
831
+ const summary = summarizePersistedBoreholeIngestReviewApproval(approval, { latestApprovalDatasetName });
832
+ return {
833
+ success: true,
834
+ data: {
835
+ ...approval,
836
+ isLatestForReview: summary.isLatestForReview,
837
+ isValidForCurrentReview: summary.isValidForCurrentReview,
838
+ invalidationReasons: summary.invalidationReasons,
839
+ },
840
+ summary: `Loaded persisted ingest review approval ${approval.datasetName} for ${approval.reviewDatasetName}${summary.isLatestForReview ? ' (latest approval)' : ''}${summary.isValidForCurrentReview ? '' : ' (invalid for current review provenance)'}.`,
841
+ };
842
+ }
843
+ catch (err) {
844
+ return { success: false, data: null, summary: '', error: err instanceof Error ? err.message : String(err) };
845
+ }
846
+ });
847
+ // ---------------------------------------------------------------------------
89
848
  // Standards Database Query (RAG-like)
90
849
  // ---------------------------------------------------------------------------
91
850
  toolRegistry.register({