Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 15 additions & 21 deletions packages/devframe/src/utils/simple-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,15 @@ function runSync<T extends StandardSchemaV1>(
return result
}

/** Validate a single field, appending any issues to `issues` prefixed by `key`. */
function pushFieldIssues(issues: Issue[], key: PropertyKey, schema: StandardSchemaV1, value: unknown): void {
const result = runSync(schema, value)
if (result.issues) {
for (const issue of result.issues)
issues.push({ message: issue.message, path: [key, ...(issue.path ?? [])] })
}
}

/** Any string. */
export function string(): SimpleSchema<string> {
return make('string', v => (typeof v === 'string' ? ok(v) : fail('Expected a string')))
Expand Down Expand Up @@ -147,13 +156,8 @@ export function record<V extends StandardSchemaV1>(
return fail('Expected an object')
const obj = v as Record<string, unknown>
const issues: Issue[] = []
for (const key of Object.keys(obj)) {
const result = runSync(value, obj[key])
if (result.issues) {
for (const issue of result.issues)
issues.push({ message: issue.message, path: [key, ...(issue.path ?? [])] })
}
}
for (const key of Object.keys(obj))
pushFieldIssues(issues, key, value, obj[key])
return issues.length ? { issues } : ok(v as any)
})
}
Expand All @@ -166,13 +170,8 @@ export function array<T extends StandardSchemaV1>(
if (!Array.isArray(v))
return fail('Expected an array')
const issues: Issue[] = []
for (let i = 0; i < v.length; i++) {
const result = runSync(item, v[i])
if (result.issues) {
for (const issue of result.issues)
issues.push({ message: issue.message, path: [i, ...(issue.path ?? [])] })
}
}
for (let i = 0; i < v.length; i++)
pushFieldIssues(issues, i, item, v[i])
return issues.length ? { issues } : ok(v as any)
})
}
Expand Down Expand Up @@ -203,13 +202,8 @@ export function object<T extends Record<string, StandardSchemaV1>>(
return fail('Expected an object')
const obj = v as Record<string, unknown>
const issues: Issue[] = []
for (const [key, schema] of entries) {
const result = runSync(schema, obj[key])
if (result.issues) {
for (const issue of result.issues)
issues.push({ message: issue.message, path: [key, ...(issue.path ?? [])] })
}
}
for (const [key, schema] of entries)
pushFieldIssues(issues, key, schema, obj[key])
// Guard-only: return the original object so extra keys survive.
return issues.length ? { issues } : ok(v as any)
})
Expand Down
37 changes: 20 additions & 17 deletions plugins/data-inspector/src/engine/query-engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,13 +107,25 @@ function getCreateQuery(): Promise<CreateQuery> {
}))
}

export async function runQuery(target: unknown, query: string, options?: NormalizeOptions): Promise<QueryOutcome> {
/**
* Run the query against the target, select the node to return from its raw
* result, and normalize that node into a wire-safe {@link QueryOutcome}.
* The `select` step runs inside the timed section so re-descent costs (used
* by {@link runQueryAtPath}) are reflected in `queryMs`.
*/
async function executeQuery(
target: unknown,
query: string,
options: NormalizeOptions | undefined,
select: (raw: unknown) => unknown,
): Promise<QueryOutcome> {
try {
const started = performance.now()
const createQuery = await getCreateQuery()
const raw = createQuery(query)(target)
const node = select(raw)
const queryMs = Math.round((performance.now() - started) * 100) / 100
const { data, stats } = normalize(raw, options)
const { data, stats } = normalize(node, options)
// The normalizer guarantees plain JSON, so this measures the actual wire payload.
const payloadBytes = new TextEncoder().encode(JSON.stringify(data) ?? '').length
return { ok: true, result: data, stats: { queryMs, normalize: stats, payloadBytes } }
Expand All @@ -124,28 +136,19 @@ export async function runQuery(target: unknown, query: string, options?: Normali
}
}

export function runQuery(target: unknown, query: string, options?: NormalizeOptions): Promise<QueryOutcome> {
return executeQuery(target, query, options, raw => raw)
}

/**
* Lazy-expand a depth-truncated node: re-run the base query against the live
* object, re-descend to the node the `NodePath` addresses, and normalize just
* that subtree with a fresh depth budget. The path comes from a `$truncated:
* 'depth'` marker the client is expanding, so the same filter options must be
* threaded through (they shift array indices and drop keys).
*/
export async function runQueryAtPath(target: unknown, query: string, path: NodePath, options?: NormalizeOptions): Promise<QueryOutcome> {
try {
const started = performance.now()
const createQuery = await getCreateQuery()
const raw = createQuery(query)(target)
const node = navigate(raw, path, options)
const queryMs = Math.round((performance.now() - started) * 100) / 100
const { data, stats } = normalize(node, options)
const payloadBytes = new TextEncoder().encode(JSON.stringify(data) ?? '').length
return { ok: true, result: data, stats: { queryMs, normalize: stats, payloadBytes } }
}
catch (error) {
const e = error instanceof Error ? error : new Error(String(error))
return { ok: false, error: { message: e.message, name: e.name } }
}
export function runQueryAtPath(target: unknown, query: string, path: NodePath, options?: NormalizeOptions): Promise<QueryOutcome> {
return executeQuery(target, query, options, raw => navigate(raw, path, options))
}

interface JoraStatEntry {
Expand Down
27 changes: 27 additions & 0 deletions plugins/inspect/src/rpc/functions/_invoke-result.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import type { InvokeResult } from '../../types'

/**
* Run an async operation and normalize it into an {@link InvokeResult}
* envelope: time the call, and capture a thrown error into a serializable
* shape rather than propagating it, so the inspector UI can render failures
* inline alongside successes.
*/
export async function toInvokeResult(run: () => Promise<unknown>): Promise<InvokeResult> {
const start = Date.now()
try {
const result = await run()
return { ok: true, result, durationMs: Date.now() - start }
}
catch (error) {
const e = error as Error
return {
ok: false,
error: {
name: e?.name ?? 'Error',
message: e?.message ?? String(error),
stack: e?.stack,
},
durationMs: Date.now() - start,
}
}
}
19 changes: 2 additions & 17 deletions plugins/inspect/src/rpc/functions/execute-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { InvokeResult } from '../../types'
import { diagnostics } from '../../diagnostics'
import { defineInspectRpc } from './_define'
import { resolveHubCommands } from './_hub-commands'
import { toInvokeResult } from './_invoke-result'

/**
* Execute a hub command by id and return a result envelope, mirroring
Expand All @@ -24,23 +25,7 @@ export const executeCommand = defineInspectRpc({
if (!host)
throw diagnostics.DP_INSPECT_0003({ id })

const start = Date.now()
try {
const result = await host.execute(id, ...args)
return { ok: true, result, durationMs: Date.now() - start }
}
catch (error) {
const e = error as Error
return {
ok: false,
error: {
name: e?.name ?? 'Error',
message: e?.message ?? String(error),
stack: e?.stack,
},
durationMs: Date.now() - start,
}
}
return toInvokeResult(() => host.execute(id, ...args))
},
}),
})
22 changes: 3 additions & 19 deletions plugins/inspect/src/rpc/functions/invoke-agent-tool.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,12 @@
import type { InvokeResult } from '../../types'
import { defineInspectRpc } from './_define'
import { toInvokeResult } from './_invoke-result'

export const invokeAgentTool = defineInspectRpc({
name: 'devframes:plugin:inspect:invoke-agent-tool',
type: 'action',
setup: ctx => ({
handler: async (id: string, args: unknown): Promise<InvokeResult> => {
const start = Date.now()
try {
const result = await ctx.agent.invoke(id, args)
return { ok: true, result, durationMs: Date.now() - start }
}
catch (error) {
const e = error as Error
return {
ok: false,
error: {
name: e?.name ?? 'Error',
message: e?.message ?? String(error),
stack: e?.stack,
},
durationMs: Date.now() - start,
}
}
},
handler: (id: string, args: unknown): Promise<InvokeResult> =>
toInvokeResult(() => ctx.agent.invoke(id, args)),
}),
})
19 changes: 2 additions & 17 deletions plugins/inspect/src/rpc/functions/invoke.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { InvokeResult } from '../../types'
import { diagnostics } from '../../diagnostics'
import { defineInspectRpc } from './_define'
import { toInvokeResult } from './_invoke-result'

const INVOKABLE_TYPES = new Set(['query', 'static'])

Expand All @@ -26,23 +27,7 @@ export const invoke = defineInspectRpc({
if (!INVOKABLE_TYPES.has(type))
throw diagnostics.DP_INSPECT_0002({ name, type })

const start = Date.now()
try {
const result = await ctx.rpc.invokeLocal(name as any, ...(args as any))
return { ok: true, result, durationMs: Date.now() - start }
}
catch (error) {
const e = error as Error
return {
ok: false,
error: {
name: e?.name ?? 'Error',
message: e?.message ?? String(error),
stack: e?.stack,
},
durationMs: Date.now() - start,
}
}
return toInvokeResult(() => ctx.rpc.invokeLocal(name as any, ...(args as any)))
},
}),
})
22 changes: 3 additions & 19 deletions plugins/inspect/src/rpc/functions/read-agent-resource.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,12 @@
import type { InvokeResult } from '../../types'
import { defineInspectRpc } from './_define'
import { toInvokeResult } from './_invoke-result'

export const readAgentResource = defineInspectRpc({
name: 'devframes:plugin:inspect:read-agent-resource',
type: 'action',
setup: ctx => ({
handler: async (id: string): Promise<InvokeResult> => {
const start = Date.now()
try {
const result = await ctx.agent.read(id)
return { ok: true, result, durationMs: Date.now() - start }
}
catch (error) {
const e = error as Error
return {
ok: false,
error: {
name: e?.name ?? 'Error',
message: e?.message ?? String(error),
stack: e?.stack,
},
durationMs: Date.now() - start,
}
}
},
handler: (id: string): Promise<InvokeResult> =>
toInvokeResult(() => ctx.agent.read(id)),
}),
})
27 changes: 10 additions & 17 deletions plugins/inspect/src/spa/components/AgentSmart.vue
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,13 @@ async function fetchData(): Promise<void> {
useRefreshProvider(fetchData)
onMounted(fetchData)

async function onInvoke(id: string, parsedArgs: unknown) {
if (!rpc.value)
async function runAction(id: string, call: (rpc: NonNullable<typeof rpc.value>) => Promise<InvokeResult>) {
const client = rpc.value
if (!client)
return
pending[id] = true
try {
results[id] = await rpc.value.call('devframes:plugin:inspect:invoke-agent-tool', id, parsedArgs)
results[id] = await call(client)
}
catch (e) {
const err = e as Error
Expand All @@ -35,20 +36,12 @@ async function onInvoke(id: string, parsedArgs: unknown) {
}
}

async function onRead(id: string) {
if (!rpc.value)
return
pending[id] = true
try {
results[id] = await rpc.value.call('devframes:plugin:inspect:read-agent-resource', id)
}
catch (e) {
const err = e as Error
results[id] = { ok: false, error: { name: err?.name ?? 'Error', message: err?.message ?? String(e) } }
}
finally {
pending[id] = false
}
function onInvoke(id: string, parsedArgs: unknown) {
return runAction(id, client => client.call('devframes:plugin:inspect:invoke-agent-tool', id, parsedArgs))
}

function onRead(id: string) {
return runAction(id, client => client.call('devframes:plugin:inspect:read-agent-resource', id))
}
</script>

Expand Down
Loading