@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.
Files changed (132) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/NOTICE.md +22 -0
  3. package/README.md +67 -36
  4. package/dist/cli.js +6023 -6621
  5. package/dist/terminal-pi.js +19 -24
  6. package/docs/terminal-tui.md +27 -7
  7. package/package.json +2 -2
  8. package/src/accounts/accounts.ts +4 -3
  9. package/src/capabilities/api.ts +1 -1
  10. package/src/cli.ts +7 -3
  11. package/src/core/caller.ts +34 -0
  12. package/src/core/fs.ts +17 -1
  13. package/src/harness.ts +55 -66
  14. package/src/{memory → history}/recall.test.ts +1 -1
  15. package/src/{memory → history}/recall.ts +1 -1
  16. package/src/{memory → history}/trajectory.test.ts +4 -4
  17. package/src/{memory → history}/trajectory.ts +1 -1
  18. package/src/hub/hub.test.ts +4 -5
  19. package/src/hub/hub.ts +2 -2
  20. package/src/index.ts +28 -14
  21. package/src/mcp/connections.ts +1 -1
  22. package/src/mcp/service.ts +1 -1
  23. package/src/pi/proxy.test.ts +68 -0
  24. package/src/pi/runtime.test.ts +61 -0
  25. package/src/pi/runtime.ts +50 -0
  26. package/src/plugins/api.ts +11 -9
  27. package/src/plugins/builtin.ts +22 -40
  28. package/src/plugins/catalogue.test.ts +13 -41
  29. package/src/plugins/catalogue.ts +3 -32
  30. package/src/plugins/discover.ts +2 -1
  31. package/src/plugins/firstParty.test.ts +1 -1
  32. package/src/plugins/hooks.test.ts +21 -71
  33. package/src/plugins/hooks.ts +6 -7
  34. package/src/plugins/identity.ts +16 -2
  35. package/src/plugins/install.test.ts +1 -1
  36. package/src/plugins/install.ts +2 -1
  37. package/src/plugins/inventory.ts +19 -18
  38. package/src/plugins/plugins.ts +38 -2
  39. package/src/prompt/compile.test.ts +82 -193
  40. package/src/prompt/compile.ts +98 -363
  41. package/src/refine/cli.ts +114 -0
  42. package/src/refine/demo.ts +124 -0
  43. package/src/refine/evidence.ts +349 -0
  44. package/src/refine/host.ts +102 -0
  45. package/src/refine/ledger.ts +104 -0
  46. package/src/refine/library.ts +192 -0
  47. package/src/refine/pi.test.ts +90 -0
  48. package/src/refine/pi.ts +218 -0
  49. package/src/refine/prompts.ts +430 -0
  50. package/src/refine/recursion.ts +353 -0
  51. package/src/refine/refine.test.ts +140 -0
  52. package/src/refine/runner.ts +205 -0
  53. package/src/refine/selection.ts +291 -0
  54. package/src/refine/service.ts +194 -0
  55. package/src/refine/trajectory.ts +71 -0
  56. package/src/refine/types.ts +91 -0
  57. package/src/server/methods.ts +137 -123
  58. package/src/server/server.test.ts +37 -30
  59. package/src/sessions/contracts.ts +6 -1
  60. package/src/sessions/conversation.test.ts +2 -27
  61. package/src/sessions/policy.ts +1 -1
  62. package/src/sessions/pricing.ts +3 -1
  63. package/src/sessions/registry.ts +5 -1
  64. package/src/sessions/runStore.ts +3 -1
  65. package/src/sessions/runner.ts +10 -5
  66. package/src/sessions/sessions.test.ts +45 -6
  67. package/src/sessions/sessions.ts +40 -17
  68. package/src/sessions/subagentService.test.ts +22 -1
  69. package/src/sessions/subagentService.ts +14 -1
  70. package/src/sessions/subagents.ts +2 -0
  71. package/src/terminal/cli.ts +24 -48
  72. package/src/terminal/pluginControl.test.ts +7 -8
  73. package/src/terminal/socket.ts +3 -3
  74. package/src/terminal/terminal.test.ts +21 -39
  75. package/src/terminal/terminals.ts +20 -13
  76. package/src/terminal/tui/application.test.ts +58 -0
  77. package/src/terminal/tui/application.ts +10 -4
  78. package/src/terminal/tui/clipboardFiles.test.ts +94 -0
  79. package/src/terminal/tui/clipboardFiles.ts +86 -0
  80. package/src/terminal/tui/piAdapter.ts +7 -4
  81. package/src/terminal/tui/runtime.ts +2 -0
  82. package/src/tools/definitions.ts +49 -132
  83. package/src/tools/dispatch.ts +78 -0
  84. package/src/tools/harnessTools.ts +23 -0
  85. package/src/tools/spawnSubagent.ts +3 -3
  86. package/src/tools/types.ts +1 -1
  87. package/src/tree/components.ts +221 -0
  88. package/src/tree/materialise.ts +122 -0
  89. package/src/tree/seed.ts +123 -0
  90. package/src/tree/store.ts +193 -0
  91. package/src/tree/surface.ts +135 -0
  92. package/src/tree/tree.test.ts +126 -0
  93. package/src/tree/tree.ts +346 -0
  94. package/src/tree/trust.ts +86 -0
  95. package/src/wiki/notes.ts +313 -0
  96. package/src/memory/api.test.ts +0 -82
  97. package/src/memory/api.ts +0 -303
  98. package/src/memory/blocks.test.ts +0 -78
  99. package/src/memory/blocks.ts +0 -408
  100. package/src/memory/caller.ts +0 -69
  101. package/src/memory/curation.test.ts +0 -206
  102. package/src/memory/curation.ts +0 -64
  103. package/src/memory/dispatch.ts +0 -64
  104. package/src/memory/facts.test.ts +0 -41
  105. package/src/memory/facts.ts +0 -248
  106. package/src/memory/graph.ts +0 -103
  107. package/src/memory/health.test.ts +0 -130
  108. package/src/memory/health.ts +0 -74
  109. package/src/memory/maintainer.ts +0 -272
  110. package/src/memory/maintenance.test.ts +0 -192
  111. package/src/memory/maintenance.ts +0 -679
  112. package/src/memory/migrate.ts +0 -79
  113. package/src/memory/operations.ts +0 -150
  114. package/src/memory/proposals.test.ts +0 -168
  115. package/src/memory/proposals.ts +0 -584
  116. package/src/memory/references.ts +0 -77
  117. package/src/memory/runContext.ts +0 -93
  118. package/src/memory/store.test.ts +0 -66
  119. package/src/memory/store.ts +0 -385
  120. package/src/memory/wiki.test.ts +0 -47
  121. package/src/memory/wiki.ts +0 -423
  122. package/src/memory/wikiIndex.test.ts +0 -88
  123. package/src/memory/wikiIndex.ts +0 -117
  124. package/src/plugins/proposal.ts +0 -121
  125. package/src/skills/registry.test.ts +0 -104
  126. package/src/skills/registry.ts +0 -413
  127. package/src/tools/maintenance.ts +0 -29
  128. package/src/tools/memoryTools.ts +0 -22
  129. package/src/tools/pluginPropose.ts +0 -41
  130. package/src/tools/registryWrite.test.ts +0 -101
  131. package/src/tools/registryWrite.ts +0 -182
  132. /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
- })