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:
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user