@xingular/harness 0.0.0-stage → 0.1.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/README.md +269 -2
- package/dist/cli.js +11046 -0
- package/dist/terminal-pi.js +326 -0
- package/package.json +41 -4
- package/src/accounts/accounts.test.ts +223 -0
- package/src/accounts/accounts.ts +522 -0
- package/src/accounts/env.ts +204 -0
- package/src/accounts/secrets.ts +50 -0
- package/src/accounts/store.ts +338 -0
- package/src/capabilities/api.ts +75 -0
- package/src/capabilities/runSnapshot.ts +59 -0
- package/src/capabilities/types.ts +34 -0
- package/src/cli.ts +110 -0
- package/src/core/bus.ts +34 -0
- package/src/core/changes.ts +33 -0
- package/src/core/env.ts +27 -0
- package/src/core/errors.ts +32 -0
- package/src/core/frontmatter.ts +47 -0
- package/src/core/fs.test.ts +38 -0
- package/src/core/fs.ts +92 -0
- package/src/core/index.ts +193 -0
- package/src/core/path.ts +45 -0
- package/src/core/projects.ts +90 -0
- package/src/core/text.ts +39 -0
- package/src/harness.ts +289 -0
- package/src/index.ts +69 -0
- package/src/mcp/connections.ts +441 -0
- package/src/mcp/manager.ts +130 -0
- package/src/mcp/oauth.ts +195 -0
- package/src/mcp/service.test.ts +376 -0
- package/src/mcp/service.ts +567 -0
- package/src/mcp/types.ts +105 -0
- package/src/memory/api.test.ts +82 -0
- package/src/memory/api.ts +287 -0
- package/src/memory/blocks.test.ts +78 -0
- package/src/memory/blocks.ts +406 -0
- package/src/memory/caller.ts +57 -0
- package/src/memory/dispatch.ts +59 -0
- package/src/memory/facts.test.ts +41 -0
- package/src/memory/facts.ts +248 -0
- package/src/memory/graph.ts +103 -0
- package/src/memory/health.test.ts +130 -0
- package/src/memory/health.ts +74 -0
- package/src/memory/maintainer.ts +272 -0
- package/src/memory/maintenance.test.ts +176 -0
- package/src/memory/maintenance.ts +330 -0
- package/src/memory/migrate.ts +79 -0
- package/src/memory/operations.ts +138 -0
- package/src/memory/proposals.test.ts +168 -0
- package/src/memory/proposals.ts +576 -0
- package/src/memory/recall.test.ts +104 -0
- package/src/memory/recall.ts +271 -0
- package/src/memory/references.ts +77 -0
- package/src/memory/runContext.ts +93 -0
- package/src/memory/secrets.ts +6 -0
- package/src/memory/store.test.ts +66 -0
- package/src/memory/store.ts +385 -0
- package/src/memory/wiki.test.ts +47 -0
- package/src/memory/wiki.ts +422 -0
- package/src/memory/wikiIndex.test.ts +88 -0
- package/src/memory/wikiIndex.ts +116 -0
- package/src/plugins/api.ts +181 -0
- package/src/plugins/attachments.test.ts +84 -0
- package/src/plugins/attachments.ts +128 -0
- package/src/plugins/builtin.ts +82 -0
- package/src/plugins/catalogue.test.ts +254 -0
- package/src/plugins/catalogue.ts +250 -0
- package/src/plugins/cli.ts +184 -0
- package/src/plugins/discover.ts +151 -0
- package/src/plugins/firstParty.test.ts +143 -0
- package/src/plugins/firstParty.ts +129 -0
- package/src/plugins/hooks.test.ts +250 -0
- package/src/plugins/hooks.ts +239 -0
- package/src/plugins/identity.ts +37 -0
- package/src/plugins/install.test.ts +107 -0
- package/src/plugins/install.ts +106 -0
- package/src/plugins/inventory.ts +123 -0
- package/src/plugins/manifest.ts +44 -0
- package/src/plugins/plugins.test.ts +205 -0
- package/src/plugins/plugins.ts +184 -0
- package/src/plugins/proposal.ts +121 -0
- package/src/plugins/runtime.test.ts +40 -0
- package/src/plugins/runtime.ts +82 -0
- package/src/plugins/trust.ts +142 -0
- package/src/plugins/types.ts +130 -0
- package/src/prompt/compile.test.ts +261 -0
- package/src/prompt/compile.ts +732 -0
- package/src/server/methods.ts +308 -0
- package/src/server/remote.ts +77 -0
- package/src/server/rpc.ts +116 -0
- package/src/server/serve.ts +28 -0
- package/src/server/server.test.ts +174 -0
- package/src/sessions/approvals.ts +118 -0
- package/src/sessions/contracts.ts +299 -0
- package/src/sessions/conversation.test.ts +150 -0
- package/src/sessions/extensionSession.test.ts +74 -0
- package/src/sessions/extensionUi.test.ts +65 -0
- package/src/sessions/extensionUi.ts +102 -0
- package/src/sessions/history.ts +51 -0
- package/src/sessions/lineageJoin.ts +75 -0
- package/src/sessions/policy.ts +32 -0
- package/src/sessions/pricing.ts +60 -0
- package/src/sessions/reasoningSummaries.ts +59 -0
- package/src/sessions/registry.ts +294 -0
- package/src/sessions/runIndex.ts +99 -0
- package/src/sessions/runStore.test.ts +152 -0
- package/src/sessions/runStore.ts +332 -0
- package/src/sessions/runner.ts +556 -0
- package/src/sessions/runtimeResources.test.ts +61 -0
- package/src/sessions/sessions.test.ts +434 -0
- package/src/sessions/sessions.ts +295 -0
- package/src/skills/registry.test.ts +104 -0
- package/src/skills/registry.ts +394 -0
- package/src/terminal/cli.ts +149 -0
- package/src/terminal/client.ts +74 -0
- package/src/terminal/controller.ts +227 -0
- package/src/terminal/piExtension.ts +208 -0
- package/src/terminal/pluginControl.test.ts +146 -0
- package/src/terminal/pluginMenu.ts +112 -0
- package/src/terminal/socket.ts +182 -0
- package/src/terminal/standalone.test.ts +115 -0
- package/src/terminal/standalone.ts +126 -0
- package/src/terminal/terminal.test.ts +302 -0
- package/src/terminal/terminals.ts +429 -0
- package/src/tools/askUser.ts +45 -0
- package/src/tools/definitions.ts +187 -0
- package/src/tools/memoryTools.ts +22 -0
- package/src/tools/pluginPropose.ts +41 -0
- package/src/tools/types.ts +36 -0
- package/src/web/safeRequest.ts +171 -0
- package/src/web/text.ts +53 -0
- package/src/web/tools.ts +134 -0
- package/src/web/web.test.ts +141 -0
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
// Terminal sessions: a person working with the `pi` command in a terminal that the embedding
|
|
2
|
+
// application draws. The harness prepares the launch (isolated account, compiled instructions,
|
|
3
|
+
// skills, the session socket), then follows the process through its extension. It never owns
|
|
4
|
+
// the terminal: the application spawns the process from the spec returned here and reports when
|
|
5
|
+
// it exits.
|
|
6
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
7
|
+
import { dirname, join } from 'node:path'
|
|
8
|
+
import { fileURLToPath } from 'node:url'
|
|
9
|
+
|
|
10
|
+
import type { Accounts } from '../accounts/accounts'
|
|
11
|
+
import { configDir, launchEnv } from '../accounts/env'
|
|
12
|
+
import type { Account } from '../accounts/store'
|
|
13
|
+
import type { Bus } from '../core/bus'
|
|
14
|
+
import { log } from '../core/bus'
|
|
15
|
+
import type { HarnessEnv } from '../core/env'
|
|
16
|
+
import { HarnessError, invalid } from '../core/errors'
|
|
17
|
+
import { newId, now } from '../core/fs'
|
|
18
|
+
import { findBinary, searchPath } from '../core/path'
|
|
19
|
+
import type { BrokerSession, ConnectedApps } from '../mcp/service'
|
|
20
|
+
import type { CallerContext } from '../memory/caller'
|
|
21
|
+
import { dispatch } from '../memory/dispatch'
|
|
22
|
+
import type { AttachParams, PluginScope } from '../plugins/api'
|
|
23
|
+
import type { Plugins } from '../plugins/plugins'
|
|
24
|
+
import type { Plugin } from '../plugins/types'
|
|
25
|
+
import { compile, copyToRun, memoryDelta, record, render, saveForSession, type Launch } from '../prompt/compile'
|
|
26
|
+
import type { RunRecord } from '../sessions/contracts'
|
|
27
|
+
import { isActiveStatus } from '../sessions/contracts'
|
|
28
|
+
import type { ToolPolicy } from '../sessions/policy'
|
|
29
|
+
import type { ActiveRun, RunRegistry } from '../sessions/registry'
|
|
30
|
+
import { findRun, loadRun, saveRun, updateRun } from '../sessions/runStore'
|
|
31
|
+
import { resources } from '../skills/registry'
|
|
32
|
+
import type { ToolSpec } from '../tools/types'
|
|
33
|
+
import { APPS_COMMAND, WIKI_COMMAND } from './cli'
|
|
34
|
+
import { SOCKET_ENV, TOKEN_ENV } from './client'
|
|
35
|
+
import { TerminalController, type PolicyReply } from './controller'
|
|
36
|
+
import { SessionSocket, socketPath } from './socket'
|
|
37
|
+
|
|
38
|
+
/** The files a terminal session needs on disk, outside this process. */
|
|
39
|
+
export interface TerminalAssets {
|
|
40
|
+
/** The Node binary that runs the commands. */
|
|
41
|
+
node: string
|
|
42
|
+
/** The built `xingular-harness` command (`dist/cli.js`). */
|
|
43
|
+
cli: string
|
|
44
|
+
/** The built Pi extension (`dist/terminal-pi.js`). */
|
|
45
|
+
piExtension: string
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Where the package's own build puts them. An application that bundles the harness passes its own. */
|
|
49
|
+
export function defaultAssets(): TerminalAssets {
|
|
50
|
+
const here = dirname(fileURLToPath(import.meta.url))
|
|
51
|
+
const dist = [here, join(here, '..', '..', 'dist'), join(here, '..', 'dist')].find((folder) => existsSync(join(folder, 'terminal-pi.js'))) ?? here
|
|
52
|
+
return { node: process.execPath, cli: join(dist, 'cli.js'), piExtension: join(dist, 'terminal-pi.js') }
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface TerminalPrepareInput {
|
|
56
|
+
sessionId: string
|
|
57
|
+
/** The account the session is pinned to. Omitted only for a session that has none yet; it then takes the default and keeps it. */
|
|
58
|
+
accountId?: string | null
|
|
59
|
+
projectId?: string | null
|
|
60
|
+
cwd: string
|
|
61
|
+
/** The session's own folder. Its compiled instructions and native history are kept under it. */
|
|
62
|
+
sessionFolder: string
|
|
63
|
+
nativeSessionId?: string | null
|
|
64
|
+
model?: string | null
|
|
65
|
+
readOnly?: boolean
|
|
66
|
+
/** What the application adds to the instructions, and which of its tools it describes. */
|
|
67
|
+
launch?: Launch
|
|
68
|
+
/** Give the session the connected-apps broker. Defaults to yes. */
|
|
69
|
+
connections?: boolean
|
|
70
|
+
/** Fields copied onto each run this session records. */
|
|
71
|
+
run?: Partial<Pick<RunRecord, 'mode' | 'worktreeId'>>
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface TerminalLaunch {
|
|
75
|
+
program: string
|
|
76
|
+
args: string[]
|
|
77
|
+
/** Variables to set; `null` removes an inherited one. */
|
|
78
|
+
env: Record<string, string | null>
|
|
79
|
+
accountId: string
|
|
80
|
+
nativeSessionId: string | null
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
interface Live {
|
|
84
|
+
input: TerminalPrepareInput
|
|
85
|
+
account: Account
|
|
86
|
+
controller: TerminalController
|
|
87
|
+
token: string
|
|
88
|
+
broker: BrokerSession | null
|
|
89
|
+
caller: CallerContext
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export interface TerminalsOptions {
|
|
93
|
+
env: HarnessEnv
|
|
94
|
+
stateDir: string
|
|
95
|
+
accounts: Accounts
|
|
96
|
+
runs: RunRegistry
|
|
97
|
+
apps: ConnectedApps
|
|
98
|
+
policy: ToolPolicy
|
|
99
|
+
bus: Bus
|
|
100
|
+
/** Chooses what a terminal loads. Without it Pi discovers extensions and skills on its own. */
|
|
101
|
+
plugins?: Plugins
|
|
102
|
+
/** Lists plugins and applies a person's choices, for the session's `/plugins` command. */
|
|
103
|
+
pluginControl?: PluginControl
|
|
104
|
+
assets?: TerminalAssets
|
|
105
|
+
/** The `pi` executable. Defaults to the one on the search path. */
|
|
106
|
+
program?: () => string | null
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** What `/plugins` in a terminal can do. Each takes the session's folder, account and project. */
|
|
110
|
+
export interface PluginControl {
|
|
111
|
+
list(scope: PluginScope): Promise<Plugin[]>
|
|
112
|
+
attach(params: AttachParams, on: boolean): Promise<Plugin[]>
|
|
113
|
+
trust(scope: PluginScope & { hash: string }): Promise<Plugin[]>
|
|
114
|
+
untrust(scope: PluginScope & { hash: string }): Promise<Plugin[]>
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const READ_ONLY_TOOLS = new Set(['read', 'grep', 'find', 'ls'])
|
|
118
|
+
|
|
119
|
+
export class Terminals {
|
|
120
|
+
private readonly sessions = new Map<string, Live>()
|
|
121
|
+
private readonly socket: SessionSocket
|
|
122
|
+
private readonly assets: TerminalAssets
|
|
123
|
+
|
|
124
|
+
constructor(private readonly options: TerminalsOptions) {
|
|
125
|
+
this.assets = options.assets ?? defaultAssets()
|
|
126
|
+
this.socket = new SessionSocket(socketPath(options.stateDir), {
|
|
127
|
+
memory: (caller, call, params) => dispatch(options.env, caller, call, params),
|
|
128
|
+
apps: (session, call, params) => this.appsCall(session, call, params),
|
|
129
|
+
plugins: (grant, call, params, control) => this.pluginsCall(grant.sessionId, call, params, control)
|
|
130
|
+
})
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** The commands a terminal session finds on PATH. Rewritten only when they change. */
|
|
134
|
+
private ensureCommands(): string {
|
|
135
|
+
const bin = join(this.options.stateDir, 'run', 'bin')
|
|
136
|
+
mkdirSync(bin, { recursive: true })
|
|
137
|
+
const quote = (value: string) => `'${value.replaceAll("'", "'\\''")}'`
|
|
138
|
+
for (const [name, mode] of [[WIKI_COMMAND, 'wiki'], [APPS_COMMAND, 'mcp']] as const) {
|
|
139
|
+
const wrapper = join(bin, name)
|
|
140
|
+
const text = `#!/bin/sh\nexec ${quote(this.assets.node)} ${quote(this.assets.cli)} ${mode} "$@"\n`
|
|
141
|
+
let current: string | null = null
|
|
142
|
+
try {
|
|
143
|
+
current = readFileSync(wrapper, 'utf8')
|
|
144
|
+
} catch {
|
|
145
|
+
current = null
|
|
146
|
+
}
|
|
147
|
+
if (current !== text) {
|
|
148
|
+
writeFileSync(wrapper, text)
|
|
149
|
+
chmodSync(wrapper, 0o755)
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return bin
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
private tools(session: BrokerSession): ToolSpec[] {
|
|
156
|
+
return this.options.apps.brokerTools(session)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
private async appsCall(session: BrokerSession, call: string, params: Record<string, unknown>): Promise<unknown> {
|
|
160
|
+
const { apps } = this.options
|
|
161
|
+
switch (call) {
|
|
162
|
+
case 'mcp.list':
|
|
163
|
+
return apps.list(session)
|
|
164
|
+
case 'mcp.call':
|
|
165
|
+
return apps.invoke(session, String(params.name ?? ''), (params.args && typeof params.args === 'object' ? params.args : {}) as Record<string, unknown>)
|
|
166
|
+
case 'mcp.revision':
|
|
167
|
+
return apps.revision(session)
|
|
168
|
+
case 'tools.list': {
|
|
169
|
+
const { Type } = await import('typebox')
|
|
170
|
+
return { tools: this.tools(session).map((tool) => ({ name: tool.name, label: tool.label, description: tool.description, parameters: tool.parameters(Type) })) }
|
|
171
|
+
}
|
|
172
|
+
case 'tools.call': {
|
|
173
|
+
const tool = this.tools(session).find((candidate) => candidate.name === params.name)
|
|
174
|
+
if (!tool) throw new HarnessError('unknown_method', `Unknown tool ${String(params.name)}.`)
|
|
175
|
+
const result = await tool.execute((params.input && typeof params.input === 'object' ? params.input : {}) as Record<string, unknown>)
|
|
176
|
+
return { text: typeof result === 'string' ? result : result.text }
|
|
177
|
+
}
|
|
178
|
+
default:
|
|
179
|
+
throw new HarnessError('unknown_method', `Unknown call ${call}.`)
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* `/plugins` in a terminal. Listing is open to the session. A change is taken only from the
|
|
185
|
+
* session's own extension, which asks the person in the terminal first: the model can reach
|
|
186
|
+
* this socket from a shell command, and must not be able to trust code or switch it on.
|
|
187
|
+
*/
|
|
188
|
+
private async pluginsCall(sessionId: string, call: string, params: Record<string, unknown>, control: boolean): Promise<unknown> {
|
|
189
|
+
const api = this.options.pluginControl
|
|
190
|
+
const live = this.sessions.get(sessionId)
|
|
191
|
+
if (!api || !live) throw new HarnessError('unknown_method', `Unknown call ${call}.`)
|
|
192
|
+
const projectId = live.input.projectId ?? null
|
|
193
|
+
const scope = { cwd: live.input.cwd, accountId: live.account.id, projectId }
|
|
194
|
+
if (call === 'plugins.list') return api.list(scope)
|
|
195
|
+
if (call === 'plugins.session') return { projectId }
|
|
196
|
+
if (!control) throw new HarnessError('unauthorized', 'Only a person at this terminal can change plugins. Type /plugins.')
|
|
197
|
+
const text = (name: string): string => {
|
|
198
|
+
if (typeof params[name] !== 'string' || !params[name]) throw invalid(`${name} is required.`)
|
|
199
|
+
return params[name] as string
|
|
200
|
+
}
|
|
201
|
+
switch (call) {
|
|
202
|
+
case 'plugins.attach':
|
|
203
|
+
case 'plugins.detach':
|
|
204
|
+
return api.attach({ ...scope, id: text('id'), scope: params.everywhere === true || !projectId ? {} : { projectId } }, call === 'plugins.attach')
|
|
205
|
+
case 'plugins.trust':
|
|
206
|
+
return api.trust({ ...scope, hash: text('hash') })
|
|
207
|
+
case 'plugins.untrust':
|
|
208
|
+
return api.untrust({ ...scope, hash: text('hash') })
|
|
209
|
+
default:
|
|
210
|
+
throw new HarnessError('unknown_method', `Unknown call ${call}.`)
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
private live(sessionId: string): Live {
|
|
215
|
+
const live = this.sessions.get(sessionId)
|
|
216
|
+
if (!live) throw new HarnessError('terminal_not_ready', 'The terminal controller is unavailable. Reopen this session.')
|
|
217
|
+
return live
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
private historyDir(input: TerminalPrepareInput): string {
|
|
221
|
+
return join(input.sessionFolder, 'pi')
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
private runRecord(live: Live, runId: string, text: string | null): RunRecord {
|
|
225
|
+
const { input } = live
|
|
226
|
+
return {
|
|
227
|
+
resumedFromRunId: null,
|
|
228
|
+
parentRunId: null,
|
|
229
|
+
relation: null,
|
|
230
|
+
terminalReason: null,
|
|
231
|
+
agentId: null,
|
|
232
|
+
trigger: null,
|
|
233
|
+
id: runId,
|
|
234
|
+
projectId: input.projectId ?? null,
|
|
235
|
+
mode: input.run?.mode ?? 'code',
|
|
236
|
+
harness: 'pi',
|
|
237
|
+
sessionType: 'terminal',
|
|
238
|
+
sessionId: input.sessionId,
|
|
239
|
+
nativeSessionId: live.controller.nativeSessionId,
|
|
240
|
+
workingDirectory: input.cwd,
|
|
241
|
+
worktreeId: input.run?.worktreeId ?? null,
|
|
242
|
+
goal: text,
|
|
243
|
+
attachmentCount: 0,
|
|
244
|
+
startedAt: now(),
|
|
245
|
+
endedAt: null,
|
|
246
|
+
status: 'queued',
|
|
247
|
+
exitCode: null,
|
|
248
|
+
handoffId: null,
|
|
249
|
+
accountId: live.account.id
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** The run a native turn reports into: the one reserved by `send`, or a new one for what the person typed. */
|
|
254
|
+
private begin(live: Live, reservedRunId: string | null, text: string): ActiveRun {
|
|
255
|
+
const { root } = this.options.env
|
|
256
|
+
const { sessionId } = live.input
|
|
257
|
+
if (this.options.runs.activeForSession(sessionId)) throw new HarnessError('session_busy', 'A native turn already owns this session.')
|
|
258
|
+
let runId = reservedRunId
|
|
259
|
+
if (runId) {
|
|
260
|
+
const reserved = loadRun(root, runId)
|
|
261
|
+
if (reserved.sessionId !== sessionId || reserved.status !== 'queued') throw invalid('Delivery does not own this queued run.')
|
|
262
|
+
} else {
|
|
263
|
+
runId = newId()
|
|
264
|
+
saveRun(root, this.runRecord(live, runId, text || null))
|
|
265
|
+
copyToRun(root, live.input.sessionFolder, runId)
|
|
266
|
+
}
|
|
267
|
+
return this.options.runs.start(sessionId, runId)
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
private async decide(live: Live, run: ActiveRun, tool: string, args: unknown): Promise<PolicyReply> {
|
|
271
|
+
const record = loadRun(this.options.env.root, run.runId)
|
|
272
|
+
if (!isActiveStatus(record.status)) throw invalid('The run is no longer active.')
|
|
273
|
+
const call = { run: record, caller: { ...live.caller, runId: run.runId }, tool, args }
|
|
274
|
+
const { policy } = this.options
|
|
275
|
+
if (policy.scope) {
|
|
276
|
+
const scope = await policy.scope(call)
|
|
277
|
+
if (!scope.allowed && !scope.requiresApproval) return { allowed: false, reason: scope.reason ?? 'Outside what this session may do.' }
|
|
278
|
+
try {
|
|
279
|
+
await policy.authorize?.(call)
|
|
280
|
+
} catch (error) {
|
|
281
|
+
return { allowed: false, reason: error instanceof Error ? error.message : 'Tool unavailable in the settings.' }
|
|
282
|
+
}
|
|
283
|
+
return { allowed: scope.allowed, requiresApproval: !!scope.requiresApproval, reason: scope.reason ?? null }
|
|
284
|
+
}
|
|
285
|
+
// With no application policy: reads run, a read-only session refuses the rest, and anything else asks.
|
|
286
|
+
const name = tool.toLowerCase()
|
|
287
|
+
if (READ_ONLY_TOOLS.has(name) || this.options.apps.toolNames.includes(name as never)) return { allowed: true }
|
|
288
|
+
if (live.input.readOnly) return { allowed: false, reason: 'This session is read-only.' }
|
|
289
|
+
return { allowed: false, requiresApproval: true, reason: 'This native action requires approval.' }
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Prepares a `pi` terminal session and returns what to spawn. The caller must report the
|
|
294
|
+
* process's end with `dispose`, also when it never starts.
|
|
295
|
+
*/
|
|
296
|
+
async prepare(input: TerminalPrepareInput): Promise<TerminalLaunch> {
|
|
297
|
+
const { env, accounts, apps, bus } = this.options
|
|
298
|
+
const { root } = env
|
|
299
|
+
if (!input?.sessionId || !input.cwd || !input.sessionFolder) throw invalid('A terminal session needs an id, a working folder, and its own folder.')
|
|
300
|
+
await this.dispose(input.sessionId)
|
|
301
|
+
const program = this.options.program ? this.options.program() : findBinary('pi')
|
|
302
|
+
if (!program) throw new HarnessError('harness_unavailable', 'The Pi CLI was not found on PATH.')
|
|
303
|
+
// A pinned account is used as it is; it is never exchanged for another one.
|
|
304
|
+
const account = input.accountId ? accounts.find(input.accountId) : accounts.resolve(null, null)
|
|
305
|
+
const launch = launchEnv(accounts.dir, account)
|
|
306
|
+
const loading = (await this.options.plugins?.loading({ cwd: input.cwd, agentDir: configDir(accounts.dir, account), install: true, caller: { projectId: input.projectId ?? null } })) ?? null
|
|
307
|
+
await this.socket.listen()
|
|
308
|
+
await accounts.terminalBegin(account)
|
|
309
|
+
|
|
310
|
+
const sessionId = input.sessionId
|
|
311
|
+
const caller: CallerContext = { sessionId, projectId: input.projectId ?? null, actor: 'Code', surface: 'cli' }
|
|
312
|
+
let token: string | null = null
|
|
313
|
+
try {
|
|
314
|
+
const broker = input.connections === false ? null : apps.grant({ sessionId, runId: null, caller, readOnly: !!input.readOnly, depth: 0 })
|
|
315
|
+
// The session keeps these instructions; each of its runs records a copy, and a memory
|
|
316
|
+
// block that changes later is sent along as a note.
|
|
317
|
+
const pack = compile(env, caller, { input: '', ...input.launch })
|
|
318
|
+
record(root, caller, pack)
|
|
319
|
+
mkdirSync(this.historyDir(input), { recursive: true })
|
|
320
|
+
saveForSession(input.sessionFolder, pack)
|
|
321
|
+
|
|
322
|
+
const live: Live = {
|
|
323
|
+
input,
|
|
324
|
+
account,
|
|
325
|
+
broker,
|
|
326
|
+
caller,
|
|
327
|
+
token: '',
|
|
328
|
+
controller: new TerminalController({
|
|
329
|
+
sessionId,
|
|
330
|
+
nativeSessionId: input.nativeSessionId ?? null,
|
|
331
|
+
begin: ({ reservedRunId, text }) => this.begin(live, reservedRunId, text),
|
|
332
|
+
policy: (run, tool, args) => this.decide(live, run, tool, args),
|
|
333
|
+
native: (nativeSessionId) => bus.emit({ type: 'terminal.native', sessionId, nativeSessionId }),
|
|
334
|
+
telemetry: (event) => bus.emit({ type: 'terminal.telemetry', sessionId, event })
|
|
335
|
+
})
|
|
336
|
+
}
|
|
337
|
+
token = this.socket.issue({
|
|
338
|
+
sessionId,
|
|
339
|
+
caller: () => ({ ...caller, runId: live.controller.activeRun?.runId ?? null }),
|
|
340
|
+
broker,
|
|
341
|
+
terminal: { accept: (event, data) => live.controller.accept(event, data), disconnected: () => live.controller.disconnected() }
|
|
342
|
+
})
|
|
343
|
+
live.token = token
|
|
344
|
+
this.sessions.set(sessionId, live)
|
|
345
|
+
|
|
346
|
+
const args = ['--append-system-prompt', render(env, pack)]
|
|
347
|
+
if (input.model) args.push('--model', input.model)
|
|
348
|
+
if (loading) {
|
|
349
|
+
// Pi's own discovery is off: the terminal loads what the harness found and nothing else.
|
|
350
|
+
args.push('--no-extensions', '--no-skills', '--no-prompt-templates', '--no-themes')
|
|
351
|
+
const enabled = resources(root, input.projectId ?? null)
|
|
352
|
+
for (const path of [...loading.extensionPaths, ...loading.builtins]) args.push('--extension', path)
|
|
353
|
+
for (const path of [...enabled.skillPaths, ...loading.skillPaths]) args.push('--skill', path)
|
|
354
|
+
for (const path of [...enabled.promptPaths, ...loading.promptPaths]) args.push('--prompt-template', path)
|
|
355
|
+
for (const path of loading.themePaths) args.push('--theme', path)
|
|
356
|
+
}
|
|
357
|
+
// Last, so its gate sees a tool call as every other extension left it.
|
|
358
|
+
args.push('--extension', this.assets.piExtension, '--session-dir', this.historyDir(input))
|
|
359
|
+
if (input.nativeSessionId) args.push('--session', input.nativeSessionId)
|
|
360
|
+
return {
|
|
361
|
+
program,
|
|
362
|
+
args,
|
|
363
|
+
env: { ...launch, PATH: `${this.ensureCommands()}:${searchPath()}`, [SOCKET_ENV]: this.socket.path, [TOKEN_ENV]: token },
|
|
364
|
+
accountId: account.id,
|
|
365
|
+
nativeSessionId: input.nativeSessionId ?? null
|
|
366
|
+
}
|
|
367
|
+
} catch (error) {
|
|
368
|
+
if (token) this.socket.revoke(token)
|
|
369
|
+
this.sessions.delete(sessionId)
|
|
370
|
+
apps.revokeSession(sessionId)
|
|
371
|
+
await accounts.terminalEnd(account).catch(() => undefined)
|
|
372
|
+
throw error
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/** Hands the terminal an instruction as a new run. Resolves once the native session has taken it. */
|
|
377
|
+
async send(input: { sessionId: string; runId?: string | null; text: string }): Promise<{ runId: string }> {
|
|
378
|
+
const { env } = this.options
|
|
379
|
+
const { root } = env
|
|
380
|
+
if (!input.text?.trim()) throw invalid('Message is empty.')
|
|
381
|
+
const live = this.live(input.sessionId)
|
|
382
|
+
if (this.options.runs.activeForSession(input.sessionId) || live.controller.state().active) throw new HarnessError('session_busy', 'This session is still working.')
|
|
383
|
+
const runId = input.runId || newId()
|
|
384
|
+
if (findRun(root, runId)) throw invalid('This run already exists.')
|
|
385
|
+
saveRun(root, this.runRecord(live, runId, input.text))
|
|
386
|
+
const note = memoryDelta(env, live.input.sessionFolder, { ...live.caller, runId })
|
|
387
|
+
copyToRun(root, live.input.sessionFolder, runId)
|
|
388
|
+
try {
|
|
389
|
+
await live.controller.send(runId, note ? `${note}\n\n${input.text}` : input.text)
|
|
390
|
+
} catch (error) {
|
|
391
|
+
updateRun(root, runId, (run) => {
|
|
392
|
+
if (!isActiveStatus(run.status)) return
|
|
393
|
+
run.status = 'failed'
|
|
394
|
+
run.endedAt = now()
|
|
395
|
+
})
|
|
396
|
+
throw error
|
|
397
|
+
}
|
|
398
|
+
return { runId }
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/** What the person typed into the terminal. */
|
|
402
|
+
input(sessionId: string, data: string): void {
|
|
403
|
+
this.sessions.get(sessionId)?.controller.onInput(data)
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
state(sessionId: string): { ready: boolean; dirty: boolean; active: boolean } {
|
|
407
|
+
return this.live(sessionId).controller.state()
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
has(sessionId: string): boolean {
|
|
411
|
+
return this.sessions.has(sessionId)
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/** The terminal process ended (or never started): its turn, tokens, and written credentials go with it. */
|
|
415
|
+
async dispose(sessionId: string): Promise<void> {
|
|
416
|
+
const live = this.sessions.get(sessionId)
|
|
417
|
+
if (!live) return
|
|
418
|
+
this.sessions.delete(sessionId)
|
|
419
|
+
live.controller.dispose()
|
|
420
|
+
this.socket.revoke(live.token)
|
|
421
|
+
this.options.apps.revokeSession(sessionId)
|
|
422
|
+
await this.options.accounts.terminalEnd(live.account).catch((error) => log('terminal credentials', error instanceof Error ? error.message : String(error)))
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
async close(): Promise<void> {
|
|
426
|
+
await Promise.allSettled([...this.sessions.keys()].map((sessionId) => this.dispose(sessionId)))
|
|
427
|
+
await this.socket.close()
|
|
428
|
+
}
|
|
429
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// Asking the person a question and waiting for the answer: part of every session.
|
|
2
|
+
import type { ActiveRun, QuestionAnswer } from '../sessions/registry'
|
|
3
|
+
import type { ToolSpec } from './types'
|
|
4
|
+
|
|
5
|
+
export const ASK_USER = 'ask_user'
|
|
6
|
+
const MAX_OPTIONS = 6
|
|
7
|
+
|
|
8
|
+
const text = (value: unknown): string => (typeof value === 'string' ? value.trim() : '')
|
|
9
|
+
|
|
10
|
+
/** The answer as the model reads it. */
|
|
11
|
+
export function describeAnswer(answer: QuestionAnswer): string {
|
|
12
|
+
const parts: string[] = []
|
|
13
|
+
if (answer.selected.length) parts.push(`The user chose: ${answer.selected.join(', ')}.`)
|
|
14
|
+
if (answer.text) parts.push(answer.selected.length ? `They added: ${answer.text}` : `The user answered: ${answer.text}`)
|
|
15
|
+
return parts.join(' ') || 'The user did not answer.'
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function askUserTool(run: ActiveRun): ToolSpec {
|
|
19
|
+
return {
|
|
20
|
+
name: ASK_USER,
|
|
21
|
+
label: 'Ask the user',
|
|
22
|
+
description:
|
|
23
|
+
'Ask the user a question and wait for the answer. Offer up to 6 short options when the choices are clear; ' +
|
|
24
|
+
'set multiSelect when several can apply. The user can also type their own answer. Use it when you need a decision, not for small talk.',
|
|
25
|
+
parameters: (Type) =>
|
|
26
|
+
Type.Object({
|
|
27
|
+
question: Type.String({ description: 'The question, in one sentence' }),
|
|
28
|
+
options: Type.Optional(
|
|
29
|
+
Type.Array(Type.Object({ label: Type.String({ description: 'A short choice' }), description: Type.Optional(Type.String({ description: 'One line about what it means' })) }), { maxItems: MAX_OPTIONS })
|
|
30
|
+
),
|
|
31
|
+
multiSelect: Type.Optional(Type.Boolean()),
|
|
32
|
+
allowText: Type.Optional(Type.Boolean({ description: 'Let the user type their own answer (default true)' }))
|
|
33
|
+
}),
|
|
34
|
+
execute: async (input) => {
|
|
35
|
+
const question = text(input.question)
|
|
36
|
+
if (!question) return 'The question is empty.'
|
|
37
|
+
const options = (Array.isArray(input.options) ? input.options : [])
|
|
38
|
+
.map((option) => ({ label: text((option as { label?: unknown }).label), description: text((option as { description?: unknown }).description) }))
|
|
39
|
+
.filter((option) => option.label)
|
|
40
|
+
.slice(0, MAX_OPTIONS)
|
|
41
|
+
.map((option) => (option.description ? option : { label: option.label }))
|
|
42
|
+
return describeAnswer(await run.requestAnswer({ question, options, multiSelect: options.length > 1 && input.multiSelect === true, allowText: options.length === 0 || input.allowText !== false }))
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
// The memory and Wiki tools a session gets, defined once. An SDK session gets them as Pi tools;
|
|
2
|
+
// a terminal session gets them from the `agentx-wiki` command or the bundled MCP server. All of
|
|
3
|
+
// them reach the same calls.
|
|
4
|
+
|
|
5
|
+
export type WikiCall =
|
|
6
|
+
| 'memory.append' | 'memory.replace' | 'memory.rethink'
|
|
7
|
+
| 'recall.search' | 'recall.read'
|
|
8
|
+
| 'wiki.entrypoint' | 'wiki.search' | 'wiki.read' | 'wiki.list' | 'wiki.skill' | 'wiki.memory' | 'wiki.propose'
|
|
9
|
+
|
|
10
|
+
/** A JSON Schema object, kept loose so TypeBox, MCP, and the CLI can all use it. */
|
|
11
|
+
export type JsonSchema = { type: string; properties?: Record<string, unknown>; required?: string[]; [key: string]: unknown }
|
|
12
|
+
|
|
13
|
+
export interface WikiToolDefinition {
|
|
14
|
+
name: string
|
|
15
|
+
call: WikiCall
|
|
16
|
+
label: string
|
|
17
|
+
description: string
|
|
18
|
+
parameters: JsonSchema
|
|
19
|
+
/** The compiled prompt is injected at launch; the tool exists for sessions that need it again. */
|
|
20
|
+
bootstrap?: boolean
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** The kinds of entry a model names. A fact was called `memory` before; that word is still accepted. */
|
|
24
|
+
export const WIKI_ENTRY_KINDS = ['page', 'fact', 'block', 'skill', 'rule', 'prompt'] as const
|
|
25
|
+
export const WIKI_PAGE_TYPES = ['project', 'person', 'concept', 'decision', 'summary', 'question', 'reference'] as const
|
|
26
|
+
export const WIKI_MEMORY_KINDS = ['preference', 'fact', 'decision', 'feedback'] as const
|
|
27
|
+
export const WIKI_LINK_TYPES = ['references', 'depends_on', 'supersedes', 'contradicts', 'produced_by', 'uses'] as const
|
|
28
|
+
|
|
29
|
+
const text = (description: string) => ({ type: 'string', description })
|
|
30
|
+
const block = text('The block\'s label as shown in your memory blocks: user, persona, notes, project/<slug>, or a shared block\'s label')
|
|
31
|
+
|
|
32
|
+
/** Core memory: the blocks that are always in the caller's context. */
|
|
33
|
+
export const MEMORY_TOOLS: WikiToolDefinition[] = [
|
|
34
|
+
{
|
|
35
|
+
name: 'memory_append',
|
|
36
|
+
call: 'memory.append',
|
|
37
|
+
label: 'Add to memory',
|
|
38
|
+
description:
|
|
39
|
+
'Add a line or short passage to the end of one of your memory blocks. Use it when you learn something worth keeping across conversations: about the user, about how you work, or about a project. The change is saved at once and the user can undo it.',
|
|
40
|
+
parameters: { type: 'object', properties: { block, text: text('What to add') }, required: ['block', 'text'] }
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
name: 'memory_replace',
|
|
44
|
+
call: 'memory.replace',
|
|
45
|
+
label: 'Correct memory',
|
|
46
|
+
description: 'Replace one exact passage in a memory block, to correct or remove something that is no longer true. Copy the old passage exactly; pass an empty new passage to delete it.',
|
|
47
|
+
parameters: { type: 'object', properties: { block, old: text('The exact passage to replace'), new: text('What replaces it') }, required: ['block', 'old', 'new'] }
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
name: 'memory_rethink',
|
|
51
|
+
call: 'memory.rethink',
|
|
52
|
+
label: 'Rewrite memory',
|
|
53
|
+
description: 'Rewrite a whole memory block, to condense it when it is close to its limit or has grown repetitive. Keep everything that is still true and useful.',
|
|
54
|
+
parameters: { type: 'object', properties: { block, body: text('The complete new content of the block') }, required: ['block', 'body'] }
|
|
55
|
+
}
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
/** History: what was said and done in earlier conversations and runs. */
|
|
59
|
+
export const RECALL_TOOLS: WikiToolDefinition[] = [
|
|
60
|
+
{
|
|
61
|
+
name: 'history_search',
|
|
62
|
+
call: 'recall.search',
|
|
63
|
+
label: 'Search history',
|
|
64
|
+
description:
|
|
65
|
+
'Search what was said and done before: your earlier conversations (including a chat the user cleared), tool use, and Harness runs. Any word may match; the best matches come first. An agent searches its own conversations unless scope is "all" or a project is given. A Harness session searches Harness runs in its own project.',
|
|
66
|
+
parameters: {
|
|
67
|
+
type: 'object',
|
|
68
|
+
properties: {
|
|
69
|
+
query: text('Words to look for'),
|
|
70
|
+
scope: { type: 'string', enum: ['mine', 'all'], description: 'mine (default): your own conversations. all: every conversation and Harness run.' },
|
|
71
|
+
project: text('Only Harness runs in this project (its name or slug)'),
|
|
72
|
+
since: text('Only from this date on, as YYYY-MM-DD'),
|
|
73
|
+
limit: { type: 'integer', minimum: 1, maximum: 30 }
|
|
74
|
+
},
|
|
75
|
+
required: ['query']
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
name: 'history_read',
|
|
80
|
+
call: 'recall.read',
|
|
81
|
+
label: 'Read a run',
|
|
82
|
+
description: 'Read one earlier run in order: the request, the replies, and the tools used. Pass a run id from history_search.',
|
|
83
|
+
parameters: { type: 'object', properties: { run: text('Run id') }, required: ['run'] }
|
|
84
|
+
}
|
|
85
|
+
]
|
|
86
|
+
|
|
87
|
+
export const WIKI_TOOLS: WikiToolDefinition[] = [
|
|
88
|
+
...MEMORY_TOOLS,
|
|
89
|
+
...RECALL_TOOLS,
|
|
90
|
+
{
|
|
91
|
+
name: 'wiki_entrypoint',
|
|
92
|
+
call: 'wiki.entrypoint',
|
|
93
|
+
label: 'Context pack',
|
|
94
|
+
description: 'Your {product} context pack again: memory blocks, rules, the skills index, the project map, and tool guidance. It is already in your instructions at the start; call this to see its current state.',
|
|
95
|
+
parameters: { type: 'object', properties: {} },
|
|
96
|
+
bootstrap: true
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
name: 'wiki_search',
|
|
100
|
+
call: 'wiki.search',
|
|
101
|
+
label: 'Search Wiki',
|
|
102
|
+
description:
|
|
103
|
+
'Search everything written down in {product}: the Library, every project namespace, facts, skills, rules, and prompts. Results from the current project rank higher. Each hit has a path and an [id@revision] citation.',
|
|
104
|
+
parameters: {
|
|
105
|
+
type: 'object',
|
|
106
|
+
properties: {
|
|
107
|
+
query: text('What to look for'),
|
|
108
|
+
kinds: { type: 'array', items: { type: 'string', enum: [...WIKI_ENTRY_KINDS] }, description: 'Only these kinds of entries' },
|
|
109
|
+
namespace: text('Only this namespace, such as /library or /projects/<name>'),
|
|
110
|
+
limit: { type: 'integer', minimum: 1, maximum: 50 }
|
|
111
|
+
},
|
|
112
|
+
required: ['query']
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
name: 'wiki_read',
|
|
117
|
+
call: 'wiki.read',
|
|
118
|
+
label: 'Read Wiki entry',
|
|
119
|
+
description: 'Read one Wiki page, fact, skill, rule, or prompt in full, with its sources, links, and backlinks. Pass an id (arch, skills/review) or a Wiki path (/projects/agent-x/arch).',
|
|
120
|
+
parameters: { type: 'object', properties: { id: text('Entry id or Wiki path'), revision: text('Optional pinned revision selected for this reply'), offset: { type: 'integer', minimum: 0, description: 'Character offset when reading a pinned revision; use nextOffset to continue.' } }, required: ['id'] }
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
name: 'wiki_list',
|
|
124
|
+
call: 'wiki.list',
|
|
125
|
+
label: 'List Wiki',
|
|
126
|
+
description: 'List Wiki namespaces and pages (optionally in one namespace), or all facts, skills, rules, or prompts.',
|
|
127
|
+
parameters: {
|
|
128
|
+
type: 'object',
|
|
129
|
+
properties: { namespace: text('A namespace such as /library or /projects/<name>'), kind: { type: 'string', enum: [...WIKI_ENTRY_KINDS] } }
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
name: 'skill_load',
|
|
134
|
+
call: 'wiki.skill',
|
|
135
|
+
label: 'Load skill',
|
|
136
|
+
description: "Load a skill's instructions by name, or list every skill (and which are active) when no name is given. Follow a skill's steps when the task matches it.",
|
|
137
|
+
parameters: { type: 'object', properties: { name: text('Skill name or id, such as review or skills/review') } }
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
name: 'memory_facts',
|
|
141
|
+
call: 'wiki.memory',
|
|
142
|
+
label: 'Recall facts',
|
|
143
|
+
description: 'Recall facts: short statements, preferences, decisions, and feedback. Without a query it returns the facts that apply to the current project.',
|
|
144
|
+
parameters: { type: 'object', properties: { query: text('What to recall'), namespace: text('/ or a project namespace') } }
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
name: 'wiki_propose',
|
|
148
|
+
call: 'wiki.propose',
|
|
149
|
+
label: 'Propose Wiki update',
|
|
150
|
+
description:
|
|
151
|
+
'Record durable knowledge. kind "fact" for a short fact, preference, decision, or piece of feedback, "page" to update an existing page or add one about an entity or concept (not about one run), or "skill"/"rule"/"prompt" to suggest a change to those. ' +
|
|
152
|
+
'Cite evidence in sourceRefs: {"kind":"run","ref":"current","excerpt":"…"} with a passage copied exactly from this conversation or run (what the user wrote, or what you already said), a source id, or a URL. ' +
|
|
153
|
+
'A run reference without an excerpt found in the run is not evidence. Cited pages and facts apply right away (with undo); uncited or low-confidence ones, and every skill, rule, or prompt change, wait in the Wiki Inbox for the user. Never include secrets.',
|
|
154
|
+
parameters: {
|
|
155
|
+
type: 'object',
|
|
156
|
+
properties: {
|
|
157
|
+
kind: { type: 'string', enum: [...WIKI_ENTRY_KINDS] },
|
|
158
|
+
title: text('Title (or the skill/rule/prompt name)'),
|
|
159
|
+
body: text('Markdown body. For a fact, a few lines.'),
|
|
160
|
+
id: text('Existing id to update'),
|
|
161
|
+
type: text(`Page type (${WIKI_PAGE_TYPES.join(', ')}) or what sort of fact (${WIKI_MEMORY_KINDS.join(', ')})`),
|
|
162
|
+
namespace: text('Where it belongs: /library, /projects/<name>, or / for a fact that applies everywhere. Defaults to the current project.'),
|
|
163
|
+
confidence: { type: 'string', enum: ['high', 'medium', 'low'] },
|
|
164
|
+
description: text('One line for a skill, rule, or prompt'),
|
|
165
|
+
supersedes: text('Id of a fact this one replaces'),
|
|
166
|
+
volatile: { type: 'boolean', description: 'For a fact: true when it describes current state that will change (an open issue list, who is on call)' },
|
|
167
|
+
reason: text('Why this change is needed (skills, rules, prompts)'),
|
|
168
|
+
sourceRefs: {
|
|
169
|
+
type: 'array',
|
|
170
|
+
items: { type: 'object', properties: { kind: { type: 'string', enum: ['run', 'source', 'url'] }, ref: { type: 'string' }, excerpt: { type: 'string', description: 'For a run or source: a passage copied exactly from it' } }, required: ['kind'] }
|
|
171
|
+
},
|
|
172
|
+
links: {
|
|
173
|
+
type: 'array',
|
|
174
|
+
items: { type: 'object', properties: { toId: { type: 'string' }, type: { type: 'string', enum: [...WIKI_LINK_TYPES] } }, required: ['toId', 'type'] }
|
|
175
|
+
}
|
|
176
|
+
},
|
|
177
|
+
required: ['kind', 'title', 'body']
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
]
|
|
181
|
+
|
|
182
|
+
/** Tools agents get when the context pack is already in their instructions. */
|
|
183
|
+
/** A tool's description with the application's name in it. */
|
|
184
|
+
export const describeTool = (tool: WikiToolDefinition, productName: string): string => tool.description.replaceAll('{product}', productName)
|
|
185
|
+
|
|
186
|
+
export const WIKI_AGENT_TOOLS = WIKI_TOOLS.filter((tool) => !tool.bootstrap)
|
|
187
|
+
export const WIKI_TOOL_NAMES = WIKI_AGENT_TOOLS.map((tool) => tool.name)
|