refactor(core-agent): move spark-command agent from stage-ui to core-agent (#2528)
## Summary Move `spark-command` and `spark-notify` agents from `packages/stage-ui/src/tools/character/orchestrator/` to `packages/core-agent/src/agents/` for better separation of concerns and reusability across packages. ## Changes - **Relocate spark-command agent** — moved `spark-command-shared.ts`, `spark-command.ts`, and `spark-command.test.ts` to `packages/core-agent/src/agents/spark-command/` - **Relocate spark-notify test** — moved `spark-notify.test.ts` to `packages/core-agent/src/agents/spark-notify/` - **Add barrel export** — created `packages/core-agent/src/agents/spark-command/index.ts` with re-exports - **Update package.json** — added `./agents/spark-command` entry in `packages/core-agent/package.json` - **Update build config** — added `src/agents/spark-command/index.ts` to `tsdown.config.ts` entry list - **Consolidate schemas** — merged duplicate `sparkNotifyCommandGuidanceSchema` into `sparkCommandGuidanceSchema` - **Add JSDoc** — added `@example` blocks to all normalize functions in `schema.ts` - **Update imports** — provider tests and `tool-resolver.ts` now import from `@proj-airi/core-agent/agents/spark-command` - **Delete legacy shims** — removed `stage-ui/src/tools/character/` re-export layer ## Verification ```bash pnpm -F @proj-airi/core-agent typecheck pnpm -F @proj-airi/stage-ui typecheck pnpm -F @proj-airi/core-agent exec vitest run pnpm lint ```
This commit is contained in:
@@ -23,6 +23,10 @@
|
||||
"./agents/spark-notify": {
|
||||
"types": "./dist/agents/spark-notify/index.d.mts",
|
||||
"default": "./dist/agents/spark-notify/index.mjs"
|
||||
},
|
||||
"./agents/spark-command": {
|
||||
"types": "./dist/agents/spark-command/index.d.mts",
|
||||
"default": "./dist/agents/spark-command/index.mjs"
|
||||
}
|
||||
},
|
||||
"main": "./dist/index.mjs",
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
export {
|
||||
normalizeSparkCommandDestinations,
|
||||
normalizeSparkCommandGuidanceOptions,
|
||||
normalizeSparkCommandMetadata,
|
||||
normalizeSparkCommandPersona,
|
||||
normalizeSparkCommandStringList,
|
||||
normalizeSparkCommandStringValue,
|
||||
sparkCommandGuidanceOptionSchema,
|
||||
sparkCommandGuidanceSchema,
|
||||
sparkCommandIntentSchema,
|
||||
sparkCommandInterruptSchema,
|
||||
sparkCommandPersonaSchema,
|
||||
sparkCommandPrioritySchema,
|
||||
sparkCommandToolSchema,
|
||||
} from './schema'
|
||||
export type { CreateSparkCommandToolOptions } from './tools'
|
||||
export { createSparkCommandTool } from './tools'
|
||||
+78
-17
@@ -1,10 +1,21 @@
|
||||
import { ContextUpdateStrategy } from '@proj-airi/server-sdk'
|
||||
import { z } from 'zod/v4'
|
||||
|
||||
/** Allowed intent values for a `spark:command` event. */
|
||||
export const sparkCommandIntentSchema = z.enum(['plan', 'proposal', 'action', 'pause', 'resume', 'reroute', 'context'])
|
||||
|
||||
/** Allowed priority values for a `spark:command` event. */
|
||||
export const sparkCommandPrioritySchema = z.enum(['critical', 'high', 'normal', 'low'])
|
||||
|
||||
/** Allowed interrupt values before the parent tool schema adds its nullable provider form. */
|
||||
export const sparkCommandInterruptSchema = z.union([z.literal('force'), z.literal('soft'), z.literal(false)])
|
||||
|
||||
/**
|
||||
* Provider-facing schema for one structured guidance option.
|
||||
*
|
||||
* Strict providers require every property in this object. Use `null` for an omitted optional
|
||||
* value. The command tool removes those null and empty values before it emits `spark:command`.
|
||||
*/
|
||||
export const sparkCommandGuidanceOptionSchema = z.object({
|
||||
label: z.string().describe('Short label for the option.'),
|
||||
steps: z.array(z.string()).min(1).describe('Step-by-step actions the target should follow.'),
|
||||
@@ -15,24 +26,26 @@ export const sparkCommandGuidanceOptionSchema = z.object({
|
||||
triggers: z.union([z.array(z.string()), z.null()]).describe('Conditions that should trigger this option.'),
|
||||
}).strict()
|
||||
|
||||
/**
|
||||
* Provider-facing persona entry used to avoid dynamic object keys in generated JSON Schema.
|
||||
*
|
||||
* The command tool converts these entries into the runtime persona map keyed by `traits`.
|
||||
*/
|
||||
export const sparkCommandPersonaSchema = z.object({
|
||||
traits: z.string().describe('Trait name to adjust behavior. For example, "bravery", "cautiousness", "friendliness".'),
|
||||
strength: z.enum(['very-high', 'high', 'medium', 'low', 'very-low']),
|
||||
}).strict()
|
||||
|
||||
export const sparkNotifyCommandGuidanceSchema = z.object({
|
||||
/**
|
||||
* Provider-facing guidance schema for a Spark Command.
|
||||
*
|
||||
* `persona` uses an array of entries instead of a record because some providers reject
|
||||
* `propertyNames`. The command tool converts it back to the runtime record shape.
|
||||
*/
|
||||
export const sparkCommandGuidanceSchema = z.object({
|
||||
type: z.enum(['proposal', 'instruction', 'memory-recall']),
|
||||
persona: z.union([z.array(sparkCommandPersonaSchema), z.null()]).describe('Personas can be used to adjust the behavior of sub-agents. For example, when using as NPC in games, or player in Minecraft, the persona can help define the character\'s traits and decision-making style.'),
|
||||
options: z.array(sparkCommandGuidanceOptionSchema),
|
||||
}).strict()
|
||||
|
||||
export const sparkNotifyCommandItemSchema = z.object({
|
||||
destinations: z.array(z.string()).min(1).describe('List of sub-agent IDs to send the command to'),
|
||||
interrupt: z.union([z.enum(['force', 'soft', 'false']), z.null()]).describe('Interrupt type: force, soft, or false (no interrupt). A option to control whether this command is urgent enough to preempt ongoing tasks and require immediate attention.'),
|
||||
priority: z.union([z.enum(['critical', 'high', 'normal', 'low']), z.null()]).describe('Semantic priority of the command, this affects how sub-agents prioritize it (queues, interruption queues, mq, etc.).'),
|
||||
intent: z.union([z.enum(['plan', 'proposal', 'action', 'pause', 'resume', 'reroute', 'context']), z.null()]).describe('Intent of the command, indicating the nature of the instruction. If you attend to call other tools, use "plan" to reply with quick response to corresponding module / sub-agent.'),
|
||||
ack: z.string().describe('Acknowledgment content used to be passed to sub-agents upon command receipt.'),
|
||||
guidance: z.union([sparkNotifyCommandGuidanceSchema, z.null()]).describe('Guidance for the sub-agent on how to interpret and execute the command with given context, persona settings, and reasoning.'),
|
||||
options: z.array(sparkCommandGuidanceOptionSchema).min(1).describe('Concrete execution options for the target.'),
|
||||
}).strict()
|
||||
|
||||
export const sparkCommandMetadataEntrySchema = z.object({
|
||||
@@ -59,12 +72,18 @@ export const sparkCommandContextSchema = z.object({
|
||||
metadata: z.union([z.array(sparkCommandMetadataEntrySchema), z.null()]).describe('JSON-like metadata for the context update, expressed as key-value pairs for schema compatibility.'),
|
||||
}).strict()
|
||||
|
||||
export const sparkCommandGuidanceSchema = z.object({
|
||||
type: z.enum(['proposal', 'instruction', 'memory-recall']),
|
||||
persona: z.union([z.array(sparkCommandPersonaSchema), z.null()]).describe('Persona traits that shape the target behavior.'),
|
||||
options: z.array(sparkCommandGuidanceOptionSchema).min(1).describe('Concrete execution options for the target.'),
|
||||
}).strict()
|
||||
|
||||
/**
|
||||
* Provider-facing parameter schema for `builtIn_emitSparkCommand`.
|
||||
*
|
||||
* Strict providers require every root property. Use `null` when an optional value is absent.
|
||||
* The command tool converts null and empty values to runtime omissions or defaults before it
|
||||
* emits `spark:command`.
|
||||
*
|
||||
* This schema does not equal the wire-event shape. Persona and metadata use arrays here so
|
||||
* generated JSON Schema does not contain dynamic `propertyNames`. The tool converts them back
|
||||
* to records before delivery. The input requires at least one destination; a transport adapter
|
||||
* can clear that list when its protocol uses an empty list for broadcast delivery.
|
||||
*/
|
||||
export const sparkCommandToolSchema = z.object({
|
||||
destinations: z.array(z.string()).min(1).describe('One or more target module or agent IDs for this command.'),
|
||||
// NOTICE: Azure/OpenAI-compatible tool validators reject strict object schemas when some
|
||||
@@ -80,6 +99,13 @@ export const sparkCommandToolSchema = z.object({
|
||||
contexts: z.union([z.array(sparkCommandContextSchema), z.null()]).describe('Optional context updates to attach to the command.'),
|
||||
}).strict()
|
||||
|
||||
/**
|
||||
* Converts provider-safe metadata entries into the runtime metadata map.
|
||||
*
|
||||
* @example
|
||||
* normalizeSparkCommandMetadata([{ key: 'urgent', value: true }])
|
||||
* // => { urgent: true }
|
||||
*/
|
||||
export function normalizeSparkCommandMetadata(
|
||||
metadata: z.infer<typeof sparkCommandMetadataEntrySchema>[] | undefined,
|
||||
): Record<string, string | number | boolean | null> | undefined {
|
||||
@@ -95,6 +121,13 @@ export function normalizeSparkCommandMetadata(
|
||||
}, {})
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts provider-safe persona entries into the runtime persona map.
|
||||
*
|
||||
* @example
|
||||
* normalizeSparkCommandPersona([{ traits: 'bravery', strength: 'high' }])
|
||||
* // => { bravery: 'high' }
|
||||
*/
|
||||
export function normalizeSparkCommandPersona(
|
||||
persona: z.infer<typeof sparkCommandPersonaSchema>[] | undefined,
|
||||
): Record<string, 'very-high' | 'high' | 'medium' | 'low' | 'very-low'> | undefined {
|
||||
@@ -110,6 +143,13 @@ export function normalizeSparkCommandPersona(
|
||||
}, {})
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes empty optional fields from guidance options.
|
||||
*
|
||||
* @example
|
||||
* normalizeSparkCommandGuidanceOptions([{ label: 'Wait', steps: ['Wait'], rationale: null, possibleOutcome: [], risk: null, fallback: [], triggers: [] }])
|
||||
* // => [{ label: 'Wait', steps: ['Wait'], rationale: undefined, possibleOutcome: undefined, risk: undefined, fallback: undefined, triggers: undefined }]
|
||||
*/
|
||||
export function normalizeSparkCommandGuidanceOptions(
|
||||
options: z.infer<typeof sparkCommandGuidanceOptionSchema>[],
|
||||
) {
|
||||
@@ -126,6 +166,13 @@ export function normalizeSparkCommandGuidanceOptions(
|
||||
}))
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes empty routing filters from a context update.
|
||||
*
|
||||
* @example
|
||||
* normalizeSparkCommandDestinations({ include: ['memory'], exclude: null })
|
||||
* // => { include: ['memory'], exclude: undefined }
|
||||
*/
|
||||
export function normalizeSparkCommandDestinations(
|
||||
destinations: z.infer<typeof sparkCommandContextSchema>['destinations'],
|
||||
) {
|
||||
@@ -150,12 +197,26 @@ export function normalizeSparkCommandDestinations(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes an empty provider-safe string list.
|
||||
*
|
||||
* @example
|
||||
* normalizeSparkCommandStringList([])
|
||||
* // => undefined
|
||||
*/
|
||||
export function normalizeSparkCommandStringList(value: string[] | null): string[] | undefined {
|
||||
// NOTICE: Several provider-facing fields are required-but-nullable to satisfy strict object
|
||||
// validation. Runtime context updates treat missing lists as omitted, not `null` or `[]`.
|
||||
return value?.length ? value : undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a nullable provider-safe string into the runtime optional value.
|
||||
*
|
||||
* @example
|
||||
* normalizeSparkCommandStringValue(null)
|
||||
* // => undefined
|
||||
*/
|
||||
export function normalizeSparkCommandStringValue(value: string | null): string | undefined {
|
||||
// NOTICE: Required-but-nullable provider fields are normalized back to the runtime
|
||||
// convention of omitting absent scalar values with `undefined`.
|
||||
+2
-2
@@ -3,7 +3,7 @@ import type { JsonSchema } from 'xsschema'
|
||||
import { ContextUpdateStrategy } from '@proj-airi/server-sdk'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { createSparkCommandTool } from './spark-command'
|
||||
import { createSparkCommandTool } from './tools'
|
||||
|
||||
function isJsonSchema(value: JsonSchema | boolean | undefined): value is JsonSchema {
|
||||
return Boolean(value && typeof value === 'object')
|
||||
@@ -48,7 +48,7 @@ function findObjectSchema(schema: JsonSchema | undefined, predicate: (schema: Js
|
||||
return undefined
|
||||
}
|
||||
|
||||
describe('tools/character/orchestrator/spark-command', () => {
|
||||
describe('agents/spark-command/tools', () => {
|
||||
it('emits a strict parameter schema', async () => {
|
||||
const tools = await createSparkCommandTool({
|
||||
sendSparkCommand: () => undefined,
|
||||
+9
-1
@@ -13,12 +13,20 @@ import {
|
||||
normalizeSparkCommandStringList,
|
||||
normalizeSparkCommandStringValue,
|
||||
sparkCommandToolSchema,
|
||||
} from './spark-command-shared'
|
||||
} from './schema'
|
||||
|
||||
/** Options for the Spark Command LLM tool. */
|
||||
export interface CreateSparkCommandToolOptions {
|
||||
/** Receives a protocol-ready `spark:command` event. */
|
||||
sendSparkCommand: (command: WebSocketEvents['spark:command']) => void
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the LLM tool that emits one `spark:command` event.
|
||||
*
|
||||
* The caller owns transport delivery. This function creates provider-facing schemas,
|
||||
* normalizes the tool payload, and passes the resulting event to `sendSparkCommand`.
|
||||
*/
|
||||
export async function createSparkCommandTool(options: CreateSparkCommandToolOptions) {
|
||||
// Keep the generated JSON Schema provider-neutral. Each provider adapter
|
||||
// converts unsupported schema forms before it sends the request.
|
||||
+2
-2
@@ -2,9 +2,9 @@ import type { JsonSchema } from 'xsschema'
|
||||
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { createSparkNotifyTools } from './spark-notify'
|
||||
import { createSparkNotifyTools } from './tools'
|
||||
|
||||
describe('tools/character/orchestrator/spark-notify', () => {
|
||||
describe('agents/spark-notify/tools', () => {
|
||||
it('emits strict parameter objects for spark notify tools', async () => {
|
||||
const { tools } = await createSparkNotifyTools({
|
||||
onNoResponse: () => undefined,
|
||||
@@ -3,6 +3,7 @@ import { defineConfig } from 'tsdown'
|
||||
export default defineConfig({
|
||||
entry: [
|
||||
'src/index.ts',
|
||||
'src/agents/spark-command/index.ts',
|
||||
'src/agents/spark-notify/index.ts',
|
||||
],
|
||||
dts: true,
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import type { JsonSchema } from 'xsschema'
|
||||
|
||||
import { createSparkCommandTool } from '@proj-airi/core-agent/agents/spark-command'
|
||||
import { getDefinedProvider } from '@proj-airi/provider-inference'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { createSparkCommandTool } from '../../../../tools/character/orchestrator/spark-command'
|
||||
|
||||
interface ChatRequestBody {
|
||||
tools: Array<{
|
||||
function: {
|
||||
|
||||
@@ -2,11 +2,10 @@ import type { ChatRequestOptions } from '@proj-airi/provider-inference'
|
||||
import type { ChatProviderWithExtraOptions } from '@xsai-ext/providers/utils'
|
||||
import type { JsonSchema } from 'xsschema'
|
||||
|
||||
import { createSparkCommandTool } from '@proj-airi/core-agent/agents/spark-command'
|
||||
import { getDefinedProvider } from '@proj-airi/provider-inference'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { createSparkCommandTool } from '../../../../tools/character/orchestrator/spark-command'
|
||||
|
||||
interface ChatRequestBody {
|
||||
tools: Array<{
|
||||
function: {
|
||||
|
||||
@@ -2,9 +2,10 @@ import type { StreamOptions } from '@proj-airi/core-agent'
|
||||
import type { WebSocketEvents } from '@proj-airi/server-sdk'
|
||||
import type { Tool } from '@xsai/shared-chat'
|
||||
|
||||
import { createSparkCommandTool } from '@proj-airi/core-agent/agents/spark-command'
|
||||
import { uniqBy } from 'es-toolkit'
|
||||
|
||||
import { createSparkCommandTool, createWebSearchTools, debug, mcp } from '../../../tools'
|
||||
import { createWebSearchTools, debug, mcp } from '../../../tools'
|
||||
import { useModsServerChannelStore } from '../../mods/api/channel-server'
|
||||
import { useWebSearchStore } from '../../modules/web-search'
|
||||
import { useLlmToolsStore } from './tools'
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export * from './orchestrator'
|
||||
@@ -1,3 +0,0 @@
|
||||
export * from './spark-command'
|
||||
export * from './spark-command-shared'
|
||||
export * from './spark-notify'
|
||||
@@ -1,9 +0,0 @@
|
||||
export type {
|
||||
CreateSparkNotifyToolsOptions,
|
||||
SparkNotifyCommandDraft,
|
||||
} from '@proj-airi/core-agent/agents/spark-notify'
|
||||
|
||||
export {
|
||||
createSparkNotifyTools,
|
||||
sparkNotifyCommandSchema,
|
||||
} from '@proj-airi/core-agent/agents/spark-notify'
|
||||
@@ -1,4 +1,3 @@
|
||||
export * from './character'
|
||||
export * from './debug'
|
||||
export * from './mcp'
|
||||
export * from './web-search'
|
||||
|
||||
Reference in New Issue
Block a user