@frontera-sdk/cli 1.53.22 → 1.53.24
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/package.json +4 -4
- package/src/api/knowledge-base-api.ts +230 -0
- package/src/commands/knowledge/index-commands.ts +6 -6
- package/src/commands/knowledge-base/index-commands.ts +328 -0
- package/src/commands/local/check.ts +12 -0
- package/src/commands/local/dev.ts +64 -2
- package/src/commands/local/index-commands.ts +34 -10
- package/src/commands/local/init.ts +11 -2
- package/src/commands/local/schema.ts +1 -1
- package/src/commands/local/sync-request.ts +135 -29
- package/src/commands/local/sync.ts +40 -25
- package/src/commands/registry.ts +4 -1
- package/src/commands/types.ts +6 -0
- package/src/flag-help.ts +10 -4
- package/src/forge/touches.ts +29 -0
- package/src/local/local-applied.ts +28 -0
- package/src/local/local-build-summary.ts +1 -0
- package/src/local/local-component-health.ts +7 -2
- package/src/local/local-dev-references.ts +6 -2
- package/src/local/local-dev-sync.ts +43 -4
- package/src/local/local-docker.ts +16 -1
- package/src/local/local-env.ts +19 -0
- package/src/local/local-file-checks.ts +56 -8
- package/src/local/local-knowledge-base-apply.ts +714 -0
- package/src/local/local-knowledge-base-files.ts +195 -0
- package/src/local/local-knowledge-base-pull.ts +458 -0
- package/src/local/local-knowledge-base-sources-off.ts +67 -0
- package/src/local/local-person-steps.ts +2 -1
- package/src/local/local-personas.ts +64 -7
- package/src/local/local-portability.ts +27 -0
- package/src/local/local-ports.ts +4 -2
- package/src/local/local-project-secrets.ts +30 -5
- package/src/local/local-scaffold.ts +2 -0
- package/src/local/local-schema-check.ts +3 -1
- package/src/local/local-schemas.ts +11 -1
- package/src/local/local-search-index.ts +92 -0
- package/src/local/local-secrets.ts +7 -2
- package/src/local/local-seed.ts +17 -0
- package/src/local/local-state.ts +7 -5
- package/src/local/local-studio-server.ts +3 -3
- package/src/local/local-supervisor.ts +45 -8
- package/src/local/local-sync-link.ts +16 -0
- package/src/local/local-sync-names.ts +10 -1
- package/src/local/local-sync-plan.ts +44 -5
- package/src/local/local-sync-push.ts +165 -34
- package/src/local/local-sync-refs.ts +3 -1
- package/src/local/local-sync-remote.ts +44 -12
- package/src/local/local-sync-snapshot.ts +156 -12
- package/src/main.ts +13 -0
- package/src/mcp/mcp-server.ts +3 -0
- package/src/mcp/tools/docs-tools.ts +1 -1
- package/src/mcp/tools/knowledge-base-tools.ts +85 -0
|
@@ -20,6 +20,8 @@ import { projectModels } from './local-sync-models'
|
|
|
20
20
|
import { deepEqual, sameContent } from './local-sync-merge'
|
|
21
21
|
import { portableNotes, sameDocumentText, toLocal, toWorkspace } from './local-sync-portable'
|
|
22
22
|
import { itemTitle } from './local-sync-names'
|
|
23
|
+
import { KNOWLEDGE_BASE_DIR, KNOWLEDGE_BASE_FILE, KNOWLEDGE_BASE_SETTINGS_FILE, KNOWLEDGE_BASE_SOURCES_DIR } from './local-knowledge-base-files'
|
|
24
|
+
import { KNOWLEDGE_BASES_FEATURE, KNOWLEDGE_SETTINGS_NAME, KNOWLEDGE_SETTINGS_REF, knowledgeBaseDocuments, platformRefName } from './local-knowledge-base-pull'
|
|
23
25
|
import { isScaffoldSample } from './local-scaffold'
|
|
24
26
|
import { hash, itemHash, readLocal, safePath, writeLock, type LockEntry, type PlanItem, type RemoteItem, type SyncKind, type SyncLock } from './local-sync-plan'
|
|
25
27
|
import { dropBase, SYNC_DIR, writeBase, type RemoteSnapshot } from './local-sync-snapshot'
|
|
@@ -44,7 +46,7 @@ import { dropBase, SYNC_DIR, writeBase, type RemoteSnapshot } from './local-sync
|
|
|
44
46
|
*/
|
|
45
47
|
|
|
46
48
|
/** The kinds a change request carries. */
|
|
47
|
-
export const PUSH_KINDS: readonly SyncKind[] = ['blueprint', 'dataset', 'skill', 'knowledge', 'plugin', 'function', 'workflow', 'agent', 'evaluation', 'chat-app', 'lessons', 'app']
|
|
49
|
+
export const PUSH_KINDS: readonly SyncKind[] = ['blueprint', 'dataset', 'skill', 'knowledge', 'knowledge-base', 'plugin', 'function', 'workflow', 'agent', 'evaluation', 'chat-app', 'lessons', 'app']
|
|
48
50
|
|
|
49
51
|
/** Why the other kinds are not proposed, in the reader's words. */
|
|
50
52
|
export const NOT_PROPOSED: Partial<Record<SyncKind, string>> = {
|
|
@@ -65,7 +67,7 @@ const NOT_DELETED: Partial<Record<SyncKind, string>> = {
|
|
|
65
67
|
* lacks and `sync.models` maps to none; `refused`: the workspace's checks
|
|
66
68
|
* would refuse it (`POST /v1/change-requests/check`), said before proposing.
|
|
67
69
|
*/
|
|
68
|
-
export type BlockCode = 'moved' | 'conflicts' | 'deleted' | 'workspace-deleted' | 'kind' | 'shape' | 'model' | 'refused'
|
|
70
|
+
export type BlockCode = 'moved' | 'conflicts' | 'deleted' | 'workspace-deleted' | 'kind' | 'shape' | 'model' | 'refused' | 'sign-in'
|
|
69
71
|
|
|
70
72
|
/** A skill's file that is not text (an image, a font, a template): uploaded as a skill asset when proposed. */
|
|
71
73
|
export interface PushAsset {
|
|
@@ -238,6 +240,20 @@ export function itemFiles(root: string, kind: SyncKind, name: string, entry?: Lo
|
|
|
238
240
|
if (documents.length && !opts.documents) notes.push(`Its ${documents.length} document${documents.length === 1 ? '' : 's'} stay${documents.length === 1 ? 's' : ''} as the workspace has them unless you bring them; the request carries the knowledge base’s settings`)
|
|
239
241
|
break
|
|
240
242
|
}
|
|
243
|
+
case 'knowledge-base': {
|
|
244
|
+
if (name === KNOWLEDGE_SETTINGS_NAME) {
|
|
245
|
+
if (existsSync(join(root, KNOWLEDGE_BASE_DIR, name, KNOWLEDGE_BASE_FILE))) return { files: {}, notes, problem: `${KNOWLEDGE_BASE_DIR}/${name}/ is named like the organization’s Knowledge settings (${KNOWLEDGE_BASE_DIR}/${KNOWLEDGE_BASE_SETTINGS_FILE}): rename the folder to propose that knowledge base` }
|
|
246
|
+
paths = [`${KNOWLEDGE_BASE_DIR}/${KNOWLEDGE_BASE_SETTINGS_FILE}`]
|
|
247
|
+
break
|
|
248
|
+
}
|
|
249
|
+
// The base and every one of its sources: a source with no file here is one the request removes.
|
|
250
|
+
const dir = `${KNOWLEDGE_BASE_DIR}/${name}`
|
|
251
|
+
paths = [`${dir}/${KNOWLEDGE_BASE_FILE}`, ...filesUnder(root, `${dir}/${KNOWLEDGE_BASE_SOURCES_DIR}`).filter((p) => /^[^/]+\.json$/.test(p.slice(`${dir}/${KNOWLEDGE_BASE_SOURCES_DIR}/`.length)))]
|
|
252
|
+
const { documents, nested } = knowledgeBaseDocuments(root, name)
|
|
253
|
+
if (documents.length && !opts.documents) notes.push(`Its ${documents.length} document${documents.length === 1 ? '' : 's'} stay${documents.length === 1 ? 's' : ''} as the workspace has them unless you bring them: push with \`--documents ${dir}\``)
|
|
254
|
+
if (nested.length) notes.push(`${nested.map((n) => `${dir}/documents/${n}/`).join(', ')} stay${nested.length === 1 ? 's' : ''} here: a knowledge base has no folders, so put its files directly in ${dir}/documents/`)
|
|
255
|
+
break
|
|
256
|
+
}
|
|
241
257
|
case 'blueprint':
|
|
242
258
|
paths = [`blueprint/${name}.yaml`, `blueprint/${name}.yml`, `blueprint/${name}.json`]
|
|
243
259
|
break
|
|
@@ -257,7 +273,7 @@ export function itemFiles(root: string, kind: SyncKind, name: string, entry?: Lo
|
|
|
257
273
|
return { files: {}, notes }
|
|
258
274
|
}
|
|
259
275
|
// What the last pull wrote for it comes along when it is still here (an agent's files are fixed; the rest may not be).
|
|
260
|
-
if (kind !== 'agent' && kind !== 'knowledge' && kind !== 'app') for (const p of Object.keys(entry?.files ?? {})) if (!paths.includes(p)) paths.push(p)
|
|
276
|
+
if (kind !== 'agent' && kind !== 'knowledge' && kind !== 'knowledge-base' && kind !== 'app') for (const p of Object.keys(entry?.files ?? {})) if (!paths.includes(p)) paths.push(p)
|
|
261
277
|
const files: Record<string, string> = {}
|
|
262
278
|
const here: Record<string, string> = {}
|
|
263
279
|
const models = projectModels(root)
|
|
@@ -283,10 +299,11 @@ export function itemFiles(root: string, kind: SyncKind, name: string, entry?: Lo
|
|
|
283
299
|
* The items the project holds, by ref, as a request names them: Agents,
|
|
284
300
|
* workflows, Functions (a file or a folder of their own), skills, knowledge
|
|
285
301
|
* bases, Blueprint files, and plugins (listed so a push can say why not).
|
|
302
|
+
* `file` is the file that makes it an item, and names it.
|
|
286
303
|
*/
|
|
287
|
-
export function projectRefs(root: string): Map<string, { kind: SyncKind; name: string }> {
|
|
288
|
-
const out = new Map<string, { kind: SyncKind; name: string }>()
|
|
289
|
-
const add = (kind: SyncKind, name: string) => out.set(`${kind}/${name}`, { kind, name })
|
|
304
|
+
export function projectRefs(root: string): Map<string, { kind: SyncKind; name: string; file: string }> {
|
|
305
|
+
const out = new Map<string, { kind: SyncKind; name: string; file: string }>()
|
|
306
|
+
const add = (kind: SyncKind, name: string, file: string) => out.set(`${kind}/${name}`, { kind, name, file })
|
|
290
307
|
const dirs = (rel: string) => {
|
|
291
308
|
try {
|
|
292
309
|
return readdirSync(join(root, rel)).filter((n) => !isIgnoredName(n) && !SKIP_DIRS.has(n)).sort()
|
|
@@ -294,30 +311,33 @@ export function projectRefs(root: string): Map<string, { kind: SyncKind; name: s
|
|
|
294
311
|
return []
|
|
295
312
|
}
|
|
296
313
|
}
|
|
297
|
-
for (const slug of dirs('agents')) if (existsSync(join(root, 'agents', slug, 'agent.json'))) add('agent', slug)
|
|
314
|
+
for (const slug of dirs('agents')) if (existsSync(join(root, 'agents', slug, 'agent.json'))) add('agent', slug, `agents/${slug}/agent.json`)
|
|
298
315
|
for (const f of dirs('workflows')) {
|
|
299
316
|
const m = /^(.+?)(?:\.workflow)?\.(ya?ml|json)$/.exec(f)
|
|
300
|
-
if (m) add('workflow', m[1]
|
|
317
|
+
if (m) add('workflow', m[1]!, `workflows/${f}`)
|
|
301
318
|
}
|
|
302
319
|
for (const f of dirs('functions')) {
|
|
303
|
-
if (/\.ts$/.test(f) && !/\.(test|d)\.ts$/.test(f)) add('function', f.replace(/\.ts$/, ''))
|
|
304
|
-
else if (existsSync(join(root, 'functions', f, 'package.json'))) add('function', f)
|
|
320
|
+
if (/\.ts$/.test(f) && !/\.(test|d)\.ts$/.test(f)) add('function', f.replace(/\.ts$/, ''), `functions/${f}`)
|
|
321
|
+
else if (existsSync(join(root, 'functions', f, 'package.json'))) add('function', f, `functions/${f}/package.json`)
|
|
305
322
|
}
|
|
306
|
-
for (const n of dirs('skills')) if (existsSync(join(root, 'skills', n, 'SKILL.md'))) add('skill', n)
|
|
307
|
-
for (const n of dirs('knowledge')) if (existsSync(join(root, 'knowledge', n, 'knowledge.json'))) add('knowledge', n)
|
|
308
|
-
|
|
323
|
+
for (const n of dirs('skills')) if (existsSync(join(root, 'skills', n, 'SKILL.md'))) add('skill', n, `skills/${n}/SKILL.md`)
|
|
324
|
+
for (const n of dirs('knowledge')) if (existsSync(join(root, 'knowledge', n, 'knowledge.json'))) add('knowledge', n, `knowledge/${n}/knowledge.json`)
|
|
325
|
+
// The Knowledge app's bases, and the organization's Knowledge settings (`knowledge-base/settings`).
|
|
326
|
+
for (const n of dirs(KNOWLEDGE_BASE_DIR)) if (existsSync(join(root, KNOWLEDGE_BASE_DIR, n, KNOWLEDGE_BASE_FILE))) add('knowledge-base', n, `${KNOWLEDGE_BASE_DIR}/${n}/${KNOWLEDGE_BASE_FILE}`)
|
|
327
|
+
if (existsSync(join(root, KNOWLEDGE_BASE_DIR, KNOWLEDGE_BASE_SETTINGS_FILE))) add('knowledge-base', KNOWLEDGE_SETTINGS_NAME, `${KNOWLEDGE_BASE_DIR}/${KNOWLEDGE_BASE_SETTINGS_FILE}`)
|
|
328
|
+
for (const dir of BLUEPRINT_DIRS) for (const f of dirs(`blueprint/${dir}`)) if (/\.(ya?ml|json)$/.test(f)) add('blueprint', `${dir}/${f.replace(/\.(ya?ml|json)$/, '')}`, `blueprint/${dir}/${f}`)
|
|
309
329
|
// The workspace's own grants beyond read: one Blueprint item, `blueprint/workspace-grants`.
|
|
310
|
-
if (existsSync(join(root, WORKSPACE_GRANTS_FILE))) add('blueprint', WORKSPACE_GRANTS_NAME)
|
|
311
|
-
for (const f of dirs('mcp')) if (f.endsWith('.plugin.json') && f !== '.plugin.json') add('plugin', f.slice(0, -'.plugin.json'.length))
|
|
312
|
-
for (const f of dirs('datasets')) if (f.endsWith('.json') && f !== '.json') add('dataset', f.slice(0, -'.json'.length))
|
|
313
|
-
for (const f of dirs('chat-apps')) if (f.endsWith('.json') && f !== '.json') add('chat-app', f.slice(0, -'.json'.length))
|
|
330
|
+
if (existsSync(join(root, WORKSPACE_GRANTS_FILE))) add('blueprint', WORKSPACE_GRANTS_NAME, WORKSPACE_GRANTS_FILE)
|
|
331
|
+
for (const f of dirs('mcp')) if (f.endsWith('.plugin.json') && f !== '.plugin.json') add('plugin', f.slice(0, -'.plugin.json'.length), `mcp/${f}`)
|
|
332
|
+
for (const f of dirs('datasets')) if (f.endsWith('.json') && f !== '.json') add('dataset', f.slice(0, -'.json'.length), `datasets/${f}`)
|
|
333
|
+
for (const f of dirs('chat-apps')) if (f.endsWith('.json') && f !== '.json') add('chat-app', f.slice(0, -'.json'.length), `chat-apps/${f}`)
|
|
314
334
|
// A Custom App's settings file (X6); its code is the folder beside it, and its release a request's own file.
|
|
315
|
-
for (const f of dirs('apps')) if (/^[a-z0-9][a-z0-9-]*\.json$/.test(f) && !f.endsWith('.release.json')) add('app', f.slice(0, -'.json'.length))
|
|
316
|
-
for (const slug of dirs('agents')) if (existsSync(join(root, lessonsPath(slug)))) add('lessons', slug)
|
|
335
|
+
for (const f of dirs('apps')) if (/^[a-z0-9][a-z0-9-]*\.json$/.test(f) && !f.endsWith('.release.json')) add('app', f.slice(0, -'.json'.length), `apps/${f}`)
|
|
336
|
+
for (const slug of dirs('agents')) if (existsSync(join(root, lessonsPath(slug)))) add('lessons', slug, lessonsPath(slug))
|
|
317
337
|
for (const slug of dirs('agents')) {
|
|
318
338
|
for (const f of dirs(`agents/${slug}/evals`)) {
|
|
319
339
|
const m = /^([^.]+)\.ya?ml$/.exec(f)
|
|
320
|
-
if (m) add('evaluation', `${slug}.${m[1]}`)
|
|
340
|
+
if (m) add('evaluation', `${slug}.${m[1]}`, `agents/${slug}/evals/${f}`)
|
|
321
341
|
}
|
|
322
342
|
}
|
|
323
343
|
return out
|
|
@@ -339,7 +359,9 @@ export interface CandidateInput {
|
|
|
339
359
|
remote: RemoteItem[]
|
|
340
360
|
/**
|
|
341
361
|
* Knowledge bases whose documents are brought (opt-in, as for a pull):
|
|
342
|
-
* what the pull wrote of each (`documents.json`) and what the workspace has
|
|
362
|
+
* what the pull wrote of each (`documents.json`) and what the workspace has
|
|
363
|
+
* now. Agent knowledge by its name; a Knowledge app base by its ref
|
|
364
|
+
* (`knowledge-base/<folder>`), as `documents.json` records it.
|
|
343
365
|
*/
|
|
344
366
|
documents?: {
|
|
345
367
|
names: Set<string>
|
|
@@ -348,7 +370,7 @@ export interface CandidateInput {
|
|
|
348
370
|
}
|
|
349
371
|
}
|
|
350
372
|
|
|
351
|
-
const BLOCK_REASON: Record<Exclude<BlockCode, 'kind' | 'shape' | 'model' | 'refused'>, string> = {
|
|
373
|
+
const BLOCK_REASON: Record<Exclude<BlockCode, 'kind' | 'shape' | 'model' | 'refused' | 'sign-in'>, string> = {
|
|
352
374
|
moved: 'Changed in the workspace too since your last pull: pull and merge it first',
|
|
353
375
|
conflicts: 'Has conflicts from a pull: resolve them first',
|
|
354
376
|
deleted: 'Deleted here',
|
|
@@ -400,8 +422,71 @@ export function skillAssets(root: string, name: string, paths: string[], theirs:
|
|
|
400
422
|
return { assets, removed, changed: removed.length > 0 || assets.some((a) => a.change !== 'same') }
|
|
401
423
|
}
|
|
402
424
|
|
|
425
|
+
/**
|
|
426
|
+
* Source files named otherwise than the workspace names their source. Apply
|
|
427
|
+
* matches a base's sources by file name, and the workspace names a source after
|
|
428
|
+
* its `name` (numbered `-2` and on when two share one), so a file named
|
|
429
|
+
* otherwise would be taken for a new source on the next push, and the one
|
|
430
|
+
* before removed with its documents.
|
|
431
|
+
*/
|
|
432
|
+
export function misnamedSources(files: Record<string, string>): Array<{ path: string; want: string }> {
|
|
433
|
+
return Object.entries(files).flatMap(([path, text]) => {
|
|
434
|
+
const m = new RegExp(`/${KNOWLEDGE_BASE_SOURCES_DIR}/([^/]+)\\.json$`).exec(path)
|
|
435
|
+
if (!m) return []
|
|
436
|
+
let name: unknown
|
|
437
|
+
try {
|
|
438
|
+
name = (JSON.parse(text) as { name?: unknown }).name
|
|
439
|
+
} catch {
|
|
440
|
+
return []
|
|
441
|
+
}
|
|
442
|
+
if (typeof name !== 'string') return []
|
|
443
|
+
const want = platformRefName(name)
|
|
444
|
+
const key = m[1]!
|
|
445
|
+
const numbered = key.startsWith(`${want}-`) && /^\d+$/.test(key.slice(want.length + 1))
|
|
446
|
+
return key === want || numbered ? [] : [{ path, want: `${path.slice(0, -m[0].length)}/${KNOWLEDGE_BASE_SOURCES_DIR}/${want}.json` }]
|
|
447
|
+
})
|
|
448
|
+
}
|
|
449
|
+
|
|
403
450
|
const kb = (n: number) => (n < 1024 ? `${n} B` : n < 1024 * 1024 ? `${Math.round(n / 1024)} KB` : `${(n / 1024 / 1024).toFixed(1)} MB`)
|
|
404
451
|
|
|
452
|
+
/**
|
|
453
|
+
* An Agent's files as a request sends them: its `knowledgeBases` name each
|
|
454
|
+
* base kept in a folder of its own by the name the workspace's index gives it
|
|
455
|
+
* (`platformName`), which is how Apply finds it.
|
|
456
|
+
*/
|
|
457
|
+
export function withPlatformPicks(files: Record<string, string>, names: ReadonlyMap<string, string>): Record<string, string> {
|
|
458
|
+
if (!names.size) return files
|
|
459
|
+
return Object.fromEntries(Object.entries(files).map(([path, text]) => {
|
|
460
|
+
if (!path.endsWith('/agent.json')) return [path, text]
|
|
461
|
+
let doc: { knowledgeBases?: unknown }
|
|
462
|
+
try {
|
|
463
|
+
doc = JSON.parse(text) as { knowledgeBases?: unknown }
|
|
464
|
+
} catch {
|
|
465
|
+
return [path, text]
|
|
466
|
+
}
|
|
467
|
+
if (!Array.isArray(doc.knowledgeBases)) return [path, text]
|
|
468
|
+
const picks = doc.knowledgeBases.map((p) => {
|
|
469
|
+
const folder = typeof p === 'string' && p.startsWith(`${KNOWLEDGE_BASE_DIR}/`) ? p.slice(KNOWLEDGE_BASE_DIR.length + 1) : null
|
|
470
|
+
return folder && names.has(folder) ? `${KNOWLEDGE_BASE_DIR}/${names.get(folder)}` : p
|
|
471
|
+
})
|
|
472
|
+
return [path, `${JSON.stringify({ ...doc, knowledgeBases: picks }, null, 2)}\n`]
|
|
473
|
+
}))
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
const NOT_TAKEN = 'this workspace’s platform does not take knowledge bases from a project yet'
|
|
477
|
+
const BASES_NOT_TAKEN_FIX = `update the platform, or leave ${KNOWLEDGE_BASE_DIR}/ out of this push`
|
|
478
|
+
|
|
479
|
+
/** An agent.json without its `knowledgeBases`, for a platform that does not take them; the text as it is when it has none. */
|
|
480
|
+
function withoutPicks(text: string | null): string | null {
|
|
481
|
+
if (text === null) return null
|
|
482
|
+
try {
|
|
483
|
+
const { knowledgeBases, ...rest } = JSON.parse(text) as { knowledgeBases?: unknown }
|
|
484
|
+
return knowledgeBases === undefined ? text : `${JSON.stringify(rest, null, 2)}\n`
|
|
485
|
+
} catch {
|
|
486
|
+
return text
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
405
490
|
/**
|
|
406
491
|
* What a push would propose, item by item: every item changed here or only
|
|
407
492
|
* here, with what it sends, or why it cannot be proposed now. Items that
|
|
@@ -414,6 +499,10 @@ export function pushCandidates(input: CandidateInput): PushItem[] {
|
|
|
414
499
|
const byPlan = new Map(plan.map((p) => [p.ref, p]))
|
|
415
500
|
const here = projectRefs(root)
|
|
416
501
|
const refs = new Set<string>([...plan.map((p) => p.ref), ...here.keys()])
|
|
502
|
+
// Knowledge bases filed in a folder other than the name the workspace gives them, by folder.
|
|
503
|
+
const platformNames = new Map(input.remote.flatMap((r) => (r.kind === 'knowledge-base' && r.platformName ? [[r.name, r.platformName] as const] : [])))
|
|
504
|
+
// An older platform runs the Knowledge app but refuses a request that carries a base, and drops an Agent's picks.
|
|
505
|
+
const takesBases = !!snapshot?.capabilities.includes(KNOWLEDGE_BASES_FEATURE)
|
|
417
506
|
const out: PushItem[] = []
|
|
418
507
|
for (const ref of [...refs].sort((a, b) => (String(a) < String(b) ? -1 : String(a) > String(b) ? 1 : 0))) {
|
|
419
508
|
const slash = ref.indexOf('/')
|
|
@@ -422,8 +511,6 @@ export function pushCandidates(input: CandidateInput): PushItem[] {
|
|
|
422
511
|
const p = byPlan.get(ref)
|
|
423
512
|
const entry = lock?.items[ref]
|
|
424
513
|
const theirs = remote.get(ref)
|
|
425
|
-
// A plan's "only here" for a Function in its own folder is named after its entry file: the folder names it here.
|
|
426
|
-
if (p?.status === 'local-only' && !entry && !here.has(ref)) continue
|
|
427
514
|
// An Agent's lessons are never in the plan: they are proposed while the file differs from what the last pull wrote.
|
|
428
515
|
const lessons = kind === 'lessons' ? lessonsProposal(root, input.lock?.workspace ?? snapshot?.workspace ?? '', name) : null
|
|
429
516
|
if (kind === 'lessons' && !lessons) continue
|
|
@@ -431,12 +518,14 @@ export function pushCandidates(input: CandidateInput): PushItem[] {
|
|
|
431
518
|
const status = p?.status ?? 'local-only'
|
|
432
519
|
if (status === 'new' || status === 'changed') continue
|
|
433
520
|
const deleted = status === 'deleted-local'
|
|
434
|
-
|
|
521
|
+
// Agent knowledge's are keyed by name, a Knowledge app base's by its ref.
|
|
522
|
+
const documentsKey = kind === 'knowledge' ? name : ref
|
|
523
|
+
const bringDocuments = (kind === 'knowledge' || (kind === 'knowledge-base' && ref !== KNOWLEDGE_SETTINGS_REF)) && !!input.documents?.names.has(documentsKey)
|
|
435
524
|
const { files, notes, problem, assets: assetPaths } = deleted ? { files: {} as Record<string, string>, notes: [] as string[], problem: undefined, assets: undefined } : itemFiles(root, kind, name, entry, { documents: bringDocuments, ...(theirs ? { upstream: theirs.files } : {}) })
|
|
436
525
|
// What a file hash does not cover: a skill's assets, and a knowledge base's documents when they are brought.
|
|
437
526
|
const assetState = kind === 'skill' && !deleted && (assetPaths?.length || theirs?.assets?.length) ? skillAssets(root, name, assetPaths ?? [], theirs) : null
|
|
438
527
|
const docs = bringDocuments && !deleted && input.documents
|
|
439
|
-
? documentChanges(localDocuments(root, name), input.documents.pulled[
|
|
528
|
+
? documentChanges(kind === 'knowledge' ? localDocuments(root, name) : knowledgeBaseDocuments(root, name).documents, input.documents.pulled[documentsKey] ?? {}, input.documents.platform.get(documentsKey) ?? [])
|
|
440
529
|
: null
|
|
441
530
|
const extra = Boolean(assetState?.changed) || Boolean(docs && (docs.upload.length || docs.remove.length))
|
|
442
531
|
// `same` in the plan reads the base's paths; a file added to the item since is still a change here.
|
|
@@ -466,7 +555,7 @@ export function pushCandidates(input: CandidateInput): PushItem[] {
|
|
|
466
555
|
reason: null,
|
|
467
556
|
id: entry?.id ?? theirs?.id ?? null,
|
|
468
557
|
baseRevision: null,
|
|
469
|
-
files,
|
|
558
|
+
files: kind === 'agent' ? withPlatformPicks(files, platformNames) : files,
|
|
470
559
|
diff,
|
|
471
560
|
base: baseFiles,
|
|
472
561
|
notes,
|
|
@@ -492,9 +581,22 @@ export function pushCandidates(input: CandidateInput): PushItem[] {
|
|
|
492
581
|
if (lessons.removed.length) notes.push(`${lessons.removed.length} rule${lessons.removed.length === 1 ? '' : 's'} removed here ${lessons.removed.length === 1 ? 'isn’t' : 'aren’t'} proposed: an active rule is retired on the platform, where rules are reviewed`)
|
|
493
582
|
if (!lessons.added.length && !lessons.removed.length) notes.push('Proposes the rules the workspace doesn’t have yet, for review')
|
|
494
583
|
}
|
|
584
|
+
const agentPath = `agents/${name}/agent.json`
|
|
585
|
+
const sentAgent = kind === 'agent' && !takesBases && item.files[agentPath] !== undefined ? withoutPicks(item.files[agentPath]!)! : null
|
|
586
|
+
const picksLeftOut = sentAgent !== null && sentAgent !== item.files[agentPath]
|
|
587
|
+
if (picksLeftOut) {
|
|
588
|
+
item.files = { ...item.files, [agentPath]: sentAgent }
|
|
589
|
+
item.diff = item.diff.map((f) => (f.path === agentPath ? { ...f, base: withoutPicks(f.base), local: sentAgent } : f)).filter((f) => f.base !== f.local && !(f.base !== null && f.local !== null && sameContent(f.path, f.base, f.local)))
|
|
590
|
+
notes.push(`Its knowledgeBases are not sent: ${NOT_TAKEN}, so the Agent keeps the knowledge bases it searches there`)
|
|
591
|
+
}
|
|
495
592
|
const block = (code: BlockCode, reason: string) => Object.assign(item, { proposable: false, blocked: code, reason })
|
|
593
|
+
const misnamed = kind === 'knowledge-base' ? misnamedSources(files) : []
|
|
496
594
|
if (NOT_PROPOSED[kind]) block('kind', NOT_PROPOSED[kind]!)
|
|
497
595
|
else if (!PUSH_KINDS.includes(kind)) block('kind', 'Not a kind a change request carries')
|
|
596
|
+
else if ((kind === 'knowledge-base' || (picksLeftOut && !item.diff.length)) && !takesBases) {
|
|
597
|
+
block('kind', kind === 'knowledge-base' ? 'This workspace’s platform does not take knowledge bases from a project yet' : `Its only change is knowledgeBases, and ${NOT_TAKEN}`)
|
|
598
|
+
item.fix = kind === 'knowledge-base' ? BASES_NOT_TAKEN_FIX : 'update the platform, then push it again'
|
|
599
|
+
}
|
|
498
600
|
else if (status === 'conflict') block('moved', BLOCK_REASON.moved)
|
|
499
601
|
else if (status === 'unresolved') block('conflicts', BLOCK_REASON.conflicts)
|
|
500
602
|
else if (deleted && NOT_DELETED[kind]) block('shape', NOT_DELETED[kind]!)
|
|
@@ -502,6 +604,7 @@ export function pushCandidates(input: CandidateInput): PushItem[] {
|
|
|
502
604
|
// A saved object set is saved by Apply, never removed: what else names it would stop resolving (X6).
|
|
503
605
|
else if (deleted && ref.startsWith('blueprint/object-sets/')) block('shape', 'A change request doesn’t remove a saved object set: remove it on the platform, or put its file back')
|
|
504
606
|
else if (deleted && ref === WORKSPACE_GRANTS_REF) block('shape', `Deleting ${WORKSPACE_GRANTS_FILE} changes no grant: to take read_sensitive away, put the file back and list the type with \`permissions: [read]\``)
|
|
607
|
+
else if (deleted && ref === KNOWLEDGE_SETTINGS_REF) block('shape', `Deleting ${KNOWLEDGE_BASE_DIR}/${KNOWLEDGE_BASE_SETTINGS_FILE} changes no setting: put it back with the settings you want (a part it leaves out stays as the workspace has it)`)
|
|
505
608
|
else if (status === 'deleted-remote') block('workspace-deleted', BLOCK_REASON['workspace-deleted'])
|
|
506
609
|
else if (deleted) {
|
|
507
610
|
// Proposable: Apply deletes it there, after everything that names it is updated.
|
|
@@ -509,6 +612,16 @@ export function pushCandidates(input: CandidateInput): PushItem[] {
|
|
|
509
612
|
else if (!Object.keys(files).length) block('shape', 'No files to propose')
|
|
510
613
|
else if (kind === 'blueprint' && Object.keys(files).length > 1) block('shape', `${Object.keys(files).join(' and ')}: keep one file per Blueprint artifact`)
|
|
511
614
|
else if (ref === WORKSPACE_GRANTS_REF && readWorkspaceGrants(root)?.problems.length) block('shape', readWorkspaceGrants(root)!.problems.join('; '))
|
|
615
|
+
else if (misnamed.length) {
|
|
616
|
+
block('shape', `${misnamed.map((w) => w.path).join(', ')}: the workspace names ${misnamed.length === 1 ? 'this source' : 'these sources'} by ${misnamed.length === 1 ? 'its' : 'their'} name, and a file named otherwise is taken for a new source, the one before removed with its documents`)
|
|
617
|
+
item.fix = `rename ${misnamed.map((w) => `${w.path} to ${w.want}`).join(', ')}`
|
|
618
|
+
}
|
|
619
|
+
// A request carries every source a base has: one the read could not file would be removed.
|
|
620
|
+
else if (theirs?.keptOnPlatform?.length) {
|
|
621
|
+
const kept = theirs.keptOnPlatform.map((n) => `“${n}”`).join(', ')
|
|
622
|
+
block('shape', `Its source${theirs.keptOnPlatform.length === 1 ? '' : 's'} ${kept} ${theirs.keptOnPlatform.length === 1 ? 'is' : 'are'} on the platform but not in ${KNOWLEDGE_BASE_SOURCES_DIR}/, and a change request would remove ${theirs.keptOnPlatform.length === 1 ? 'it' : 'them'}`)
|
|
623
|
+
item.fix = 'propose it signed in as someone who reads every source of the knowledge base (a Knowledge steward for an organization knowledge base), after a pull brings them'
|
|
624
|
+
}
|
|
512
625
|
else if (assetState?.changed && entry?.revision && theirs?.revision && entry.revision !== theirs.revision && textSame) {
|
|
513
626
|
// A pull never writes over an asset the folder has, so one the workspace changed since reads as a change here.
|
|
514
627
|
block('moved', 'Its assets changed in the workspace since your last pull: delete the copies here to bring the workspace’s, then propose')
|
|
@@ -915,7 +1028,8 @@ export const KIND_WORD: Record<SyncKind, string> = {
|
|
|
915
1028
|
workflow: 'Workflow',
|
|
916
1029
|
function: 'Function',
|
|
917
1030
|
skill: 'Skill',
|
|
918
|
-
knowledge: '
|
|
1031
|
+
knowledge: 'Agent knowledge',
|
|
1032
|
+
'knowledge-base': 'Knowledge base',
|
|
919
1033
|
blueprint: 'Blueprint',
|
|
920
1034
|
plugin: 'Plugin',
|
|
921
1035
|
evaluation: 'Evaluation',
|
|
@@ -962,6 +1076,7 @@ export function changedParts(kind: SyncKind, name: string, paths: string[], file
|
|
|
962
1076
|
} else if (kind === 'workflow') parts.add('definition')
|
|
963
1077
|
else if (kind === 'function') parts.add('code')
|
|
964
1078
|
else if (kind === 'knowledge') parts.add(path.endsWith(`/${QUALITY_CHECKS_FILE}`) ? 'quality checks' : 'settings')
|
|
1079
|
+
else if (kind === 'knowledge-base') parts.add(path.includes(`/${KNOWLEDGE_BASE_SOURCES_DIR}/`) ? 'sources' : 'settings')
|
|
965
1080
|
else if (kind === 'plugin') parts.add('settings and policy')
|
|
966
1081
|
else if (kind === 'evaluation') parts.add('cases')
|
|
967
1082
|
else if (kind === 'chat-app') parts.add('experience')
|
|
@@ -975,12 +1090,17 @@ function joinWords(words: string[]): string {
|
|
|
975
1090
|
return `${words.slice(0, -1).join(', ')} and ${words[words.length - 1]}`
|
|
976
1091
|
}
|
|
977
1092
|
|
|
978
|
-
/**
|
|
979
|
-
export function
|
|
1093
|
+
/** An item as a line names it: "Agent Support Concierge"; a Blueprint file says what it is ("Object type Ticket"). */
|
|
1094
|
+
export function itemLabel(item: { kind: SyncKind; name: string; title: string }): string {
|
|
980
1095
|
const [dir, apiName] = item.name.split('/')
|
|
981
1096
|
const word = BLUEPRINT_WORD[dir ?? '']
|
|
982
|
-
|
|
983
|
-
|
|
1097
|
+
if (item.kind === 'blueprint') return word && apiName ? `${word[0]!.toUpperCase()}${word.slice(1)} ${item.title}` : `Blueprint ${item.title}`
|
|
1098
|
+
return item.kind === 'knowledge-base' && item.name === KNOWLEDGE_SETTINGS_NAME ? item.title : `${KIND_WORD[item.kind]} ${item.title}`
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
/** One line per item, as the Overview says it: "Agent Support Concierge: behavior settings changed". */
|
|
1102
|
+
export function itemLine(item: { kind: SyncKind; name: string; title: string; change: 'changed' | 'new' | 'deleted'; changedPaths?: string[]; changedFiles?: PushFile[]; assetsChanged?: boolean; documentsChanged?: boolean }): string {
|
|
1103
|
+
const label = itemLabel(item)
|
|
984
1104
|
if (item.change === 'deleted') return `${label}: delete`
|
|
985
1105
|
// The platform keeps no lessons file, so "new" or "changed" depends only on
|
|
986
1106
|
// whether this project pulled one; what the request does is the same either way.
|
|
@@ -1073,11 +1193,22 @@ export function failedName(outcome: ApplyOutcome, titles: Record<string, string>
|
|
|
1073
1193
|
return titles[failed.ref] ?? outcome.steps.find((s) => s.key === failed.key)?.label ?? failed.ref
|
|
1074
1194
|
}
|
|
1075
1195
|
|
|
1196
|
+
/**
|
|
1197
|
+
* A step's words with Agent knowledge and a knowledge base named as the lines
|
|
1198
|
+
* name them: the platform calls both "Knowledge base <folder>".
|
|
1199
|
+
*/
|
|
1200
|
+
function stepWords(text: string, ref: string, titles: Record<string, string>): string {
|
|
1201
|
+
const [kind, ...rest] = ref.split('/')
|
|
1202
|
+
const name = rest.join('/')
|
|
1203
|
+
if ((kind !== 'knowledge' && kind !== 'knowledge-base') || !name) return text
|
|
1204
|
+
return text.split(`Knowledge base ${name}`).join(`${KIND_WORD[kind]} ${titles[ref] ?? name}`)
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1076
1207
|
/** Apply's steps, and after a failure what was put back, as `request apply` prints them. */
|
|
1077
1208
|
export function describeOutcome(outcome: ApplyOutcome, titles: Record<string, string> = {}): string[] {
|
|
1078
1209
|
const lines = outcome.steps.flatMap((s) => [
|
|
1079
|
-
` ${STEP_MARK[s.status]} ${s.label}${s.status === 'not-run' && !outcome.ok ? ' (not run)' : ''}`,
|
|
1080
|
-
...(s.notes ?? []).map((n) => ` · ${n}`),
|
|
1210
|
+
` ${STEP_MARK[s.status]} ${stepWords(s.label, s.ref, titles)}${s.status === 'not-run' && !outcome.ok ? ' (not run)' : ''}`,
|
|
1211
|
+
...(s.notes ?? []).map((n) => ` · ${stepWords(n, s.ref, titles)}`),
|
|
1081
1212
|
])
|
|
1082
1213
|
if (outcome.ok) return [`Applied ${outcome.steps.length} step${outcome.steps.length === 1 ? '' : 's'}:`, ...lines]
|
|
1083
1214
|
const failed = outcome.failure
|
|
@@ -20,8 +20,10 @@ import { slugify } from './local-project'
|
|
|
20
20
|
/**
|
|
21
21
|
* `object` is an object definition an Agent's object tools work on
|
|
22
22
|
* (`object/claim_ticket`), by slug; its fields travel in the Agent's file.
|
|
23
|
+
* `knowledge-base` is a Knowledge app knowledge base an Agent searches
|
|
24
|
+
* (`knowledgeBases` in agent.json), by its folder under `knowledge-base/`.
|
|
23
25
|
*/
|
|
24
|
-
export const REF_KINDS = ['function', 'workflow', 'agent', 'knowledge', 'plugin', 'objectType', 'action', 'model', 'skill', 'objectSet', 'object'] as const
|
|
26
|
+
export const REF_KINDS = ['function', 'workflow', 'agent', 'knowledge', 'knowledge-base', 'plugin', 'objectType', 'action', 'model', 'skill', 'objectSet', 'object'] as const
|
|
25
27
|
export type RefKind = (typeof REF_KINDS)[number]
|
|
26
28
|
|
|
27
29
|
/** Names as resources are named: slugs, API names, install names. */
|
|
@@ -28,6 +28,7 @@ import { missingImportNote, missingImportWarning, recoverMissingImports, type Mi
|
|
|
28
28
|
import { attachDefinitions, type ObjectDefinitionRow } from './local-objects'
|
|
29
29
|
import { checksFromFixtures, fixtureRows, qualityChecksPath, qualityChecksText, sourcePaths, sourceRows } from './local-quality-checks'
|
|
30
30
|
import { readableName } from './local-sync-names'
|
|
31
|
+
import { agentPicks, knowledgeReach, readKnowledgeBases, type KnowledgeReach } from './local-knowledge-base-pull'
|
|
31
32
|
import { WORKSPACE_GRANTS_FILE, WORKSPACE_GRANTS_NAME } from './local-workspace-grants'
|
|
32
33
|
import { governedActionIds, invert, itemRef, referencesIn, refName as refNameOf, toNames, type Directory, type RefKind } from './local-sync-refs'
|
|
33
34
|
|
|
@@ -47,6 +48,14 @@ export interface SyncSource {
|
|
|
47
48
|
workspaceId: string
|
|
48
49
|
/** The person signed in, when they read nothing at organization grain (no Blueprint Steward) and no key reads for them. */
|
|
49
50
|
notSteward?: string
|
|
51
|
+
/** The person signed in for the link (the device session): the Knowledge app's bases are read as them, never with a key. */
|
|
52
|
+
person?: PersonClients
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** One session, standing in the linked workspace or at its organization: what a person reads depends on where they stand. */
|
|
56
|
+
export interface PersonClients {
|
|
57
|
+
workspace: SyncClient
|
|
58
|
+
organization: SyncClient
|
|
50
59
|
}
|
|
51
60
|
|
|
52
61
|
/** Envelopes come single (`{data}`) or double (`{data:{data}}`); lists are what is inside. */
|
|
@@ -275,6 +284,12 @@ export interface ReadOptions {
|
|
|
275
284
|
* sync index says changed. The Blueprint and evaluations are read whole.
|
|
276
285
|
*/
|
|
277
286
|
only?: Set<string>
|
|
287
|
+
/** The folder each knowledge base was pulled into, by its id (`lockedFolders`), so a base keeps its folder. */
|
|
288
|
+
knowledgeBaseFolders?: ReadonlyMap<string, string>
|
|
289
|
+
/** The Knowledge app as a fetch already read it, so the bases and the Agents' picks agree on folders. */
|
|
290
|
+
knowledge?: KnowledgeReach
|
|
291
|
+
/** The platform's sync features, as a fetch read them: knowledge bases are read only where it takes them. */
|
|
292
|
+
capabilities?: readonly string[]
|
|
278
293
|
}
|
|
279
294
|
|
|
280
295
|
const wanted = (opts: ReadOptions, ref: string) => !opts.only || opts.only.has(ref)
|
|
@@ -581,25 +596,24 @@ export async function agentOperatingConfig(
|
|
|
581
596
|
}
|
|
582
597
|
|
|
583
598
|
/**
|
|
584
|
-
* The Knowledge app's bases an Agent searches
|
|
585
|
-
*
|
|
586
|
-
*
|
|
587
|
-
*
|
|
588
|
-
* Knowledge app, the Agent searches none, or the route is not there.
|
|
599
|
+
* The Knowledge app's bases an Agent searches, said where the platform does
|
|
600
|
+
* not take knowledge bases from a project: agent.json cannot name them there,
|
|
601
|
+
* so what the Agent answers locally is not taken for what it answers on the
|
|
602
|
+
* platform. Nothing to say where it searches none, or the route is not there.
|
|
589
603
|
*/
|
|
590
|
-
|
|
604
|
+
async function knowledgeBasesNote(source: SyncSource, agentId: string): Promise<string | null> {
|
|
591
605
|
const answer = await source.workspace.get(`/v1/config/agents/${encodeURIComponent(agentId)}/knowledge-bases`).catch(() => null)
|
|
592
606
|
const bases = answer ? one<{ bases?: Array<{ name?: unknown; searched?: unknown }> } | null>(answer)?.bases ?? [] : []
|
|
593
607
|
const names = bases.filter((base) => base.searched === true && typeof base.name === 'string').map((base) => `“${base.name as string}”`)
|
|
594
608
|
if (!names.length) return null
|
|
595
609
|
const which = names.length === 1 ? `the knowledge base ${names[0]}` : `${names.length} knowledge bases (${names.join(', ')})`
|
|
596
|
-
return `Searches ${which} from the Knowledge app on the platform.
|
|
610
|
+
return `Searches ${which} from the Knowledge app on the platform. This workspace’s platform does not take knowledge bases from a project yet, so knowledgeBases is not pulled and the local runtime answers without them: update the platform, or test this Agent’s Knowledge on the platform`
|
|
597
611
|
}
|
|
598
612
|
|
|
599
613
|
/** An Agent's record (`/v1/config/agents/:id`): its published version, and its behavior settings, scrubbed. */
|
|
600
614
|
interface AgentDetail { currentVersion?: { versionNumber?: number } | null; operatingConfig?: unknown }
|
|
601
615
|
|
|
602
|
-
async function readAgents(source: SyncSource, lists: Lists, directory: Directory, opts: ReadOptions, warnings: string[], tools: () => Promise<PluginTools>): Promise<RemoteItem[]> {
|
|
616
|
+
async function readAgents(source: SyncSource, lists: Lists, directory: Directory, opts: ReadOptions, warnings: string[], tools: () => Promise<PluginTools>, knowledge: () => Promise<KnowledgeReach>): Promise<RemoteItem[]> {
|
|
603
617
|
const out: RemoteItem[] = []
|
|
604
618
|
const agents = lists.agents.filter((row) => wanted(opts, `agent/${row.agentId}`))
|
|
605
619
|
if (!agents.length) return out
|
|
@@ -637,6 +651,13 @@ async function readAgents(source: SyncSource, lists: Lists, directory: Directory
|
|
|
637
651
|
const agentDoc = projectAgent(snapshot, it.notes, toolSettings)
|
|
638
652
|
// Its object tools carry the definitions they work on, named by slug below.
|
|
639
653
|
it.notes.push(...attachDefinitions(agentDoc, lists.objectDefinitions ?? null))
|
|
654
|
+
// The knowledge bases it searches; left out (the picks left alone) when they could not be read, or the platform does not take them.
|
|
655
|
+
const picks = await knowledge().then(async (reach) => (reach.state === 'not-taken' ? { refs: [], notes: [await knowledgeBasesNote(source, row.id)].filter((n): n is string => !!n) } : agentPicks(reach, row.id))).catch((err: Error) => {
|
|
656
|
+
warnings.push(`Agent ${row.agentId} knowledge bases: ${err.message}`)
|
|
657
|
+
return null
|
|
658
|
+
})
|
|
659
|
+
if (picks?.refs.length) agentDoc.knowledgeBases = picks.refs
|
|
660
|
+
if (picks) it.notes.push(...picks.notes)
|
|
640
661
|
const docs: Array<{ path: string; doc: unknown; format: 'json' }> = [{ path: `agents/${row.agentId}/agent.json`, doc: agentDoc, format: 'json' }]
|
|
641
662
|
if (operating) docs.push({ path: `agents/${row.agentId}/operating.json`, doc: operating, format: 'json' })
|
|
642
663
|
const bound: AgentBindings = { ...(bindings.get(row.agentId) ?? { functions: [], workflows: [] }) }
|
|
@@ -650,10 +671,11 @@ async function readAgents(source: SyncSource, lists: Lists, directory: Directory
|
|
|
650
671
|
it.notes.push(`Its Context and data couldn’t be read on the platform: ${context.unread}. Until it can be read, local dev gives it read access to the object types the project holds, as an adaptation, so locally it may read more than on the platform`)
|
|
651
672
|
}
|
|
652
673
|
if (bound.functions.length || bound.workflows.length || bound.blueprint?.length || bound.blueprintUnread) docs.push({ path: `agents/${row.agentId}/bindings.json`, doc: bound, format: 'json' })
|
|
653
|
-
const knowledgeBases = await knowledgeBasesNote(source, row.id)
|
|
654
|
-
if (knowledgeBases) it.notes.push(knowledgeBases)
|
|
655
674
|
await nameUnknownAgents(source, directory, docs)
|
|
656
|
-
|
|
675
|
+
finish(it, directory, docs)
|
|
676
|
+
// Pulling the Agent brings the bases it searches.
|
|
677
|
+
if (picks?.refs.length) it.requires = [...new Set([...(it.requires ?? []), ...picks.refs])].sort((a, b) => (a < b ? -1 : a > b ? 1 : 0))
|
|
678
|
+
out.push(it)
|
|
657
679
|
}
|
|
658
680
|
return out
|
|
659
681
|
}
|
|
@@ -1670,6 +1692,9 @@ export async function readWorkspace(source: SyncSource, opts: ReadOptions = {}):
|
|
|
1670
1692
|
// Each Plugin's tools, read once and only when a Plugin file or an Agent's tool settings need them.
|
|
1671
1693
|
let toolsRead: Promise<PluginTools> | null = null
|
|
1672
1694
|
const tools = () => (toolsRead ??= readPluginTools(source, lists.apps, warnings))
|
|
1695
|
+
// The Knowledge app, read once and only when its bases or an Agent's picks are wanted.
|
|
1696
|
+
let knowledgeRead: Promise<KnowledgeReach> | null = opts.knowledge ? Promise.resolve(opts.knowledge) : null
|
|
1697
|
+
const knowledge = () => (knowledgeRead ??= knowledgeReach(source, opts.knowledgeBaseFolders, warnings, opts.capabilities))
|
|
1673
1698
|
const pluginsWanted = want('plugin') && lists.apps.some((a) => a.install && (a.catalog?.kind ?? '').toLowerCase() === 'mcp' && wanted(opts, `plugin/${a.install.installName}`))
|
|
1674
1699
|
const groups = await Promise.all([
|
|
1675
1700
|
want('blueprint')
|
|
@@ -1679,8 +1704,15 @@ export async function readWorkspace(source: SyncSource, opts: ReadOptions = {}):
|
|
|
1679
1704
|
want('function') ? readFunctions(source, lists, opts, warnings) : [],
|
|
1680
1705
|
pluginsWanted ? tools().then((t) => readPlugins(lists, opts, t)) : [],
|
|
1681
1706
|
want('knowledge') ? readKnowledge(source, lists, opts, warnings) : [],
|
|
1707
|
+
// The Knowledge app's bases and settings: those `only` names, or every one.
|
|
1708
|
+
want('knowledge-base')
|
|
1709
|
+
? knowledge().then((reach) => readKnowledgeBases(reach, warnings, opts.only)).then((read) => {
|
|
1710
|
+
notes.push(...read.notes)
|
|
1711
|
+
return read.items
|
|
1712
|
+
})
|
|
1713
|
+
: [],
|
|
1682
1714
|
want('workflow') ? readWorkflows(source, lists, directory, opts, warnings) : [],
|
|
1683
|
-
want('agent') ? readAgents(source, lists, directory, opts, warnings, tools) : [],
|
|
1715
|
+
want('agent') ? readAgents(source, lists, directory, opts, warnings, tools, knowledge) : [],
|
|
1684
1716
|
want('evaluation') ? quietly(warnings, 'Evaluations', () => readEvaluations(source, lists, directory, warnings), []) : [],
|
|
1685
1717
|
// Slide themes: the workspace's own; a platform without the route has none to bring.
|
|
1686
1718
|
want('theme')
|