@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
|
@@ -1,192 +0,0 @@
|
|
|
1
|
-
import { mkdtempSync, readFileSync, writeFileSync } from 'node:fs'
|
|
2
|
-
import { tmpdir } from 'node:os'
|
|
3
|
-
import { join } from 'node:path'
|
|
4
|
-
import { afterEach, describe, expect, it } from 'vitest'
|
|
5
|
-
|
|
6
|
-
import { memorySecretStore } from '../accounts/secrets'
|
|
7
|
-
import { writeJson } from '../core/fs'
|
|
8
|
-
import { setLogSink, type Notice } from '../core/bus'
|
|
9
|
-
import { createHarness, type Harness } from '../harness'
|
|
10
|
-
import type { PiSessionLike } from '../sessions/runner'
|
|
11
|
-
import * as blocks from './blocks'
|
|
12
|
-
import { findMemory } from './facts'
|
|
13
|
-
import { prompt, runMaintenance, toolNames, type MaintainerFactory, type MaintenanceRequest } from './maintainer'
|
|
14
|
-
import { chatUpdatesPaused, fullBlocks } from './maintenance'
|
|
15
|
-
import { listInbox } from './operations'
|
|
16
|
-
import { wikiDir } from './store'
|
|
17
|
-
|
|
18
|
-
const request: MaintenanceRequest = { jobId: 'job-1', namespace: '/library', trigger: { kind: 'ingest', sourceId: 'src-1', sourceName: 'notes.md', text: 'We chose Tauri.' }, pages: [], index: '# Index' }
|
|
19
|
-
const input = { cwd: '/tmp', product: 'Xingular Agent', account: { id: 'a', provider: 'pi' as const, label: 'A', env: {}, configDir: '/tmp', sharing: [] }, runtime: async () => ({}) as never }
|
|
20
|
-
|
|
21
|
-
describe('the maintainer session', () => {
|
|
22
|
-
it('collects proposals and flags without writing anything itself', async () => {
|
|
23
|
-
let prompted = ''
|
|
24
|
-
const factory: MaintainerFactory = async ({ collect }) => ({
|
|
25
|
-
prompt: async (text) => {
|
|
26
|
-
prompted = text
|
|
27
|
-
collect.page({ title: 'Use Tauri', type: 'decision', body: 'We use Tauri.', sourceRefs: [{ kind: 'source', ref: 'src-1' }], links: [] })
|
|
28
|
-
collect.memory({ title: 'Prefers Tauri', kind: 'preference', body: 'Use Tauri for desktop apps.', namespace: '/', sourceRefs: [{ kind: 'source', ref: 'src-1' }] })
|
|
29
|
-
collect.flag({ claim: 'Electron is slow', kind: 'uncited', reason: 'No source' })
|
|
30
|
-
},
|
|
31
|
-
dispose: () => undefined,
|
|
32
|
-
error: () => null
|
|
33
|
-
})
|
|
34
|
-
const result = await runMaintenance(request, input, factory)
|
|
35
|
-
expect(prompted).toContain('src-1')
|
|
36
|
-
expect(prompted).toContain('Namespace for this job: /library')
|
|
37
|
-
expect(result.pages).toHaveLength(1)
|
|
38
|
-
expect(result.memories[0]).toMatchObject({ kind: 'preference', namespace: '/' })
|
|
39
|
-
expect(result.flags[0]).toMatchObject({ kind: 'uncited' })
|
|
40
|
-
})
|
|
41
|
-
|
|
42
|
-
it('gives a run its blocks and page bodies, and each job only its tools', async () => {
|
|
43
|
-
const block = { id: 'blocks/shared/user', label: 'user', description: 'About the user.', limit: 2000, body: 'Likes pnpm.' }
|
|
44
|
-
const run: MaintenanceRequest = { ...request, trigger: { kind: 'run', runId: 'run-1', transcript: 'User: hi\nAssistant: hello' }, pages: [{ id: 'shell', title: 'Shell', type: 'decision', summary: 'Tauri', body: 'We use Tauri.' }], blocks: [block] }
|
|
45
|
-
const text = prompt(run)
|
|
46
|
-
expect(text).toContain('<block id="blocks/shared/user" label="user" chars="11/2000">')
|
|
47
|
-
expect(text).toContain('<page id="shell" title="Shell">\nWe use Tauri.')
|
|
48
|
-
expect(text).toContain('Settled run run-1 transcript')
|
|
49
|
-
expect(prompt({ ...request, trigger: { kind: 'consolidate' }, blocks: [block] })).toBe('Memory blocks to condense:\n\n<block id="blocks/shared/user" label="user" chars="11/2000">\nAbout the user.\n---\nLikes pnpm.\n</block>')
|
|
50
|
-
expect(toolNames('run')).toEqual(['rethink_block', 'propose_memory', 'propose_page', 'flag_claim'])
|
|
51
|
-
expect(toolNames('ingest')).not.toContain('rethink_block')
|
|
52
|
-
expect(toolNames('consolidate')).toEqual(['rethink_block'])
|
|
53
|
-
})
|
|
54
|
-
|
|
55
|
-
it('reports provider failures', async () => {
|
|
56
|
-
const factory: MaintainerFactory = async () => ({ prompt: async () => undefined, dispose: () => undefined, error: () => 'quota exceeded' })
|
|
57
|
-
await expect(runMaintenance(request, input, factory)).rejects.toMatchObject({ code: 'provider_error' })
|
|
58
|
-
})
|
|
59
|
-
})
|
|
60
|
-
|
|
61
|
-
const open: Harness[] = []
|
|
62
|
-
afterEach(async () => {
|
|
63
|
-
for (const harness of open.splice(0)) await harness.close()
|
|
64
|
-
})
|
|
65
|
-
|
|
66
|
-
function setup(factory: MaintainerFactory) {
|
|
67
|
-
const reply: PiSessionLike = {
|
|
68
|
-
sessionId: 'native',
|
|
69
|
-
subscribe: (listener) => {
|
|
70
|
-
emit = listener as never
|
|
71
|
-
return () => undefined
|
|
72
|
-
},
|
|
73
|
-
prompt: async () => emit({ type: 'message_end', message: { role: 'assistant', content: [{ type: 'text', text: 'We decided to use Tauri. I prefer small commits.' }] } }),
|
|
74
|
-
sendCustomMessage: async () => undefined,
|
|
75
|
-
abort: async () => undefined,
|
|
76
|
-
dispose: () => undefined
|
|
77
|
-
}
|
|
78
|
-
let emit: (event: unknown) => void = () => undefined
|
|
79
|
-
const harness = createHarness({ dataRoot: mkdtempSync(join(tmpdir(), 'harness-maint-data-')), stateDir: mkdtempSync(join(tmpdir(), 'harness-maint-state-')), secrets: memorySecretStore(), sessionFactory: async () => reply, maintainerFactory: factory })
|
|
80
|
-
open.push(harness)
|
|
81
|
-
harness.accounts.create('Main', 'apiKey')
|
|
82
|
-
const notices: Notice[] = []
|
|
83
|
-
harness.bus.on((notice) => void notices.push(notice))
|
|
84
|
-
const turn = (id: string) => harness.sessions.run({ run: { id, sessionId: 'nora' }, cwd: harness.env.root, sessionDir: join(harness.env.root, 'history'), text: 'Which shell do we use?', caller: { agentId: 'nora', agentName: 'Nora', actor: 'Chat' }, tools: { allow: [] } })
|
|
85
|
-
return { harness, notices, turn, wiki: wikiDir(harness.env.root) }
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
describe('the memory lifecycle', () => {
|
|
89
|
-
it('a settled run is consolidated: quoted changes apply, invented ones wait, and the session never writes', async () => {
|
|
90
|
-
let seen: { kind: string; text: string } | null = null
|
|
91
|
-
const { harness, notices, turn, wiki } = setup(async ({ kind, collect }) => ({
|
|
92
|
-
prompt: async (text) => {
|
|
93
|
-
seen = { kind, text }
|
|
94
|
-
collect.memory({ title: 'Small commits', kind: 'preference', body: 'Prefer small commits.', confidence: 'high', sourceRefs: [{ kind: 'run', ref: 'run-1', excerpt: 'I prefer small commits' }] })
|
|
95
|
-
collect.memory({ title: 'Invented', kind: 'fact', body: 'Never said.', confidence: 'high', sourceRefs: [{ kind: 'run', ref: 'run-1', excerpt: 'something never said at all' }] })
|
|
96
|
-
collect.block({ block: 'blocks/agents/nora/notes', body: 'The shell is Tauri.', sourceRefs: [{ kind: 'run', ref: 'run-1', excerpt: 'We decided to use Tauri' }] })
|
|
97
|
-
collect.page({ title: 'A new page', type: 'concept', body: 'From one run.', sourceRefs: [{ kind: 'run', ref: 'run-1', excerpt: 'We decided to use Tauri' }], links: [] })
|
|
98
|
-
},
|
|
99
|
-
dispose: () => undefined,
|
|
100
|
-
error: () => null
|
|
101
|
-
}))
|
|
102
|
-
await turn('run-1')
|
|
103
|
-
await new Promise((resolve) => setTimeout(resolve, 30))
|
|
104
|
-
await harness.maintenance.settled()
|
|
105
|
-
expect(seen!.kind).toBe('run')
|
|
106
|
-
expect(seen!.text).toContain('Settled run run-1 transcript:\n\nUser: Which shell do we use?\nAssistant: We decided to use Tauri.')
|
|
107
|
-
expect(seen!.text).toContain('<block id="blocks/agents/nora/notes" label="notes"')
|
|
108
|
-
expect(findMemory(wiki, 'small-commits').front).toMatchObject({ namespace: '/', sourceIds: ['run:run-1'] })
|
|
109
|
-
expect(() => findMemory(wiki, 'invented')).toThrow()
|
|
110
|
-
expect(blocks.find(wiki, 'blocks/agents/nora/notes').body).toBe('The shell is Tauri.')
|
|
111
|
-
// One run is an episode: it never becomes a page by itself.
|
|
112
|
-
expect(listInbox(wiki).map((item) => item.kind).sort()).toEqual(['applied', 'unresolved', 'unresolved'])
|
|
113
|
-
expect(notices.some((notice) => notice.type === 'wiki.maintenance' && String(notice.detail).includes('2 parked'))).toBe(true)
|
|
114
|
-
expect(readFileSync(join(wiki, 'log.md'), 'utf8')).toContain('Sleep-time · run run-1')
|
|
115
|
-
// A person can undo the whole consolidation as one operation.
|
|
116
|
-
harness.maintenance.undoOperation(listInbox(wiki).find((item) => item.operationId)!.operationId!)
|
|
117
|
-
expect(blocks.find(wiki, 'blocks/agents/nora/notes').body).toBe('')
|
|
118
|
-
})
|
|
119
|
-
|
|
120
|
-
it('skips a run that settings exclude, and a paused conversation', async () => {
|
|
121
|
-
let jobs = 0
|
|
122
|
-
const { harness, turn } = setup(async () => ({ prompt: async () => void (jobs += 1), dispose: () => undefined, error: () => null }))
|
|
123
|
-
writeJson(join(harness.env.root, 'config/settings.json'), { excludedSourceIds: ['run-1'] })
|
|
124
|
-
await turn('run-1')
|
|
125
|
-
await new Promise((resolve) => setTimeout(resolve, 30))
|
|
126
|
-
await harness.maintenance.settled()
|
|
127
|
-
writeJson(join(harness.env.root, 'config/settings.json'), { autoMaintenanceChat: false })
|
|
128
|
-
await turn('run-2')
|
|
129
|
-
await new Promise((resolve) => setTimeout(resolve, 30))
|
|
130
|
-
await harness.maintenance.settled()
|
|
131
|
-
expect(jobs).toBe(0)
|
|
132
|
-
expect([chatUpdatesPaused({}), chatUpdatesPaused({ autoMaintenanceChat: true }), chatUpdatesPaused({ autoMaintenanceChat: false })]).toEqual([false, false, true])
|
|
133
|
-
})
|
|
134
|
-
|
|
135
|
-
it('a code run outside any project is not maintained, and it leaves no log line', async () => {
|
|
136
|
-
const lines: string[] = []
|
|
137
|
-
setLogSink((text) => void lines.push(text))
|
|
138
|
-
try {
|
|
139
|
-
let jobs = 0
|
|
140
|
-
const { harness } = setup(async () => ({ prompt: async () => void (jobs += 1), dispose: () => undefined, error: () => null }))
|
|
141
|
-
await harness.sessions.run({ run: { id: 'code-1', sessionId: 'nora', mode: 'code' },cwd: harness.env.root, sessionDir: join(harness.env.root, 'history'), text: 'Which shell do we use?', caller: { agentId: 'nora', agentName: 'Nora', actor: 'Code' }, tools: { allow: [] } })
|
|
142
|
-
await new Promise((resolve) => setTimeout(resolve, 30))
|
|
143
|
-
await harness.maintenance.settled()
|
|
144
|
-
expect(jobs).toBe(0)
|
|
145
|
-
expect(lines.filter((line) => line.includes('maintenance'))).toEqual([])
|
|
146
|
-
} finally {
|
|
147
|
-
setLogSink(null)
|
|
148
|
-
}
|
|
149
|
-
})
|
|
150
|
-
|
|
151
|
-
it('an imported source is read once and its proposals are checked against its text', async () => {
|
|
152
|
-
const { harness, wiki } = setup(async ({ kind, collect }) => ({
|
|
153
|
-
prompt: async () => {
|
|
154
|
-
expect(kind).toBe('ingest')
|
|
155
|
-
const source = (await import('./store')).listSources(wiki)[0]
|
|
156
|
-
collect.page({ id: 'shell', title: 'Shell', type: 'decision', body: 'We use Tauri.', sourceRefs: [{ kind: 'source', ref: source.id, excerpt: 'we chose Tauri for the shell' }], links: [] })
|
|
157
|
-
collect.page({ id: 'rumor', title: 'Rumor', type: 'concept', body: 'Not in the source.', sourceRefs: [{ kind: 'source', ref: source.id, excerpt: 'Electron is faster than everything' }], links: [] })
|
|
158
|
-
},
|
|
159
|
-
dispose: () => undefined,
|
|
160
|
-
error: () => null
|
|
161
|
-
}))
|
|
162
|
-
harness.maintenance.ingest('notes.md', Buffer.from('After the review we chose Tauri for the shell.'))
|
|
163
|
-
await harness.maintenance.settled()
|
|
164
|
-
expect(readFileSync(join(wiki, 'library/shell.md'), 'utf8')).toContain('We use Tauri.')
|
|
165
|
-
const parked = listInbox(wiki).find((item) => item.payload !== undefined)!
|
|
166
|
-
expect(parked.title).toContain('Rumor')
|
|
167
|
-
// The user is the authority when they accept what was parked.
|
|
168
|
-
expect(harness.maintenance.acceptInboxItem(parked.id)).toContain('created')
|
|
169
|
-
expect(readFileSync(join(wiki, 'library/rumor.md'), 'utf8')).toContain('user:accepted')
|
|
170
|
-
})
|
|
171
|
-
|
|
172
|
-
it('condenses a block that is close to its limit once nothing else is happening, and only to something shorter', async () => {
|
|
173
|
-
const harness = createHarness({
|
|
174
|
-
dataRoot: mkdtempSync(join(tmpdir(), 'harness-maint-data-')),
|
|
175
|
-
stateDir: mkdtempSync(join(tmpdir(), 'harness-maint-state-')),
|
|
176
|
-
secrets: memorySecretStore(),
|
|
177
|
-
idleMs: 20,
|
|
178
|
-
maintainerFactory: async ({ kind, collect }) => ({ prompt: async () => void (kind === 'consolidate' && collect.block({ block: 'blocks/shared/user', body: 'Uses pnpm.' })), dispose: () => undefined, error: () => null })
|
|
179
|
-
})
|
|
180
|
-
open.push(harness)
|
|
181
|
-
harness.accounts.create('Main', 'apiKey')
|
|
182
|
-
const wiki = wikiDir(harness.env.root)
|
|
183
|
-
const user = blocks.ensureUser(wiki)
|
|
184
|
-
writeFileSync(user.path, blocks.serializeBlock({ ...user.front, limit: 60 }, 'Likes pnpm. Likes pnpm a lot. Uses pnpm everywhere, always.'))
|
|
185
|
-
expect(fullBlocks(wiki, new Set())).toEqual(['blocks/shared/user'])
|
|
186
|
-
await new Promise((resolve) => setTimeout(resolve, 120))
|
|
187
|
-
await harness.maintenance.settled()
|
|
188
|
-
expect(blocks.find(wiki, 'blocks/shared/user').body).toBe('Uses pnpm.')
|
|
189
|
-
const tried = new Set<string>()
|
|
190
|
-
expect(fullBlocks(wiki, tried)).toEqual([])
|
|
191
|
-
})
|
|
192
|
-
})
|