@xingular/harness 2.1.2 → 3.0.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/CHANGELOG.md +12 -0
- package/NOTICE.md +22 -0
- package/README.md +67 -36
- package/dist/cli.js +6023 -6621
- package/dist/terminal-pi.js +19 -24
- package/docs/terminal-tui.md +27 -7
- package/package.json +2 -2
- package/src/accounts/accounts.ts +4 -3
- package/src/capabilities/api.ts +1 -1
- package/src/cli.ts +7 -3
- package/src/core/caller.ts +34 -0
- package/src/core/fs.ts +17 -1
- package/src/harness.ts +55 -66
- package/src/{memory → history}/recall.test.ts +1 -1
- package/src/{memory → history}/recall.ts +1 -1
- package/src/{memory → history}/trajectory.test.ts +4 -4
- package/src/{memory → history}/trajectory.ts +1 -1
- package/src/hub/hub.test.ts +4 -5
- package/src/hub/hub.ts +2 -2
- package/src/index.ts +28 -14
- package/src/mcp/connections.ts +1 -1
- package/src/mcp/service.ts +1 -1
- package/src/pi/proxy.test.ts +68 -0
- package/src/pi/runtime.test.ts +61 -0
- package/src/pi/runtime.ts +50 -0
- package/src/plugins/api.ts +11 -9
- package/src/plugins/builtin.ts +22 -40
- package/src/plugins/catalogue.test.ts +13 -41
- package/src/plugins/catalogue.ts +3 -32
- package/src/plugins/discover.ts +2 -1
- package/src/plugins/firstParty.test.ts +1 -1
- package/src/plugins/hooks.test.ts +21 -71
- package/src/plugins/hooks.ts +6 -7
- package/src/plugins/identity.ts +16 -2
- package/src/plugins/install.test.ts +1 -1
- package/src/plugins/install.ts +2 -1
- package/src/plugins/inventory.ts +19 -18
- package/src/plugins/plugins.ts +38 -2
- package/src/prompt/compile.test.ts +82 -193
- package/src/prompt/compile.ts +98 -363
- package/src/refine/cli.ts +114 -0
- package/src/refine/demo.ts +124 -0
- package/src/refine/evidence.ts +349 -0
- package/src/refine/host.ts +102 -0
- package/src/refine/ledger.ts +104 -0
- package/src/refine/library.ts +192 -0
- package/src/refine/pi.test.ts +90 -0
- package/src/refine/pi.ts +218 -0
- package/src/refine/prompts.ts +430 -0
- package/src/refine/recursion.ts +353 -0
- package/src/refine/refine.test.ts +140 -0
- package/src/refine/runner.ts +205 -0
- package/src/refine/selection.ts +291 -0
- package/src/refine/service.ts +194 -0
- package/src/refine/trajectory.ts +71 -0
- package/src/refine/types.ts +91 -0
- package/src/server/methods.ts +137 -123
- package/src/server/server.test.ts +37 -30
- package/src/sessions/contracts.ts +6 -1
- package/src/sessions/conversation.test.ts +2 -27
- package/src/sessions/policy.ts +1 -1
- package/src/sessions/pricing.ts +3 -1
- package/src/sessions/registry.ts +5 -1
- package/src/sessions/runStore.ts +3 -1
- package/src/sessions/runner.ts +10 -5
- package/src/sessions/sessions.test.ts +45 -6
- package/src/sessions/sessions.ts +40 -17
- package/src/sessions/subagentService.test.ts +22 -1
- package/src/sessions/subagentService.ts +14 -1
- package/src/sessions/subagents.ts +2 -0
- package/src/terminal/cli.ts +24 -48
- package/src/terminal/pluginControl.test.ts +7 -8
- package/src/terminal/socket.ts +3 -3
- package/src/terminal/terminal.test.ts +21 -39
- package/src/terminal/terminals.ts +20 -13
- package/src/terminal/tui/application.test.ts +58 -0
- package/src/terminal/tui/application.ts +10 -4
- package/src/terminal/tui/clipboardFiles.test.ts +94 -0
- package/src/terminal/tui/clipboardFiles.ts +86 -0
- package/src/terminal/tui/piAdapter.ts +7 -4
- package/src/terminal/tui/runtime.ts +2 -0
- package/src/tools/definitions.ts +49 -132
- package/src/tools/dispatch.ts +78 -0
- package/src/tools/harnessTools.ts +23 -0
- package/src/tools/spawnSubagent.ts +3 -3
- package/src/tools/types.ts +1 -1
- package/src/tree/components.ts +221 -0
- package/src/tree/materialise.ts +122 -0
- package/src/tree/seed.ts +123 -0
- package/src/tree/store.ts +193 -0
- package/src/tree/surface.ts +135 -0
- package/src/tree/tree.test.ts +126 -0
- package/src/tree/tree.ts +346 -0
- package/src/tree/trust.ts +86 -0
- package/src/wiki/notes.ts +313 -0
- package/src/memory/api.test.ts +0 -82
- package/src/memory/api.ts +0 -303
- package/src/memory/blocks.test.ts +0 -78
- package/src/memory/blocks.ts +0 -408
- package/src/memory/caller.ts +0 -69
- package/src/memory/curation.test.ts +0 -206
- package/src/memory/curation.ts +0 -64
- package/src/memory/dispatch.ts +0 -64
- package/src/memory/facts.test.ts +0 -41
- package/src/memory/facts.ts +0 -248
- package/src/memory/graph.ts +0 -103
- package/src/memory/health.test.ts +0 -130
- package/src/memory/health.ts +0 -74
- package/src/memory/maintainer.ts +0 -272
- package/src/memory/maintenance.test.ts +0 -192
- package/src/memory/maintenance.ts +0 -679
- package/src/memory/migrate.ts +0 -79
- package/src/memory/operations.ts +0 -150
- package/src/memory/proposals.test.ts +0 -168
- package/src/memory/proposals.ts +0 -584
- package/src/memory/references.ts +0 -77
- package/src/memory/runContext.ts +0 -93
- package/src/memory/store.test.ts +0 -66
- package/src/memory/store.ts +0 -385
- package/src/memory/wiki.test.ts +0 -47
- package/src/memory/wiki.ts +0 -423
- package/src/memory/wikiIndex.test.ts +0 -88
- package/src/memory/wikiIndex.ts +0 -117
- package/src/plugins/proposal.ts +0 -121
- package/src/skills/registry.test.ts +0 -104
- package/src/skills/registry.ts +0 -413
- package/src/tools/maintenance.ts +0 -29
- package/src/tools/memoryTools.ts +0 -22
- package/src/tools/pluginPropose.ts +0 -41
- package/src/tools/registryWrite.test.ts +0 -101
- package/src/tools/registryWrite.ts +0 -182
- /package/src/{memory → core}/secrets.ts +0 -0
package/src/memory/graph.ts
DELETED
|
@@ -1,103 +0,0 @@
|
|
|
1
|
-
// One graph of the Wiki: pages, facts, memory blocks, registry skills/prompts/rules, and sources
|
|
2
|
-
// as nodes; frontmatter links, citations, and namespace membership as edges.
|
|
3
|
-
import type { HarnessEnv } from '../core/env'
|
|
4
|
-
import { charCount } from '../core/text'
|
|
5
|
-
import * as registry from '../skills/registry'
|
|
6
|
-
import * as blocks from './blocks'
|
|
7
|
-
import * as facts from './facts'
|
|
8
|
-
import * as store from './store'
|
|
9
|
-
import { registryEntryKind } from './wiki'
|
|
10
|
-
|
|
11
|
-
export interface GraphNode {
|
|
12
|
-
id: string
|
|
13
|
-
label: string
|
|
14
|
-
type: 'project' | 'page' | 'memory' | 'skill' | 'prompt' | 'rule' | 'block' | 'source'
|
|
15
|
-
pageType: string | null
|
|
16
|
-
namespace: string
|
|
17
|
-
summary: string
|
|
18
|
-
status: 'unresolved' | 'established' | 'stale'
|
|
19
|
-
provenance: string
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export interface GraphEdge {
|
|
23
|
-
id: string
|
|
24
|
-
source: string
|
|
25
|
-
target: string
|
|
26
|
-
type: string
|
|
27
|
-
evidence: string | null
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export interface GraphResponse {
|
|
31
|
-
nodes: GraphNode[]
|
|
32
|
-
edges: GraphEdge[]
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
const statusOf = (sourceIds: readonly string[]): 'unresolved' | 'established' => (sourceIds.length ? 'established' : 'unresolved')
|
|
36
|
-
|
|
37
|
-
export function build(env: HarnessEnv): GraphResponse {
|
|
38
|
-
const wiki = store.wikiDir(env.root)
|
|
39
|
-
const pages = store.loadPages(wiki)
|
|
40
|
-
const memories = facts.loadMemories(wiki)
|
|
41
|
-
const nodes: GraphNode[] = pages.map((page) => ({
|
|
42
|
-
id: page.frontmatter.id,
|
|
43
|
-
label: page.frontmatter.title,
|
|
44
|
-
type: page.frontmatter.type === 'project' ? 'project' : 'page',
|
|
45
|
-
pageType: page.frontmatter.type,
|
|
46
|
-
namespace: page.location.namespace,
|
|
47
|
-
summary: store.firstParagraph(page.body),
|
|
48
|
-
status: statusOf(page.frontmatter.sourceIds),
|
|
49
|
-
provenance: `Wiki page ${page.location.path} · ${page.frontmatter.sourceIds.length} source(s)`
|
|
50
|
-
}))
|
|
51
|
-
for (const memory of memories) {
|
|
52
|
-
nodes.push({
|
|
53
|
-
id: memory.front.id,
|
|
54
|
-
label: memory.front.title,
|
|
55
|
-
type: 'memory',
|
|
56
|
-
pageType: memory.front.kind,
|
|
57
|
-
namespace: memory.front.namespace,
|
|
58
|
-
summary: store.firstParagraph(memory.body),
|
|
59
|
-
status: memory.front.status !== 'active' ? 'stale' : statusOf(memory.front.sourceIds),
|
|
60
|
-
provenance: `Memory · ${memory.front.confidence} confidence · ${memory.front.sourceIds.length} source(s)`
|
|
61
|
-
})
|
|
62
|
-
}
|
|
63
|
-
for (const item of registry.list(env.root)) {
|
|
64
|
-
const type = registryEntryKind(item.kind)
|
|
65
|
-
if (type) nodes.push({ id: item.id, label: item.name, type, pageType: null, namespace: store.ROOT_NAMESPACE, summary: item.description, status: 'established', provenance: `Registry item ${item.id}` })
|
|
66
|
-
}
|
|
67
|
-
for (const block of blocks.loadAll(wiki)) {
|
|
68
|
-
nodes.push({
|
|
69
|
-
id: block.id,
|
|
70
|
-
label: block.scope === 'agent' && block.owner ? `${env.directory.agentName(block.owner) ?? 'Removed agent'} · ${block.front.label}` : block.front.label,
|
|
71
|
-
type: 'block',
|
|
72
|
-
pageType: block.scope,
|
|
73
|
-
namespace: block.scope === 'project' && block.owner ? `/projects/${block.owner}` : store.ROOT_NAMESPACE,
|
|
74
|
-
summary: block.body.trim() ? store.firstParagraph(block.body) : block.front.description,
|
|
75
|
-
status: 'established',
|
|
76
|
-
provenance: `Memory block · ${charCount(block.body)} of ${block.front.limit} characters`
|
|
77
|
-
})
|
|
78
|
-
}
|
|
79
|
-
for (const source of store.listSources(wiki)) {
|
|
80
|
-
nodes.push({ id: source.id, label: source.name, type: 'source', pageType: null, namespace: store.ROOT_NAMESPACE, summary: `Imported source, version ${source.version}`, status: 'established', provenance: `Imported ${source.importedAt}` })
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
const ids = new Set(nodes.map((node) => node.id))
|
|
84
|
-
const edges: GraphEdge[] = []
|
|
85
|
-
const pushLinks = (from: string, links: readonly store.WikiLink[], sourceIds: readonly string[]): void => {
|
|
86
|
-
for (const link of links) if (ids.has(link.toId)) edges.push({ id: `${from}->${link.toId}:${link.type}`, source: from, target: link.toId, type: link.type, evidence: null })
|
|
87
|
-
for (const sourceId of sourceIds) if (ids.has(sourceId)) edges.push({ id: `${from}->${sourceId}:cites`, source: from, target: sourceId, type: 'references', evidence: sourceId })
|
|
88
|
-
}
|
|
89
|
-
for (const page of pages) pushLinks(page.frontmatter.id, page.frontmatter.links, page.frontmatter.sourceIds)
|
|
90
|
-
for (const memory of memories) pushLinks(memory.front.id, memory.front.links, memory.front.sourceIds)
|
|
91
|
-
// Namespace membership: entries in a project namespace attach to that project's overview page.
|
|
92
|
-
const projectPages = pages.filter((page) => page.frontmatter.type === 'project' && page.location.namespace.startsWith('/projects/')).map((page) => ({ namespace: page.location.namespace, id: page.frontmatter.id }))
|
|
93
|
-
const members = [
|
|
94
|
-
...pages.map((page) => ({ id: page.frontmatter.id, namespace: page.location.namespace })),
|
|
95
|
-
...memories.map((memory) => ({ id: memory.front.id, namespace: memory.front.namespace })),
|
|
96
|
-
...nodes.filter((node) => node.type === 'block').map((node) => ({ id: node.id, namespace: node.namespace }))
|
|
97
|
-
]
|
|
98
|
-
for (const member of members) {
|
|
99
|
-
const project = projectPages.find((entry) => entry.namespace === member.namespace && entry.id !== member.id)
|
|
100
|
-
if (project) edges.push({ id: `${member.id}->${project.id}:part_of`, source: member.id, target: project.id, type: 'part_of', evidence: null })
|
|
101
|
-
}
|
|
102
|
-
return { nodes, edges }
|
|
103
|
-
}
|
|
@@ -1,130 +0,0 @@
|
|
|
1
|
-
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
|
2
|
-
import { tmpdir } from 'node:os'
|
|
3
|
-
import { join } from 'node:path'
|
|
4
|
-
import { expect, test } from 'vitest'
|
|
5
|
-
|
|
6
|
-
import { dataRoot, writeFile } from '../../tests/helpers'
|
|
7
|
-
import { createEnv } from '../core/env'
|
|
8
|
-
import { now, readJson, writeJson } from '../core/fs'
|
|
9
|
-
import * as blocks from './blocks'
|
|
10
|
-
import { loadMemories } from './facts'
|
|
11
|
-
import { build } from './graph'
|
|
12
|
-
import { check } from './health'
|
|
13
|
-
import { run, version, WIKI_VERSION } from './migrate'
|
|
14
|
-
import { ensureProjectNamespace, isDir, isFile, loadPages, wikiDir } from './store'
|
|
15
|
-
|
|
16
|
-
test('reports problems across the wiki', () => {
|
|
17
|
-
const root = dataRoot()
|
|
18
|
-
const wiki = wikiDir(root)
|
|
19
|
-
writeFileSync(join(wiki, 'library/tauri.md'), '---\nid: tauri\ntitle: Use Tauri\ntype: decision\nsourceIds: []\nlinks:\n - toId: xingular-agent\n type: references\n - toId: electron\n type: contradicts\n---\n\nWe use Tauri 2.\n')
|
|
20
|
-
writeFile(join(wiki, 'projects/xingular-agent/xingular-agent.md'), '---\nid: xingular-agent\ntitle: Xingular Agent\ntype: concept\n---\n\nThe app.\n')
|
|
21
|
-
writeFileSync(join(wiki, 'library/lonely.md'), '---\nid: lonely\ntitle: Lonely\ntype: concept\nsourceIds: [run:r1]\n---\n\nAlone.\n')
|
|
22
|
-
writeFileSync(join(wiki, 'facts/m.md'), '---\nid: m\ntitle: M\nkind: fact\nsourceIds: [src-missing]\n---\n\nFact.\n')
|
|
23
|
-
const report = check(root)
|
|
24
|
-
expect(report.brokenLinks).toHaveLength(1)
|
|
25
|
-
expect(report.contradictions).toHaveLength(1)
|
|
26
|
-
expect(report.orphanPages).toEqual(['lonely'])
|
|
27
|
-
expect([...report.missingEvidence].sort()).toEqual(['m', 'tauri', 'xingular-agent'])
|
|
28
|
-
expect([report.staleFacts, report.similarTitles, report.fullBlocks]).toEqual([[], [], []])
|
|
29
|
-
})
|
|
30
|
-
|
|
31
|
-
test('reports stale facts, near-duplicate titles and full blocks', () => {
|
|
32
|
-
const root = dataRoot()
|
|
33
|
-
ensureProjectNamespace(root, 'app', 'App')
|
|
34
|
-
const wiki = wikiDir(root)
|
|
35
|
-
for (const [file, title] of [['a', 'Linear MCP availability observed in run a3695ebe'], ['b', 'Linear MCP availability observed in run c54c128a'], ['c', 'Release plan']]) {
|
|
36
|
-
writeFileSync(join(wiki, `library/${file}.md`), `---\nid: ${file}\ntitle: ${title}\ntype: reference\nsourceIds: [user]\n---\n\nText.\n`)
|
|
37
|
-
}
|
|
38
|
-
writeFileSync(join(wiki, 'facts/old.md'), '---\nid: old\ntitle: Open issues\nkind: fact\nvolatile: true\nobservedAt: 2020-01-01T00:00:00Z\nsourceIds: [user]\n---\n\nThree open.\n')
|
|
39
|
-
writeFileSync(join(wiki, 'facts/new.md'), `---\nid: new\ntitle: On call\nkind: fact\nvolatile: true\nobservedAt: ${now()}\nsourceIds: [user]\n---\n\nNora.\n`)
|
|
40
|
-
const user = blocks.ensureUser(wiki)
|
|
41
|
-
writeFileSync(user.path, blocks.serializeBlock({ ...user.front, limit: 20 }, 'Nineteen characters'))
|
|
42
|
-
const report = check(root)
|
|
43
|
-
expect(report.staleFacts).toEqual(['old'])
|
|
44
|
-
expect(report.similarTitles).toEqual([['a', 'b']])
|
|
45
|
-
expect(report.fullBlocks).toEqual(['blocks/shared/user'])
|
|
46
|
-
// A project's skeleton pages start empty and unlinked without being reported.
|
|
47
|
-
expect(report.orphanPages).toEqual(['a', 'b', 'c'])
|
|
48
|
-
expect(report.missingEvidence).toEqual([])
|
|
49
|
-
})
|
|
50
|
-
|
|
51
|
-
test('graph spans namespaces, memories and registry', () => {
|
|
52
|
-
const root = dataRoot()
|
|
53
|
-
ensureProjectNamespace(root, 'app', 'App')
|
|
54
|
-
const wiki = wikiDir(root)
|
|
55
|
-
writeFileSync(join(wiki, 'projects/app/arch.md'), '---\nid: arch\ntitle: Arch\ntype: concept\nlinks:\n - toId: skills/review\n type: uses\n---\n\nBody.\n')
|
|
56
|
-
writeFileSync(join(wiki, 'facts/m.md'), '---\nid: m\ntitle: M\nkind: fact\nnamespace: /projects/app\n---\n\nFact.\n')
|
|
57
|
-
writeFile(join(root, 'registry/skills/review.md'), 'Review code.\n')
|
|
58
|
-
const graph = build(createEnv(root))
|
|
59
|
-
const types = new Set(graph.nodes.map((node) => node.type))
|
|
60
|
-
for (const type of ['project', 'memory', 'skill'] as const) expect(types.has(type)).toBe(true)
|
|
61
|
-
expect(graph.edges.some((edge) => edge.source === 'arch' && edge.target === 'skills/review')).toBe(true)
|
|
62
|
-
expect(graph.edges.some((edge) => edge.source === 'm' && edge.target === 'project-app' && edge.type === 'part_of')).toBe(true)
|
|
63
|
-
})
|
|
64
|
-
|
|
65
|
-
function v2Layout(): string {
|
|
66
|
-
const root = mkdtempSync(join(tmpdir(), 'harness-migrate-'))
|
|
67
|
-
writeJson(join(root, 'projects.json'), [{ id: 'p1', name: 'Xingular Agent', slug: 'xingular-agent', rootPath: '/tmp/x', autoMaintenance: true, createdAt: '2026-01-01T00:00:00Z' }])
|
|
68
|
-
writeFile(join(root, 'config/storage.json'), '{"wikiVersion":2}')
|
|
69
|
-
writeFile(join(root, 'wiki/docs/observed.md'), '---\nid: observed\ntitle: Observed in run\ntype: reference\n---\n\nA run.\n')
|
|
70
|
-
writeFile(join(root, 'wiki/memory/pnpm.md'), '---\nid: pnpm\ntitle: Use pnpm\nkind: preference\n---\n\nUse pnpm.\n')
|
|
71
|
-
writeFile(join(root, 'wiki/log.md'), '# Maintenance log\n\n- t · user · ingest\n')
|
|
72
|
-
writeFile(join(root, 'registry/rules/style.md'), 'Use spaces.\n')
|
|
73
|
-
return root
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
test('version 3 starts clean and keeps the old wiki as a backup', () => {
|
|
77
|
-
const root = v2Layout()
|
|
78
|
-
expect(version(root)).toBe(2)
|
|
79
|
-
expect(run(createEnv(root), ['a1'])).toBe(true)
|
|
80
|
-
const wiki = wikiDir(root)
|
|
81
|
-
expect(loadPages(wiki).every((page) => page.frontmatter.id !== 'observed')).toBe(true)
|
|
82
|
-
expect(loadMemories(wiki)).toEqual([])
|
|
83
|
-
for (const relative of ['blocks/shared/user.md', 'blocks/agents/a1/persona.md', 'blocks/agents/a1/notes.md', 'blocks/projects/xingular-agent/brief.md', 'projects/xingular-agent/overview.md']) expect(isFile(join(wiki, relative))).toBe(true)
|
|
84
|
-
expect(isDir(join(wiki, 'library')) && isDir(join(wiki, 'facts'))).toBe(true)
|
|
85
|
-
expect(isFile(join(root, 'wiki.pre-v3/docs/observed.md')) && isFile(join(root, 'wiki.pre-v3/memory/pnpm.md'))).toBe(true)
|
|
86
|
-
expect(readFileSync(join(root, 'registry/rules/style.md'), 'utf8')).toBe('Use spaces.\n')
|
|
87
|
-
expect(isDir(join(root, 'wiki.migrating'))).toBe(false)
|
|
88
|
-
expect(version(root)).toBe(WIKI_VERSION)
|
|
89
|
-
expect(run(createEnv(root))).toBe(false)
|
|
90
|
-
})
|
|
91
|
-
|
|
92
|
-
test('a version 2 wiki without a marker is still migrated', () => {
|
|
93
|
-
const root = v2Layout()
|
|
94
|
-
rmSync(join(root, 'config/storage.json'))
|
|
95
|
-
expect(version(root)).toBe(2)
|
|
96
|
-
expect(run(createEnv(root))).toBe(true)
|
|
97
|
-
expect(isFile(join(root, 'wiki.pre-v3/docs/observed.md'))).toBe(true)
|
|
98
|
-
})
|
|
99
|
-
|
|
100
|
-
test('version 1 knowledge is kept as a backup and personal items become global', () => {
|
|
101
|
-
const root = mkdtempSync(join(tmpdir(), 'harness-migrate-'))
|
|
102
|
-
writeJson(join(root, 'projects.json'), [{ id: 'p1', name: 'Xingular Agent', rootPath: '/tmp/x', autoMaintenance: true, createdAt: '2026-01-01T00:00:00Z' }])
|
|
103
|
-
writeFile(join(root, 'knowledge/personal/wiki/overview.md'), '---\nid: overview\ntitle: Mine\ntype: summary\n---\n\nMine.\n')
|
|
104
|
-
writeFile(join(root, 'config/registry-enabled.json'), '{"personal":["skills/a"],"p1":["rules/b"]}')
|
|
105
|
-
expect(version(root)).toBe(1)
|
|
106
|
-
expect(run(createEnv(root))).toBe(true)
|
|
107
|
-
expect(isDir(join(root, 'knowledge'))).toBe(false)
|
|
108
|
-
expect(isFile(join(root, 'knowledge.pre-v2/personal/wiki/overview.md'))).toBe(true)
|
|
109
|
-
expect(isFile(join(wikiDir(root), 'blocks/projects/xingular-agent/brief.md'))).toBe(true)
|
|
110
|
-
const enablement = readJson<Record<string, string[]>>(join(root, 'config/registry-enabled.json'))!
|
|
111
|
-
expect(enablement.global[0]).toBe('skills/a')
|
|
112
|
-
expect('personal' in enablement).toBe(false)
|
|
113
|
-
})
|
|
114
|
-
|
|
115
|
-
test('a failed migration leaves the wiki untouched', () => {
|
|
116
|
-
const root = v2Layout()
|
|
117
|
-
// An unreadable project registry makes staging fail.
|
|
118
|
-
writeFileSync(join(root, 'projects.json'), 'not json')
|
|
119
|
-
expect(() => run(createEnv(root))).toThrow()
|
|
120
|
-
expect(isFile(join(root, 'wiki/docs/observed.md'))).toBe(true)
|
|
121
|
-
expect(isDir(join(root, 'wiki.migrating')) || isDir(join(root, 'wiki.pre-v3'))).toBe(false)
|
|
122
|
-
expect(version(root)).toBe(2)
|
|
123
|
-
})
|
|
124
|
-
|
|
125
|
-
test('fresh data roots need no migration', () => {
|
|
126
|
-
const root = mkdtempSync(join(tmpdir(), 'harness-migrate-'))
|
|
127
|
-
mkdirSync(root, { recursive: true })
|
|
128
|
-
expect(version(root)).toBe(WIKI_VERSION)
|
|
129
|
-
expect(run(createEnv(root))).toBe(false)
|
|
130
|
-
})
|
package/src/memory/health.ts
DELETED
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
// Health checks over the whole Wiki. They report problems with a route to the affected
|
|
2
|
-
// entries and never delete anything.
|
|
3
|
-
import { charCount } from '../core/text'
|
|
4
|
-
import * as registry from '../skills/registry'
|
|
5
|
-
import * as blocks from './blocks'
|
|
6
|
-
import * as facts from './facts'
|
|
7
|
-
import { lastOperationAt } from './operations'
|
|
8
|
-
import * as store from './store'
|
|
9
|
-
|
|
10
|
-
export interface HealthReport {
|
|
11
|
-
brokenLinks: { pageId: string; targetId: string }[]
|
|
12
|
-
orphanPages: string[]
|
|
13
|
-
missingEvidence: string[]
|
|
14
|
-
contradictions: { fromId: string; toId: string }[]
|
|
15
|
-
duplicateIds: string[]
|
|
16
|
-
/** Volatile facts that have not been observed for a while and may no longer be true. */
|
|
17
|
-
staleFacts: string[]
|
|
18
|
-
/** Groups of pages whose titles differ only by a run id, a number, or punctuation. */
|
|
19
|
-
similarTitles: string[][]
|
|
20
|
-
/** Memory blocks at 90% of their limit or more. */
|
|
21
|
-
fullBlocks: string[]
|
|
22
|
-
lastMaintenanceAt: string | null
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
const IGNORED_WORDS = new Set(['the', 'a', 'an', 'in', 'of', 'run'])
|
|
26
|
-
|
|
27
|
-
/** A title without the parts that make near-duplicates look different: case, punctuation, numbers, and id-like words. */
|
|
28
|
-
function titleKey(title: string): string {
|
|
29
|
-
return title.toLowerCase().split(/[^\p{Alphabetic}\p{N}]+/u).filter((word) => word && !/[0-9]/.test(word) && !IGNORED_WORDS.has(word)).join(' ')
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export function check(root: string): HealthReport {
|
|
33
|
-
const wiki = store.wikiDir(root)
|
|
34
|
-
const pages = store.loadPages(wiki)
|
|
35
|
-
const memories = facts.loadMemories(wiki)
|
|
36
|
-
const sourceIds = new Set(store.listSources(wiki).map((source) => source.id))
|
|
37
|
-
const registryIds = registry.list(root).map((item) => item.id)
|
|
38
|
-
|
|
39
|
-
// Every page and fact; `standalone` ones need neither links nor sources.
|
|
40
|
-
const entries = [
|
|
41
|
-
...pages.map((page) => ({ id: page.frontmatter.id, links: page.frontmatter.links, sources: page.frontmatter.sourceIds, standalone: page.frontmatter.id === 'home' || page.frontmatter.type === 'project' || store.isSkeleton(page.location) })),
|
|
42
|
-
...memories.map((memory) => ({ id: memory.front.id, links: memory.front.links, sources: memory.front.sourceIds, standalone: true }))
|
|
43
|
-
]
|
|
44
|
-
const seen = new Set<string>()
|
|
45
|
-
const duplicateIds = entries.filter((entry) => seen.size === seen.add(entry.id).size).map((entry) => entry.id)
|
|
46
|
-
const known = new Set([...entries.map((entry) => entry.id), ...sourceIds, ...registryIds])
|
|
47
|
-
const linked = new Set(entries.flatMap((entry) => entry.links.map((link) => link.toId)))
|
|
48
|
-
const validEvidence = (id: string): boolean => sourceIds.has(id) || store.isExternalEvidence(id)
|
|
49
|
-
|
|
50
|
-
// Pages in one namespace whose titles reduce to the same key, in namespace then key order.
|
|
51
|
-
const groups = new Map<string, { namespace: string; key: string; ids: string[] }>()
|
|
52
|
-
for (const page of pages) {
|
|
53
|
-
if (store.isSkeleton(page.location) || page.frontmatter.type === 'project') continue
|
|
54
|
-
const key = titleKey(page.frontmatter.title)
|
|
55
|
-
if (!key) continue
|
|
56
|
-
const name = `${page.location.namespace}\u0000${key}`
|
|
57
|
-
if (!groups.has(name)) groups.set(name, { namespace: page.location.namespace, key, ids: [] })
|
|
58
|
-
groups.get(name)!.ids.push(page.frontmatter.id)
|
|
59
|
-
}
|
|
60
|
-
const ordered = [...groups.values()].sort((left, right) => (left.namespace === right.namespace ? (left.key < right.key ? -1 : left.key > right.key ? 1 : 0) : left.namespace < right.namespace ? -1 : 1))
|
|
61
|
-
|
|
62
|
-
return {
|
|
63
|
-
brokenLinks: entries.flatMap((entry) => entry.links.filter((link) => !known.has(link.toId)).map((link) => ({ pageId: entry.id, targetId: link.toId }))),
|
|
64
|
-
// Home, project overviews, and facts stand on their own; other pages should be linked.
|
|
65
|
-
orphanPages: entries.filter((entry) => !entry.standalone && !entry.links.length && !linked.has(entry.id)).map((entry) => entry.id),
|
|
66
|
-
missingEvidence: entries.filter((entry) => (!entry.standalone && !entry.sources.length) || entry.sources.some((id) => !validEvidence(id))).map((entry) => entry.id),
|
|
67
|
-
contradictions: entries.flatMap((entry) => entry.links.filter((link) => link.type === 'contradicts').map((link) => ({ fromId: entry.id, toId: link.toId }))),
|
|
68
|
-
duplicateIds,
|
|
69
|
-
staleFacts: memories.filter((memory) => facts.isStale(memory.front)).map((memory) => memory.front.id),
|
|
70
|
-
similarTitles: ordered.filter((group) => group.ids.length > 1).map((group) => group.ids),
|
|
71
|
-
fullBlocks: blocks.loadAll(wiki).filter((block) => block.front.limit > 0 && charCount(block.body) * 100 >= block.front.limit * 90).map((block) => block.id),
|
|
72
|
-
lastMaintenanceAt: lastOperationAt(wiki)
|
|
73
|
-
}
|
|
74
|
-
}
|
package/src/memory/maintainer.ts
DELETED
|
@@ -1,272 +0,0 @@
|
|
|
1
|
-
// Sleep-time consolidation: a Pi session that reads new evidence (an imported source or a
|
|
2
|
-
// settled run) or a set of full memory blocks, and proposes changes. It has no file or shell
|
|
3
|
-
// tools, only proposal tools, so every change goes back to the proposal check, which looks each
|
|
4
|
-
// quoted excerpt up in the evidence before anything is written.
|
|
5
|
-
import type { ModelRuntime } from '@earendil-works/pi-coding-agent'
|
|
6
|
-
|
|
7
|
-
import type { AccountContext } from '../accounts/env'
|
|
8
|
-
import { HarnessError } from '../core/errors'
|
|
9
|
-
import { MEMORY_KINDS } from './facts'
|
|
10
|
-
import { LINK_TYPES, PAGE_TYPES } from './store'
|
|
11
|
-
|
|
12
|
-
export interface ExistingPage {
|
|
13
|
-
id: string
|
|
14
|
-
title: string
|
|
15
|
-
type: string
|
|
16
|
-
path?: string
|
|
17
|
-
summary: string
|
|
18
|
-
/** The page's current text, for the few pages closest to the evidence. */
|
|
19
|
-
body?: string
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export interface ExistingMemory {
|
|
23
|
-
id: string
|
|
24
|
-
title: string
|
|
25
|
-
kind: string
|
|
26
|
-
namespace: string
|
|
27
|
-
body: string
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/** A memory block attached to the run, in full. */
|
|
31
|
-
export interface ExistingBlock {
|
|
32
|
-
id: string
|
|
33
|
-
label: string
|
|
34
|
-
description: string
|
|
35
|
-
limit: number
|
|
36
|
-
body: string
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export type MaintenanceTrigger = { kind: 'ingest'; sourceId: string; sourceName: string; text: string } | { kind: 'run'; runId: string; transcript: string } | { kind: 'consolidate' }
|
|
40
|
-
|
|
41
|
-
export interface MaintenanceRequest {
|
|
42
|
-
jobId: string
|
|
43
|
-
/** Where new pages go by default: /library or /projects/<slug>. */
|
|
44
|
-
namespace: string
|
|
45
|
-
trigger: MaintenanceTrigger
|
|
46
|
-
pages: ExistingPage[]
|
|
47
|
-
memories?: ExistingMemory[]
|
|
48
|
-
blocks?: ExistingBlock[]
|
|
49
|
-
namespaces?: { namespace: string; label: string }[]
|
|
50
|
-
index: string
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/** What the session proposed, as its tools received it. Nothing here has been checked yet. */
|
|
54
|
-
export interface MaintenanceResult {
|
|
55
|
-
pages: unknown[]
|
|
56
|
-
memories: unknown[]
|
|
57
|
-
blocks: unknown[]
|
|
58
|
-
flags: unknown[]
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
const EVIDENCE_RULE = `Every change must carry sourceRefs with an excerpt: a short passage (at least a few words) copied
|
|
62
|
-
exactly from the evidence. The host looks the excerpt up in the evidence and discards any change whose
|
|
63
|
-
excerpt it cannot find, so never paraphrase an excerpt and never cite something the evidence does not say.`
|
|
64
|
-
|
|
65
|
-
const runInstructions = (product: string): string => `You consolidate memory for ${product} after a run has finished ("sleep-time" work).
|
|
66
|
-
You are given the run's transcript, the memory blocks that were in its context, the facts that apply,
|
|
67
|
-
and related Wiki pages. Decide what is worth keeping for future work, then:
|
|
68
|
-
- rethink_block: rewrite a memory block when the run taught something that belongs in it (see each
|
|
69
|
-
block's description). Give the complete new content, merged with what is already there. Remove
|
|
70
|
-
what the run showed to be wrong or finished. Stay within the block's limit.
|
|
71
|
-
- propose_memory: record a short standing fact, preference, decision, or piece of feedback. Update
|
|
72
|
-
an existing fact by id, or replace one with supersedes. Use low confidence when unsure.
|
|
73
|
-
- propose_page: update a page that ALREADY EXISTS, by its id, with its complete new body (merge with
|
|
74
|
-
the current text shown to you; never drop established content unless the evidence supersedes it).
|
|
75
|
-
You cannot create pages: one run is an episode, not a page. What a run merely observed (a tool
|
|
76
|
-
result, a status, a list that will change) is not durable knowledge; leave it out.
|
|
77
|
-
- flag_claim: send an uncertain, contradictory, or sensitive claim to the review inbox instead.
|
|
78
|
-
${EVIDENCE_RULE}
|
|
79
|
-
Use {"kind":"run","ref":"<the run id>","excerpt":"…"}.
|
|
80
|
-
Skip small talk and transient details. Most runs need few changes; it is fine to change nothing.`
|
|
81
|
-
|
|
82
|
-
const ingestInstructions = (product: string): string => `You maintain ${product}'s global knowledge Wiki (Karpathy-style LLM Wiki).
|
|
83
|
-
Raw sources are immutable evidence; Wiki pages are durable, interlinked Markdown syntheses, one per
|
|
84
|
-
entity, concept, or decision, updated in place. The Wiki is one corpus: the Library (/library) and one
|
|
85
|
-
namespace per project (/projects/<name>). Facts are short typed statements.
|
|
86
|
-
Read the imported source and decide what durable knowledge it adds. Then:
|
|
87
|
-
- propose_page once per page to create or update. To update, pass the existing page id and the
|
|
88
|
-
complete new body (merge with what the page already says; never drop established content unless
|
|
89
|
-
the source supersedes it, and say so). Prefer updating an existing page over creating a near
|
|
90
|
-
duplicate. New pages go to the job's namespace unless they clearly belong elsewhere.
|
|
91
|
-
- propose_memory for a short standing fact, preference, decision, or piece of feedback. Use namespace
|
|
92
|
-
"/" when it applies everywhere.
|
|
93
|
-
- Link related pages with typed links (references, depends_on, supersedes, contradicts,
|
|
94
|
-
produced_by, uses) using existing page ids or ids you propose.
|
|
95
|
-
- flag_claim for a claim that is uncertain, contradicts an existing page, or contains secrets or
|
|
96
|
-
personal data that should not be stored.
|
|
97
|
-
${EVIDENCE_RULE}
|
|
98
|
-
Use {"kind":"source","ref":"<the source id>","excerpt":"…"}.
|
|
99
|
-
Prefer few, well-structured pages. Page ids are short kebab-case slugs. It is fine to propose nothing.`
|
|
100
|
-
|
|
101
|
-
const consolidateInstructions = (product: string): string => `You tidy ${product}'s memory blocks. Each block below is close to its
|
|
102
|
-
character limit. For each one, call rethink_block with a shorter version that keeps everything still
|
|
103
|
-
true and useful: merge repeats, drop what is finished or superseded, and tighten the wording. Do not
|
|
104
|
-
add anything that is not already in the block. The new content must be shorter than the current one.`
|
|
105
|
-
|
|
106
|
-
export function instructions(kind: MaintenanceTrigger['kind'], product: string): string {
|
|
107
|
-
return kind === 'run' ? runInstructions(product) : kind === 'ingest' ? ingestInstructions(product) : consolidateInstructions(product)
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
/** The proposal tools each kind of job gets. */
|
|
111
|
-
export function toolNames(kind: MaintenanceTrigger['kind']): string[] {
|
|
112
|
-
if (kind === 'consolidate') return ['rethink_block']
|
|
113
|
-
if (kind === 'run') return ['rethink_block', 'propose_memory', 'propose_page', 'flag_claim']
|
|
114
|
-
return ['propose_page', 'propose_memory', 'flag_claim']
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
export function prompt(request: MaintenanceRequest): string {
|
|
118
|
-
const blocks = request.blocks?.length
|
|
119
|
-
? request.blocks.map((block) => `<block id="${block.id}" label="${block.label}" chars="${Array.from(block.body).length}/${block.limit}">\n${block.description}\n---\n${block.body.trim() || '(empty)'}\n</block>`).join('\n\n')
|
|
120
|
-
: '(none)'
|
|
121
|
-
if (request.trigger.kind === 'consolidate') return `Memory blocks to condense:\n\n${blocks}`
|
|
122
|
-
const existing = request.pages.length ? request.pages.map((page) => `- ${page.id} (${page.type}${page.path ? `, ${page.path}` : ''}): ${page.title} — ${page.summary}`).join('\n') : '(no pages yet)'
|
|
123
|
-
const bodies = request.pages.filter((page) => page.body?.trim()).map((page) => `<page id="${page.id}" title="${page.title}">\n${page.body}\n</page>`).join('\n\n')
|
|
124
|
-
const memories = request.memories?.length ? request.memories.map((memory) => `- ${memory.id} [${memory.kind}, ${memory.namespace}] ${memory.title}: ${memory.body}`).join('\n') : '(none)'
|
|
125
|
-
const namespaces = ['/library', ...(request.namespaces ?? []).map((namespace) => `${namespace.namespace} (${namespace.label})`)].join(', ')
|
|
126
|
-
const evidence = request.trigger.kind === 'ingest' ? `Imported source ${request.trigger.sourceId} ("${request.trigger.sourceName}"):\n\n${request.trigger.text}` : `Settled run ${request.trigger.runId} transcript:\n\n${request.trigger.transcript}`
|
|
127
|
-
return [
|
|
128
|
-
`Namespace for this job: ${request.namespace}`,
|
|
129
|
-
`Namespaces: ${namespaces}`,
|
|
130
|
-
...(request.trigger.kind === 'run' ? [`Memory blocks in this run's context:\n\n${blocks}`] : []),
|
|
131
|
-
`Current index:\n${request.index}`,
|
|
132
|
-
`Relevant pages:\n${existing}`,
|
|
133
|
-
...(bodies ? [`Current text of the closest pages:\n\n${bodies}`] : []),
|
|
134
|
-
`Facts:\n${memories}`,
|
|
135
|
-
`New evidence:\n${evidence}`
|
|
136
|
-
].join('\n\n')
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
export interface MaintainerSession {
|
|
140
|
-
prompt(text: string): Promise<void>
|
|
141
|
-
dispose(): void
|
|
142
|
-
/** Assistant error text, if the provider failed. */
|
|
143
|
-
error(): string | null
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
type Collector = { page: (proposal: unknown) => string; memory: (proposal: unknown) => string; block: (proposal: unknown) => string; flag: (flag: unknown) => string }
|
|
147
|
-
|
|
148
|
-
export type MaintainerFactory = (input: {
|
|
149
|
-
cwd: string
|
|
150
|
-
/** Which job this is; it decides the instructions and the tools. */
|
|
151
|
-
kind: MaintenanceTrigger['kind']
|
|
152
|
-
product: string
|
|
153
|
-
account: AccountContext
|
|
154
|
-
runtime: () => Promise<ModelRuntime>
|
|
155
|
-
collect: Collector
|
|
156
|
-
}) => Promise<MaintainerSession>
|
|
157
|
-
|
|
158
|
-
export const piMaintainerFactory: MaintainerFactory = async ({ cwd, kind, product, account, runtime, collect }) => {
|
|
159
|
-
const pi = await import('@earendil-works/pi-coding-agent')
|
|
160
|
-
const { Type } = await import('typebox')
|
|
161
|
-
const agentDir = account.configDir
|
|
162
|
-
const resourceLoader = new pi.DefaultResourceLoader({ cwd, agentDir, noExtensions: true, noSkills: true, noPromptTemplates: true, noContextFiles: true, appendSystemPromptOverride: (base) => [...base, instructions(kind, product)] })
|
|
163
|
-
await resourceLoader.reload()
|
|
164
|
-
const sourceRefs = Type.Array(
|
|
165
|
-
Type.Object({
|
|
166
|
-
kind: Type.Union([Type.Literal('source'), Type.Literal('run'), Type.Literal('url')]),
|
|
167
|
-
ref: Type.String(),
|
|
168
|
-
excerpt: Type.Optional(Type.String({ description: 'A passage copied exactly from the evidence' }))
|
|
169
|
-
})
|
|
170
|
-
)
|
|
171
|
-
const reply = (text: string) => ({ content: [{ type: 'text' as const, text }], details: {} })
|
|
172
|
-
const tools = [
|
|
173
|
-
pi.defineTool({
|
|
174
|
-
name: 'rethink_block',
|
|
175
|
-
label: 'Rewrite block',
|
|
176
|
-
description: 'Rewrite one memory block with its complete new content. The host checks the excerpt and the block limit before anything is written.',
|
|
177
|
-
parameters: Type.Object({ block: Type.String({ description: 'The block id, as given in <block id="…">' }), body: Type.String({ description: 'The complete new content of the block' }), sourceRefs: Type.Optional(sourceRefs) }),
|
|
178
|
-
execute: async (_id, input) => reply(collect.block({ sourceRefs: [], ...(input as object) }))
|
|
179
|
-
}),
|
|
180
|
-
pi.defineTool({
|
|
181
|
-
name: 'propose_page',
|
|
182
|
-
label: 'Propose page',
|
|
183
|
-
description: 'Create or update one Wiki page. The host validates it before anything is written.',
|
|
184
|
-
parameters: Type.Object({
|
|
185
|
-
id: Type.Optional(Type.String({ description: 'Existing page id to update, or a new kebab-case id' })),
|
|
186
|
-
title: Type.String(),
|
|
187
|
-
type: Type.Union(PAGE_TYPES.map((value) => Type.Literal(value))),
|
|
188
|
-
body: Type.String({ description: 'Complete Markdown body' }),
|
|
189
|
-
namespace: Type.Optional(Type.String({ description: 'For a new page: /library or /projects/<name>; defaults to the job namespace' })),
|
|
190
|
-
sourceRefs,
|
|
191
|
-
links: Type.Array(Type.Object({ toId: Type.String(), type: Type.Union(LINK_TYPES.map((value) => Type.Literal(value))) }))
|
|
192
|
-
}),
|
|
193
|
-
execute: async (_id, input) => reply(collect.page(input))
|
|
194
|
-
}),
|
|
195
|
-
pi.defineTool({
|
|
196
|
-
name: 'propose_memory',
|
|
197
|
-
label: 'Propose fact',
|
|
198
|
-
description: 'Create or update one fact: a short standing statement agents can look up. The host validates it before anything is written.',
|
|
199
|
-
parameters: Type.Object({
|
|
200
|
-
id: Type.Optional(Type.String({ description: 'Existing fact id to update, or a new kebab-case id' })),
|
|
201
|
-
title: Type.String(),
|
|
202
|
-
kind: Type.Union(MEMORY_KINDS.map((value) => Type.Literal(value))),
|
|
203
|
-
body: Type.String({ description: 'One to a few sentences' }),
|
|
204
|
-
namespace: Type.Optional(Type.String({ description: '/ for everywhere, or /projects/<name>' })),
|
|
205
|
-
confidence: Type.Optional(Type.Union([Type.Literal('high'), Type.Literal('medium'), Type.Literal('low')])),
|
|
206
|
-
supersedes: Type.Optional(Type.String({ description: 'Id of the fact this replaces' })),
|
|
207
|
-
sourceRefs
|
|
208
|
-
}),
|
|
209
|
-
execute: async (_id, input) => reply(collect.memory(input))
|
|
210
|
-
}),
|
|
211
|
-
pi.defineTool({
|
|
212
|
-
name: 'flag_claim',
|
|
213
|
-
label: 'Flag claim',
|
|
214
|
-
description: 'Send an uncited, contradictory, uncertain, or sensitive claim to the review inbox instead of the Wiki.',
|
|
215
|
-
parameters: Type.Object({ claim: Type.String(), kind: Type.Union([Type.Literal('uncited'), Type.Literal('contradiction'), Type.Literal('low_confidence'), Type.Literal('sensitive')]), reason: Type.String() }),
|
|
216
|
-
execute: async (_id, input) => reply(collect.flag(input))
|
|
217
|
-
})
|
|
218
|
-
]
|
|
219
|
-
const allowed = toolNames(kind)
|
|
220
|
-
const { session } = await pi.createAgentSession({
|
|
221
|
-
cwd,
|
|
222
|
-
agentDir,
|
|
223
|
-
modelRuntime: await runtime(),
|
|
224
|
-
tools: allowed,
|
|
225
|
-
customTools: tools.filter((tool) => allowed.includes((tool as { name: string }).name)) as never,
|
|
226
|
-
resourceLoader,
|
|
227
|
-
sessionManager: pi.SessionManager.inMemory(cwd)
|
|
228
|
-
})
|
|
229
|
-
let lastError: string | null = null
|
|
230
|
-
session.subscribe((event) => {
|
|
231
|
-
if (event.type !== 'message_end') return
|
|
232
|
-
const message = event.message as { role?: string; stopReason?: string; errorMessage?: string }
|
|
233
|
-
if (message.role === 'assistant' && message.stopReason === 'error') lastError = message.errorMessage ?? 'Provider error'
|
|
234
|
-
})
|
|
235
|
-
return { prompt: (text) => session.prompt(text), dispose: () => session.dispose(), error: () => lastError }
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
/** Runs one job's session and returns what it proposed. Nothing is written here. */
|
|
239
|
-
export async function runMaintenance(request: MaintenanceRequest, input: Omit<Parameters<MaintainerFactory>[0], 'kind' | 'collect'>, factory: MaintainerFactory = piMaintainerFactory): Promise<MaintenanceResult> {
|
|
240
|
-
const result: MaintenanceResult = { pages: [], memories: [], blocks: [], flags: [] }
|
|
241
|
-
const titled = (proposal: unknown): string => String((proposal as { title?: unknown } | null)?.title ?? '')
|
|
242
|
-
const session = await factory({
|
|
243
|
-
...input,
|
|
244
|
-
kind: request.trigger.kind,
|
|
245
|
-
collect: {
|
|
246
|
-
page: (proposal) => {
|
|
247
|
-
result.pages.push(proposal)
|
|
248
|
-
return `Proposed "${titled(proposal)}". The host will validate it.`
|
|
249
|
-
},
|
|
250
|
-
memory: (proposal) => {
|
|
251
|
-
result.memories.push(proposal)
|
|
252
|
-
return `Proposed memory "${titled(proposal)}". The host will validate it.`
|
|
253
|
-
},
|
|
254
|
-
block: (proposal) => {
|
|
255
|
-
result.blocks.push(proposal)
|
|
256
|
-
return `Proposed a rewrite of ${String((proposal as { block?: unknown } | null)?.block ?? '')}. The host will validate it.`
|
|
257
|
-
},
|
|
258
|
-
flag: (flag) => {
|
|
259
|
-
result.flags.push(flag)
|
|
260
|
-
return 'Sent to the review inbox.'
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
})
|
|
264
|
-
try {
|
|
265
|
-
await session.prompt(prompt(request))
|
|
266
|
-
const error = session.error()
|
|
267
|
-
if (error) throw new HarnessError('provider_error', error)
|
|
268
|
-
} finally {
|
|
269
|
-
session.dispose()
|
|
270
|
-
}
|
|
271
|
-
return result
|
|
272
|
-
}
|