feat(tool-cordis): flatten the inspect plugins section to a capability list

The tree SHAPE was the wrong surface for the model: what it needs from
cordis_inspect is what services, plugins, and capabilities are loaded, not the
fiber hierarchy. The plugins section is now a flat name + lifecycle-state list
from ctx.registry (deterministically sorted, one line per instance); the ASCII
tree renderer, the parent→child rebuild, and the dyn-id tree annotation are
deleted — dynamic mounts keep their own richer dynamic section (id, state,
provides, waits). Net -49 lines; RFC and READMEs state the flat-list contract.
This commit is contained in:
imccyu
2026-07-08 21:01:49 +08:00
parent a500c791f7
commit ea66641b84
8 changed files with 47 additions and 96 deletions

View File

@@ -1,7 +1,7 @@
/**
* Runtime mirror of the cordis `FiberState` const enum plus human-readable
* labels, shared by the mount lifecycle (state reporting) and the inspect
* renderers (tree and mount-table labels).
* renderers (plugin-list and mount-table labels).
*
* Cordis exposes `FiberState` as a `const enum`: there is no runtime object for
* Node's type-stripping runner to import, so the members are mirrored here as

View File

@@ -2,8 +2,8 @@
* The self-referential cordis toolset: three model-facing tools that let the
* agent inspect and MODIFY the live cordis runtime it is running inside.
*
* - `cordis_inspect` — read-only: provided services, the plugin fiber tree
* (rendered as an ASCII tree), registered tools, the dynamic mounts, and the
* - `cordis_inspect` — read-only: provided services, the flat plugin list
* with lifecycle states, registered tools, the dynamic mounts, and the
* catalog-backed `api` / `events` references.
* - `cordis_mount` — evaluate model-written code in a `node:vm` sandbox; the
* code returns a cordis plugin, which is mounted as a child of a dedicated
@@ -14,8 +14,8 @@
* `harness.registerTool`, services via `ctx.provide`) is an effect on the
* dynamic fiber, so unmounting — or disposing this plugin itself (HMR) — cleans
* it all up through the ordinary cordis lifecycle. The group fiber exists
* exactly so the dynamic mounts form ONE subtree: visible as a unit in the
* inspect tree and disposed as a unit with this plugin. Design home:
* exactly so the dynamic mounts form ONE subtree, disposed as a unit with
* this plugin. Design home:
* docs/rfc/implemented/feature/2026-07-08-self-referential-cordis-toolset.md.
*
* The vm sandbox guards against ACCIDENTAL global pollution only — it is not a
@@ -31,12 +31,12 @@
* @module @deepseek-ai/dsh-tool-cordis
*/
import type { Context, Fiber } from 'cordis'
import type { Context } from 'cordis'
import z from 'schemastery'
import { defineTool } from '@deepseek-ai/dsh-tools'
import { STATE_LABELS } from './fiber-state.ts'
import { isPlugin, pluginName } from './guard.ts'
import { describeApi, describeDynamic, describeEvents, describePluginTree, describeServices, describeTools } from './inspect.ts'
import { describeApi, describeDynamic, describeEvents, describePlugins, describeServices, describeTools } from './inspect.ts'
import { missingServices, mountDynamic } from './mount.ts'
import type { DynamicMount } from './mount.ts'
import { presentInspectCall, presentMountCall, presentUnmountCall } from './present.ts'
@@ -79,21 +79,12 @@ export function apply(ctx: Context, config: Config): void {
const mounts = new Map<string, DynamicMount>()
let nextId = 1
/** The dynamic-mount id for a fiber, when that fiber is a tracked mount. */
function mountIdOf(fiber: Fiber): string | undefined {
for (const [id, mount] of mounts) {
if (mount.fiber === fiber) return id
}
return undefined
}
ctx.tools.register(defineTool({
name: 'cordis_inspect',
description:
'Inspect the live cordis runtime that is running THIS agent. Read-only. '
+ 'Sections: `services` (every provided ctx service and the plugin fiber that owns it), '
+ '`plugins` (the whole plugin fiber tree with lifecycle states, as an ASCII tree — '
+ 'dynamic mounts appear under the `cordis-dynamic` group with their ids), '
+ '`plugins` (a flat list of the loaded plugins with their lifecycle states), '
+ '`tools` (the model-facing tools currently registered, i.e. what you can call), '
+ '`dynamic` (plugins you mounted via cordis_mount: id, name, state, provided services, awaited services), '
+ '`api` (method signatures AND argument/return type shapes for every LIVE service — read this before writing plugin code that calls a service), '
@@ -109,7 +100,7 @@ export function apply(ctx: Context, config: Config): void {
execute(args): Promise<{ type: 'text'; text: string }[]> {
const sections: [heading: string, body: () => string[]][] = [
['services', () => describeServices(ctx)],
['plugins', () => describePluginTree(ctx, mountIdOf)],
['plugins', () => describePlugins(ctx)],
['tools', () => describeTools(ctx)],
['dynamic', () => describeDynamic(ctx, mounts)],
['api', () => describeApi(ctx)],

View File

@@ -1,6 +1,6 @@
/**
* Read-only renderers over the live runtime for `cordis_inspect`: the service
* list, the plugin fiber tree (ASCII), the registered tools, the dynamic-mount
* list, the flat plugin list, the registered tools, the dynamic-mount
* table (with per-mount provides/waits), and the catalog-backed `api` /
* `events` sections. Every renderer is a pure function of the runtime handles
* it receives — no session state, no clock — so inspect output is exactly the
@@ -57,56 +57,22 @@ export function describeServices(ctx: Context): string[] {
return lines.length > 0 ? lines : ['(no services provided)']
}
/** The tree node shape {@link renderTree} draws: one line per fiber, children indented. */
interface TreeNode {
label: string
children: TreeNode[]
}
/** Render a node list as an ASCII tree (`├─`/`└─` box drawing). */
function renderTree(nodes: TreeNode[], prefix = ''): string[] {
return nodes.flatMap((node, index) => {
const last = index === nodes.length - 1
const line = `${prefix}${last ? '└─' : '├─'} ${node.label}`
const childPrefix = `${prefix}${last ? ' ' : '│ '}`
return [line, ...renderTree(node.children, childPrefix)]
})
}
/**
* The `plugins` section: every fiber the registry knows, rebuilt into the
* parent→child tree from each fiber's mounting context and rendered as an
* ASCII tree with lifecycle states. Fibers whose parent fiber is outside the
* registry (i.e. mounted on the root context) become roots.
* @param ctx - the runtime whose registry is walked.
* @param mountIdOf - resolves a fiber to its dynamic-mount id, so mounts render as `dyn-<n>: name`.
* @returns the tree lines, starting at the synthetic `root` line.
* The `plugins` section: a flat list of every fiber the registry knows, one
* line per fiber with its lifecycle state, sorted by plugin name (a plugin
* mounted more than once repeats — one line per instance). Dynamic mounts are
* listed like any other plugin; their ids live in the `dynamic` section.
* @param ctx - the runtime whose registry is enumerated.
* @returns one line per loaded plugin fiber.
*/
export function describePluginTree(ctx: Context, mountIdOf: (fiber: Fiber) => string | undefined): string[] {
const fibers = new Set<Fiber>()
export function describePlugins(ctx: Context): string[] {
const fibers: Fiber[] = []
for (const runtime of ctx.registry.values()) {
for (const fiber of runtime.fibers) fibers.add(fiber)
for (const fiber of runtime.fibers) fibers.push(fiber)
}
const childrenOf = new Map<Fiber, Fiber[]>()
const roots: Fiber[] = []
for (const fiber of fibers) {
const parent = fiber.parent.fiber
if (fibers.has(parent)) {
const siblings = childrenOf.get(parent) ?? []
siblings.push(fiber)
childrenOf.set(parent, siblings)
} else {
roots.push(fiber)
}
}
const byUid = (a: Fiber, b: Fiber): number => (a.uid ?? Infinity) - (b.uid ?? Infinity)
const toNode = (fiber: Fiber): TreeNode => {
const id = mountIdOf(fiber)
const label = `${id ? `${id}: ` : ''}${fiber.name} [${STATE_LABELS[fiber.state]}]`
const children = (childrenOf.get(fiber) ?? []).sort(byUid).map(toNode)
return { label, children }
}
return ['root', ...renderTree(roots.sort(byUid).map(toNode))]
return fibers
.sort((a, b) => a.name.localeCompare(b.name))
.map(fiber => `- ${fiber.name} [${STATE_LABELS[fiber.state]}]`)
}
/**