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