index.ts 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533
  1. /**
  2. * Model-facing Cordis runtime/package inspection, define, run, stop, and remove tools.
  3. * @module @deepseek-ai/dsh-tool-cordis
  4. */
  5. import type { Context } from '@deepseek-ai/cordis'
  6. import type { Agent, PreStepDecision } from '@deepseek-ai/dsh-agent'
  7. import {
  8. CordisDynamicPackageId, CordisDynamicPluginId,
  9. } from '@deepseek-ai/dsh-cordis-host-runner'
  10. import type { DynamicCordisReference } from '@deepseek-ai/dsh-cordis-host-runner'
  11. import { createUserMessage } from '@deepseek-ai/dsh-llm'
  12. import type { JsonValue } from '@deepseek-ai/dsh-util-values'
  13. import type { UserMessage } from '@deepseek-ai/dsh-session'
  14. import { defineTool } from '@deepseek-ai/dsh-tools'
  15. import type { ToolExecution } from '@deepseek-ai/dsh-tools'
  16. import { missingServices, providedServices } from './inspect.ts'
  17. import {
  18. presentDefineCall, presentInspectListCall, presentInspectQueryCall, presentInspectSelfCall, presentRunCall,
  19. presentStopCall, presentUndefineCall,
  20. } from './present.ts'
  21. import { CORDIS_SYSTEM_PROMPT } from './prompt.ts'
  22. import { hostInspectProviders } from './providers.ts'
  23. export const name = 'tool-cordis'
  24. export const inject = ['tools', 'systemPrompt', 'dynamicCordisRunner', 'cordisInspect']
  25. function requireAgent(exec: ToolExecution): Agent {
  26. if (exec.agent === undefined) throw new Error('Cordis dynamic tools require an Agent-backed session')
  27. return exec.agent
  28. }
  29. /** Register the Cordis tools and explicit `@pluginId` context injection. */
  30. export function apply(ctx: Context): void {
  31. ctx.systemPrompt.section({
  32. name: 'tool:cordis',
  33. order: ctx.systemPrompt.getSectionOrder('TOOL_CORDIS'),
  34. text: CORDIS_SYSTEM_PROMPT,
  35. })
  36. for (const provider of hostInspectProviders(ctx)) {
  37. ctx.effect(() => ctx.cordisInspect.register(provider), `tool-cordis: inspect ${provider.manifest.id}`)
  38. }
  39. ctx.tools.register(defineTool({
  40. name: 'cordis_inspect_list',
  41. description:
  42. 'List every Cordis Inspect Provider currently known to the Host, including local Host Providers and the latest '
  43. + 'manifests synchronized from the Client. Each entry includes its platform, purpose, read-only methods, and '
  44. + 'input/output schemas. Call this Tool before creating or modifying a Package, then select the provider and '
  45. + 'method for cordis_inspect_query from its result. Do not guess names or treat an Inspect method as a business '
  46. + 'Service that Plugin code can call.',
  47. parameters: {},
  48. output: {
  49. schema: { type: 'json' },
  50. render: (_args, value) => [{ type: 'text', text: JSON.stringify(value, null, 2) }],
  51. },
  52. execute(_args, _exec): Promise<JsonValue> {
  53. return Promise.resolve({ providers: ctx.cordisInspect.list() } as unknown as JsonValue)
  54. },
  55. presentCall: presentInspectListCall,
  56. }))
  57. ctx.tools.register(defineTool({
  58. name: 'cordis_inspect_query',
  59. description:
  60. 'Run a read-only query explicitly declared by an Inspect Provider. platform, provider, and method must come '
  61. + 'from cordis_inspect_list, and input must satisfy that method\'s schema. Use this Tool before cordis_define '
  62. + 'to read exact Service methods, Event modes, Builtin signatures, Tool schemas, theme tokens, or live Slot '
  63. + 'trees and props. Host queries run locally. A Client query waits for the first valid page response and '
  64. + 'remains pending until a page answers or the Tool is cancelled. This Tool cannot invoke business Service '
  65. + 'methods or modify the runtime. For Service.listService and Event.listEvents, query without input to navigate '
  66. + 'the compact signature directory, then query the exact service or event for its structured contract and '
  67. + 'referenced types. For Slots.listSubTree, query without root to navigate the compact tree, then query the '
  68. + 'exact root for its complete registration contract and props.',
  69. parameters: {
  70. platform: { type: 'string', required: true, enum: ['host', 'client'], description: 'Runtime platform that owns the Provider.' },
  71. provider: { type: 'string', required: true, description: 'Exact Provider ID returned by cordis_inspect_list.' },
  72. method: { type: 'string', required: true, description: 'Exact method name declared by the Provider manifest.' },
  73. input: { type: 'json', description: 'Optional query input; it must satisfy the method input schema.' },
  74. },
  75. output: {
  76. schema: { type: 'json' },
  77. render: (_args, value) => [{ type: 'text', text: JSON.stringify(value, null, 2) }],
  78. },
  79. async execute(args, exec) {
  80. const data = await ctx.cordisInspect.query(
  81. args.platform,
  82. args.provider,
  83. args.method,
  84. args.input,
  85. requireAgent(exec),
  86. exec.signal,
  87. )
  88. return { platform: args.platform, provider: args.provider, method: args.method, data }
  89. },
  90. presentCall: presentInspectQueryCall,
  91. }))
  92. ctx.tools.register(defineTool({
  93. name: 'cordis_inspect_self',
  94. description:
  95. 'Inspect dynamic Cordis objects owned by the current Session at increasing levels of detail. With no IDs, '
  96. + 'list only Plugin summaries. With pluginId alone, return version pointers, the latest Run, and every Package '
  97. + 'summary. Only pluginId plus packageId returns that immutable Package\'s Host/Client source and runtime '
  98. + 'diagnostics. packageId cannot be supplied alone. Query an exact Package before handling @pluginId, repairing '
  99. + 'an asynchronous failure, or defining an updated version. This Tool is read-only: it neither executes code '
  100. + 'nor changes version pointers.',
  101. parameters: {
  102. pluginId: { type: 'string', description: 'Stable Plugin ID returned by cordis_define or injected by @pluginId; omit it to list every current Plugin.' },
  103. packageId: { type: 'string', description: 'Exact immutable Package ID owned by pluginId; when specified, source and diagnostics are returned.' },
  104. },
  105. output: {
  106. schema: { type: 'json' },
  107. render: (_args, value) => [{ type: 'text', text: JSON.stringify(value, null, 2) }],
  108. },
  109. execute(args, exec): Promise<JsonValue> {
  110. const agent = requireAgent(exec)
  111. if (args.packageId !== undefined && args.pluginId === undefined) {
  112. throw new Error('cordis_inspect_self packageId requires pluginId')
  113. }
  114. if (args.pluginId === undefined) {
  115. return Promise.resolve({
  116. mode: 'plugins',
  117. plugins: ctx.dynamicCordisRunner.listPlugins(agent).map(reference => selfSummary(reference)),
  118. } as unknown as JsonValue)
  119. }
  120. const pluginId = CordisDynamicPluginId(args.pluginId)
  121. if (args.packageId === undefined) {
  122. const plugin = ctx.dynamicCordisRunner.inspectPlugin(agent, pluginId)
  123. return Promise.resolve({
  124. mode: 'plugin',
  125. ...selfSummary(plugin),
  126. packages: plugin.packages.map(pkg => ({
  127. ...pkg,
  128. packageId: String(pkg.packageId),
  129. isCurrent: pkg.packageId === plugin.currentPackageId,
  130. isNext: pkg.packageId === plugin.nextPackageId,
  131. })),
  132. } as unknown as JsonValue)
  133. }
  134. return Promise.resolve(inspectSelfPackage(
  135. ctx,
  136. agent,
  137. pluginId,
  138. CordisDynamicPackageId(args.packageId),
  139. ) as unknown as JsonValue)
  140. },
  141. presentCall: presentInspectSelfCall,
  142. }))
  143. ctx.tools.register(defineTool({
  144. name: 'cordis_define',
  145. description:
  146. 'Define an immutable Cordis Package. For a new Plugin, use kind:"new" and provide only a semantic prefix of '
  147. + '3–6 lowercase English letters; the Host returns the final pluginId and packageId. To modify an existing '
  148. + 'Plugin, use kind:"existing" with its exact pluginId to append a Package without overwriting older versions. '
  149. + 'Provide at least one of code.host and code.client. Each value is a plain JavaScript function body that returns '
  150. + 'a Cordis Plugin; no TypeScript, JSX, or import transformation occurs. Query Inspect before depending on a '
  151. + 'Service, Event, Builtin, Slot, or token. Define only validates parameters and syntax and records source: it '
  152. + 'does not request approval, execute apply, or change currentPackageId. On success, call cordis_run with the '
  153. + 'returned IDs.',
  154. parameters: {
  155. plugin: {
  156. required: true,
  157. oneOf: [
  158. {
  159. type: 'object',
  160. additionalProperties: false,
  161. properties: {
  162. kind: { type: 'string', const: 'new', required: true },
  163. idPrefix: {
  164. type: 'string',
  165. required: true,
  166. description: 'Suggested semantic prefix of 3–6 lowercase English letters; the Host adds a unique numeric suffix.',
  167. },
  168. },
  169. },
  170. {
  171. type: 'object',
  172. additionalProperties: false,
  173. properties: {
  174. kind: { type: 'string', const: 'existing', required: true },
  175. pluginId: { type: 'string', required: true, description: 'Exact ID of an existing Plugin; the new Package is appended to that instance.' },
  176. },
  177. },
  178. ],
  179. },
  180. name: { type: 'string', required: true, description: 'Short, readable Package name.' },
  181. purpose: { type: 'string', required: true, description: 'One-sentence, user-facing description of the Package purpose.' },
  182. code: {
  183. type: 'object',
  184. additionalProperties: false,
  185. required: true,
  186. properties: {
  187. host: { type: 'string', description: 'Plain JavaScript function body that returns the Host-half Cordis Plugin.' },
  188. client: { type: 'string', description: 'Plain JavaScript function body that returns the browser Client-half Cordis Plugin.' },
  189. },
  190. },
  191. },
  192. output: {
  193. schema: {
  194. type: 'object',
  195. additionalProperties: false,
  196. properties: {
  197. pluginId: { type: 'string', required: true },
  198. packageId: { type: 'string', required: true },
  199. name: { type: 'string', required: true },
  200. purpose: { type: 'string', required: true },
  201. hasHostHalf: { type: 'boolean', required: true },
  202. hasClientHalf: { type: 'boolean', required: true },
  203. },
  204. },
  205. render: (_args, value) => [{
  206. type: 'text',
  207. text: `Defined ${value.pluginId}/${value.packageId} (${value.name}); it is not running yet. `
  208. + 'Use cordis_run to activate this Package.',
  209. }],
  210. presentationMeta: (_args, value) => ({ pluginId: value.pluginId, packageId: value.packageId }),
  211. },
  212. execute(args, exec) {
  213. const plugin = args.plugin.kind === 'new'
  214. ? { kind: 'new' as const, idPrefix: args.plugin.idPrefix }
  215. : { kind: 'existing' as const, pluginId: CordisDynamicPluginId(args.plugin.pluginId) }
  216. const receipt = ctx.dynamicCordisRunner.define({
  217. sessionId: requireAgent(exec).id,
  218. plugin,
  219. name: args.name,
  220. purpose: args.purpose,
  221. code: {
  222. ...args.code.host === undefined ? {} : { host: args.code.host },
  223. ...args.code.client === undefined ? {} : { client: args.code.client },
  224. },
  225. })
  226. return Promise.resolve({
  227. ...receipt,
  228. pluginId: String(receipt.pluginId),
  229. packageId: String(receipt.packageId),
  230. })
  231. },
  232. presentCall: presentDefineCall,
  233. }))
  234. ctx.tools.register(defineTool({
  235. name: 'cordis_run',
  236. description:
  237. 'Activate one exact Package of a dynamic Plugin. Use mode:"run" for the first activation, restarting '
  238. + 'currentPackageId, or rollback. When current exists, use mode:"update" to switch to a different Package, '
  239. + 'even if the Plugin is currently stopped. An unauthorized Client Package creates an approval request and '
  240. + 'returns awaiting-approval; an authorized Package returns starting and continues asynchronously in the '
  241. + 'browser. Neither result waits for the final outcome inside the Tool. currentPackageId changes only after '
  242. + 'complete success; on failure, the old current and target next remain. Asynchronous success, rejection, or '
  243. + 'technical failure is reported through state and steering. After a technical failure, read diagnostics with '
  244. + 'cordis_inspect_self, correct the same Plugin, and retry autonomously. Do not request approval again after '
  245. + 'the user rejects it.',
  246. parameters: {
  247. pluginId: { type: 'string', required: true, description: 'Stable Plugin ID returned by cordis_define.' },
  248. packageId: { type: 'string', required: true, description: 'Exact immutable Package ID to activate under that Plugin.' },
  249. mode: {
  250. type: 'string',
  251. required: true,
  252. enum: ['run', 'update'],
  253. description: 'Use run for the first activation, restarting current, or rollback; use update to switch from current to a different Package.',
  254. },
  255. },
  256. output: {
  257. schema: { type: 'json' },
  258. render: (_args, value) => {
  259. const result = requireJsonObject(value)
  260. const pluginId = requireJsonString(result, 'pluginId')
  261. const packageId = requireJsonString(result, 'packageId')
  262. const pluginRunId = requireJsonString(result, 'pluginRunId')
  263. return [{
  264. type: 'text',
  265. text: result.status === 'awaiting-approval'
  266. ? `${pluginId}/${packageId} is awaiting user approval (${pluginRunId}).`
  267. : result.status === 'starting'
  268. ? `${pluginId}/${packageId} is starting asynchronously (${pluginRunId}).`
  269. : `${pluginId}/${packageId} is running (${pluginRunId}).`,
  270. }]
  271. },
  272. presentationMeta: (_args, value) => {
  273. const result = requireJsonObject(value)
  274. return {
  275. pluginId: requireJsonString(result, 'pluginId'),
  276. packageId: requireJsonString(result, 'packageId'),
  277. pluginRunId: requireJsonString(result, 'pluginRunId'),
  278. }
  279. },
  280. },
  281. async execute(args, exec) {
  282. const agent = requireAgent(exec)
  283. const pluginId = CordisDynamicPluginId(args.pluginId)
  284. const packageId = CordisDynamicPackageId(args.packageId)
  285. const receipt = await ctx.dynamicCordisRunner.run(agent, pluginId, packageId, args.mode, exec.signal)
  286. if (!receipt.ok) throw new Error(receipt.message)
  287. if (receipt.status !== 'running') {
  288. return {
  289. status: receipt.status,
  290. pluginId: args.pluginId,
  291. packageId: args.packageId,
  292. pluginRunId: String(receipt.pluginRunId),
  293. mode: receipt.mode,
  294. ...receipt.currentPackageId === undefined ? {} : { currentPackageId: String(receipt.currentPackageId) },
  295. nextPackageId: String(receipt.nextPackageId),
  296. }
  297. }
  298. const row = ctx.dynamicCordisRunner.snapshot(agent).find(candidate => candidate.pluginId === pluginId)
  299. const fiber = row?.activeRun?.pluginRunId === receipt.pluginRunId ? row.activeRun.fiber : undefined
  300. return {
  301. status: 'running',
  302. pluginId: args.pluginId,
  303. packageId: args.packageId,
  304. pluginRunId: String(receipt.pluginRunId),
  305. currentPackageId: String(receipt.currentPackageId),
  306. ...receipt.nextPackageId === undefined ? {} : { nextPackageId: String(receipt.nextPackageId) },
  307. host: {
  308. status: fiber === undefined ? 'absent' : missingServices(ctx, fiber).length === 0 ? 'running' : 'waiting',
  309. provides: fiber === undefined ? [] : providedServices(ctx, fiber),
  310. waitingFor: fiber === undefined ? [] : missingServices(ctx, fiber),
  311. },
  312. client: {
  313. status: receipt.clientWaitingFor === undefined
  314. ? 'absent'
  315. : receipt.clientWaitingFor.length === 0 ? 'running' : 'waiting',
  316. waitingFor: [...(receipt.clientWaitingFor ?? [])],
  317. },
  318. }
  319. },
  320. presentCall: presentRunCall,
  321. }))
  322. ctx.tools.register(defineTool({
  323. name: 'cordis_stop',
  324. description:
  325. 'Stop the current Run of a dynamic Plugin and cancel unfinished approval or activation requests. Retain the '
  326. + 'Plugin, every immutable Package, grants, currentPackageId, and nextPackageId so it can later run or update '
  327. + 'directly. Stopping an already stopped Plugin succeeds idempotently. Use this Tool to disable effects '
  328. + 'temporarily; use cordis_undefine for permanent removal.',
  329. parameters: {
  330. pluginId: { type: 'string', required: true, description: 'Stable dynamic Plugin ID to stop.' },
  331. },
  332. output: {
  333. schema: { type: 'object', additionalProperties: false, properties: { pluginId: { type: 'string', required: true } } },
  334. render: (_args, value) => [{ type: 'text', text: `Dynamic Plugin ${value.pluginId} is stopped; its definition and versions remain.` }],
  335. },
  336. async execute(args, exec) {
  337. const receipt = await ctx.dynamicCordisRunner.stop(requireAgent(exec), CordisDynamicPluginId(args.pluginId))
  338. if (!receipt.ok && receipt.reason !== 'not-running') throw new Error(receipt.message)
  339. return { pluginId: args.pluginId }
  340. },
  341. presentCall: presentStopCall,
  342. }))
  343. ctx.tools.register(defineTool({
  344. name: 'cordis_undefine',
  345. description:
  346. 'Permanently remove a dynamic Plugin owned by the current Session. If it is running or awaiting approval, '
  347. + 'first stop it and cancel the request, then delete every Package, grant, and version pointer. After this '
  348. + 'returns, its pluginId, packageIds, @ reference, and Package business views are invalid; historical cards '
  349. + 'retain only a "Plugin removed" record. Do not call this Tool when versions must remain available for restart '
  350. + 'or rollback; use cordis_stop instead.',
  351. parameters: {
  352. pluginId: { type: 'string', required: true, description: 'Stable dynamic Plugin ID to remove permanently.' },
  353. },
  354. output: {
  355. schema: {
  356. type: 'object',
  357. additionalProperties: false,
  358. properties: {
  359. pluginId: { type: 'string', required: true },
  360. wasRunning: { type: 'boolean', required: true },
  361. },
  362. },
  363. render: (_args, value) => [{ type: 'text', text: `Removed dynamic Plugin ${value.pluginId} and all of its Packages.` }],
  364. },
  365. async execute(args, exec) {
  366. const receipt = await ctx.dynamicCordisRunner.undefine(requireAgent(exec), CordisDynamicPluginId(args.pluginId))
  367. if (!receipt.ok) throw new Error(receipt.message)
  368. return { pluginId: args.pluginId, wasRunning: receipt.wasRunning }
  369. },
  370. presentCall: presentUndefineCall,
  371. }))
  372. ctx.on('agent/pre-step', async ({ agent, messages, signal }, next): Promise<PreStepDecision> => {
  373. const decision = await next()
  374. if (decision.kind === 'reject') return decision
  375. const ids = referencedPluginIds(messages)
  376. if (ids.length === 0) return decision
  377. signal.throwIfAborted()
  378. const contexts = ids.map((id) => {
  379. const reference = ctx.dynamicCordisRunner.reference(agent, CordisDynamicPluginId(id))
  380. return createUserMessage({
  381. content: [{
  382. type: 'text',
  383. text: reference === undefined ? renderUnavailableReference(id) : renderReference(reference),
  384. }],
  385. source: { kind: 'plugin', plugin: name, form: 'instructions' },
  386. })
  387. })
  388. return { ...decision, messages: [...decision.messages, ...contexts] }
  389. })
  390. }
  391. function requireJsonObject(value: JsonValue): Record<string, JsonValue> {
  392. if (typeof value !== 'object' || value === null || Array.isArray(value)) {
  393. throw new Error('expected a JSON object')
  394. }
  395. return value
  396. }
  397. function requireJsonString(value: Record<string, JsonValue>, key: string): string {
  398. const field = value[key]
  399. if (typeof field !== 'string') throw new Error(`expected JSON string field "${key}"`)
  400. return field
  401. }
  402. type SelfState = 'defined' | 'awaiting-approval' | 'client-pending' | 'stopped' | 'running' | 'waiting' | 'failed'
  403. function selfSummary(reference: DynamicCordisReference & { packages?: readonly unknown[] }): Record<string, JsonValue> {
  404. const latest = reference.latestRun
  405. const state = selfState(reference)
  406. return {
  407. pluginId: String(reference.pluginId),
  408. name: reference.name,
  409. packageCount: reference.packages?.length ?? 1,
  410. state,
  411. ...reference.currentPackageId === undefined ? {} : { currentPackageId: String(reference.currentPackageId) },
  412. ...reference.nextPackageId === undefined ? {} : { nextPackageId: String(reference.nextPackageId) },
  413. ...reference.activeRun === undefined ? {} : {
  414. activeRun: {
  415. pluginRunId: String(reference.activeRun.pluginRunId),
  416. packageId: String(reference.activeRun.packageId),
  417. },
  418. },
  419. ...latest?.status !== 'awaiting-approval' ? {} : {
  420. pendingApproval: {
  421. pluginRunId: String(latest.pluginRunId),
  422. packageId: String(latest.packageId),
  423. mode: latest.mode,
  424. },
  425. },
  426. }
  427. }
  428. function selfState(reference: DynamicCordisReference): SelfState {
  429. const status = reference.latestRun?.status
  430. if (status === 'awaiting-approval') return 'awaiting-approval'
  431. if (status === 'client-pending' || status === 'starting-host') return 'client-pending'
  432. if (status === 'failed' || status === 'rejected' || status === 'cancelled') return 'failed'
  433. if (status === 'waiting') return 'waiting'
  434. if (status === 'running') return 'running'
  435. if (reference.activeRun !== undefined) return 'running'
  436. return reference.currentPackageId === undefined ? 'defined' : 'stopped'
  437. }
  438. function inspectSelfPackage(
  439. ctx: Context,
  440. agent: Agent,
  441. pluginId: ReturnType<typeof CordisDynamicPluginId>,
  442. packageId: ReturnType<typeof CordisDynamicPackageId>,
  443. ): Record<string, JsonValue> {
  444. const inspected = ctx.dynamicCordisRunner.inspectPackage(agent, pluginId, packageId)
  445. const row = ctx.dynamicCordisRunner.snapshot(agent).find(candidate => candidate.pluginId === pluginId)
  446. const pkg = row?.packages.find(candidate => candidate.packageId === packageId)
  447. const active = row?.activeRun?.packageId === packageId ? row.activeRun : undefined
  448. const latest = inspected.latestRun?.packageId === packageId ? inspected.latestRun : undefined
  449. const hostWaiting = active?.fiber === undefined ? [...(latest?.host.waitingFor ?? [])] : missingServices(ctx, active.fiber)
  450. const hostStatus = pkg?.hasHostHalf !== true
  451. ? 'absent'
  452. : latest?.host.status ?? (active === undefined ? 'stopped' : hostWaiting.length === 0 ? 'running' : 'waiting')
  453. const clientStatus = pkg?.hasClientHalf !== true
  454. ? 'absent'
  455. : latest?.client.status ?? 'stopped'
  456. return {
  457. mode: 'package',
  458. plugin: selfSummary(inspected),
  459. packageId: String(packageId),
  460. name: inspected.name,
  461. purpose: inspected.purpose,
  462. code: inspected.code,
  463. runtime: {
  464. state: selfState(inspected),
  465. host: {
  466. status: hostStatus,
  467. provides: active?.fiber === undefined ? [] : providedServices(ctx, active.fiber),
  468. waitingFor: hostWaiting,
  469. handlers: active?.handlers ?? [],
  470. ...latest?.host.error === undefined ? {} : { error: latest.host.error },
  471. },
  472. client: {
  473. status: clientStatus,
  474. waitingFor: [...(latest?.client.waitingFor ?? [])],
  475. ...latest?.client.error === undefined ? {} : { error: latest.client.error },
  476. ...active?.renderFailure === undefined ? {} : { renderFailure: active.renderFailure },
  477. },
  478. },
  479. } as unknown as Record<string, JsonValue>
  480. }
  481. function referencedPluginIds(messages: readonly UserMessage[]): string[] {
  482. const found = new Set<string>()
  483. const pattern = /(?:^|\s)@([a-z]{3,6}-\d+)(?=\s|$)/g
  484. for (const message of messages) {
  485. if (message.source.kind !== 'user') continue
  486. const text = message.content.flatMap(block => block.type === 'text' ? [block.text] : []).join('\n')
  487. for (const match of text.matchAll(pattern)) if (match[1] !== undefined) found.add(match[1])
  488. }
  489. return [...found]
  490. }
  491. function renderReference(reference: ReturnType<Context['dynamicCordisRunner']['reference']> & {}): string {
  492. const mode = reference.currentPackageId === undefined ? 'run' : 'update'
  493. return [
  494. '<cordis_dynamic_plugin_context>',
  495. JSON.stringify(reference, null, 2),
  496. '',
  497. `The user explicitly referenced @${reference.pluginId}. Use Package ${reference.packageId} as the base for this modification.`,
  498. `Before modifying it, call cordis_inspect_self with pluginId="${reference.pluginId}" and packageId="${reference.packageId}" to read the exact metadata and source.`,
  499. `Use cordis_define with plugin.kind="existing" and the original pluginId="${reference.pluginId}" to append an immutable Package.`,
  500. `Do not create a new Plugin for this request. After cordis_define succeeds, call cordis_run mode="${mode}" with the returned packageId.`,
  501. '</cordis_dynamic_plugin_context>',
  502. ].join('\n')
  503. }
  504. function renderUnavailableReference(id: string): string {
  505. return [
  506. '<cordis_dynamic_plugin_context>',
  507. `The user explicitly referenced @${id}, but this Plugin is unavailable in the current Session.`,
  508. 'It may have been removed, belong to another Session, or have been lost when the DSH process restarted.',
  509. 'Do not claim that it was updated or silently create a replacement Plugin. Tell the user that the reference is currently unavailable.',
  510. '</cordis_dynamic_plugin_context>',
  511. ].join('\n')
  512. }