Merge remote-tracking branch 'origin/master' into worktree/minimal-no-runtime-context
# Conflicts: # packages/core/system-prompt/README.i18n.yaml # packages/core/system-prompt/README.md # packages/core/system-prompt/README.zh.md # packages/examples/agent-spine-demo/README.i18n.yaml # packages/examples/agent-spine-demo/README.md # packages/examples/agent-spine-demo/README.zh.md # packages/self-modification/tool-cordis/src/api-catalog.ts
This commit is contained in:
4751
packages/extensions/tool-cordis/src/api-catalog.ts
Normal file
4751
packages/extensions/tool-cordis/src/api-catalog.ts
Normal file
File diff suppressed because it is too large
Load Diff
31
packages/extensions/tool-cordis/src/fiber-state.ts
Normal file
31
packages/extensions/tool-cordis/src/fiber-state.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* Runtime mirror and labels for Cordis's `FiberState` const enum. A const enum has no runtime
|
||||
* object to import, so these values mirror the pinned vendored definition while retaining its
|
||||
* type.
|
||||
* @module @deepseek-ai/dsh-tool-cordis/fiber-state
|
||||
*/
|
||||
|
||||
import type { FiberState as FiberStateEnum } from '@deepseek-ai/cordis'
|
||||
|
||||
/** Value mirror of the cordis `FiberState` const enum (see the module doc for why a mirror exists). */
|
||||
export const FiberState = {
|
||||
PENDING: 0 as FiberStateEnum.PENDING,
|
||||
LOADING: 1 as FiberStateEnum.LOADING,
|
||||
ACTIVE: 2 as FiberStateEnum.ACTIVE,
|
||||
FAILED: 3 as FiberStateEnum.FAILED,
|
||||
DISPOSED: 4 as FiberStateEnum.DISPOSED,
|
||||
UNLOADING: 5 as FiberStateEnum.UNLOADING,
|
||||
} as const
|
||||
|
||||
/** The cordis `FiberState` enum type, re-exported so mirror consumers need one import. */
|
||||
export type FiberState = FiberStateEnum
|
||||
|
||||
/** Human-readable label for each {@link FiberState}, keyed by member (inlining-safe — no reverse mapping). */
|
||||
export const STATE_LABELS = {
|
||||
[FiberState.PENDING]: 'pending',
|
||||
[FiberState.LOADING]: 'loading',
|
||||
[FiberState.ACTIVE]: 'active',
|
||||
[FiberState.FAILED]: 'failed',
|
||||
[FiberState.DISPOSED]: 'disposed',
|
||||
[FiberState.UNLOADING]: 'unloading',
|
||||
} as const satisfies Record<FiberState, string>
|
||||
530
packages/extensions/tool-cordis/src/index.ts
Normal file
530
packages/extensions/tool-cordis/src/index.ts
Normal file
@@ -0,0 +1,530 @@
|
||||
/**
|
||||
* Model-facing Cordis runtime/package inspection, define, run, stop, and remove tools.
|
||||
* @module @deepseek-ai/dsh-tool-cordis
|
||||
*/
|
||||
|
||||
import type { Context } from '@deepseek-ai/cordis'
|
||||
import type { Agent, PreStepDecision } from '@deepseek-ai/dsh-agent'
|
||||
import {
|
||||
CordisDynamicPackageId, CordisDynamicPluginId,
|
||||
} from '@deepseek-ai/dsh-cordis-host-runner'
|
||||
import type { DynamicCordisReference } from '@deepseek-ai/dsh-cordis-host-runner'
|
||||
import { createUserMessage } from '@deepseek-ai/dsh-llm'
|
||||
import type { JsonValue } from '@deepseek-ai/dsh-session'
|
||||
import type { UserMessage } from '@deepseek-ai/dsh-session'
|
||||
import { defineTool } from '@deepseek-ai/dsh-tools'
|
||||
import type { ToolExecution } from '@deepseek-ai/dsh-tools'
|
||||
import type {} from '@deepseek-ai/dsh-system-prompt'
|
||||
import { missingServices, providedServices } from './inspect.ts'
|
||||
import {
|
||||
presentDefineCall, presentInspectListCall, presentInspectQueryCall, presentInspectSelfCall, presentRunCall,
|
||||
presentStopCall, presentUndefineCall,
|
||||
} from './present.ts'
|
||||
import { CORDIS_SYSTEM_PROMPT } from './prompt.ts'
|
||||
import { hostInspectProviders } from './providers.ts'
|
||||
|
||||
export const name = 'tool-cordis'
|
||||
export const inject = ['tools', 'systemPrompt', 'dynamicCordisRunner', 'cordisInspect']
|
||||
|
||||
function requireAgent(exec: ToolExecution): Agent {
|
||||
if (exec.agent === undefined) throw new Error('Cordis dynamic tools require an Agent-backed session')
|
||||
return exec.agent
|
||||
}
|
||||
|
||||
/** Register the Cordis tools and explicit `@pluginId` context injection. */
|
||||
export function apply(ctx: Context): void {
|
||||
ctx.systemPrompt.section({ name: 'tool:cordis', order: 115, text: CORDIS_SYSTEM_PROMPT })
|
||||
for (const provider of hostInspectProviders(ctx)) {
|
||||
ctx.effect(() => ctx.cordisInspect.register(provider), `tool-cordis: inspect ${provider.manifest.id}`)
|
||||
}
|
||||
|
||||
ctx.tools.register(defineTool({
|
||||
name: 'cordis_inspect_list',
|
||||
description:
|
||||
'List every Cordis Inspect Provider currently known to the Host, including local Host Providers and the latest '
|
||||
+ 'manifests synchronized from the Client. Each entry includes its platform, purpose, read-only methods, and '
|
||||
+ 'input/output schemas. Call this Tool before creating or modifying a Package, then select the provider and '
|
||||
+ 'method for cordis_inspect_query from its result. Do not guess names or treat an Inspect method as a business '
|
||||
+ 'Service that Plugin code can call.',
|
||||
parameters: {},
|
||||
output: {
|
||||
schema: { type: 'json' },
|
||||
render: (_args, value) => [{ type: 'text', text: JSON.stringify(value, null, 2) }],
|
||||
},
|
||||
execute(_args, _exec): Promise<JsonValue> {
|
||||
return Promise.resolve({ providers: ctx.cordisInspect.list() } as unknown as JsonValue)
|
||||
},
|
||||
presentCall: presentInspectListCall,
|
||||
}))
|
||||
|
||||
ctx.tools.register(defineTool({
|
||||
name: 'cordis_inspect_query',
|
||||
description:
|
||||
'Run a read-only query explicitly declared by an Inspect Provider. platform, provider, and method must come '
|
||||
+ 'from cordis_inspect_list, and input must satisfy that method\'s schema. Use this Tool before cordis_define '
|
||||
+ 'to read exact Service methods, Event modes, Builtin signatures, Tool schemas, theme tokens, or live Slot '
|
||||
+ 'trees and props. Host queries run locally. A Client query waits for the first valid page response and '
|
||||
+ 'remains pending until a page answers or the Tool is cancelled. This Tool cannot invoke business Service '
|
||||
+ 'methods or modify the runtime. For Service.listService and Event.listEvents, query without input to navigate '
|
||||
+ 'the compact signature directory, then query the exact service or event for its structured contract and '
|
||||
+ 'referenced types. For Slots.listSubTree, query without root to navigate the compact tree, then query the '
|
||||
+ 'exact root for its complete registration contract and props.',
|
||||
parameters: {
|
||||
platform: { type: 'string', required: true, enum: ['host', 'client'], description: 'Runtime platform that owns the Provider.' },
|
||||
provider: { type: 'string', required: true, description: 'Exact Provider ID returned by cordis_inspect_list.' },
|
||||
method: { type: 'string', required: true, description: 'Exact method name declared by the Provider manifest.' },
|
||||
input: { type: 'json', description: 'Optional query input; it must satisfy the method input schema.' },
|
||||
},
|
||||
output: {
|
||||
schema: { type: 'json' },
|
||||
render: (_args, value) => [{ type: 'text', text: JSON.stringify(value, null, 2) }],
|
||||
},
|
||||
async execute(args, exec) {
|
||||
const data = await ctx.cordisInspect.query(
|
||||
args.platform,
|
||||
args.provider,
|
||||
args.method,
|
||||
args.input,
|
||||
requireAgent(exec),
|
||||
exec.signal,
|
||||
)
|
||||
return { platform: args.platform, provider: args.provider, method: args.method, data }
|
||||
},
|
||||
presentCall: presentInspectQueryCall,
|
||||
}))
|
||||
|
||||
ctx.tools.register(defineTool({
|
||||
name: 'cordis_inspect_self',
|
||||
description:
|
||||
'Inspect dynamic Cordis objects owned by the current Session at increasing levels of detail. With no IDs, '
|
||||
+ 'list only Plugin summaries. With pluginId alone, return version pointers, the latest Run, and every Package '
|
||||
+ 'summary. Only pluginId plus packageId returns that immutable Package\'s Host/Client source and runtime '
|
||||
+ 'diagnostics. packageId cannot be supplied alone. Query an exact Package before handling @pluginId, repairing '
|
||||
+ 'an asynchronous failure, or defining an updated version. This Tool is read-only: it neither executes code '
|
||||
+ 'nor changes version pointers.',
|
||||
parameters: {
|
||||
pluginId: { type: 'string', description: 'Stable Plugin ID returned by cordis_define or injected by @pluginId; omit it to list every current Plugin.' },
|
||||
packageId: { type: 'string', description: 'Exact immutable Package ID owned by pluginId; when specified, source and diagnostics are returned.' },
|
||||
},
|
||||
output: {
|
||||
schema: { type: 'json' },
|
||||
render: (_args, value) => [{ type: 'text', text: JSON.stringify(value, null, 2) }],
|
||||
},
|
||||
execute(args, exec): Promise<JsonValue> {
|
||||
const agent = requireAgent(exec)
|
||||
if (args.packageId !== undefined && args.pluginId === undefined) {
|
||||
throw new Error('cordis_inspect_self packageId requires pluginId')
|
||||
}
|
||||
if (args.pluginId === undefined) {
|
||||
return Promise.resolve({
|
||||
mode: 'plugins',
|
||||
plugins: ctx.dynamicCordisRunner.listPlugins(agent).map(reference => selfSummary(reference)),
|
||||
} as unknown as JsonValue)
|
||||
}
|
||||
const pluginId = CordisDynamicPluginId(args.pluginId)
|
||||
if (args.packageId === undefined) {
|
||||
const plugin = ctx.dynamicCordisRunner.inspectPlugin(agent, pluginId)
|
||||
return Promise.resolve({
|
||||
mode: 'plugin',
|
||||
...selfSummary(plugin),
|
||||
packages: plugin.packages.map(pkg => ({
|
||||
...pkg,
|
||||
packageId: String(pkg.packageId),
|
||||
isCurrent: pkg.packageId === plugin.currentPackageId,
|
||||
isNext: pkg.packageId === plugin.nextPackageId,
|
||||
})),
|
||||
} as unknown as JsonValue)
|
||||
}
|
||||
return Promise.resolve(inspectSelfPackage(
|
||||
ctx,
|
||||
agent,
|
||||
pluginId,
|
||||
CordisDynamicPackageId(args.packageId),
|
||||
) as unknown as JsonValue)
|
||||
},
|
||||
presentCall: presentInspectSelfCall,
|
||||
}))
|
||||
|
||||
ctx.tools.register(defineTool({
|
||||
name: 'cordis_define',
|
||||
description:
|
||||
'Define an immutable Cordis Package. For a new Plugin, use kind:"new" and provide only a semantic prefix of '
|
||||
+ '3–6 lowercase English letters; the Host returns the final pluginId and packageId. To modify an existing '
|
||||
+ 'Plugin, use kind:"existing" with its exact pluginId to append a Package without overwriting older versions. '
|
||||
+ 'Provide at least one of code.host and code.client. Each value is a plain JavaScript function body that returns '
|
||||
+ 'a Cordis Plugin; no TypeScript, JSX, or import transformation occurs. Query Inspect before depending on a '
|
||||
+ 'Service, Event, Builtin, Slot, or token. Define only validates parameters and syntax and records source: it '
|
||||
+ 'does not request approval, execute apply, or change currentPackageId. On success, call cordis_run with the '
|
||||
+ 'returned IDs.',
|
||||
parameters: {
|
||||
plugin: {
|
||||
required: true,
|
||||
oneOf: [
|
||||
{
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
kind: { type: 'string', const: 'new', required: true },
|
||||
idPrefix: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
description: 'Suggested semantic prefix of 3–6 lowercase English letters; the Host adds a unique numeric suffix.',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
kind: { type: 'string', const: 'existing', required: true },
|
||||
pluginId: { type: 'string', required: true, description: 'Exact ID of an existing Plugin; the new Package is appended to that instance.' },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
name: { type: 'string', required: true, description: 'Short, readable Package name.' },
|
||||
purpose: { type: 'string', required: true, description: 'One-sentence, user-facing description of the Package purpose.' },
|
||||
code: {
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
required: true,
|
||||
properties: {
|
||||
host: { type: 'string', description: 'Plain JavaScript function body that returns the Host-half Cordis Plugin.' },
|
||||
client: { type: 'string', description: 'Plain JavaScript function body that returns the browser Client-half Cordis Plugin.' },
|
||||
},
|
||||
},
|
||||
},
|
||||
output: {
|
||||
schema: {
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
pluginId: { type: 'string', required: true },
|
||||
packageId: { type: 'string', required: true },
|
||||
name: { type: 'string', required: true },
|
||||
purpose: { type: 'string', required: true },
|
||||
hasHostHalf: { type: 'boolean', required: true },
|
||||
hasClientHalf: { type: 'boolean', required: true },
|
||||
},
|
||||
},
|
||||
render: (_args, value) => [{
|
||||
type: 'text',
|
||||
text: `Defined ${value.pluginId}/${value.packageId} (${value.name}); it is not running yet. `
|
||||
+ 'Use cordis_run to activate this Package.',
|
||||
}],
|
||||
presentationMeta: (_args, value) => ({ pluginId: value.pluginId, packageId: value.packageId }),
|
||||
},
|
||||
execute(args, exec) {
|
||||
const plugin = args.plugin.kind === 'new'
|
||||
? { kind: 'new' as const, idPrefix: args.plugin.idPrefix }
|
||||
: { kind: 'existing' as const, pluginId: CordisDynamicPluginId(args.plugin.pluginId) }
|
||||
const receipt = ctx.dynamicCordisRunner.define({
|
||||
sessionId: requireAgent(exec).id,
|
||||
plugin,
|
||||
name: args.name,
|
||||
purpose: args.purpose,
|
||||
code: {
|
||||
...args.code.host === undefined ? {} : { host: args.code.host },
|
||||
...args.code.client === undefined ? {} : { client: args.code.client },
|
||||
},
|
||||
})
|
||||
return Promise.resolve({
|
||||
...receipt,
|
||||
pluginId: String(receipt.pluginId),
|
||||
packageId: String(receipt.packageId),
|
||||
})
|
||||
},
|
||||
presentCall: presentDefineCall,
|
||||
}))
|
||||
|
||||
ctx.tools.register(defineTool({
|
||||
name: 'cordis_run',
|
||||
description:
|
||||
'Activate one exact Package of a dynamic Plugin. Use mode:"run" for the first activation, restarting '
|
||||
+ 'currentPackageId, or rollback. When current exists, use mode:"update" to switch to a different Package, '
|
||||
+ 'even if the Plugin is currently stopped. An unauthorized Client Package creates an approval request and '
|
||||
+ 'returns awaiting-approval; an authorized Package returns starting and continues asynchronously in the '
|
||||
+ 'browser. Neither result waits for the final outcome inside the Tool. currentPackageId changes only after '
|
||||
+ 'complete success; on failure, the old current and target next remain. Asynchronous success, rejection, or '
|
||||
+ 'technical failure is reported through state and steering. After a technical failure, read diagnostics with '
|
||||
+ 'cordis_inspect_self, correct the same Plugin, and retry autonomously. Do not request approval again after '
|
||||
+ 'the user rejects it.',
|
||||
parameters: {
|
||||
pluginId: { type: 'string', required: true, description: 'Stable Plugin ID returned by cordis_define.' },
|
||||
packageId: { type: 'string', required: true, description: 'Exact immutable Package ID to activate under that Plugin.' },
|
||||
mode: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
enum: ['run', 'update'],
|
||||
description: 'Use run for the first activation, restarting current, or rollback; use update to switch from current to a different Package.',
|
||||
},
|
||||
},
|
||||
output: {
|
||||
schema: { type: 'json' },
|
||||
render: (_args, value) => {
|
||||
const result = requireJsonObject(value)
|
||||
const pluginId = requireJsonString(result, 'pluginId')
|
||||
const packageId = requireJsonString(result, 'packageId')
|
||||
const pluginRunId = requireJsonString(result, 'pluginRunId')
|
||||
return [{
|
||||
type: 'text',
|
||||
text: result.status === 'awaiting-approval'
|
||||
? `${pluginId}/${packageId} is awaiting user approval (${pluginRunId}).`
|
||||
: result.status === 'starting'
|
||||
? `${pluginId}/${packageId} is starting asynchronously (${pluginRunId}).`
|
||||
: `${pluginId}/${packageId} is running (${pluginRunId}).`,
|
||||
}]
|
||||
},
|
||||
presentationMeta: (_args, value) => {
|
||||
const result = requireJsonObject(value)
|
||||
return {
|
||||
pluginId: requireJsonString(result, 'pluginId'),
|
||||
packageId: requireJsonString(result, 'packageId'),
|
||||
pluginRunId: requireJsonString(result, 'pluginRunId'),
|
||||
}
|
||||
},
|
||||
},
|
||||
async execute(args, exec) {
|
||||
const agent = requireAgent(exec)
|
||||
const pluginId = CordisDynamicPluginId(args.pluginId)
|
||||
const packageId = CordisDynamicPackageId(args.packageId)
|
||||
const receipt = await ctx.dynamicCordisRunner.run(agent, pluginId, packageId, args.mode, exec.signal)
|
||||
if (!receipt.ok) throw new Error(receipt.message)
|
||||
if (receipt.status !== 'running') {
|
||||
return {
|
||||
status: receipt.status,
|
||||
pluginId: args.pluginId,
|
||||
packageId: args.packageId,
|
||||
pluginRunId: String(receipt.pluginRunId),
|
||||
mode: receipt.mode,
|
||||
...receipt.currentPackageId === undefined ? {} : { currentPackageId: String(receipt.currentPackageId) },
|
||||
nextPackageId: String(receipt.nextPackageId),
|
||||
}
|
||||
}
|
||||
const row = ctx.dynamicCordisRunner.snapshot(agent).find(candidate => candidate.pluginId === pluginId)
|
||||
const fiber = row?.activeRun?.pluginRunId === receipt.pluginRunId ? row.activeRun.fiber : undefined
|
||||
return {
|
||||
status: 'running',
|
||||
pluginId: args.pluginId,
|
||||
packageId: args.packageId,
|
||||
pluginRunId: String(receipt.pluginRunId),
|
||||
currentPackageId: String(receipt.currentPackageId),
|
||||
...receipt.nextPackageId === undefined ? {} : { nextPackageId: String(receipt.nextPackageId) },
|
||||
host: {
|
||||
status: fiber === undefined ? 'absent' : missingServices(ctx, fiber).length === 0 ? 'running' : 'waiting',
|
||||
provides: fiber === undefined ? [] : providedServices(ctx, fiber),
|
||||
waitingFor: fiber === undefined ? [] : missingServices(ctx, fiber),
|
||||
},
|
||||
client: {
|
||||
status: receipt.clientWaitingFor === undefined
|
||||
? 'absent'
|
||||
: receipt.clientWaitingFor.length === 0 ? 'running' : 'waiting',
|
||||
waitingFor: [...(receipt.clientWaitingFor ?? [])],
|
||||
},
|
||||
}
|
||||
},
|
||||
presentCall: presentRunCall,
|
||||
}))
|
||||
|
||||
ctx.tools.register(defineTool({
|
||||
name: 'cordis_stop',
|
||||
description:
|
||||
'Stop the current Run of a dynamic Plugin and cancel unfinished approval or activation requests. Retain the '
|
||||
+ 'Plugin, every immutable Package, grants, currentPackageId, and nextPackageId so it can later run or update '
|
||||
+ 'directly. Stopping an already stopped Plugin succeeds idempotently. Use this Tool to disable effects '
|
||||
+ 'temporarily; use cordis_undefine for permanent removal.',
|
||||
parameters: {
|
||||
pluginId: { type: 'string', required: true, description: 'Stable dynamic Plugin ID to stop.' },
|
||||
},
|
||||
output: {
|
||||
schema: { type: 'object', additionalProperties: false, properties: { pluginId: { type: 'string', required: true } } },
|
||||
render: (_args, value) => [{ type: 'text', text: `Dynamic Plugin ${value.pluginId} is stopped; its definition and versions remain.` }],
|
||||
},
|
||||
async execute(args, exec) {
|
||||
const receipt = await ctx.dynamicCordisRunner.stop(requireAgent(exec), CordisDynamicPluginId(args.pluginId))
|
||||
if (!receipt.ok && receipt.reason !== 'not-running') throw new Error(receipt.message)
|
||||
return { pluginId: args.pluginId }
|
||||
},
|
||||
presentCall: presentStopCall,
|
||||
}))
|
||||
|
||||
ctx.tools.register(defineTool({
|
||||
name: 'cordis_undefine',
|
||||
description:
|
||||
'Permanently remove a dynamic Plugin owned by the current Session. If it is running or awaiting approval, '
|
||||
+ 'first stop it and cancel the request, then delete every Package, grant, and version pointer. After this '
|
||||
+ 'returns, its pluginId, packageIds, @ reference, and Package business views are invalid; historical cards '
|
||||
+ 'retain only a "Plugin removed" record. Do not call this Tool when versions must remain available for restart '
|
||||
+ 'or rollback; use cordis_stop instead.',
|
||||
parameters: {
|
||||
pluginId: { type: 'string', required: true, description: 'Stable dynamic Plugin ID to remove permanently.' },
|
||||
},
|
||||
output: {
|
||||
schema: {
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
pluginId: { type: 'string', required: true },
|
||||
wasRunning: { type: 'boolean', required: true },
|
||||
},
|
||||
},
|
||||
render: (_args, value) => [{ type: 'text', text: `Removed dynamic Plugin ${value.pluginId} and all of its Packages.` }],
|
||||
},
|
||||
async execute(args, exec) {
|
||||
const receipt = await ctx.dynamicCordisRunner.undefine(requireAgent(exec), CordisDynamicPluginId(args.pluginId))
|
||||
if (!receipt.ok) throw new Error(receipt.message)
|
||||
return { pluginId: args.pluginId, wasRunning: receipt.wasRunning }
|
||||
},
|
||||
presentCall: presentUndefineCall,
|
||||
}))
|
||||
|
||||
ctx.on('agent/pre-step', async ({ agent, messages, signal }, next): Promise<PreStepDecision> => {
|
||||
const decision = await next()
|
||||
if (decision.kind === 'reject') return decision
|
||||
const ids = referencedPluginIds(messages)
|
||||
if (ids.length === 0) return decision
|
||||
signal.throwIfAborted()
|
||||
const contexts = ids.map((id) => {
|
||||
const reference = ctx.dynamicCordisRunner.reference(agent, CordisDynamicPluginId(id))
|
||||
return createUserMessage({
|
||||
content: [{
|
||||
type: 'text',
|
||||
text: reference === undefined ? renderUnavailableReference(id) : renderReference(reference),
|
||||
}],
|
||||
source: { kind: 'plugin', plugin: name, form: 'instructions' },
|
||||
})
|
||||
})
|
||||
return { kind: 'enter', messages: [...decision.messages, ...contexts] }
|
||||
})
|
||||
}
|
||||
|
||||
function requireJsonObject(value: JsonValue): Record<string, JsonValue> {
|
||||
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
||||
throw new Error('expected a JSON object')
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
function requireJsonString(value: Record<string, JsonValue>, key: string): string {
|
||||
const field = value[key]
|
||||
if (typeof field !== 'string') throw new Error(`expected JSON string field "${key}"`)
|
||||
return field
|
||||
}
|
||||
|
||||
type SelfState = 'defined' | 'awaiting-approval' | 'client-pending' | 'stopped' | 'running' | 'waiting' | 'failed'
|
||||
|
||||
function selfSummary(reference: DynamicCordisReference & { packages?: readonly unknown[] }): Record<string, JsonValue> {
|
||||
const latest = reference.latestRun
|
||||
const state = selfState(reference)
|
||||
return {
|
||||
pluginId: String(reference.pluginId),
|
||||
name: reference.name,
|
||||
packageCount: reference.packages?.length ?? 1,
|
||||
state,
|
||||
...reference.currentPackageId === undefined ? {} : { currentPackageId: String(reference.currentPackageId) },
|
||||
...reference.nextPackageId === undefined ? {} : { nextPackageId: String(reference.nextPackageId) },
|
||||
...reference.activeRun === undefined ? {} : {
|
||||
activeRun: {
|
||||
pluginRunId: String(reference.activeRun.pluginRunId),
|
||||
packageId: String(reference.activeRun.packageId),
|
||||
},
|
||||
},
|
||||
...latest?.status !== 'awaiting-approval' ? {} : {
|
||||
pendingApproval: {
|
||||
pluginRunId: String(latest.pluginRunId),
|
||||
packageId: String(latest.packageId),
|
||||
mode: latest.mode,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function selfState(reference: DynamicCordisReference): SelfState {
|
||||
const status = reference.latestRun?.status
|
||||
if (status === 'awaiting-approval') return 'awaiting-approval'
|
||||
if (status === 'client-pending' || status === 'starting-host') return 'client-pending'
|
||||
if (status === 'failed' || status === 'rejected' || status === 'cancelled') return 'failed'
|
||||
if (status === 'waiting') return 'waiting'
|
||||
if (status === 'running') return 'running'
|
||||
if (reference.activeRun !== undefined) return 'running'
|
||||
return reference.currentPackageId === undefined ? 'defined' : 'stopped'
|
||||
}
|
||||
|
||||
function inspectSelfPackage(
|
||||
ctx: Context,
|
||||
agent: Agent,
|
||||
pluginId: ReturnType<typeof CordisDynamicPluginId>,
|
||||
packageId: ReturnType<typeof CordisDynamicPackageId>,
|
||||
): Record<string, JsonValue> {
|
||||
const inspected = ctx.dynamicCordisRunner.inspectPackage(agent, pluginId, packageId)
|
||||
const row = ctx.dynamicCordisRunner.snapshot(agent).find(candidate => candidate.pluginId === pluginId)
|
||||
const pkg = row?.packages.find(candidate => candidate.packageId === packageId)
|
||||
const active = row?.activeRun?.packageId === packageId ? row.activeRun : undefined
|
||||
const latest = inspected.latestRun?.packageId === packageId ? inspected.latestRun : undefined
|
||||
const hostWaiting = active?.fiber === undefined ? [...(latest?.host.waitingFor ?? [])] : missingServices(ctx, active.fiber)
|
||||
const hostStatus = pkg?.hasHostHalf !== true
|
||||
? 'absent'
|
||||
: latest?.host.status ?? (active === undefined ? 'stopped' : hostWaiting.length === 0 ? 'running' : 'waiting')
|
||||
const clientStatus = pkg?.hasClientHalf !== true
|
||||
? 'absent'
|
||||
: latest?.client.status ?? 'stopped'
|
||||
return {
|
||||
mode: 'package',
|
||||
plugin: selfSummary(inspected),
|
||||
packageId: String(packageId),
|
||||
name: inspected.name,
|
||||
purpose: inspected.purpose,
|
||||
code: inspected.code,
|
||||
runtime: {
|
||||
state: selfState(inspected),
|
||||
host: {
|
||||
status: hostStatus,
|
||||
provides: active?.fiber === undefined ? [] : providedServices(ctx, active.fiber),
|
||||
waitingFor: hostWaiting,
|
||||
handlers: active?.handlers ?? [],
|
||||
...latest?.host.error === undefined ? {} : { error: latest.host.error },
|
||||
},
|
||||
client: {
|
||||
status: clientStatus,
|
||||
waitingFor: [...(latest?.client.waitingFor ?? [])],
|
||||
...latest?.client.error === undefined ? {} : { error: latest.client.error },
|
||||
...active?.renderFailure === undefined ? {} : { renderFailure: active.renderFailure },
|
||||
},
|
||||
},
|
||||
} as unknown as Record<string, JsonValue>
|
||||
}
|
||||
|
||||
function referencedPluginIds(messages: readonly UserMessage[]): string[] {
|
||||
const found = new Set<string>()
|
||||
const pattern = /(?:^|\s)@([a-z]{3,6}-\d+)(?=\s|$)/g
|
||||
for (const message of messages) {
|
||||
if (message.source.kind !== 'user') continue
|
||||
const text = message.content.flatMap(block => block.type === 'text' ? [block.text] : []).join('\n')
|
||||
for (const match of text.matchAll(pattern)) if (match[1] !== undefined) found.add(match[1])
|
||||
}
|
||||
return [...found]
|
||||
}
|
||||
|
||||
function renderReference(reference: ReturnType<Context['dynamicCordisRunner']['reference']> & {}): string {
|
||||
const mode = reference.currentPackageId === undefined ? 'run' : 'update'
|
||||
return [
|
||||
'<cordis_dynamic_plugin_context>',
|
||||
JSON.stringify(reference, null, 2),
|
||||
'',
|
||||
`The user explicitly referenced @${reference.pluginId}. Use Package ${reference.packageId} as the base for this modification.`,
|
||||
`Before modifying it, call cordis_inspect_self with pluginId="${reference.pluginId}" and packageId="${reference.packageId}" to read the exact metadata and source.`,
|
||||
`Use cordis_define with plugin.kind="existing" and the original pluginId="${reference.pluginId}" to append an immutable Package.`,
|
||||
`Do not create a new Plugin for this request. After cordis_define succeeds, call cordis_run mode="${mode}" with the returned packageId.`,
|
||||
'</cordis_dynamic_plugin_context>',
|
||||
].join('\n')
|
||||
}
|
||||
|
||||
function renderUnavailableReference(id: string): string {
|
||||
return [
|
||||
'<cordis_dynamic_plugin_context>',
|
||||
`The user explicitly referenced @${id}, but this Plugin is unavailable in the current Session.`,
|
||||
'It may have been removed, belong to another Session, or have been lost when the DSH process restarted.',
|
||||
'Do not claim that it was updated or silently create a replacement Plugin. Tell the user that the reference is currently unavailable.',
|
||||
'</cordis_dynamic_plugin_context>',
|
||||
].join('\n')
|
||||
}
|
||||
332
packages/extensions/tool-cordis/src/inspect.ts
Normal file
332
packages/extensions/tool-cordis/src/inspect.ts
Normal file
@@ -0,0 +1,332 @@
|
||||
/**
|
||||
* Text renderers for `cordis_runtime_inspect`. Live facts come from the service store and
|
||||
* the plugin registry; what each service CAN DO comes from the generated
|
||||
* `api-catalog.ts`. This module owns the join of the two plus presentation: which
|
||||
* lines a section prints, how compact the default report stays, and what an exact
|
||||
* `name` adds.
|
||||
* @module @deepseek-ai/dsh-tool-cordis/inspect
|
||||
*/
|
||||
|
||||
import type { Context, Fiber } from '@deepseek-ai/cordis'
|
||||
import type { ScopeKey } from '@deepseek-ai/dsh-scope'
|
||||
import type { Agent } from '@deepseek-ai/dsh-agent'
|
||||
// Type-only: resolves `ctx.dynamicCordisRunner` (the registry this report reads).
|
||||
import type {} from '@deepseek-ai/dsh-cordis-host-runner'
|
||||
import { EVENT_API, INHERITED_CTX_API, SERVICE_API, TYPE_API } from './api-catalog.ts'
|
||||
import type { EventApiEntry, InheritedApiEntry, ServiceApiEntry, ServiceApiMethod, TypeApiEntry } from './api-catalog.ts'
|
||||
import { FiberState, STATE_LABELS } from './fiber-state.ts'
|
||||
|
||||
/** One live service joined with what the generated catalog knows about it. */
|
||||
interface LiveService {
|
||||
/** The `ctx.<name>` key. */
|
||||
name: string
|
||||
/** Plugin fiber providing it. */
|
||||
owner: string
|
||||
/** Lifecycle state of that fiber; `active` while it is serving. */
|
||||
state: string
|
||||
/** First sentence of the catalog summary; empty when the catalog has no entry. */
|
||||
summary: string
|
||||
/** Whether the generated catalog carries signatures for it. */
|
||||
catalogued: boolean
|
||||
/** Public method signatures from the catalog, empty for an uncatalogued service. */
|
||||
methods: readonly string[]
|
||||
}
|
||||
|
||||
/** The live service registrations, read from the reflect store. */
|
||||
function liveImpls(ctx: Context): { name: string; fiber: Fiber }[] {
|
||||
const store = ctx.reflect.store
|
||||
return Object.getOwnPropertySymbols(store)
|
||||
.map(key => store[key])
|
||||
.filter((impl): impl is NonNullable<typeof impl> => impl !== undefined)
|
||||
}
|
||||
|
||||
/**
|
||||
* A summary as prose. JSDoc may name a symbol with an inline `{@link Foo.bar}`
|
||||
* tag, which the generated catalog retains verbatim; a report is read, not
|
||||
* compiled, so the link syntax is spent context and the bare symbol says the same
|
||||
* thing.
|
||||
*/
|
||||
function plainSummary(summary: string): string {
|
||||
return summary.replace(/\{@link\s+([^}]+)\}/g, '$1')
|
||||
}
|
||||
|
||||
/**
|
||||
* Every service this process provides, joined with the generated catalog: what is
|
||||
* RUNNING comes from the store, what each service CAN DO comes from the catalog,
|
||||
* and a live service the catalog does not cover stays in the list as reachable
|
||||
* with no signatures rather than being dropped.
|
||||
*/
|
||||
function liveServices(ctx: Context, api: readonly ServiceApiEntry[]): LiveService[] {
|
||||
const catalogued = new Map(api.map(entry => [entry.key, entry]))
|
||||
return liveImpls(ctx)
|
||||
.map((impl) => {
|
||||
const entry = catalogued.get(impl.name)
|
||||
return {
|
||||
name: impl.name,
|
||||
owner: impl.fiber.name,
|
||||
state: STATE_LABELS[impl.fiber.state],
|
||||
summary: entry === undefined ? '' : plainSummary(entry.summary),
|
||||
catalogued: entry !== undefined,
|
||||
methods: entry === undefined ? [] : entry.methods.map(method => method.signature),
|
||||
}
|
||||
})
|
||||
.sort((left, right) => left.name.localeCompare(right.name))
|
||||
}
|
||||
|
||||
/** Catalogued services with no live provider: loadable in principle, absent here. */
|
||||
function absentServices(ctx: Context, api: readonly ServiceApiEntry[]): string[] {
|
||||
const live = new Set(liveImpls(ctx).map(impl => impl.name))
|
||||
return api.filter(entry => !live.has(entry.key)).map(entry => entry.key).sort()
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a fiber is `root` itself or mounted anywhere inside `root`'s subtree.
|
||||
* @param fiber - the fiber to locate.
|
||||
* @param root - the subtree root to test against.
|
||||
* @returns true when `fiber` belongs to that subtree.
|
||||
*/
|
||||
export function withinFiber(fiber: Fiber, root: Fiber): boolean {
|
||||
let current = fiber
|
||||
while (true) {
|
||||
if (current === root) return true
|
||||
const parent = current.parent.fiber
|
||||
if (parent === current) return false
|
||||
current = parent
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Service names provided by one mount's fiber subtree.
|
||||
* @param ctx - the runtime whose service registrations are inspected.
|
||||
* @param fiber - the root of the mounted fiber subtree.
|
||||
* @returns the provided service names in lexical order.
|
||||
*/
|
||||
export function providedServices(ctx: Context, fiber: Fiber): string[] {
|
||||
return liveImpls(ctx)
|
||||
.filter(impl => withinFiber(impl.fiber, fiber))
|
||||
.map(impl => impl.name)
|
||||
.sort()
|
||||
}
|
||||
|
||||
/**
|
||||
* Services a fiber declared in `inject` that do not exist yet — a settled fiber
|
||||
* that is not active is waiting on exactly these (legal cordis semantics: it
|
||||
* activates when the service appears).
|
||||
* @param ctx - the context to resolve service existence against.
|
||||
* @param fiber - the fiber whose `inject` declarations are checked.
|
||||
* @returns the missing service names, in declaration order.
|
||||
*/
|
||||
export function missingServices(ctx: Context, fiber: Fiber): string[] {
|
||||
return Object.keys(fiber.inject).filter(service => ctx.get(service) === undefined)
|
||||
}
|
||||
|
||||
/**
|
||||
* The `services` section: every live ctx service with its owning fiber and, when
|
||||
* the generated catalog covers it, a one-line summary. The `api` section is the
|
||||
* one that carries signatures; this one answers what exists and who provides it.
|
||||
* @param ctx - the runtime to enumerate.
|
||||
* @param api - the generated service entries whose summaries annotate the live ones.
|
||||
* @returns one line per service, or a single placeholder line when none are provided.
|
||||
*/
|
||||
export function describeServices(ctx: Context, api: readonly ServiceApiEntry[] = SERVICE_API): string[] {
|
||||
const live = liveServices(ctx, api)
|
||||
if (live.length === 0) return ['(no services provided)']
|
||||
return live.map((service) => {
|
||||
const state = service.state === STATE_LABELS[FiberState.ACTIVE] ? '' : `, ${service.state}`
|
||||
const summary = service.summary === '' ? '' : ` — ${service.summary}`
|
||||
return `- ${service.name} (provided by ${service.owner}${state})${summary}`
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 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). Temporary plugins are listed
|
||||
* like any other plugin; their ids live in the `temporary` section.
|
||||
* @param ctx - the runtime whose registry is enumerated.
|
||||
* @returns one line per loaded plugin fiber.
|
||||
*/
|
||||
export function describePlugins(ctx: Context): string[] {
|
||||
const fibers: Fiber[] = []
|
||||
for (const runtime of ctx.registry.values()) {
|
||||
for (const fiber of runtime.fibers) fibers.push(fiber)
|
||||
}
|
||||
return fibers
|
||||
.sort((left, right) => left.name.localeCompare(right.name))
|
||||
.map(fiber => `- ${fiber.name} [${STATE_LABELS[fiber.state]}]`)
|
||||
}
|
||||
|
||||
/**
|
||||
* The `tools` section: the model-facing tool names the CALLING agent can see
|
||||
* (its scoped layer shadowing/joining the restricted global tool set) — the
|
||||
* honest answer to the tool description's "what you can call".
|
||||
* @param ctx - the runtime whose tool registry is read.
|
||||
* @param scope - the calling agent (the viewing scope); omitted = global view.
|
||||
* @returns one line per visible tool.
|
||||
*/
|
||||
export function describeTools(ctx: Context, scope?: ScopeKey): string[] {
|
||||
return ctx.tools.schemas(scope).map(schema => `- ${schema.name}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* The `temporary` section: one line per dynamic package this session defined,
|
||||
* with its metadata, which halves exist, the host half's lifecycle state and
|
||||
* provides/waits, the invoke methods it registered, and the last browser-half
|
||||
* load report. Session-scoped like every runner verb.
|
||||
* @param ctx - the runtime the packages live in.
|
||||
* @param agent - the calling agent; without one there is no definition space to report.
|
||||
* @returns one line per package, or a single placeholder line when none exist.
|
||||
*/
|
||||
export function describeDynamic(ctx: Context, agent?: Agent): string[] {
|
||||
const rows = agent === undefined ? [] : ctx.dynamicCordisRunner.snapshot(agent)
|
||||
if (rows.length === 0) {
|
||||
return ['No dynamic Plugins are defined in this session. Definitions live only in this process\'s memory, so a DSH restart clears them.']
|
||||
}
|
||||
return rows.flatMap((row) => {
|
||||
const head = `- Plugin ${row.pluginId}; current: ${row.currentPackageId ?? 'none'}; next: ${row.nextPackageId ?? 'none'}`
|
||||
+ (row.activeRun === undefined
|
||||
? '; stopped'
|
||||
: `; active: ${row.activeRun.packageId} as ${row.activeRun.pluginRunId}`)
|
||||
const packages = row.packages.map((pkg) => {
|
||||
const halves = [...pkg.hasHostHalf ? ['host'] : [], ...pkg.hasClientHalf ? ['client'] : []].join('+')
|
||||
const active = row.activeRun?.packageId === pkg.packageId ? row.activeRun : undefined
|
||||
if (active === undefined) return ` - ${pkg.packageId}: ${pkg.name} (${halves}) — ${pkg.purpose}`
|
||||
const fiber = active.fiber
|
||||
const state = fiber === undefined ? 'running' : fiber.state === FiberState.ACTIVE ? 'running' : STATE_LABELS[fiber.state]
|
||||
const provides = fiber === undefined ? [] : providedServices(ctx, fiber)
|
||||
const waiting = fiber === undefined ? [] : missingServices(ctx, fiber)
|
||||
const failure = active.renderFailure
|
||||
const rendered = failure === undefined
|
||||
? ''
|
||||
: `; CLIENT RENDER FAILED at ${failure.slot}: ${failure.message}${failure.abdicated ? ' (entry removed)' : ''}`
|
||||
return ` - ${pkg.packageId}: ${pkg.name} [${state}, ${active.pluginRunId}] (${halves}) — ${pkg.purpose}`
|
||||
+ `; provides: ${provides.join(', ') || 'none'}; waiting for: ${waiting.join(', ') || 'none'}`
|
||||
+ (active.handlers.length === 0 ? '' : `; host methods: ${active.handlers.join(', ')}`)
|
||||
+ rendered
|
||||
})
|
||||
return [head, ...packages]
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* The transitive closure of catalogued type shapes referenced (word-bounded)
|
||||
* by the seed texts — the runtime scoping that keeps the `api` section to the
|
||||
* shapes the LIVE signatures actually mention.
|
||||
*/
|
||||
function typeClosure(seeds: string[], types: readonly TypeApiEntry[]): TypeApiEntry[] {
|
||||
const included = new Map<string, TypeApiEntry>()
|
||||
let frontier = seeds
|
||||
while (frontier.length > 0) {
|
||||
const next: string[] = []
|
||||
for (const entry of types) {
|
||||
if (included.has(entry.name)) continue
|
||||
const pattern = new RegExp(`\\b${entry.name}\\b`)
|
||||
if (frontier.some(text => pattern.test(text))) {
|
||||
included.set(entry.name, entry)
|
||||
next.push(entry.declaration)
|
||||
}
|
||||
}
|
||||
frontier = next
|
||||
}
|
||||
return [...included.values()].sort((left, right) => left.name.localeCompare(right.name))
|
||||
}
|
||||
|
||||
/** Render one live catalogued service; `documented` is non-empty only for an exact-name report. */
|
||||
function serviceLines(
|
||||
service: LiveService,
|
||||
documented: readonly ServiceApiMethod[],
|
||||
): string[] {
|
||||
const lines = [`- ${service.name} — ${service.summary}`]
|
||||
for (const signature of service.methods) {
|
||||
const contract = documented.find(entry => entry.signature === signature)
|
||||
if (contract !== undefined) {
|
||||
lines.push(` ${contract.description}`)
|
||||
for (const parameter of contract.parameters) lines.push(` @param ${parameter.name} — ${parameter.description}`)
|
||||
if (contract.returns !== undefined) lines.push(` @returns ${contract.returns}`)
|
||||
for (const failure of contract.throws ?? []) lines.push(` @throws ${failure}`)
|
||||
}
|
||||
lines.push(` ${signature}`)
|
||||
}
|
||||
return lines
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the generated catalog against the live runtime: live catalogued services with methods,
|
||||
* uncatalogued live services with owners, absent loadable services, referenced type shapes, and
|
||||
* inherited Context APIs.
|
||||
* @param ctx - the runtime to intersect the catalog with.
|
||||
* @param api - generated service entries, replaceable in tests.
|
||||
* @param name - exact live service key whose methods should include structured contracts; omitted for the compact catalog.
|
||||
* @param inherited - inherited `ctx` entries, replaceable in tests.
|
||||
* @param types - public type shapes, replaceable in tests.
|
||||
* @returns the section lines.
|
||||
*/
|
||||
export function describeApi(
|
||||
ctx: Context,
|
||||
api: readonly ServiceApiEntry[] = SERVICE_API,
|
||||
name?: string,
|
||||
inherited: readonly InheritedApiEntry[] = INHERITED_CTX_API,
|
||||
types: readonly TypeApiEntry[] = TYPE_API,
|
||||
): string[] {
|
||||
const live = liveServices(ctx, api)
|
||||
const byKey = new Map(api.map(entry => [entry.key, entry]))
|
||||
const lines: string[] = []
|
||||
let selected = live.filter(service => service.catalogued)
|
||||
let documented: readonly ServiceApiMethod[] = []
|
||||
if (name !== undefined) {
|
||||
const entry = byKey.get(name)
|
||||
if (entry === undefined) throw new Error(`no catalogued service named "${name}"`)
|
||||
const service = live.find(candidate => candidate.name === name)
|
||||
if (service === undefined) throw new Error(`catalogued service "${name}" is not running`)
|
||||
selected = [service]
|
||||
documented = entry.methods
|
||||
}
|
||||
for (const service of selected) lines.push(...serviceLines(service, documented))
|
||||
if (name === undefined) {
|
||||
for (const service of live.filter(candidate => !candidate.catalogued)) {
|
||||
lines.push(`- ${service.name} (provided by ${service.owner}) — running, but this catalog has no signature for it;`
|
||||
+ ` inject: ['${service.name}'] still reaches it`)
|
||||
}
|
||||
const notRunning = absentServices(ctx, api)
|
||||
if (notRunning.length > 0) lines.push(`not running (loadable services with no live provider): ${notRunning.join(', ')}`)
|
||||
}
|
||||
const shapes = typeClosure(selected.flatMap(service => [...service.methods]), types)
|
||||
if (shapes.length > 0) {
|
||||
lines.push('type shapes (referenced by the signatures above — read these before assuming a field is a string):')
|
||||
for (const shape of shapes) {
|
||||
for (const declLine of shape.declaration.split('\n')) lines.push(` ${declLine}`)
|
||||
}
|
||||
}
|
||||
if (name === undefined) {
|
||||
lines.push('inherited ctx API:')
|
||||
for (const entry of inherited) lines.push(`- ${entry.name} — ${entry.summary}`)
|
||||
}
|
||||
return lines
|
||||
}
|
||||
|
||||
/**
|
||||
* The `events` section: every harness event with its dispatch mode, one-line
|
||||
* summary, and exact signature, closed by the waterfall caution.
|
||||
* @param events - the event catalog (the generated one by default; injectable for tests).
|
||||
* @param name - exact event name whose signature should include its structured contract; omitted for the compact catalog.
|
||||
* @returns the section lines.
|
||||
*/
|
||||
export function describeEvents(events: readonly EventApiEntry[] = EVENT_API, name?: string): string[] {
|
||||
let selected = events
|
||||
if (name !== undefined) {
|
||||
const event = events.find(candidate => candidate.name === name)
|
||||
if (!event) throw new Error(`no catalogued event named "${name}"`)
|
||||
selected = [event]
|
||||
}
|
||||
const lines = selected.flatMap((event) => {
|
||||
const entry = [`- ${event.name} [${event.mode}] — ${event.summary}`]
|
||||
if (name !== undefined) {
|
||||
entry.push(` ${event.description}`)
|
||||
for (const parameter of event.parameters) entry.push(` @param ${parameter.name} — ${parameter.description}`)
|
||||
}
|
||||
entry.push(` ${event.signature}`)
|
||||
return entry
|
||||
})
|
||||
lines.push('waterfall listeners receive a trailing next() and MUST call it to delegate — returning without next() short-circuits the chain.')
|
||||
return lines
|
||||
}
|
||||
30
packages/extensions/tool-cordis/src/invariant.ts
Normal file
30
packages/extensions/tool-cordis/src/invariant.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Package-owned invariant companion for `@deepseek-ai/dsh-tool-cordis`.
|
||||
* @module @deepseek-ai/dsh-tool-cordis/invariant
|
||||
*/
|
||||
|
||||
/* jscpd:ignore-start */
|
||||
import type { Context } from '@deepseek-ai/cordis'
|
||||
import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants'
|
||||
|
||||
const PACKAGE_NAME = '@deepseek-ai/dsh-tool-cordis'
|
||||
|
||||
/** Cordis companion plugin name. */
|
||||
export const name = 'tool-cordis-invariant'
|
||||
/** Service required before the companion can reserve package ownership. */
|
||||
export const inject = ['invariants']
|
||||
|
||||
/**
|
||||
* No runtime invariant: this model-facing adapter has no independent lifecycle stream; execution
|
||||
* relations are owned by the capability seam it calls.
|
||||
*/
|
||||
const install: InvariantInstaller = () => {}
|
||||
|
||||
/**
|
||||
* Register this package's invariant companion.
|
||||
* @param ctx - Cordis context carrying the invariant service.
|
||||
* @returns the installed registration's disposer after setup succeeds.
|
||||
*/
|
||||
export const apply = (ctx: Context): Promise<() => void> =>
|
||||
Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install))
|
||||
/* jscpd:ignore-end */
|
||||
102
packages/extensions/tool-cordis/src/present.ts
Normal file
102
packages/extensions/tool-cordis/src/present.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
/** Pure replay-safe render intents for Cordis tools. */
|
||||
|
||||
import type { GenericCallView } from '@deepseek-ai/dsh-tools'
|
||||
|
||||
/**
|
||||
* Render a runtime-inspection call.
|
||||
* @param args - requested runtime category and optional member name.
|
||||
* @returns replay-safe generic call presentation.
|
||||
*/
|
||||
export function presentRuntimeInspectCall(args: { what?: string; name?: string }): GenericCallView {
|
||||
const target = args.name === undefined ? args.what : `${args.what}: ${args.name}`
|
||||
return { card: 'generic', kind: 'read', title: target === undefined ? 'Inspect Cordis runtime' : `Inspect Cordis runtime: ${target}` }
|
||||
}
|
||||
|
||||
/**
|
||||
* Render provider-directory inspection.
|
||||
* @returns replay-safe generic call presentation.
|
||||
*/
|
||||
export function presentInspectListCall(): GenericCallView {
|
||||
return { card: 'generic', kind: 'read', title: 'List Cordis Inspect Providers' }
|
||||
}
|
||||
|
||||
/**
|
||||
* Render one provider query.
|
||||
* @param args - target platform, provider, and method.
|
||||
* @returns replay-safe generic call presentation.
|
||||
*/
|
||||
export function presentInspectQueryCall(args: { platform: string; provider: string; method: string }): GenericCallView {
|
||||
return { card: 'generic', kind: 'read', title: `Query Cordis ${args.platform} ${args.provider}.${args.method}` }
|
||||
}
|
||||
|
||||
/**
|
||||
* Render layered self-inspection.
|
||||
* @param args - optional Plugin and Package identity.
|
||||
* @returns replay-safe generic call presentation.
|
||||
*/
|
||||
export function presentInspectSelfCall(args: { pluginId?: string; packageId?: string }): GenericCallView {
|
||||
const target = args.pluginId === undefined
|
||||
? 'dynamic Cordis Plugins'
|
||||
: args.packageId === undefined ? args.pluginId : `${args.pluginId}/${args.packageId}`
|
||||
return { card: 'generic', kind: 'read', title: `Inspect ${target}` }
|
||||
}
|
||||
|
||||
/**
|
||||
* Render an immutable Package source-inspection call.
|
||||
* @param args - exact Plugin and Package identity.
|
||||
* @returns replay-safe generic call presentation.
|
||||
*/
|
||||
export function presentPackageInspectCall(args: { pluginId: string; packageId: string }): GenericCallView {
|
||||
return { card: 'generic', kind: 'read', title: `Inspect Cordis Package ${args.pluginId}/${args.packageId}` }
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a new or appended Package definition.
|
||||
* @param args - target Plugin, Package metadata, and source halves.
|
||||
* @returns replay-safe generic call presentation with source in raw input.
|
||||
*/
|
||||
export function presentDefineCall(args: {
|
||||
plugin: { kind: 'new'; idPrefix: string } | { kind: 'existing'; pluginId: string }
|
||||
name: string
|
||||
purpose: string
|
||||
code: { host?: string; client?: string }
|
||||
}): GenericCallView {
|
||||
const target = args.plugin.kind === 'new' ? `new ${args.plugin.idPrefix}-*` : args.plugin.pluginId
|
||||
return {
|
||||
card: 'generic',
|
||||
kind: 'execute',
|
||||
title: `Register Cordis Plugin "${args.name}" for ${target}: ${args.purpose}`,
|
||||
rawInput: args.code,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render Plugin removal.
|
||||
* @param args - Plugin identity to remove.
|
||||
* @returns replay-safe generic call presentation.
|
||||
*/
|
||||
export function presentUndefineCall(args: { pluginId: string }): GenericCallView {
|
||||
return { card: 'generic', kind: 'delete', title: `Remove Cordis Plugin ${args.pluginId}` }
|
||||
}
|
||||
|
||||
/**
|
||||
* Render one exact Package activation.
|
||||
* @param args - Plugin, Package, and activation mode.
|
||||
* @returns replay-safe generic call presentation.
|
||||
*/
|
||||
export function presentRunCall(args: { pluginId: string; packageId: string; mode: 'run' | 'update' }): GenericCallView {
|
||||
return {
|
||||
card: 'generic',
|
||||
kind: 'execute',
|
||||
title: `${args.mode === 'update' ? 'Update' : 'Run'} Cordis Plugin ${args.pluginId} · ${args.packageId}`,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render Plugin stop.
|
||||
* @param args - Plugin identity to stop.
|
||||
* @returns replay-safe generic call presentation.
|
||||
*/
|
||||
export function presentStopCall(args: { pluginId: string }): GenericCallView {
|
||||
return { card: 'generic', kind: 'execute', title: `Stop Cordis Plugin ${args.pluginId}` }
|
||||
}
|
||||
107
packages/extensions/tool-cordis/src/prompt.ts
Normal file
107
packages/extensions/tool-cordis/src/prompt.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
/** Model guidance shared by the Cordis dynamic-plugin tools. */
|
||||
|
||||
export const CORDIS_SYSTEM_PROMPT = `# Dynamic Cordis Plugins
|
||||
|
||||
Dynamic Cordis plugins temporarily extend the current DSH process. A Plugin uses apply(ctx) to consume Services, listen to Events, provide Services, register model Tools, or register browser UI in Slots.
|
||||
|
||||
- Plugin and Package definitions exist only in the current process. define itself does not modify repository source, configuration, or disk, and definitions do not survive a process restart.
|
||||
- The restricted execution environment prevents accidental misuse; it is not a security boundary for malicious code. Services obtained by dynamic code connect to the real runtime.
|
||||
|
||||
## Make the user-facing plan clear first
|
||||
|
||||
- Dynamic Cordis Plugins are one available implementation mechanism, not the default for every request. Consider whether one could help only when the user intends to design or create something, or when a temporary interface could materially aid the current work. The presence of these instructions or Tools, and discussion of Cordis itself, do not make a request a dynamic-Plugin task.
|
||||
- When Cordis is a plausible fit, infer the intended work target and lifetime from the request and conversation. Use it only when the outcome belongs to the current running harness and should be delivered as a temporary runtime extension. If that distinction is materially ambiguous, ask at most one concise question about the intended result or lifetime. Otherwise proceed with the matching workflow; do not require the user to know or choose Cordis as an implementation mechanism.
|
||||
- Once a dynamic Plugin is appropriate, decide whether the task creates a new Plugin or modifies the Plugin named by the user with @pluginId. Proceed directly when the goal is clear; do not ask for repeated confirmation.
|
||||
- Choose Host, Client, or both from the requested outcome. Do not propose a Client/browser UI when the task does not need visible page behavior, and do not avoid Client when the requested outcome is visual, interactive, or depends on page state. Host versus Client is an implementation choice; do not make the user choose it.
|
||||
- When a design direction or a potentially useful interface would materially affect the result, ask at most one concise outcome or creative-preference question and offer a few candidate directions. Otherwise proceed directly; do not conduct a multi-round interview or a complex questionnaire.
|
||||
- cordis_define only defines and presents code; it does not run it. After definition, explain the pluginId and packageId returned by the Host and whether the next step is a run or update.
|
||||
- cordis_run may require user approval. When it returns awaiting-approval, explain that the user must allow or reject it in the UI. Do not wait, retry, or claim that it is running.
|
||||
- When it returns starting, explain that the request has entered the asynchronous flow and the Client is still activating. starting does not mean success. Wait for the system to report the final result through steering context.
|
||||
- Do not request approval again after the user rejects it. After a technical failure, fix the same Plugin from its diagnostics; do not silently create a replacement Plugin.
|
||||
|
||||
## Recommended workflow and Tools
|
||||
|
||||
Before creating, modifying, or repairing a Plugin, load the cordis-plugin-development Skill. The Skill provides requirement navigation, capability composition, complete examples, and troubleshooting. Treat Inspect Provider results as the source of truth for exact APIs.
|
||||
|
||||
1. cordis_inspect_list: discover the current Host and Client Providers and their read-only query methods.
|
||||
2. cordis_inspect_query: use the returned platform, provider, method, and schema to query exact Service, Event, Builtin, Slot, Theme token, or Tool information.
|
||||
3. cordis_inspect_self: inspect the current Session's Plugins, Packages, version pointers, source, and diagnostics. Source is returned only when both pluginId and packageId are specified.
|
||||
4. cordis_define: create the first Package for a new Plugin or append an immutable Package to an existing Plugin. It defines code but does not run it.
|
||||
5. cordis_run: activate an exact Package. Use run for the first activation, restarting current, or rollback; use update to switch versions.
|
||||
6. cordis_stop: remove the current Run and pending approval request while retaining definitions, grants, and version pointers.
|
||||
7. cordis_undefine: permanently stop and delete a Plugin and all of its Packages. Use it only after confirming that the user no longer needs them.
|
||||
|
||||
- Inspect and Catalog data only confirm capabilities, names, signatures, types, and registration protocols before code is written; they do not replace business APIs.
|
||||
- Query Service.listService and Event.listEvents without input to choose from their compact signature directories, then query the exact service or event before using it. Exact queries return the structured contract and only its referenced types.
|
||||
- At runtime, a Plugin must call real Services or listen to real Events. Do not cache, display, or depend on Inspect results as business data.
|
||||
|
||||
## Identity, versions, and approval
|
||||
|
||||
- pluginId identifies a Plugin that can be modified over time. For a new Plugin, submit only a semantic idPrefix of 3–6 lowercase English letters; the Host allocates the final ID.
|
||||
- packageId identifies one immutable Host/Client source version under a Plugin. To change code, define a new Package; never overwrite an old version.
|
||||
- pluginRunId identifies one activation attempt and connects its approval, Host/Client loading, private RPC, Run card, and errors.
|
||||
- currentPackageId is the most recent fully successful Package. Stopping, starting an update, or failing an update does not clear it.
|
||||
- nextPackageId is the target awaiting approval, being attempted, awaiting Client activation, or most recently failed.
|
||||
- A single check mark authorizes only the current Package; double check marks authorize future versions of the same Plugin. A grant remains in effect after a technical failure.
|
||||
- An update stops the old Run before starting the target Package. Failure does not automatically restart the old version; retry next with update or roll back to current with run.
|
||||
|
||||
When the user enters @pluginId, the system injects identity, the default base Package, version pointers, and runtime status, but not source code:
|
||||
|
||||
1. Call cordis_inspect_self(pluginId, packageId) to read the target source.
|
||||
2. Use cordis_define in existing mode to append a Package to the same Plugin.
|
||||
3. Call cordis_run in run or update mode according to the version relationship.
|
||||
|
||||
Never silently create another Plugin for @pluginId. If the reference is unavailable because it was removed, belongs to another Session, or was lost on process restart, tell the user directly.
|
||||
|
||||
## High-frequency errors that must be avoided
|
||||
|
||||
### Services: ctx.get and inject
|
||||
|
||||
- Read an optional Service with ctx.get('serviceName') by default and handle undefined.
|
||||
- Declare inject: ['serviceName'] on the returned Plugin object only when the Service is a hard dependency and the Plugin must enter waiting until Cordis reactivates it after the Service appears.
|
||||
- Read ctx.serviceName only after declaring that Service in inject. Never access an undeclared Service as a ctx property.
|
||||
|
||||
\`\`\`js
|
||||
return {
|
||||
inject: ['requiredService'],
|
||||
apply(ctx) {
|
||||
ctx.requiredService.someMethod()
|
||||
const optionalService = ctx.get('optionalService')
|
||||
if (optionalService !== undefined) optionalService.someMethod()
|
||||
},
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
### Code: use plain JavaScript only
|
||||
|
||||
- Host and Client code is not transformed by TypeScript, JSX, or a bundler.
|
||||
- Do not use TypeScript types, as, decorators, import, require, or JSX.
|
||||
- Client React code must use React.createElement(...); never write <Component />.
|
||||
- Do not assume that process, Buffer, window, document, fetch, native timers, or any other global is available. Query the corresponding platform's Builtins and Services first.
|
||||
|
||||
### Data: do not serialize live data
|
||||
|
||||
- Services, Events, Slots, Sessions, and their derived Cordis/DSH objects are internal live data, not ordinary JSON that can be dumped.
|
||||
- Do not apply JSON.stringify, structuredClone, recursive enumeration, full copying, or whole-object display to live data.
|
||||
- Read only the leaf fields required by the task, then construct the smallest owned data object without Host references.
|
||||
|
||||
### Lifecycle: every side effect must be reversible
|
||||
|
||||
- Services, Events, Tools, handlers, timers, Slots, styles, and theme overrides must all belong to the current Fiber.
|
||||
- Use ctx.effect(), ctx.on(), or official APIs that return a disposer so stop, update, or undefine removes every side effect.
|
||||
- The cordis-plugin-development Skill contains complete timer, Waterfall, Slot, theme, Tool, RPC, and React examples and troubleshooting guidance.
|
||||
|
||||
## Host and Client
|
||||
|
||||
- Host runs in the DSH Node.js process and is appropriate for files, networking, commands, Agent/Session access, Host Events, Services, model Tools, and JSON methods callable by the Client.
|
||||
- Client runs in the browser page and is appropriate for themes, layout, current page state, Tool cards, and Slot UI.
|
||||
- Host and Client communicate through Package-private JSON methods: Host uses harness.handle(method, handler), and Client uses host.call(method, args). The direction is Client→Host, and only lossless JSON may cross it.
|
||||
- Client UI must be registered in a queried Slot; apply() cannot directly return a React Element. Query Slots.listSubTree without root to choose from the compact purpose/topology tree, then query the exact root for its full registration contract and props before writing code.
|
||||
- See the Skill and Inspect Providers for Run-specific panels and exact Slot registration patterns.
|
||||
|
||||
## Asynchronous results and recovery
|
||||
|
||||
- Do not wait inside a Tool for approval or browser work that can happen only after the current turn ends.
|
||||
- Asynchronous success, rejection, and runtime errors update Run state and notify you through steering context.
|
||||
- After a technical failure, use cordis_inspect_self to read the exact Package source and its message/stack. Define a corrected Package under the same Plugin and retry autonomously.
|
||||
- Use the cordis-plugin-development Skill for other failure causes, repair procedures, and complete extension patterns.`
|
||||
101
packages/extensions/tool-cordis/src/providers.ts
Normal file
101
packages/extensions/tool-cordis/src/providers.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
/** First-party Host inspect providers registered by the Cordis tool package. */
|
||||
|
||||
import type { Context } from '@deepseek-ai/cordis'
|
||||
import { HOST_BUILTIN_INSPECTION } from '@deepseek-ai/dsh-cordis-host-runner'
|
||||
import type { HostCordisInspectProviderRegistration } from '@deepseek-ai/dsh-cordis-host-runner'
|
||||
import type { JsonValue } from '@deepseek-ai/dsh-session'
|
||||
import { EVENT_API, queryEventApi, queryServiceApi } from './api-catalog.ts'
|
||||
|
||||
const EMPTY_INPUT = { type: 'object', properties: {}, additionalProperties: false } as const
|
||||
const ANY_OUTPUT = { description: 'JSON data owned by this inspect provider.' } as const
|
||||
const SERVICE_INPUT = exactInput('service', 'Exact Service key. Omit it for the compact Service and method-signature directory.')
|
||||
const EVENT_INPUT = exactInput('event', 'Exact Event name. Omit it for the compact Event and listener-signature directory.')
|
||||
const SERVICE_OUTPUT = {
|
||||
description: 'Compact Service directory, or one exact Service contract with only its referenced type declarations.',
|
||||
} as const
|
||||
const EVENT_OUTPUT = {
|
||||
description: 'Compact Event directory, or one exact Event contract with only its referenced type declarations.',
|
||||
} as const
|
||||
const HOST_EVENTS = EVENT_API.filter(event => !event.name.startsWith('cordis/'))
|
||||
|
||||
/**
|
||||
* Construct Host providers over generated Catalogs, evaluator declarations, and live Tool scope.
|
||||
* @param ctx - Host context used for Agent-scoped live Tool queries.
|
||||
* @returns registrations for static catalogs and live Host capabilities.
|
||||
*/
|
||||
export function hostInspectProviders(ctx: Context): HostCordisInspectProviderRegistration[] {
|
||||
return [
|
||||
registration(
|
||||
'Service',
|
||||
'Progressive Host Service discovery: compact capability/signature directory, then one exact coding contract.',
|
||||
'listService',
|
||||
input => queryServiceApi(readExact(input, 'service')) as unknown as JsonValue,
|
||||
SERVICE_INPUT,
|
||||
SERVICE_OUTPUT,
|
||||
),
|
||||
registration(
|
||||
'Event',
|
||||
'Progressive Host Event discovery: compact listener directory, then one exact event contract.',
|
||||
'listEvents',
|
||||
input => queryEventApi(readExact(input, 'event'), HOST_EVENTS) as unknown as JsonValue,
|
||||
EVENT_INPUT,
|
||||
EVENT_OUTPUT,
|
||||
),
|
||||
registration('Builtin', 'Plain-JavaScript symbols available to a dynamic Host half.', 'listBuiltins', () => ({
|
||||
builtins: HOST_BUILTIN_INSPECTION,
|
||||
referencedTypes: [],
|
||||
} as unknown as JsonValue)),
|
||||
{
|
||||
manifest: {
|
||||
id: 'Tool',
|
||||
description: 'Tools visible to the requesting Agent, including scoped and dynamic registrations.',
|
||||
methods: [{
|
||||
name: 'listTools',
|
||||
description: 'Return every Tool schema currently callable by this Agent.',
|
||||
inputSchema: EMPTY_INPUT,
|
||||
outputSchema: ANY_OUTPUT,
|
||||
}],
|
||||
},
|
||||
query(method, _input, context) {
|
||||
if (method !== 'listTools') throw new Error(`unknown Tool inspect method "${method}"`)
|
||||
return Promise.resolve({ tools: ctx.tools.schemas(context.agent) } as unknown as JsonValue)
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
function registration(
|
||||
id: string,
|
||||
description: string,
|
||||
method: string,
|
||||
query: (input: JsonValue | undefined) => JsonValue | Promise<JsonValue>,
|
||||
inputSchema: JsonValue = EMPTY_INPUT,
|
||||
outputSchema: JsonValue = ANY_OUTPUT,
|
||||
): HostCordisInspectProviderRegistration {
|
||||
return {
|
||||
manifest: {
|
||||
id,
|
||||
description,
|
||||
methods: [{
|
||||
name: method,
|
||||
description,
|
||||
inputSchema,
|
||||
outputSchema,
|
||||
}],
|
||||
},
|
||||
async query(requested, input) {
|
||||
if (requested !== method) throw new Error(`unknown ${id} inspect method "${requested}"`)
|
||||
return await query(input)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function exactInput(field: string, description: string): JsonValue {
|
||||
return { type: 'object', properties: { [field]: { type: 'string', description } }, additionalProperties: false }
|
||||
}
|
||||
|
||||
function readExact(input: JsonValue | undefined, field: string): string | undefined {
|
||||
if (input === undefined || input === null || Array.isArray(input) || typeof input !== 'object') return undefined
|
||||
const value = input[field]
|
||||
return typeof value === 'string' ? value : undefined
|
||||
}
|
||||
Reference in New Issue
Block a user