Files
deepseek-harness/packages/workflow/workflow-workerthread/src/meta.ts
T
Yichen Jiang 815365dbbe Merge remote-tracking branch 'origin/master' into worktree/provider-routed-llm-adapters
# Conflicts:
#	docs/config-catalog.md
#	docs/cordis-catalog/events.md
#	docs/cordis-catalog/services.md
#	docs/core-data-structures/core.md
#	docs/event-producer-consumer.md
#	docs/persistence-catalog.md
#	examples/acp-agent/tests/snapshots/advanced-toolchain/session.1.jsonl
#	examples/acp-agent/tests/snapshots/advanced-toolchain/session.2.jsonl
#	examples/acp-agent/tests/snapshots/advanced-toolchain/session.jsonl
#	examples/acp-agent/tests/snapshots/both-mode-turn/session.jsonl
#	examples/acp-agent/tests/snapshots/hook-cc-pretool-ask/session.jsonl
#	examples/acp-agent/tests/snapshots/skill-load/session.jsonl
#	examples/acp-agent/tests/snapshots/text-turn/session.jsonl
#	examples/sandbox-acp-agent/cordis.yml
#	examples/sandbox-acp-agent/tests/snapshots/escalation-approved/session.jsonl
#	examples/sandbox-acp-agent/tests/snapshots/escalation-rejected/session.jsonl
#	examples/sandbox-acp-agent/tests/snapshots/mode-switching/session.jsonl
#	packages/compact/compact-basic/README.md
#	packages/compact/compact-basic/src/index.ts
#	packages/compact/compact-basic/tests/compact-basic.spec.ts
#	packages/core/agent-loop/README.md
#	packages/core/agent-loop/src/loop.ts
#	packages/core/agent-loop/tests/properties.spec.ts
#	packages/core/session/README.md
#	packages/core/session/src/types.ts
#	packages/core/session/tests/derived-cache.spec.ts
#	packages/llm/llm-deepseek/src/index.ts
#	packages/llm/llm-pi-ai/README.md
#	packages/llm/llm-pi-ai/src/adapter.ts
#	packages/llm/llm-pi-ai/src/convert.ts
#	packages/llm/llm-pi-ai/tests/adapter.spec.ts
#	packages/llm/llm/README.md
#	packages/llm/llm/src/call-config.ts
#	packages/llm/llm/src/index.ts
#	packages/ui/acp-agent/src/index.ts
#	packages/ui/acp/tests/harness.ts
#	packages/ui/jsonrpc/README.md
#	packages/ui/jsonrpc/src/server.ts
#	packages/ui/stdio-agent/README.md
#	packages/ui/stdio-agent/src/index.ts
#	python/sdk/README.i18n.yaml
2026-07-14 22:17:50 +08:00

83 lines
4.2 KiB
TypeScript

/**
* Meta validation: check the caller-provided {@link WorkflowMeta} DATA against the shape
* contract and reject everything else loud, every violation named. Meta arrives as schema-checked
* JSON data, never evaluated script text; evaluating it on the host could run getters outside the
* worker timeout that exists to isolate model-written code.
* @module @deepseek-ai/dsh-workflow-workerthread/meta
*/
import { WorkflowError } from '@deepseek-ai/dsh-workflow'
import type { WorkflowMeta, WorkflowPhase } from '@deepseek-ai/dsh-workflow'
/** Collect shape violations for a meta value (plain JSON data by the seam contract). */
function validateMetaShape(meta: unknown): { meta?: WorkflowMeta; violations: string[] } {
const violations: string[] = []
if (typeof meta !== 'object' || meta === null || Array.isArray(meta)) {
return { violations: ['meta must be an object'] }
}
const record = meta as Record<string, unknown>
const known = new Set(['name', 'description', 'whenToUse', 'phases'])
for (const key of Object.keys(record)) {
if (!known.has(key)) violations.push(`meta.${key} is not a recognized field (name/description/whenToUse/phases)`)
}
if (typeof record.name !== 'string' || record.name.length === 0) violations.push('meta.name must be a non-empty string')
if (typeof record.description !== 'string' || record.description.length === 0) violations.push('meta.description must be a non-empty string')
if (record.whenToUse !== undefined && typeof record.whenToUse !== 'string') violations.push('meta.whenToUse must be a string')
const phases: WorkflowPhase[] = []
if (record.phases !== undefined) {
if (!Array.isArray(record.phases)) {
violations.push('meta.phases must be an array')
} else {
record.phases.forEach((phase, index) => {
if (typeof phase !== 'object' || phase === null || Array.isArray(phase)) {
violations.push(`meta.phases[${index}] must be an object`)
return
}
const entry = phase as Record<string, unknown>
for (const key of Object.keys(entry)) {
if (!['title', 'detail', 'provider', 'model'].includes(key)) violations.push(`meta.phases[${index}].${key} is not a recognized field`)
}
if (typeof entry.title !== 'string' || entry.title.length === 0) violations.push(`meta.phases[${index}].title must be a non-empty string`)
if (entry.detail !== undefined && typeof entry.detail !== 'string') violations.push(`meta.phases[${index}].detail must be a string`)
if (entry.provider !== undefined && typeof entry.provider !== 'string') violations.push(`meta.phases[${index}].provider must be a string`)
if (entry.model !== undefined && typeof entry.model !== 'string') violations.push(`meta.phases[${index}].model must be a string`)
if (violations.length === 0) {
phases.push({
title: entry.title as string,
...entry.detail !== undefined ? { detail: entry.detail as string } : {},
...entry.provider !== undefined ? { provider: entry.provider as string } : {},
...entry.model !== undefined ? { model: entry.model as string } : {},
})
}
})
}
}
if (violations.length > 0) return { violations }
return {
violations,
meta: {
name: record.name as string,
description: record.description as string,
...record.whenToUse !== undefined ? { whenToUse: record.whenToUse as string } : {},
...record.phases !== undefined ? { phases } : {},
},
}
}
/**
* Validate a caller-provided meta value against the {@link WorkflowMeta}
* contract. Throws `META_INVALID` naming every violation (unknown fields,
* missing/mistyped `name`/`description`, malformed `phases`); the returned
* meta is a NORMALIZED copy built from the validated fields, so the engine
* never aliases the caller's object.
* @param value - the meta data from the start request (plain JSON by the seam contract).
* @returns the validated, normalized meta block.
*/
export function validateMeta(value: unknown): WorkflowMeta {
const { meta, violations } = validateMetaShape(value)
if (meta === undefined) {
throw new WorkflowError(`invalid meta: ${violations.join('; ')}`, 'META_INVALID')
}
return meta
}