fix: integrate audit findings round 1 - tools, worker, scheduler, main agent

- Unify ToolResultEnvelope (output vs content) for built-in tools
- Fix shell.run AsyncGenerator consumption in ToolRegistry.call/streaming
- Scheduler: consume WorkerResult.status instead of marking all running tasks completed
- WorkerProcess/WorkerManager: surface exit events and generate failed/cancelled result
- MainAgent: integrate ContextAssembler, Chinese destructive regex, ArchitectureDesigner impact gate
- run.ts: pendingConfirmation flow, dispatch extracted, .air files filtered from /results
- CapabilityRegistry wired into RuntimeApp and ServiceRegistry; DoctorService uses it
- release.ts: findRepoRoot/findBun, run air e2e + depcruise + runtime regression
- New gates: release-critical-gates, CLI run command regression
- 14/14 e2e gates pass; 3/3 release dry-run pass

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
AirCoding
2026-06-05 18:39:10 +08:00
parent a2d7aa0339
commit ddefcbb2b1
24 changed files with 1992 additions and 186 deletions

View File

@@ -12,8 +12,13 @@ import type { ToolDefinition, ToolCall, ToolResultEnvelope, ISOTimeString } from
import { PermissionEngine, createPermissionEngine, type PermissionContext, type PermissionDecision, type PermissionAction } from '../security/PermissionEngine.js'
import type { AgentType } from '@aircoding/contracts'
export type ToolExecutionReturn =
| ToolResultEnvelope
| Promise<ToolResultEnvelope>
| AsyncIterable<ToolResultEnvelope>
export interface ToolExecutor {
(call: ToolCall, context: ToolExecutionContext): Promise<ToolResultEnvelope>
(call: ToolCall, context: ToolExecutionContext): ToolExecutionReturn
}
export interface ToolExecutionContext {
@@ -140,28 +145,19 @@ export class ToolRegistry {
const permission_context = this.build_permission_context(call, context)
const decision = await this.permission_engine.evaluate(call, permission_context, definition)
if (decision.action !== 'allow') {
if (decision.action !== 'allow' && decision.action !== 'announce_then_run') {
yield create_error_result(call.call_id, 'permission_denied', decision.reason)
return
}
// Execute with streaming support
// The executor yields intermediate results, final result comes at end
let final_result: ToolResultEnvelope | undefined
let saw_final = false
for await (const chunk of this.execute_streaming(call, context, executor)) {
// Streaming signal: final result is the one with status='ok' whose metadata marks final
if (chunk.metadata && (chunk.metadata as any).is_final === true) {
final_result = chunk
} else {
yield chunk
}
if (chunk.metadata && (chunk.metadata as any).is_final === true) saw_final = true
yield chunk
}
// Yield final result exactly once
if (final_result) {
yield final_result
} else {
if (!saw_final) {
yield create_error_result(call.call_id, 'no_final_result', 'Streaming tool did not produce final result')
}
}
@@ -251,7 +247,7 @@ export class ToolRegistry {
if (!executor) {
return create_error_result(call.call_id, 'executor_not_found', 'Executor not registered')
}
return executor(call, ctx)
return this.execute_executor_final(executor, call, ctx)
}
case 'announce_then_run': {
@@ -260,7 +256,7 @@ export class ToolRegistry {
if (!executor) {
return create_error_result(call.call_id, 'executor_not_found', 'Executor not registered')
}
const result = await executor(call, ctx)
const result = await this.execute_executor_final(executor, call, ctx)
return {
...result,
metadata: { ...result.metadata, announced: true },
@@ -269,10 +265,10 @@ export class ToolRegistry {
case 'ask_user':
// Suspend; emit permission.prompt.requested
return create_error_result('', 'user_prompt_required', 'User confirmation required')
return create_error_result(call.call_id, 'user_prompt_required', 'User confirmation required')
case 'deny':
return create_error_result('', 'permission_denied', decision.reason)
return create_error_result(call.call_id, 'permission_denied', decision.reason)
case 'block': {
// Return blocked outcome → task.blocked upstream
@@ -289,6 +285,34 @@ export class ToolRegistry {
}
}
/**
* Execute a tool and return the final envelope.
* Streaming executors are consumed until their final result.
*/
private async execute_executor_final(
executor: ToolExecutor,
call: ToolCall,
context: ToolExecutionContext,
): Promise<ToolResultEnvelope> {
const result = executor(call, context)
if (this.is_async_iterable(result)) {
let final_result: ToolResultEnvelope | undefined
let last_chunk: ToolResultEnvelope | undefined
for await (const chunk of result) {
last_chunk = chunk
if (chunk.metadata && (chunk.metadata as any).is_final === true) {
final_result = chunk
}
}
return final_result || last_chunk || create_error_result(call.call_id, 'no_result', 'Tool produced no result')
}
return await result
}
private is_async_iterable(value: unknown): value is AsyncIterable<ToolResultEnvelope> {
return Boolean(value && typeof (value as any)[Symbol.asyncIterator] === 'function')
}
/**
* Execute streaming tool.
*/
@@ -297,10 +321,12 @@ export class ToolRegistry {
context: ToolExecutionContext,
executor: ToolExecutor
): AsyncGenerator<ToolResultEnvelope> {
// Tool-specific execution handler
// For now, just execute normally
const result = await executor(call, context)
yield result
const result = executor(call, context)
if (this.is_async_iterable(result)) {
for await (const chunk of result) yield chunk
return
}
yield await result
}
}