mirror of
https://github.com/hansjone/netxops.git
synced 2026-10-09 03:53:15 +08:00
Fix bizMonitor tool results failing DSH lossless JSON (strip undefined holes).
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
b235957c34
commit
4dd628b7cc
13 changed files with 414 additions and 136 deletions
|
|
@ -3,6 +3,7 @@
|
|||
*/
|
||||
|
||||
import { type NetxClient, quoteNeId, type NetxJson } from './http.ts'
|
||||
import { omitUndefined } from './json-safe.ts'
|
||||
|
||||
const EXEC_MAX_COMMANDS = 5
|
||||
|
||||
|
|
@ -343,7 +344,7 @@ function taskBriefNe(brief: unknown): string {
|
|||
|
||||
function slimMigrationProject(row: NetxJson): NetxJson {
|
||||
const mt = asRecord(row.monitor_template)
|
||||
return {
|
||||
return omitUndefined({
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
status: row.status,
|
||||
|
|
@ -360,7 +361,7 @@ function slimMigrationProject(row: NetxJson): NetxJson {
|
|||
hf_start_at: row.hf_start_at,
|
||||
hf_end_at: row.hf_end_at,
|
||||
updated_at: row.updated_at,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function matchesBizMonitorQ(row: NetxJson, q: string): boolean {
|
||||
|
|
@ -440,7 +441,7 @@ export async function listBizMonitors(
|
|||
|
||||
return {
|
||||
ok: true,
|
||||
data: {
|
||||
data: omitUndefined({
|
||||
kind,
|
||||
projects: wantProjects ? projects : undefined,
|
||||
tasks: wantTasks ? tasks : undefined,
|
||||
|
|
@ -451,12 +452,12 @@ export async function listBizMonitors(
|
|||
tasks_returned: wantTasks ? tasks.length : 0,
|
||||
},
|
||||
next: 'Pass project_id (or task_id) to netx__getBizMonitorContext; then listBizMonitorBatches for batch_id / run_id.',
|
||||
},
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
function slimCutoverBatch(row: NetxJson): NetxJson {
|
||||
return {
|
||||
return omitUndefined({
|
||||
id: row.id,
|
||||
project_id: row.project_id,
|
||||
batch_label: row.batch_label,
|
||||
|
|
@ -468,13 +469,13 @@ function slimCutoverBatch(row: NetxJson): NetxJson {
|
|||
note: row.note,
|
||||
created_at: row.created_at,
|
||||
updated_at: row.updated_at,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function slimCollectBatch(row: NetxJson, taskId = ''): NetxJson {
|
||||
return {
|
||||
return omitUndefined({
|
||||
id: row.id,
|
||||
task_id: row.task_id || taskId,
|
||||
task_id: row.task_id || taskId || undefined,
|
||||
status: row.status,
|
||||
ne_name: row.ne_name,
|
||||
command_count: row.command_count,
|
||||
|
|
@ -484,13 +485,13 @@ function slimCollectBatch(row: NetxJson, taskId = ''): NetxJson {
|
|||
message: row.message,
|
||||
started_at: row.started_at,
|
||||
ended_at: row.ended_at,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function slimEvalRun(row: NetxJson): NetxJson {
|
||||
const summary = asRecord(row.summary)
|
||||
const progress = asRecord(summary.progress)
|
||||
return {
|
||||
return omitUndefined({
|
||||
id: row.id,
|
||||
batch_id: row.batch_id,
|
||||
purpose: row.purpose,
|
||||
|
|
@ -500,7 +501,7 @@ function slimEvalRun(row: NetxJson): NetxJson {
|
|||
anomaly: summary.anomaly,
|
||||
progress_ok: progress.ok ?? summary.progress_ok,
|
||||
progress_total: progress.total ?? summary.progress_total,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -646,7 +647,7 @@ export async function listBizMonitorBatches(
|
|||
|
||||
return {
|
||||
ok: true,
|
||||
data: {
|
||||
data: omitUndefined({
|
||||
kind,
|
||||
project_id: projectId || undefined,
|
||||
task_id: taskId || undefined,
|
||||
|
|
@ -664,7 +665,7 @@ export async function listBizMonitorBatches(
|
|||
: wantCollect
|
||||
? 'Pass collect batch id to netx__getBizCollectBatch / getBizCollectCommandRaw.'
|
||||
: 'Pass run_id to netx__getBizMonitorDiffs or getBizMonitorBoard(run_id).',
|
||||
},
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
41
src/netx/json-safe.ts
Normal file
41
src/netx/json-safe.ts
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
/**
|
||||
* DSH tool outputs must be lossless JSON: no `undefined` property values,
|
||||
* no NaN/Infinity, no class instances. Build plain objects carefully, or
|
||||
* round-trip through JSON before returning from a tool.
|
||||
*/
|
||||
|
||||
import type { NetxJson } from './http.ts'
|
||||
|
||||
/**
|
||||
* Detach a plain JSON-compatible value for DSH tool results.
|
||||
* Drops object keys whose value is `undefined` (via JSON.stringify).
|
||||
* @param value - handler result (may contain undefined holes).
|
||||
* @returns parsed plain JSON object, or a safe error envelope.
|
||||
*/
|
||||
export function toLosslessJson(value: unknown): NetxJson {
|
||||
try {
|
||||
const text = JSON.stringify(value)
|
||||
if (text === undefined) {
|
||||
return { ok: false, error: 'tool_result_not_json', detail: 'undefined_root' }
|
||||
}
|
||||
const parsed: unknown = JSON.parse(text)
|
||||
if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) {
|
||||
return parsed as NetxJson
|
||||
}
|
||||
return { ok: true, data: { value: parsed } }
|
||||
} catch (error) {
|
||||
const detail = error instanceof Error ? error.message : String(error)
|
||||
return { ok: false, error: 'tool_result_not_json', detail: detail.slice(0, 400) }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a record omitting keys whose value is `undefined` (keeps null).
|
||||
*/
|
||||
export function omitUndefined(record: Record<string, unknown>): NetxJson {
|
||||
const out: NetxJson = {}
|
||||
for (const [key, value] of Object.entries(record)) {
|
||||
if (value !== undefined) out[key] = value as NetxJson[string]
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
|
@ -17,6 +17,7 @@ import {
|
|||
} from './kb-local-ops.ts'
|
||||
import { kbLocalToolsEnabled, resolveKbLocalRoot, type RefArea } from './kb-local-path.ts'
|
||||
import { getKbContext } from './kb-runtime.ts'
|
||||
import { toLosslessJson } from './json-safe.ts'
|
||||
|
||||
const str = (description?: string) => ({ type: 'string' as const, ...(description ? { description } : {}) })
|
||||
const bool = (description?: string) => ({ type: 'boolean' as const, ...(description ? { description } : {}) })
|
||||
|
|
@ -56,7 +57,7 @@ function tool(
|
|||
isConcurrencySafe: () => false,
|
||||
async execute(args) {
|
||||
try {
|
||||
return await execute(args as Record<string, unknown>)
|
||||
return toLosslessJson(await execute(args as Record<string, unknown>))
|
||||
} catch (error) {
|
||||
throw new Error(error instanceof Error ? error.message : String(error))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import {
|
|||
type NetxCapabilityGroups,
|
||||
} from './capability-groups.ts'
|
||||
import { createNetxClient, type NetxClient, type NetxJson } from './http.ts'
|
||||
import { toLosslessJson } from './json-safe.ts'
|
||||
import { getNetxConnection } from './runtime.ts'
|
||||
import * as H from './handlers.ts'
|
||||
import * as T from './topology-handlers.ts'
|
||||
|
|
@ -74,10 +75,13 @@ function tool(
|
|||
isConcurrencySafe: () => true,
|
||||
async execute(args, exec) {
|
||||
const result = await handler(getClient(), args as NetxJson, exec.signal)
|
||||
if (result.ok === false) {
|
||||
throw new Error(JSON.stringify(result))
|
||||
// DSH rejects tool results that contain `undefined` property holes
|
||||
// ("value is not lossless JSON"). Round-trip strips them.
|
||||
const safe = toLosslessJson(result)
|
||||
if (result.ok === false || safe.ok === false) {
|
||||
throw new Error(JSON.stringify(safe))
|
||||
}
|
||||
return result
|
||||
return safe
|
||||
},
|
||||
})
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue