bazis 0.96.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +146 -0
- package/cli/build.ts +147 -0
- package/cli/codegen.ts +40 -0
- package/cli/generateModule.ts +187 -0
- package/cli/generateProject.ts +183 -0
- package/cli/main.ts +125 -0
- package/cli/moduleRegistration.ts +127 -0
- package/cli/naming.ts +124 -0
- package/cli/parseCli.ts +151 -0
- package/cli/templates/module.ts +497 -0
- package/cli/templates/pack.ts +76 -0
- package/cli/templates/passport.ts +178 -0
- package/core/agent/AgentContextBuilder.ts +567 -0
- package/core/agent/AgentDataDefinition.ts +8 -0
- package/core/agent/AgentRegistry.ts +539 -0
- package/core/agent/AgentRuntime.ts +47 -0
- package/core/agent/AgentToolExecutor.ts +2130 -0
- package/core/agent/AgentToolHooks.ts +39 -0
- package/core/agent/ModelProvider.ts +21 -0
- package/core/agent/README.md +229 -0
- package/core/agent/Tool.contract.ts +20 -0
- package/core/agent/decorators/Agent.ts +11 -0
- package/core/agent/decorators/Prompt.ts +11 -0
- package/core/agent/decorators/Task.ts +29 -0
- package/core/agent/decorators/Tool.ts +12 -0
- package/core/agent/errors.ts +43 -0
- package/core/agent/generated/catalog.ts +26 -0
- package/core/agent/index.ts +170 -0
- package/core/agent/internal/AgentExecutionDriver.ts +1497 -0
- package/core/agent/internal/AgentExecutionState.ts +775 -0
- package/core/agent/internal/AgentJsonSchema.ts +150 -0
- package/core/agent/internal/AgentModelBinding.ts +57 -0
- package/core/agent/internal/Schema.contract.ts +36 -0
- package/core/agent/internal/ToolAudit.projector.ts +163 -0
- package/core/agent/internal/ToolContract.validator.ts +102 -0
- package/core/agent/metadata.ts +398 -0
- package/core/agent/moduleContributions-v1.ts +113 -0
- package/core/agent/semantic.ts +903 -0
- package/core/agent/session/codec.ts +33 -0
- package/core/agent/session/contracts.ts +96 -0
- package/core/agent/session/entities.ts +84 -0
- package/core/agent/session/protector.ts +92 -0
- package/core/app/index.ts +5 -0
- package/core/app/runApp.ts +293 -0
- package/core/app/ui/index.ts +29 -0
- package/core/app/ui/uiProfileResolver.ts +966 -0
- package/core/app/ui/uiSurfaceHosting.ts +38 -0
- package/core/app/ui/uiSurfaceHttp.ts +476 -0
- package/core/background/BackgroundService.ts +223 -0
- package/core/background/backgroundModule.ts +42 -0
- package/core/background/decorator.ts +43 -0
- package/core/background/delay.ts +27 -0
- package/core/background/index.ts +15 -0
- package/core/cache/COMPATIBILITY.md +13 -0
- package/core/cache/ICache.ts +71 -0
- package/core/cache/MemoryCache.ts +394 -0
- package/core/cache/README.md +83 -0
- package/core/cache/SPEC.md +1355 -0
- package/core/cache/cacheModule.ts +21 -0
- package/core/cache/decorators/Cacheable.ts +38 -0
- package/core/cache/decorators/CacheableRedis.ts +39 -0
- package/core/cache/decorators/OutputCache.ts +33 -0
- package/core/cache/decorators/OutputRedisCache.ts +33 -0
- package/core/cache/decorators/cacheableMetadata.ts +78 -0
- package/core/cache/decorators/cacheableRedisMetadata.ts +81 -0
- package/core/cache/decorators/outputCacheMetadata.ts +71 -0
- package/core/cache/decorators/outputRedisCacheMetadata.ts +77 -0
- package/core/cache/di/classProviderHook.ts +26 -0
- package/core/cache/distributed/CacheCodec.ts +5 -0
- package/core/cache/distributed/DistributedCache.ts +403 -0
- package/core/cache/distributed/DistributedCacheDriver.ts +83 -0
- package/core/cache/distributed/DistributedCacheStores.ts +28 -0
- package/core/cache/distributed/IDistributedCache.ts +40 -0
- package/core/cache/distributed/NamedCacheRegistry.ts +23 -0
- package/core/cache/distributed/codecs.ts +32 -0
- package/core/cache/distributed/index.ts +19 -0
- package/core/cache/errors/CacheError.ts +16 -0
- package/core/cache/http/CachedHttpPayload.ts +213 -0
- package/core/cache/http/applyClientCacheHeaders.ts +61 -0
- package/core/cache/http/buildOutputCacheKey.ts +154 -0
- package/core/cache/http/buildRedisOutputCacheKey.ts +16 -0
- package/core/cache/http/composeOutputCache.ts +130 -0
- package/core/cache/http/outputCacheMiddleware.ts +111 -0
- package/core/cache/http/outputCacheSecurityWarning.ts +58 -0
- package/core/cache/http/outputRedisCacheMiddleware.ts +118 -0
- package/core/cache/index.ts +97 -0
- package/core/cache/internal/AutomaticCacheKey.ts +106 -0
- package/core/cache/internal/CacheKeyGuard.ts +56 -0
- package/core/cache/internal/buildCacheProviders.ts +146 -0
- package/core/cache/internal/normalizeCacheKey.ts +9 -0
- package/core/cache/internal/resolveCachePolicy.ts +32 -0
- package/core/cache/internal/resolveCacheableRedisPolicy.ts +17 -0
- package/core/cache/internal/resolveOutputRedisCachePolicy.ts +17 -0
- package/core/cache/providers/cachedProviders.ts +68 -0
- package/core/cache/services/cacheProxy.ts +243 -0
- package/core/cache/tokens/CACHE_POLICIES.ts +5 -0
- package/core/cache/tokens/DISTRIBUTED_CACHE.ts +30 -0
- package/core/cache/types/CacheEntry.ts +7 -0
- package/core/cache/types/CacheOptions.ts +60 -0
- package/core/cache/types/CachePolicy.ts +74 -0
- package/core/cache/types/CacheableRedisOptions.ts +23 -0
- package/core/cache/types/OutputRedisCacheOptions.ts +25 -0
- package/core/di/README.md +104 -0
- package/core/di/ServiceCollection.ts +185 -0
- package/core/di/ServiceProvider.ts +689 -0
- package/core/di/ServiceScope.ts +61 -0
- package/core/di/container.ts +38 -0
- package/core/di/errors/AmbiguousNamedDependencyError.ts +10 -0
- package/core/di/errors/AsyncResolutionRequiredError.ts +12 -0
- package/core/di/errors/CircularDependencyError.ts +10 -0
- package/core/di/errors/ClassDependenciesMismatchError.ts +15 -0
- package/core/di/errors/DiError.ts +6 -0
- package/core/di/errors/InvalidProviderError.ts +9 -0
- package/core/di/errors/ModuleEncapsulationError.ts +7 -0
- package/core/di/errors/ModuleOwnedProviderConflictError.ts +8 -0
- package/core/di/errors/NamedDependencyNotFoundError.ts +7 -0
- package/core/di/errors/OptionsValidationError.ts +7 -0
- package/core/di/errors/ProviderNotFoundError.ts +10 -0
- package/core/di/errors/ScopeDisposedError.ts +7 -0
- package/core/di/errors/ScopedServiceFromRootError.ts +9 -0
- package/core/di/errors/ServiceValidationError.ts +7 -0
- package/core/di/errors/index.ts +14 -0
- package/core/di/extensions/activator.ts +39 -0
- package/core/di/extensions/application.ts +117 -0
- package/core/di/extensions/hosted-service.ts +124 -0
- package/core/di/extensions/index.ts +23 -0
- package/core/di/extensions/options-reloadable.ts +137 -0
- package/core/di/extensions/options.ts +91 -0
- package/core/di/extensions/service-provider-token.ts +21 -0
- package/core/di/index.ts +97 -0
- package/core/di/internal/GraphValidator.ts +119 -0
- package/core/di/internal/HostedServicePlan.ts +158 -0
- package/core/di/internal/NamedTokenIndex.ts +55 -0
- package/core/di/internal/OpenGenericRegistration.ts +14 -0
- package/core/di/internal/ResolutionPlan.ts +18 -0
- package/core/di/internal/ResolutionScopeState.ts +19 -0
- package/core/di/internal/ResolutionTracker.ts +153 -0
- package/core/di/internal/ScopeLifecycle.ts +159 -0
- package/core/di/internal/ServiceRegistration.ts +14 -0
- package/core/di/internal/ServiceRegistry.ts +252 -0
- package/core/di/internal/classDeps.ts +38 -0
- package/core/di/internal/disposal.ts +58 -0
- package/core/di/internal/providerDeps.ts +26 -0
- package/core/di/module/DI.ts +146 -0
- package/core/di/module/Global.ts +25 -0
- package/core/di/module/Module.ts +44 -0
- package/core/di/module/ModuleRegistrar.ts +69 -0
- package/core/di/module/autoDeps.ts +39 -0
- package/core/di/module/classProviderHooks.ts +66 -0
- package/core/di/module/collectModuleConfigs.ts +57 -0
- package/core/di/module/collectModuleControllers.ts +53 -0
- package/core/di/module/collectModuleUiProfiles.ts +53 -0
- package/core/di/module/createContainer.ts +261 -0
- package/core/di/module/encapsulation.ts +416 -0
- package/core/di/module/encapsulationHooks.ts +36 -0
- package/core/di/module/generatedProviderAttachments.ts +102 -0
- package/core/di/module/index.ts +41 -0
- package/core/di/module/moduleExtensions.ts +35 -0
- package/core/di/module/moduleNameBinding.ts +136 -0
- package/core/di/module/moduleOwnedProviderContributors.ts +409 -0
- package/core/di/module/providerSelection.ts +90 -0
- package/core/di/module/shortcuts.ts +111 -0
- package/core/di/module/types/BazisModule.ts +71 -0
- package/core/di/module/types/DiRegistrar.ts +24 -0
- package/core/di/module/types/index.ts +2 -0
- package/core/di/provider/index.ts +26 -0
- package/core/di/provider/providerGuards.ts +17 -0
- package/core/di/provider/types/AsyncFactoryProvider.ts +15 -0
- package/core/di/provider/types/ClassProvider.ts +18 -0
- package/core/di/provider/types/FactoryProvider.ts +20 -0
- package/core/di/provider/types/KeyedDependency.ts +20 -0
- package/core/di/provider/types/LazyDependency.ts +31 -0
- package/core/di/provider/types/NamedDependency.ts +16 -0
- package/core/di/provider/types/Provider.ts +10 -0
- package/core/di/provider/types/ProviderDefinition.ts +11 -0
- package/core/di/provider/types/ProviderDependencyList.ts +11 -0
- package/core/di/provider/types/ProviderLifetime.ts +1 -0
- package/core/di/provider/types/ProviderOwnership.ts +7 -0
- package/core/di/provider/types/ResolvedDeps.ts +18 -0
- package/core/di/provider/types/ValueProvider.ts +6 -0
- package/core/di/provider/types/index.ts +13 -0
- package/core/di/token.ts +84 -0
- package/core/di/types/BuildServiceProviderOptions.ts +4 -0
- package/core/di/types/ServiceKey.ts +1 -0
- package/core/di/types/ServiceResolver.ts +16 -0
- package/core/di/types/index.ts +3 -0
- package/core/generatedFingerprint.ts +60 -0
- package/core/generatedRuntime.ts +187 -0
- package/core/grpc/Grpc.module.ts +23 -0
- package/core/grpc/GrpcBinding.contract.ts +35 -0
- package/core/grpc/GrpcCall.ts +73 -0
- package/core/grpc/GrpcClient.ts +172 -0
- package/core/grpc/GrpcClientCall.ts +267 -0
- package/core/grpc/GrpcError.ts +19 -0
- package/core/grpc/GrpcRequest.binding.ts +45 -0
- package/core/grpc/GrpcServer.ts +204 -0
- package/core/grpc/GrpcStatus.ts +8 -0
- package/core/grpc/GrpcTransport.ts +174 -0
- package/core/grpc/Metadata.ts +73 -0
- package/core/grpc/ProtoCodec.ts +225 -0
- package/core/grpc/ProtoParser.ts +192 -0
- package/core/grpc/ProtoReader.ts +34 -0
- package/core/grpc/ProtoSchema.ts +52 -0
- package/core/grpc/ServerCredentials.ts +30 -0
- package/core/grpc/clientContracts.ts +39 -0
- package/core/grpc/contracts.ts +45 -0
- package/core/grpc/decorators.ts +82 -0
- package/core/grpc/grpcClientProvider.ts +9 -0
- package/core/grpc/index.ts +17 -0
- package/core/grpc/moduleControllers.ts +38 -0
- package/core/grpc/protobuf.ts +80 -0
- package/core/grpc/serviceDefinition.ts +20 -0
- package/core/http/Authorization/Authorize.ts +52 -0
- package/core/http/Authorization/authorizeComposer.ts +31 -0
- package/core/http/Authorization/metadata.ts +158 -0
- package/core/http/Binding/ParameterBinder.ts +109 -0
- package/core/http/Binding/RequestModel.ts +27 -0
- package/core/http/Binding/autoBindings.ts +125 -0
- package/core/http/Binding/bindings.ts +19 -0
- package/core/http/Binding/convert.ts +48 -0
- package/core/http/Binding/generatedSpec.ts +18 -0
- package/core/http/Binding/listModelRegistry.ts +47 -0
- package/core/http/Binding/modelBinder.ts +345 -0
- package/core/http/Binding/modelValidator.ts +30 -0
- package/core/http/Binding/requestModelRegistry.ts +117 -0
- package/core/http/Decorators/attributes.ts +92 -0
- package/core/http/Decorators/controller.ts +24 -0
- package/core/http/Decorators/metadata.ts +107 -0
- package/core/http/Decorators/routes.ts +131 -0
- package/core/http/Errors/HttpError.ts +92 -0
- package/core/http/HttpContext/HttpContext.ts +148 -0
- package/core/http/HttpContext/ResponseBuilder.ts +37 -0
- package/core/http/HttpContext/inspectableRedirects.ts +47 -0
- package/core/http/HttpContext/principal.ts +24 -0
- package/core/http/HttpContext/responseLifetime.ts +45 -0
- package/core/http/HttpServer.ts +627 -0
- package/core/http/Middleware/accessLog.ts +90 -0
- package/core/http/Middleware/cors.ts +160 -0
- package/core/http/Middleware/errorHandler.ts +100 -0
- package/core/http/Middleware/pipeline.ts +21 -0
- package/core/http/Middleware/rateLimit.ts +104 -0
- package/core/http/Middleware/securityHeaders.ts +118 -0
- package/core/http/Middleware/types.ts +19 -0
- package/core/http/OpenApi/generatedOpenApiRegistry.ts +102 -0
- package/core/http/OpenApi/openApiDocs.ts +75 -0
- package/core/http/OpenApi/openApiDocument.ts +356 -0
- package/core/http/README.md +139 -0
- package/core/http/Results/HttpResult.ts +19 -0
- package/core/http/Results/normalize.ts +93 -0
- package/core/http/Results/results.ts +70 -0
- package/core/http/Routing/Router.ts +201 -0
- package/core/http/Routing/RouterBuilder.ts +184 -0
- package/core/http/Routing/template.ts +112 -0
- package/core/http/SPEC.md +981 -0
- package/core/http/Versioning/VersionReader.ts +20 -0
- package/core/http/WebSocketUpgrade.ts +29 -0
- package/core/http/composeRouteMiddlewareComposers.ts +21 -0
- package/core/http/correlation/createCorrelationIdMiddleware.ts +79 -0
- package/core/http/httpModule.ts +48 -0
- package/core/http/index.ts +140 -0
- package/core/http/middlewareTokens.ts +48 -0
- package/core/http/options.ts +134 -0
- package/core/http-client/index.ts +105 -0
- package/core/infra/Infra.ts +158 -0
- package/core/infra/InfraConnector.ts +86 -0
- package/core/infra/InfraLifecycle.ts +86 -0
- package/core/infra/cache/RedisDistributedCacheBackend.ts +88 -0
- package/core/infra/cache/RedisDistributedCacheDriver.ts +186 -0
- package/core/infra/cache/index.ts +7 -0
- package/core/infra/connectorConfig.ts +29 -0
- package/core/infra/connectorIdentity.ts +39 -0
- package/core/infra/connectors/codex/CodexAppServer.ts +146 -0
- package/core/infra/connectors/codex/CodexAppServerClient.ts +258 -0
- package/core/infra/connectors/codex/CodexPolicy.ts +114 -0
- package/core/infra/connectors/codex/CodexTurn.ts +133 -0
- package/core/infra/connectors/codex/contracts.ts +82 -0
- package/core/infra/connectors/codex.ts +26 -0
- package/core/infra/connectors/llm.ts +78 -0
- package/core/infra/connectors/llmRouter.ts +443 -0
- package/core/infra/connectors/openaiCompatible.ts +590 -0
- package/core/infra/connectors/openaiTextStream.ts +134 -0
- package/core/infra/connectors/opensearch.ts +307 -0
- package/core/infra/connectors/postgres.ts +105 -0
- package/core/infra/connectors/redis.ts +132 -0
- package/core/infra/index.ts +58 -0
- package/core/kernel/ApplicationLifetime.ts +91 -0
- package/core/kernel/Bazis.ts +73 -0
- package/core/kernel/Environment.ts +53 -0
- package/core/kernel/Kernel.ts +391 -0
- package/core/kernel/KernelBuilder.ts +172 -0
- package/core/kernel/LifecycleCoordinator.ts +204 -0
- package/core/kernel/README.md +66 -0
- package/core/kernel/SupervisedHostedService.ts +147 -0
- package/core/kernel/config/ConfigRegistry.ts +48 -0
- package/core/kernel/config/Configuration.ts +110 -0
- package/core/kernel/config/README.md +62 -0
- package/core/kernel/config/Secret.ts +29 -0
- package/core/kernel/config/addConfigOptions.ts +63 -0
- package/core/kernel/config/defineConfig.ts +211 -0
- package/core/kernel/config/sources.ts +123 -0
- package/core/kernel/correlation/requestContext.ts +57 -0
- package/core/kernel/errors/ConfigKeyMissingError.ts +7 -0
- package/core/kernel/errors/KernelError.ts +7 -0
- package/core/kernel/errors/ShutdownTimeoutError.ts +7 -0
- package/core/kernel/errors/StartupAbortedError.ts +7 -0
- package/core/kernel/errors/StartupTimeoutError.ts +7 -0
- package/core/kernel/errors/index.ts +5 -0
- package/core/kernel/events/EventBus.ts +140 -0
- package/core/kernel/events/EventToken.ts +17 -0
- package/core/kernel/events/eventSubscribers.ts +126 -0
- package/core/kernel/events/kernelEvents.ts +16 -0
- package/core/kernel/events/onEvent.ts +32 -0
- package/core/kernel/health/HealthCheckContracts.ts +42 -0
- package/core/kernel/health/HealthService.ts +93 -0
- package/core/kernel/index.ts +96 -0
- package/core/kernel/internal/awaitAbortable.ts +18 -0
- package/core/kernel/internal/awaitShutdown.ts +21 -0
- package/core/kernel/internal/reportDiagnosticFailure.ts +8 -0
- package/core/kernel/internal/validateTimeout.ts +8 -0
- package/core/kernel/lifecycleHooks.ts +18 -0
- package/core/kernel/logging/ConsoleLogger.ts +68 -0
- package/core/kernel/logging/Logger.ts +22 -0
- package/core/kernel/types/ConfigSource.ts +9 -0
- package/core/kernel/types/EnvironmentName.ts +1 -0
- package/core/kernel/types/KernelOptions.ts +23 -0
- package/core/kernel/types/LifecycleHook.ts +18 -0
- package/core/kernel/types/index.ts +4 -0
- package/core/orm/DATABASE_PROVIDER.ts +5 -0
- package/core/orm/OrmHostedPlan.validator.ts +55 -0
- package/core/orm/OrmLifecycle.ts +146 -0
- package/core/orm/OrmOwnedStoreLifecycle.ts +96 -0
- package/core/orm/buildOrmProviders.ts +152 -0
- package/core/orm/databaseConnector.ts +108 -0
- package/core/orm/encapsulationHook.ts +19 -0
- package/core/orm/index.ts +95 -0
- package/core/orm/listQuery.ts +198 -0
- package/core/orm/ormModule.ts +173 -0
- package/core/orm/ownedStoreContributions.ts +195 -0
- package/core/orm/registerRepositories.ts +27 -0
- package/core/orm/repository.ts +45 -0
- package/core/orm/strictSchemaHostedIdentity.ts +17 -0
- package/core/scripts/agent-codegen.ts +1166 -0
- package/core/scripts/di-generate-target.ts +90 -0
- package/core/scripts/di-generate.ts +1837 -0
- package/core/scripts/orm-predicate-codegen.ts +33 -0
- package/core/scripts/request-model-codegen.ts +326 -0
- package/core/websocket/DESIGN.md +297 -0
- package/core/websocket/WsRateLimiter.ts +101 -0
- package/core/websocket/adapter/adapter.interface.ts +45 -0
- package/core/websocket/adapter/in-memory.adapter.ts +190 -0
- package/core/websocket/adapter/redis-delivery-scripts.ts +135 -0
- package/core/websocket/adapter/redis-delivery-store.ts +86 -0
- package/core/websocket/adapter/redis-operations.ts +33 -0
- package/core/websocket/adapter/redis.adapter.ts +218 -0
- package/core/websocket/codec/binary.codec.ts +87 -0
- package/core/websocket/codec/json.codec.ts +34 -0
- package/core/websocket/codec/packet-codec.interface.ts +17 -0
- package/core/websocket/decorators.ts +165 -0
- package/core/websocket/explorer.ts +126 -0
- package/core/websocket/index.ts +78 -0
- package/core/websocket/middleware.ts +146 -0
- package/core/websocket/packet-codec.ts +75 -0
- package/core/websocket/reliable-delivery.ts +56 -0
- package/core/websocket/session-creation.ts +16 -0
- package/core/websocket/session-manager.ts +856 -0
- package/core/websocket/socket-wrapper.ts +314 -0
- package/core/websocket/topic-cache.ts +59 -0
- package/core/websocket/types.ts +122 -0
- package/core/websocket/websocketModule.ts +96 -0
- package/core/websocket/ws-dispatch.ts +123 -0
- package/core/websocket/ws-server.ts +1707 -0
- package/index.ts +281 -0
- package/library/boundary/diagnostics-v1.ts +136 -0
- package/library/boundary/index.ts +73 -0
- package/library/boundary/json/decodeBoundedJsonV1.ts +518 -0
- package/library/boundary/json/limits-v1.ts +46 -0
- package/library/boundary/json/normalizeBoundedJsonV1.ts +256 -0
- package/library/boundary/json/types-v1.ts +11 -0
- package/library/boundary/schema/boundarySchemaHashV1.ts +6 -0
- package/library/boundary/schema/collectBoundaryValueDiagnosticsV1.ts +325 -0
- package/library/boundary/schema/decodeBoundarySchemaV1.ts +646 -0
- package/library/boundary/schema/index.ts +8 -0
- package/library/boundary/schema/types-v1.ts +99 -0
- package/library/boundary/schema/validateBoundaryValueV1.ts +58 -0
- package/library/boundary/serialization/canonicalJsonV1.ts +66 -0
- package/library/http-client/HttpClient.ts +969 -0
- package/library/http-client/HttpClientFactory.ts +45 -0
- package/library/http-client/errors.ts +68 -0
- package/library/http-client/index.ts +43 -0
- package/library/http-client/interceptors.ts +40 -0
- package/library/http-client/progress.ts +62 -0
- package/library/http-client/redirectProtocol.ts +6 -0
- package/library/http-client/serialize.ts +133 -0
- package/library/http-client/types.ts +174 -0
- package/library/jsonapi/ListQueryError.ts +25 -0
- package/library/jsonapi/ListRequest.ts +37 -0
- package/library/jsonapi/decorators.ts +161 -0
- package/library/jsonapi/document.ts +124 -0
- package/library/jsonapi/index.ts +40 -0
- package/library/jsonapi/parseListQuery.ts +291 -0
- package/library/jsonapi/types.ts +125 -0
- package/library/jwt/JwtEncoder.ts +40 -0
- package/library/jwt/JwtKeyRing.ts +164 -0
- package/library/jwt/JwtValidator.ts +210 -0
- package/library/jwt/TokenIssuer.ts +149 -0
- package/library/jwt/TokenService.ts +51 -0
- package/library/jwt/base64url.ts +61 -0
- package/library/jwt/claims.ts +36 -0
- package/library/jwt/errors.ts +44 -0
- package/library/jwt/index.ts +59 -0
- package/library/jwt/limits.ts +8 -0
- package/library/jwt/signing/Hs256Algorithm.ts +62 -0
- package/library/jwt/signing/Rs256Algorithm.ts +133 -0
- package/library/jwt/signing/SigningAlgorithm.ts +18 -0
- package/library/jwt/signing/pem.ts +30 -0
- package/library/openapi/codegen.ts +875 -0
- package/library/openapi/document.ts +326 -0
- package/library/openapi/index.ts +13 -0
- package/library/openapi/types.ts +69 -0
- package/library/openapi/ui.ts +182 -0
- package/library/orm/DatabaseFacade.ts +209 -0
- package/library/orm/DbContext.ts +107 -0
- package/library/orm/DbContextFactory.ts +25 -0
- package/library/orm/DbContextOptions.ts +33 -0
- package/library/orm/Metadata/DynamicModelBuilder.ts +497 -0
- package/library/orm/Metadata/KeyTuple.ts +36 -0
- package/library/orm/Metadata/ModelBuilder.ts +299 -0
- package/library/orm/Metadata/OrmModel.ts +80 -0
- package/library/orm/Metadata/ValueConverter.ts +57 -0
- package/library/orm/Metadata/conventions.ts +54 -0
- package/library/orm/Metadata/decorators.ts +396 -0
- package/library/orm/Metadata/types.ts +140 -0
- package/library/orm/Providers/OwnedStoreCatalog.reader.ts +1802 -0
- package/library/orm/Providers/PostgresDialect.ts +234 -0
- package/library/orm/Providers/PostgresProvider.ts +1494 -0
- package/library/orm/Providers/PostgresSchema.decoder.ts +107 -0
- package/library/orm/Providers/limits.ts +29 -0
- package/library/orm/Providers/operationDeadline.ts +43 -0
- package/library/orm/Providers/ormOwnedStoreRuntime.ts +19 -0
- package/library/orm/Providers/ormTransactionRuntime.ts +117 -0
- package/library/orm/Providers/propertyConversion.ts +14 -0
- package/library/orm/Providers/resolvedForeignKey.ts +16 -0
- package/library/orm/Providers/transactionCallbacks.ts +45 -0
- package/library/orm/Providers/transactionOutcome.ts +82 -0
- package/library/orm/Providers/transactionScopes.ts +78 -0
- package/library/orm/Providers/types.ts +207 -0
- package/library/orm/Query/DbSet.ts +78 -0
- package/library/orm/Query/EntityQuery.ts +247 -0
- package/library/orm/Query/ImmediateMutations.ts +226 -0
- package/library/orm/Query/IncludeLoader.ts +200 -0
- package/library/orm/Query/QueryPlan.ts +55 -0
- package/library/orm/Query/SqlTranslator.ts +195 -0
- package/library/orm/Query/conditions.ts +127 -0
- package/library/orm/Query/materialize.ts +39 -0
- package/library/orm/README.md +256 -0
- package/library/orm/Repository/README.md +684 -0
- package/library/orm/Repository/Repository.ts +68 -0
- package/library/orm/Saving/CommandBuilder.ts +139 -0
- package/library/orm/Saving/ExecutionStrategy.ts +128 -0
- package/library/orm/Saving/SaveExecutor.ts +413 -0
- package/library/orm/Saving/applyConventions.ts +64 -0
- package/library/orm/Schema/CheckExpression.ts +327 -0
- package/library/orm/Schema/ExactSchemaVerifier.ts +168 -0
- package/library/orm/Schema/ExpectedSchema.ts +43 -0
- package/library/orm/Schema/MigrationRunner.ts +146 -0
- package/library/orm/Schema/Migrator.ts +73 -0
- package/library/orm/Schema/OrmOwnedStore.ts +123 -0
- package/library/orm/Schema/OwnedStoreAdmission.ts +752 -0
- package/library/orm/Schema/OwnedStoreCanonical.ts +273 -0
- package/library/orm/Schema/OwnedStoreCatalog.ts +1325 -0
- package/library/orm/Schema/SafeAdditiveSchema.ts +100 -0
- package/library/orm/Schema/SchemaAdmission.ts +62 -0
- package/library/orm/Schema/SchemaDiffer.ts +78 -0
- package/library/orm/Schema/formatSchemaDifferences.ts +62 -0
- package/library/orm/Schema/introspection.ts +52 -0
- package/library/orm/Schema/physicalColumnTypes.ts +37 -0
- package/library/orm/Schema/tableKey.ts +26 -0
- package/library/orm/Tracking/ChangeTracker.ts +448 -0
- package/library/orm/Tracking/EntityState.ts +13 -0
- package/library/orm/Transactions/OrmTransaction.ts +30 -0
- package/library/orm/Transactions/TransactionScopeCoordinator.ts +377 -0
- package/library/orm/errors.ts +190 -0
- package/library/orm/index.ts +162 -0
- package/library/orm/runtime.ts +14 -0
- package/library/redaction/index.ts +166 -0
- package/library/ui/UiProfile.ts +77 -0
- package/library/ui/compiled-v1.ts +131 -0
- package/library/ui/compiler/compileUiSurfaceV1.ts +717 -0
- package/library/ui/compiler/contracts-v1.ts +25 -0
- package/library/ui/compiler/index.ts +12 -0
- package/library/ui/compiler/openApiIndex.ts +246 -0
- package/library/ui/defineUiProfile.ts +340 -0
- package/library/ui/diagnostics-v1.ts +33 -0
- package/library/ui/index.ts +129 -0
- package/library/ui/profile-v1.ts +191 -0
- package/library/ui/serialization.ts +130 -0
- package/library/ui/uiProfileAuthoring.ts +289 -0
- package/library/ui/uiProfileAuthoringMetadata.ts +78 -0
- package/library/validation/MessageRegistry.ts +144 -0
- package/library/validation/README.md +62 -0
- package/library/validation/RuleCompiler.ts +183 -0
- package/library/validation/RuleEngine.ts +401 -0
- package/library/validation/SPEC.md +579 -0
- package/library/validation/ValidationError.ts +37 -0
- package/library/validation/ValidationResult.ts +63 -0
- package/library/validation/Validator.ts +101 -0
- package/library/validation/index.ts +16 -0
- package/library/validation/metadata.ts +69 -0
- package/library/validation/modelValidatorAdapter.ts +119 -0
- package/library/validation/types/ValidationCode.ts +69 -0
- package/library/validation/types/ValidatorOptions.ts +153 -0
- package/package.json +80 -0
|
@@ -0,0 +1,2130 @@
|
|
|
1
|
+
import { ToolContractValidator, toolContractName, toolErrorMessage as errorMessageOf, type AgentToolSchemaValidator } from "./internal/ToolContract.validator";
|
|
2
|
+
import { ToolAuditProjector, type AgentToolAuditEntry, type AgentToolAuditFailureMode } from "./internal/ToolAudit.projector";
|
|
3
|
+
export type { AgentToolValidationIssue, AgentToolValidationResult, AgentToolSchemaValidator } from "./internal/ToolContract.validator";
|
|
4
|
+
export type { AgentToolAuditPhase, AgentToolAuditFailureMode, AgentToolAuditToolInfo, AgentToolAuditEntry } from "./internal/ToolAudit.projector";
|
|
5
|
+
import { DiContainer, type Class, type ServiceProvider, type ServiceScope } from "../di";
|
|
6
|
+
import { assertAgentToolHookIdV1, getAgentModuleContributionsV1, type AgentModuleContributionSnapshotV1, type AgentOwnedHookRegistrationV1 } from "./moduleContributions-v1";
|
|
7
|
+
import type { AgentToolBeforeEffectEventV1, AgentToolEnforcementDecisionV1, AgentToolHookContextV1, AgentToolRequiredHookRefV1, AgentToolAuditHookProjectionV1, AgentToolSettlementEventV1 } from "./AgentToolHooks";
|
|
8
|
+
import { agentModelJson } from "./internal/AgentModelBinding";
|
|
9
|
+
import { redactSensitive, type SensitiveRedactionOptions } from "../../library/redaction";
|
|
10
|
+
import { AgentSetupError, AgentToolExecutionError, AgentToolPreCommitError } from "./errors";
|
|
11
|
+
import type { AgentRegistry, ToolDefinition } from "./AgentRegistry";
|
|
12
|
+
import type { JsonObject, JsonValue, AgentToolCall, AgentToolResult } from "./semantic";
|
|
13
|
+
import { agentToolCall, agentToolResult, normalizeJsonValue } from "./semantic";
|
|
14
|
+
|
|
15
|
+
const DEFAULT_RETRYABLE_ERROR_CODES = new Set(["TOOL_EXECUTION_FAILED", "TOOL_RESOLVE_FAILED"]);
|
|
16
|
+
const NEVER_RETRY_ERROR_CODES = new Set([
|
|
17
|
+
"TOOL_AUDIT_FAILED",
|
|
18
|
+
"TOOL_AUDIT_FAILED_OUTCOME_UNKNOWN",
|
|
19
|
+
"TOOL_SCOPE_DISPOSE_FAILED",
|
|
20
|
+
"TOOL_SCOPE_DISPOSE_FAILED_OUTCOME_UNKNOWN",
|
|
21
|
+
"TOOL_POST_EXECUTION_FAILED_OUTCOME_UNKNOWN",
|
|
22
|
+
"TOOL_EXECUTION_FAILED_OUTCOME_UNKNOWN",
|
|
23
|
+
"TOOL_TIMEOUT",
|
|
24
|
+
"TOOL_TIMEOUT_OUTCOME_UNKNOWN",
|
|
25
|
+
"TOOL_ABORTED",
|
|
26
|
+
"TOOL_ABORTED_OUTCOME_UNKNOWN",
|
|
27
|
+
"TOOL_HOOK_DENIED",
|
|
28
|
+
"TOOL_HOOK_UNAVAILABLE",
|
|
29
|
+
"TOOL_HOOK_FAILED",
|
|
30
|
+
"TOOL_HOOK_TIMEOUT",
|
|
31
|
+
"TOOL_HOOK_CANCELLED",
|
|
32
|
+
"TOOL_HOOK_DECISION_INVALID",
|
|
33
|
+
"TOOL_HOOK_AUDIT_FAILED",
|
|
34
|
+
"TOOL_HOOK_REDACTION_FAILED",
|
|
35
|
+
"TOOL_HOOK_SETTLEMENT_FAILED",
|
|
36
|
+
"TOOL_HOOK_SETTLEMENT_OUTCOME_UNKNOWN",
|
|
37
|
+
]);
|
|
38
|
+
const DEFAULT_RETRY_SETTINGS: RetrySettings = Object.freeze({
|
|
39
|
+
maxAttempts: 1,
|
|
40
|
+
delayMs: 0,
|
|
41
|
+
backoff: "none",
|
|
42
|
+
});
|
|
43
|
+
const DEFAULT_SCOPE_DISPOSE_TIMEOUT_MS = 5_000;
|
|
44
|
+
const DEFAULT_TOOL_TIMEOUT_MS = 30_000;
|
|
45
|
+
|
|
46
|
+
export type AgentToolExecutionApproval = "not-required" | "approved";
|
|
47
|
+
export type AgentToolApprovalReason = "policy" | "required";
|
|
48
|
+
export type AgentToolRetryBackoff = "none" | "fixed" | "exponential";
|
|
49
|
+
|
|
50
|
+
export interface AgentToolExecutionContext {
|
|
51
|
+
readonly call: AgentToolCall;
|
|
52
|
+
readonly tool: ToolDefinition;
|
|
53
|
+
readonly agentName?: string;
|
|
54
|
+
readonly invocationId?: string;
|
|
55
|
+
readonly attempt: number;
|
|
56
|
+
readonly maxAttempts: number;
|
|
57
|
+
readonly idempotencyKey?: string;
|
|
58
|
+
readonly approval: AgentToolExecutionApproval;
|
|
59
|
+
readonly metadata: JsonObject;
|
|
60
|
+
readonly signal: AbortSignal;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface AgentExecutableTool {
|
|
64
|
+
execute(input: unknown, context: AgentToolExecutionContext): unknown | Promise<unknown>;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface AgentToolApprovalRequest {
|
|
68
|
+
readonly call: AgentToolCall;
|
|
69
|
+
readonly tool: ToolDefinition;
|
|
70
|
+
readonly reason: AgentToolApprovalReason;
|
|
71
|
+
readonly agentName?: string;
|
|
72
|
+
readonly invocationId?: string;
|
|
73
|
+
readonly idempotencyKey?: string;
|
|
74
|
+
readonly metadata: JsonObject;
|
|
75
|
+
readonly signal?: AbortSignal;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export type AgentToolApprovalDecision = boolean | Promise<boolean>;
|
|
79
|
+
export type AgentToolApprovalFunction = (request: AgentToolApprovalRequest) => AgentToolApprovalDecision;
|
|
80
|
+
|
|
81
|
+
export interface AgentToolApprovalPolicyObject {
|
|
82
|
+
approve(request: AgentToolApprovalRequest): AgentToolApprovalDecision;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export type AgentToolApprovalPolicy = AgentToolApprovalFunction | AgentToolApprovalPolicyObject;
|
|
86
|
+
|
|
87
|
+
export type AgentToolAuditFunction = (entry: AgentToolAuditEntry) => void | Promise<void>;
|
|
88
|
+
|
|
89
|
+
export interface AgentToolAuditSinkObject {
|
|
90
|
+
record(entry: AgentToolAuditEntry): void | Promise<void>;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export type AgentToolAuditSink = AgentToolAuditFunction | AgentToolAuditSinkObject;
|
|
94
|
+
|
|
95
|
+
export interface AgentToolRetryPolicy {
|
|
96
|
+
readonly maxAttempts?: number;
|
|
97
|
+
readonly delayMs?: number;
|
|
98
|
+
readonly maxDelayMs?: number;
|
|
99
|
+
readonly backoff?: AgentToolRetryBackoff;
|
|
100
|
+
readonly retryOnErrorCodes?: readonly string[];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export interface AgentToolExecutorOptions {
|
|
104
|
+
readonly approvalPolicy?: AgentToolApprovalPolicy;
|
|
105
|
+
readonly schemaValidator?: AgentToolSchemaValidator;
|
|
106
|
+
readonly auditSink?: AgentToolAuditSink;
|
|
107
|
+
readonly auditFailureMode?: AgentToolAuditFailureMode;
|
|
108
|
+
readonly retryPolicy?: AgentToolRetryPolicy;
|
|
109
|
+
/** Whole tool-call deadline. Defaults to 30 seconds; 0 explicitly disables it. */
|
|
110
|
+
readonly defaultTimeoutMs?: number;
|
|
111
|
+
/** Maximum time to wait for a tool DI scope disposer after execution settles. */
|
|
112
|
+
readonly scopeDisposeTimeoutMs?: number;
|
|
113
|
+
readonly allowUnboundTools?: boolean;
|
|
114
|
+
readonly allowUnvalidatedSchemas?: boolean;
|
|
115
|
+
/** Side-effecting tools require an audit sink unless this explicit escape hatch is set. */
|
|
116
|
+
readonly allowUnauditedSideEffects?: boolean;
|
|
117
|
+
/** Audit entries are redacted by default; pass false only for trusted local debugging. */
|
|
118
|
+
readonly auditRedaction?: SensitiveRedactionOptions | false;
|
|
119
|
+
readonly requiredPlatformHooks?: readonly AgentToolRequiredHookRefV1[];
|
|
120
|
+
readonly hookRedaction?: SensitiveRedactionOptions;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export interface AgentToolExecutionOptions {
|
|
124
|
+
readonly agentName?: string;
|
|
125
|
+
readonly invocationId?: string;
|
|
126
|
+
readonly metadata?: unknown;
|
|
127
|
+
readonly approvalPolicy?: AgentToolApprovalPolicy;
|
|
128
|
+
readonly schemaValidator?: AgentToolSchemaValidator;
|
|
129
|
+
readonly auditSink?: AgentToolAuditSink;
|
|
130
|
+
readonly auditFailureMode?: AgentToolAuditFailureMode;
|
|
131
|
+
readonly retryPolicy?: AgentToolRetryPolicy;
|
|
132
|
+
readonly idempotencyKey?: string;
|
|
133
|
+
/** Whole tool-call deadline override; 0 explicitly disables it. */
|
|
134
|
+
readonly timeoutMs?: number;
|
|
135
|
+
/** Per-call override for the bounded DI scope disposal wait. */
|
|
136
|
+
readonly scopeDisposeTimeoutMs?: number;
|
|
137
|
+
readonly allowUnboundTools?: boolean;
|
|
138
|
+
readonly allowUnvalidatedSchemas?: boolean;
|
|
139
|
+
readonly allowUnauditedSideEffects?: boolean;
|
|
140
|
+
readonly signal?: AbortSignal;
|
|
141
|
+
/** Overrides executor-level audit redaction for this execution. */
|
|
142
|
+
readonly auditRedaction?: SensitiveRedactionOptions | false;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
interface ApprovalDecision {
|
|
146
|
+
readonly allowed: boolean;
|
|
147
|
+
readonly executionApproval: AgentToolExecutionApproval;
|
|
148
|
+
readonly code?: string;
|
|
149
|
+
readonly message?: string;
|
|
150
|
+
readonly reason?: AgentToolApprovalReason;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
interface AgentVisibilityDecision {
|
|
154
|
+
readonly allowed: boolean;
|
|
155
|
+
readonly agentName?: string;
|
|
156
|
+
readonly code?: string;
|
|
157
|
+
readonly message?: string;
|
|
158
|
+
readonly status?: "error" | "denied";
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
interface SchemaValidationSettings {
|
|
162
|
+
readonly validator?: AgentToolSchemaValidator;
|
|
163
|
+
readonly allowUnvalidatedSchemas: boolean;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
interface AuditSettings {
|
|
167
|
+
readonly sink?: AgentToolAuditSink;
|
|
168
|
+
readonly failureMode: AgentToolAuditFailureMode;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
interface RetrySettings {
|
|
172
|
+
readonly maxAttempts: number;
|
|
173
|
+
readonly delayMs: number;
|
|
174
|
+
readonly maxDelayMs?: number;
|
|
175
|
+
readonly backoff: AgentToolRetryBackoff;
|
|
176
|
+
readonly retryOnErrorCodes?: ReadonlySet<string>;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
interface AttemptContext {
|
|
180
|
+
readonly attempt: number;
|
|
181
|
+
readonly maxAttempts: number;
|
|
182
|
+
readonly idempotencyKey?: string;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
interface ToolExecutionState {
|
|
186
|
+
executeStarted: boolean;
|
|
187
|
+
executeCompleted: boolean;
|
|
188
|
+
phase: "pre-execute" | "execute" | "post-execute" | "result-audit";
|
|
189
|
+
deadlineUnixMs?: number;
|
|
190
|
+
beforeEffect?: AgentToolBeforeEffectEventV1;
|
|
191
|
+
retryableCandidate?: boolean;
|
|
192
|
+
callerSignal?: AbortSignal;
|
|
193
|
+
timedOut?: boolean;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
type PreparedToolValue =
|
|
197
|
+
| { readonly ok: true; readonly value: unknown }
|
|
198
|
+
| { readonly ok: false; readonly result: AgentToolResult };
|
|
199
|
+
|
|
200
|
+
class AgentToolTimeoutError extends Error {
|
|
201
|
+
constructor() {
|
|
202
|
+
super("Tool execution timed out.");
|
|
203
|
+
this.name = "AgentToolTimeoutError";
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
class AgentToolAbortError extends Error {
|
|
208
|
+
constructor() {
|
|
209
|
+
super("Tool execution was aborted.");
|
|
210
|
+
this.name = "AgentToolAbortError";
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function isPlainJsonObject(value: JsonValue): value is JsonObject {
|
|
215
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function isObjectRecord(value: unknown): value is Record<string, unknown> {
|
|
219
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function normalizeMetadata(value: unknown): JsonObject {
|
|
223
|
+
const normalized = normalizeJsonValue(value ?? {}, "toolExecution.metadata");
|
|
224
|
+
if (!isPlainJsonObject(normalized)) {
|
|
225
|
+
throw new AgentToolExecutionError("toolExecution.metadata must be a JSON object.");
|
|
226
|
+
}
|
|
227
|
+
return normalized;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function assertPositiveInteger(value: number | undefined, field: string): void {
|
|
231
|
+
if (value === undefined) {
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
if (!Number.isInteger(value) || value <= 0) {
|
|
235
|
+
throw new AgentToolExecutionError(`${field} must be a positive integer.`);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function assertNonNegativeInteger(value: number | undefined, field: string): void {
|
|
240
|
+
if (value === undefined) {
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
if (!Number.isInteger(value) || value < 0) {
|
|
244
|
+
throw new AgentToolExecutionError(`${field} must be a non-negative integer.`);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function normalizeOptionalText(value: unknown, field: string): string | undefined {
|
|
249
|
+
if (value === undefined) {
|
|
250
|
+
return undefined;
|
|
251
|
+
}
|
|
252
|
+
if (typeof value !== "string") {
|
|
253
|
+
throw new AgentToolExecutionError(`${field} must be a string.`);
|
|
254
|
+
}
|
|
255
|
+
const trimmed = value.trim();
|
|
256
|
+
if (trimmed.length === 0) {
|
|
257
|
+
throw new AgentToolExecutionError(`${field} must be a non-empty string.`);
|
|
258
|
+
}
|
|
259
|
+
return trimmed;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function approvalPolicyFunction(policy: AgentToolApprovalPolicy): AgentToolApprovalFunction {
|
|
263
|
+
if (typeof policy === "function") {
|
|
264
|
+
return policy;
|
|
265
|
+
}
|
|
266
|
+
return (request) => policy.approve(request);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function auditSinkFunction(sink: AgentToolAuditSink): AgentToolAuditFunction {
|
|
270
|
+
if (typeof sink === "function") {
|
|
271
|
+
return sink;
|
|
272
|
+
}
|
|
273
|
+
return (entry) => sink.record(entry);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function auditFailureModeOf(
|
|
277
|
+
value: AgentToolAuditFailureMode | undefined,
|
|
278
|
+
field: string,
|
|
279
|
+
): AgentToolAuditFailureMode | undefined {
|
|
280
|
+
if (value === undefined) {
|
|
281
|
+
return undefined;
|
|
282
|
+
}
|
|
283
|
+
if (value !== "fail-closed" && value !== "best-effort") {
|
|
284
|
+
throw new AgentToolExecutionError(`${field} must be fail-closed or best-effort.`);
|
|
285
|
+
}
|
|
286
|
+
return value;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function retryBackoffOf(value: AgentToolRetryBackoff | undefined, field: string): AgentToolRetryBackoff | undefined {
|
|
290
|
+
if (value === undefined) {
|
|
291
|
+
return undefined;
|
|
292
|
+
}
|
|
293
|
+
if (value !== "none" && value !== "fixed" && value !== "exponential") {
|
|
294
|
+
throw new AgentToolExecutionError(`${field} must be none, fixed or exponential.`);
|
|
295
|
+
}
|
|
296
|
+
return value;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function hasExecute(value: unknown): value is AgentExecutableTool {
|
|
300
|
+
return typeof (value as { execute?: unknown }).execute === "function";
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function isHookDecision(value: unknown): value is AgentToolEnforcementDecisionV1 {
|
|
304
|
+
try {
|
|
305
|
+
if (!isObjectRecord(value) || (value.decision !== "allow" && value.decision !== "deny")) return false;
|
|
306
|
+
const allowed = value.decision === "allow" ? ["decision", "evidence"] : ["decision", "reasonCode", "message", "evidence"];
|
|
307
|
+
if (!Object.keys(value).every((key) => allowed.includes(key))) return false;
|
|
308
|
+
if (value.decision === "deny" && ((value.reasonCode !== undefined && (typeof value.reasonCode !== "string" || value.reasonCode.length > 128)) || (value.message !== undefined && (typeof value.message !== "string" || value.message.length > 512)))) return false;
|
|
309
|
+
if (value.evidence !== undefined) normalizeHookEvidence(value.evidence);
|
|
310
|
+
return true;
|
|
311
|
+
} catch { return false; }
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function normalizeHookEvidence(value: unknown): JsonObject {
|
|
315
|
+
const normalized = normalizeJsonValue(value, "hook.evidence");
|
|
316
|
+
if (!isPlainJsonObject(normalized)) throw new Error("hook evidence must be a JSON object");
|
|
317
|
+
if (JSON.stringify(normalized).length > 32_768) throw new Error("hook evidence is too large");
|
|
318
|
+
return normalized;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function isSettlementResult(value: unknown): value is { readonly status: "recorded"; readonly evidence?: JsonObject } {
|
|
322
|
+
try {
|
|
323
|
+
return isObjectRecord(value) && value.status === "recorded"
|
|
324
|
+
&& Object.keys(value).every((key) => key === "status" || key === "evidence")
|
|
325
|
+
&& (value.evidence === undefined || (normalizeHookEvidence(value.evidence), true));
|
|
326
|
+
} catch { return false; }
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
type HookOutcome<T> = { readonly kind: "value"; readonly value: T } | { readonly kind: "timeout" } | { readonly kind: "cancelled" } | { readonly kind: "failed" };
|
|
330
|
+
|
|
331
|
+
async function awaitHook<T>(operation: Promise<T>, signal: AbortSignal | undefined, timeoutMs: number | undefined): Promise<HookOutcome<T>> {
|
|
332
|
+
// Observe both branches even after the caller's deadline wins: a late hook
|
|
333
|
+
// rejection must never become an unhandled process-level rejection.
|
|
334
|
+
const observed: Promise<HookOutcome<T>> = operation
|
|
335
|
+
.then((value): HookOutcome<T> => ({ kind: "value", value }))
|
|
336
|
+
.catch((): HookOutcome<T> => ({ kind: "failed" }));
|
|
337
|
+
if (signal?.aborted) return { kind: "cancelled" };
|
|
338
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
339
|
+
let detach: (() => void) | undefined;
|
|
340
|
+
const boundary = new Promise<HookOutcome<T>>((resolve) => {
|
|
341
|
+
const finish = (outcome: HookOutcome<T>) => {
|
|
342
|
+
if (timer !== undefined) clearTimeout(timer);
|
|
343
|
+
detach?.();
|
|
344
|
+
resolve(outcome);
|
|
345
|
+
};
|
|
346
|
+
if (timeoutMs !== undefined) timer = setTimeout(() => finish({ kind: "timeout" }), timeoutMs);
|
|
347
|
+
if (signal) {
|
|
348
|
+
const abort = () => finish({ kind: "cancelled" });
|
|
349
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
350
|
+
detach = () => signal.removeEventListener("abort", abort);
|
|
351
|
+
}
|
|
352
|
+
});
|
|
353
|
+
try {
|
|
354
|
+
return await Promise.race([observed, boundary]);
|
|
355
|
+
} finally {
|
|
356
|
+
if (timer !== undefined) clearTimeout(timer);
|
|
357
|
+
detach?.();
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function timed<T>(
|
|
362
|
+
operation: Promise<T>,
|
|
363
|
+
timeoutMs: number | undefined,
|
|
364
|
+
abort: () => void,
|
|
365
|
+
signal?: AbortSignal,
|
|
366
|
+
): Promise<T> {
|
|
367
|
+
if (signal?.aborted) {
|
|
368
|
+
return Promise.reject(new AgentToolAbortError());
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
return new Promise<T>((resolve, reject) => {
|
|
372
|
+
let settled = false;
|
|
373
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
374
|
+
const finish = (callback: () => void): void => {
|
|
375
|
+
if (settled) return;
|
|
376
|
+
settled = true;
|
|
377
|
+
if (timer !== undefined) clearTimeout(timer);
|
|
378
|
+
signal?.removeEventListener("abort", onAbort);
|
|
379
|
+
callback();
|
|
380
|
+
};
|
|
381
|
+
const onAbort = () => finish(() => reject(new AgentToolAbortError()));
|
|
382
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
383
|
+
timer = timeoutMs === undefined
|
|
384
|
+
? undefined
|
|
385
|
+
: setTimeout(() => finish(() => {
|
|
386
|
+
reject(new AgentToolTimeoutError());
|
|
387
|
+
abort();
|
|
388
|
+
}), timeoutMs);
|
|
389
|
+
|
|
390
|
+
operation.then(
|
|
391
|
+
(value) => {
|
|
392
|
+
finish(() => resolve(value));
|
|
393
|
+
},
|
|
394
|
+
(error: unknown) => {
|
|
395
|
+
finish(() => reject(error));
|
|
396
|
+
},
|
|
397
|
+
);
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function attachAbortForwarding(source: AbortSignal | undefined, target: AbortController): (() => void) | undefined {
|
|
402
|
+
if (source === undefined) return undefined;
|
|
403
|
+
if (source.aborted) {
|
|
404
|
+
target.abort(source.reason);
|
|
405
|
+
return undefined;
|
|
406
|
+
}
|
|
407
|
+
const abort = () => target.abort(source.reason);
|
|
408
|
+
source.addEventListener("abort", abort, { once: true });
|
|
409
|
+
return () => source.removeEventListener("abort", abort);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
export class AgentToolExecutor {
|
|
413
|
+
private readonly approvalPolicy?: AgentToolApprovalPolicy;
|
|
414
|
+
private readonly schemaValidator?: AgentToolSchemaValidator;
|
|
415
|
+
private readonly auditSink?: AgentToolAuditSink;
|
|
416
|
+
private readonly auditFailureMode: AgentToolAuditFailureMode;
|
|
417
|
+
private readonly retryPolicy: RetrySettings;
|
|
418
|
+
private readonly defaultTimeoutMs?: number;
|
|
419
|
+
private readonly scopeDisposeTimeoutMs: number;
|
|
420
|
+
private readonly allowUnboundTools: boolean;
|
|
421
|
+
private readonly allowUnvalidatedSchemas: boolean;
|
|
422
|
+
private readonly allowUnauditedSideEffects: boolean;
|
|
423
|
+
private readonly auditProjector: ToolAuditProjector;
|
|
424
|
+
private readonly contractValidator = new ToolContractValidator();
|
|
425
|
+
private readonly hookRedaction: SensitiveRedactionOptions;
|
|
426
|
+
private readonly hookSnapshot?: AgentModuleContributionSnapshotV1;
|
|
427
|
+
private readonly requiredPlatformHooks: readonly AgentToolRequiredHookRefV1[];
|
|
428
|
+
|
|
429
|
+
constructor(
|
|
430
|
+
private readonly services: ServiceProvider,
|
|
431
|
+
private readonly registry: AgentRegistry,
|
|
432
|
+
options: AgentToolExecutorOptions = {},
|
|
433
|
+
) {
|
|
434
|
+
assertNonNegativeInteger(options.defaultTimeoutMs, "AgentToolExecutor.defaultTimeoutMs");
|
|
435
|
+
assertPositiveInteger(options.scopeDisposeTimeoutMs, "AgentToolExecutor.scopeDisposeTimeoutMs");
|
|
436
|
+
this.approvalPolicy = options.approvalPolicy;
|
|
437
|
+
this.schemaValidator = options.schemaValidator;
|
|
438
|
+
this.auditSink = options.auditSink;
|
|
439
|
+
this.auditFailureMode = auditFailureModeOf(options.auditFailureMode, "AgentToolExecutor.auditFailureMode") ?? "fail-closed";
|
|
440
|
+
this.retryPolicy = this.normalizeRetryPolicy(options.retryPolicy, "AgentToolExecutor.retryPolicy");
|
|
441
|
+
this.defaultTimeoutMs = options.defaultTimeoutMs === 0
|
|
442
|
+
? undefined
|
|
443
|
+
: options.defaultTimeoutMs ?? DEFAULT_TOOL_TIMEOUT_MS;
|
|
444
|
+
this.scopeDisposeTimeoutMs = options.scopeDisposeTimeoutMs ?? DEFAULT_SCOPE_DISPOSE_TIMEOUT_MS;
|
|
445
|
+
this.allowUnboundTools = options.allowUnboundTools === true;
|
|
446
|
+
this.allowUnvalidatedSchemas = options.allowUnvalidatedSchemas === true;
|
|
447
|
+
this.allowUnauditedSideEffects = options.allowUnauditedSideEffects === true;
|
|
448
|
+
this.auditProjector = new ToolAuditProjector(options.auditRedaction ?? {});
|
|
449
|
+
// Hook projections are always canonical redaction (`***` and default
|
|
450
|
+
// depth); callers may only extend the sensitive-key set.
|
|
451
|
+
this.hookRedaction = Object.freeze({ sensitiveKeys: options.hookRedaction?.sensitiveKeys ?? [] });
|
|
452
|
+
const requiredPlatformHooks = options.requiredPlatformHooks;
|
|
453
|
+
if (requiredPlatformHooks !== undefined && !Array.isArray(requiredPlatformHooks)) {
|
|
454
|
+
throw new AgentSetupError("requiredPlatformHooks must be an array.");
|
|
455
|
+
}
|
|
456
|
+
for (const reference of requiredPlatformHooks ?? []) this.assertRequiredPlatformHookRef(reference);
|
|
457
|
+
this.requiredPlatformHooks = Object.freeze([...(requiredPlatformHooks ?? [])]);
|
|
458
|
+
this.hookSnapshot = this.assertHookSetup(this.requiredPlatformHooks);
|
|
459
|
+
if (this.hookSnapshot) {
|
|
460
|
+
for (const tool of this.registry.listTools()) {
|
|
461
|
+
const matches = this.hookSnapshot.tools.filter((entry) => entry.payload.target === tool.target);
|
|
462
|
+
if (matches.length !== 1) throw new AgentSetupError(`Registered Tool "${tool.metadata.name}" must have exactly one owner-bound scoped contribution.`);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
private assertHookSetup(required: readonly AgentToolRequiredHookRefV1[] | undefined): AgentModuleContributionSnapshotV1 | undefined {
|
|
468
|
+
const requiredIdentities = new Set<string>();
|
|
469
|
+
for (const reference of required ?? []) {
|
|
470
|
+
const identity = `${reference.kind}:${reference.id}:${reference.version}`;
|
|
471
|
+
if (requiredIdentities.has(identity)) throw new AgentSetupError(`Duplicate required Agent Tool hook "${identity}".`);
|
|
472
|
+
requiredIdentities.add(identity);
|
|
473
|
+
}
|
|
474
|
+
if (!(this.services instanceof DiContainer)) {
|
|
475
|
+
if (required && required.length > 0) {
|
|
476
|
+
throw new AgentSetupError("requiredPlatformHooks require a framework DiContainer.");
|
|
477
|
+
}
|
|
478
|
+
return undefined;
|
|
479
|
+
}
|
|
480
|
+
const snapshot = getAgentModuleContributionsV1(this.services);
|
|
481
|
+
const mandatory = snapshot.hooks.some(({ payload }) =>
|
|
482
|
+
payload.registration.kind === "enforcement" || payload.registration.kind === "settlement",
|
|
483
|
+
);
|
|
484
|
+
if (mandatory && this.auditSink === undefined) {
|
|
485
|
+
throw new AgentSetupError("Agent Tool enforcement and settlement hooks require constructor auditSink.");
|
|
486
|
+
}
|
|
487
|
+
for (const reference of required ?? []) {
|
|
488
|
+
const found = snapshot.hooks.some(({ payload }) =>
|
|
489
|
+
payload.owner === reference.owner
|
|
490
|
+
&& payload.registration.kind === reference.kind
|
|
491
|
+
&& payload.registration.id === reference.id
|
|
492
|
+
&& payload.registration.version === reference.version,
|
|
493
|
+
);
|
|
494
|
+
if (!found) {
|
|
495
|
+
throw new AgentSetupError(`Required Agent Tool hook "${reference.kind}:${reference.id}@${reference.version}" is not contributed by its exact owner.`);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
return snapshot;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
private assertRequiredPlatformHookRef(reference: unknown): asserts reference is AgentToolRequiredHookRefV1 {
|
|
502
|
+
if (typeof reference !== "object" || reference === null) {
|
|
503
|
+
throw new AgentSetupError("requiredPlatformHooks entries must be hook references.");
|
|
504
|
+
}
|
|
505
|
+
const candidate = reference as Partial<AgentToolRequiredHookRefV1>;
|
|
506
|
+
if (typeof candidate.owner !== "function" && (typeof candidate.owner !== "object" || candidate.owner === null)) {
|
|
507
|
+
throw new AgentSetupError("requiredPlatformHooks entry owner must be an BazisModuleRef.");
|
|
508
|
+
}
|
|
509
|
+
if (candidate.kind !== "enforcement" && candidate.kind !== "settlement" && candidate.kind !== "observer") {
|
|
510
|
+
throw new AgentSetupError("requiredPlatformHooks entry kind is invalid.");
|
|
511
|
+
}
|
|
512
|
+
assertAgentToolHookIdV1(candidate.id);
|
|
513
|
+
const version = candidate.version;
|
|
514
|
+
if (typeof version !== "number" || !Number.isSafeInteger(version) || version <= 0) {
|
|
515
|
+
throw new AgentSetupError("requiredPlatformHooks entry version must be a positive safe integer.");
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
async execute(call: AgentToolCall, options: AgentToolExecutionOptions = {}): Promise<AgentToolResult> {
|
|
520
|
+
const startedAt = Date.now();
|
|
521
|
+
try {
|
|
522
|
+
call = agentToolCall(call);
|
|
523
|
+
} catch {
|
|
524
|
+
const original = agentToolResult({
|
|
525
|
+
callId: "invalid-tool-call",
|
|
526
|
+
name: "invalid-tool",
|
|
527
|
+
status: "error",
|
|
528
|
+
error: { code: "TOOL_CALL_INVALID", message: "Tool call is invalid." },
|
|
529
|
+
durationMs: Date.now() - startedAt,
|
|
530
|
+
metadata: {},
|
|
531
|
+
});
|
|
532
|
+
const requestedTimeout = typeof options.timeoutMs === "number" && Number.isSafeInteger(options.timeoutMs) && options.timeoutMs >= 0
|
|
533
|
+
? (options.timeoutMs === 0 ? undefined : options.timeoutMs)
|
|
534
|
+
: this.defaultTimeoutMs;
|
|
535
|
+
const failed = await this.recordFrameworkTerminalAudit(original, startedAt, options.signal, requestedTimeout);
|
|
536
|
+
return failed
|
|
537
|
+
? agentToolResult({ ...original, error: { code: "TOOL_HOOK_AUDIT_FAILED", message: "Agent Tool terminal audit could not be recorded." } })
|
|
538
|
+
: original;
|
|
539
|
+
}
|
|
540
|
+
let metadata: JsonObject;
|
|
541
|
+
let timeoutMs: number | undefined;
|
|
542
|
+
try {
|
|
543
|
+
metadata = normalizeMetadata(options.metadata);
|
|
544
|
+
assertNonNegativeInteger(options.timeoutMs, "toolExecution.timeoutMs");
|
|
545
|
+
const toolTimeoutMs = this.registry.getTool(call.name)?.metadata.timeoutMs;
|
|
546
|
+
const configuredTimeoutMs = options.timeoutMs ?? toolTimeoutMs ?? this.defaultTimeoutMs;
|
|
547
|
+
timeoutMs = configuredTimeoutMs === 0 ? undefined : configuredTimeoutMs;
|
|
548
|
+
} catch (error) {
|
|
549
|
+
return this.errorResult(call, "TOOL_EXECUTION_OPTIONS_INVALID", errorMessageOf(error), {}, startedAt);
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
const callerSignal = options.signal;
|
|
553
|
+
const abortController = new AbortController();
|
|
554
|
+
const detach = attachAbortForwarding(callerSignal, abortController);
|
|
555
|
+
const state: ToolExecutionState = { executeStarted: false, executeCompleted: false, phase: "pre-execute", ...(timeoutMs === undefined ? {} : { deadlineUnixMs: startedAt + timeoutMs }), ...(callerSignal === undefined ? {} : { callerSignal }) };
|
|
556
|
+
const operation = this.executePipeline(
|
|
557
|
+
call,
|
|
558
|
+
{ ...options, signal: abortController.signal },
|
|
559
|
+
state,
|
|
560
|
+
startedAt,
|
|
561
|
+
);
|
|
562
|
+
try {
|
|
563
|
+
return await timed(operation, timeoutMs, () => {
|
|
564
|
+
state.timedOut = true;
|
|
565
|
+
abortController.abort();
|
|
566
|
+
}, abortController.signal);
|
|
567
|
+
} catch (error) {
|
|
568
|
+
const tool = this.registry.getTool(call.name);
|
|
569
|
+
const sideEffecting = tool?.metadata.sideEffect === "write" || tool?.metadata.sideEffect === "external";
|
|
570
|
+
const outcomeUnknown = sideEffecting && state.executeStarted;
|
|
571
|
+
const effectPendingAtBoundary = state.executeStarted && !state.executeCompleted;
|
|
572
|
+
const code = this.interruptionCode(error, tool, state);
|
|
573
|
+
const result = this.errorResult(call, code, errorMessageOf(error), metadata, startedAt, {
|
|
574
|
+
phase: state.phase,
|
|
575
|
+
sideEffect: tool?.metadata.sideEffect ?? null,
|
|
576
|
+
});
|
|
577
|
+
if (this.hookSnapshot?.hooks.some(({ payload }) => payload.registration.kind === "settlement" || payload.registration.kind === "observer")) {
|
|
578
|
+
try {
|
|
579
|
+
const settled = await operation;
|
|
580
|
+
// A pending effect cannot become known through a late hook result.
|
|
581
|
+
// Completed tools retain the independent settlement/observer policy.
|
|
582
|
+
return effectPendingAtBoundary && (settled.status === "success" || (outcomeUnknown && !settled.error?.code?.endsWith("_OUTCOME_UNKNOWN")))
|
|
583
|
+
? result : settled;
|
|
584
|
+
} catch { return result; }
|
|
585
|
+
}
|
|
586
|
+
return result;
|
|
587
|
+
} finally {
|
|
588
|
+
detach?.();
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
private interruptionCode(error: unknown, tool: ToolDefinition | undefined, state: ToolExecutionState): string {
|
|
593
|
+
const unknown = state.executeStarted && (tool?.metadata.sideEffect === "write" || tool?.metadata.sideEffect === "external");
|
|
594
|
+
if (state.timedOut || error instanceof AgentToolTimeoutError) {
|
|
595
|
+
return unknown ? "TOOL_TIMEOUT_OUTCOME_UNKNOWN" : "TOOL_TIMEOUT";
|
|
596
|
+
}
|
|
597
|
+
if (error instanceof AgentToolAbortError) {
|
|
598
|
+
return unknown ? "TOOL_ABORTED_OUTCOME_UNKNOWN" : "TOOL_ABORTED";
|
|
599
|
+
}
|
|
600
|
+
return "TOOL_EXECUTION_FAILED";
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
private settlementEvent(
|
|
604
|
+
call: AgentToolCall,
|
|
605
|
+
tool: ToolDefinition,
|
|
606
|
+
result: AgentToolResult,
|
|
607
|
+
options: AgentToolExecutionOptions,
|
|
608
|
+
agentName: string | undefined,
|
|
609
|
+
state: ToolExecutionState,
|
|
610
|
+
attempt?: AttemptContext,
|
|
611
|
+
): AgentToolSettlementEventV1 {
|
|
612
|
+
const terminal = Object.freeze({
|
|
613
|
+
status: result.status,
|
|
614
|
+
executeStarted: state.executeStarted,
|
|
615
|
+
outcomeKnown: !String(result.error?.code ?? "").includes("OUTCOME_UNKNOWN"),
|
|
616
|
+
retryable: state.retryableCandidate === true,
|
|
617
|
+
...(result.error ? { errorCode: result.error.code } : {}),
|
|
618
|
+
...(result.status === "success" ? { output: redactSensitive(result.output, this.hookRedaction) } : {}),
|
|
619
|
+
});
|
|
620
|
+
return Object.freeze({
|
|
621
|
+
type: "agent-tool.settlement/v1",
|
|
622
|
+
callId: call.id,
|
|
623
|
+
toolName: tool.metadata.name,
|
|
624
|
+
...(options.invocationId === undefined ? {} : { invocationId: options.invocationId }),
|
|
625
|
+
...(agentName === undefined ? {} : { agentName }),
|
|
626
|
+
...(attempt === undefined ? {} : { attempt: attempt.attempt }),
|
|
627
|
+
...(state.beforeEffect === undefined ? {} : { beforeEffect: state.beforeEffect }),
|
|
628
|
+
terminal,
|
|
629
|
+
});
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
private async settleHooks(call: AgentToolCall, result: AgentToolResult, metadata: JsonObject, options: AgentToolExecutionOptions, startedAt: number, state: ToolExecutionState, event: AgentToolSettlementEventV1): Promise<AgentToolResult> {
|
|
633
|
+
const snapshot = this.hookSnapshot;
|
|
634
|
+
const tool = this.registry.getTool(call.name);
|
|
635
|
+
if (!snapshot || !tool || snapshot.hooks.every(({ payload }) => payload.registration.kind !== "settlement")) return result;
|
|
636
|
+
for (const record of this.orderedHooks(snapshot.hooks)) {
|
|
637
|
+
const registration = record.payload.registration;
|
|
638
|
+
if (registration.kind !== "settlement") continue;
|
|
639
|
+
const scope = this.services.createScope();
|
|
640
|
+
let terminal: AgentToolResult | undefined;
|
|
641
|
+
let projection: AgentToolAuditHookProjectionV1 = {
|
|
642
|
+
kind: registration.kind, id: registration.id, version: registration.version,
|
|
643
|
+
tier: this.isPlatformHook(record) ? "platform" : "application", order: registration.order ?? 0,
|
|
644
|
+
outcome: "failed",
|
|
645
|
+
};
|
|
646
|
+
const postEffect = state.executeStarted;
|
|
647
|
+
const configuredTimeoutMs = registration.timeoutMs ?? 5_000;
|
|
648
|
+
const remainingMs = !postEffect && state.deadlineUnixMs !== undefined
|
|
649
|
+
? Math.max(0, state.deadlineUnixMs - Date.now()) : undefined;
|
|
650
|
+
const timeoutMs = remainingMs === undefined ? configuredTimeoutMs : Math.min(configuredTimeoutMs, remainingMs);
|
|
651
|
+
const boundedTimeoutMs = Math.max(1, timeoutMs);
|
|
652
|
+
const controller = new AbortController();
|
|
653
|
+
const detach = postEffect ? undefined : attachAbortForwarding(options.signal, controller);
|
|
654
|
+
try {
|
|
655
|
+
if (!postEffect && (timeoutMs <= 0 || controller.signal.aborted)) {
|
|
656
|
+
// No pre-effect budget remains. Do not activate a handler outside
|
|
657
|
+
// its deadline; caller cancellation wins a simultaneous deadline.
|
|
658
|
+
controller.abort();
|
|
659
|
+
const cancelled = state.callerSignal?.aborted === true;
|
|
660
|
+
const activated = await awaitHook(record.activation.activateAsync(scope), undefined, boundedTimeoutMs);
|
|
661
|
+
if (activated.kind === "value") {
|
|
662
|
+
const handler = activated.value as { settle?: unknown };
|
|
663
|
+
if (typeof handler.settle === "function") {
|
|
664
|
+
// Start the handler under its aborted context but never await it
|
|
665
|
+
// outside the exhausted pre-effect budget.
|
|
666
|
+
await awaitHook(
|
|
667
|
+
Promise.resolve((handler.settle as (value: AgentToolSettlementEventV1, context: AgentToolHookContextV1) => unknown)(event, Object.freeze({ signal: controller.signal, deadlineUnixMs: Date.now() }))),
|
|
668
|
+
controller.signal,
|
|
669
|
+
0,
|
|
670
|
+
);
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
projection = { ...projection, outcome: cancelled ? "cancelled" : "timed-out" };
|
|
674
|
+
terminal = this.errorResult(call, "TOOL_HOOK_SETTLEMENT_FAILED", "Agent Tool settlement deadline elapsed.", metadata, startedAt);
|
|
675
|
+
} else {
|
|
676
|
+
if (timeoutMs <= 0) controller.abort();
|
|
677
|
+
const context: AgentToolHookContextV1 = Object.freeze({ signal: controller.signal, deadlineUnixMs: Date.now() + boundedTimeoutMs });
|
|
678
|
+
// Caller cancellation is forwarded to the hook context, but required
|
|
679
|
+
// settlement still invokes and records its terminal evidence.
|
|
680
|
+
const activated = await awaitHook(record.activation.activateAsync(scope), controller.signal, boundedTimeoutMs);
|
|
681
|
+
if (activated.kind !== "value") {
|
|
682
|
+
if (activated.kind === "timeout") controller.abort();
|
|
683
|
+
projection = { ...projection, outcome: activated.kind === "timeout" ? "timed-out" : activated.kind === "cancelled" ? (state.callerSignal?.aborted ? "cancelled" : "timed-out") : "failed" };
|
|
684
|
+
terminal = this.errorResult(call, "TOOL_HOOK_SETTLEMENT_FAILED", "Agent Tool settlement handler was unavailable.", metadata, startedAt);
|
|
685
|
+
} else {
|
|
686
|
+
const handler = activated.value as { settle?: unknown };
|
|
687
|
+
if (typeof handler.settle !== "function") {
|
|
688
|
+
projection = { ...projection, outcome: "failed" };
|
|
689
|
+
terminal = this.errorResult(call, "TOOL_HOOK_SETTLEMENT_FAILED", "Agent Tool settlement handler was unavailable.", metadata, startedAt);
|
|
690
|
+
} else {
|
|
691
|
+
const invocationTimeoutMs = Math.max(0, context.deadlineUnixMs - Date.now());
|
|
692
|
+
const outcome = await awaitHook(Promise.resolve((handler.settle as (value: AgentToolSettlementEventV1, context: AgentToolHookContextV1) => unknown)(event, context)), controller.signal, invocationTimeoutMs);
|
|
693
|
+
if (outcome.kind === "timeout") controller.abort();
|
|
694
|
+
if (outcome.kind !== "value") {
|
|
695
|
+
projection = { ...projection, outcome: outcome.kind === "timeout" ? "timed-out" : outcome.kind === "cancelled" ? (state.callerSignal?.aborted ? "cancelled" : "timed-out") : "failed" };
|
|
696
|
+
terminal = this.errorResult(call, "TOOL_HOOK_SETTLEMENT_FAILED", "Agent Tool settlement failed.", metadata, startedAt);
|
|
697
|
+
} else if (!isSettlementResult(outcome.value)) {
|
|
698
|
+
projection = { ...projection, outcome: "invalid-result" };
|
|
699
|
+
terminal = this.errorResult(call, "TOOL_HOOK_SETTLEMENT_FAILED", "Agent Tool settlement returned an invalid result.", metadata, startedAt);
|
|
700
|
+
} else {
|
|
701
|
+
const evidence = outcome.value.evidence === undefined ? undefined : redactSensitive(normalizeHookEvidence(outcome.value.evidence), this.hookRedaction) as JsonObject;
|
|
702
|
+
projection = { ...projection, outcome: "recorded", ...(evidence === undefined ? {} : { evidence }) };
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
} catch {
|
|
708
|
+
projection = { ...projection, outcome: "failed" };
|
|
709
|
+
terminal = this.errorResult(call, "TOOL_HOOK_SETTLEMENT_FAILED", "Agent Tool settlement failed.", metadata, startedAt);
|
|
710
|
+
} finally {
|
|
711
|
+
detach?.();
|
|
712
|
+
const disposeError = await this.disposeScope(scope, this.scopeDisposeTimeoutMs);
|
|
713
|
+
if (disposeError !== undefined) {
|
|
714
|
+
projection = { ...projection, outcome: "failed" };
|
|
715
|
+
terminal = this.errorResult(call, "TOOL_HOOK_SETTLEMENT_FAILED", "Agent Tool settlement disposal failed.", metadata, startedAt);
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
const evidenceRemainingMs = postEffect || state.deadlineUnixMs === undefined
|
|
719
|
+
? registration.timeoutMs ?? 5_000
|
|
720
|
+
: Math.max(0, Math.min(registration.timeoutMs ?? 5_000, state.deadlineUnixMs - Date.now()));
|
|
721
|
+
if (await this.recordMandatoryHook("settlement", call, tool, metadata, options, startedAt, projection, evidenceRemainingMs, postEffect ? undefined : options.signal)) {
|
|
722
|
+
terminal = this.errorResult(call, "TOOL_HOOK_SETTLEMENT_FAILED", "Agent Tool settlement evidence could not be recorded.", metadata, startedAt);
|
|
723
|
+
}
|
|
724
|
+
if (terminal !== undefined) {
|
|
725
|
+
if (result.error?.code === "TOOL_HOOK_CANCELLED" || result.error?.code === "TOOL_ABORTED") return result;
|
|
726
|
+
const unknown = (postEffect || result.status === "success")
|
|
727
|
+
&& (tool.metadata.sideEffect === "write" || tool.metadata.sideEffect === "external");
|
|
728
|
+
return unknown
|
|
729
|
+
? this.errorResult(call, "TOOL_HOOK_SETTLEMENT_OUTCOME_UNKNOWN", "Agent Tool settlement outcome is unknown.", metadata, startedAt)
|
|
730
|
+
: terminal;
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
return result;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
private async observeHooks(settlement: AgentToolSettlementEventV1, options: AgentToolExecutionOptions): Promise<void> {
|
|
737
|
+
const snapshot = this.hookSnapshot;
|
|
738
|
+
if (!snapshot) return;
|
|
739
|
+
for (const record of this.orderedHooks(snapshot.hooks)) {
|
|
740
|
+
const registration = record.payload.registration;
|
|
741
|
+
if (registration.kind !== "observer") continue;
|
|
742
|
+
const scope = this.services.createScope();
|
|
743
|
+
let outcome: AgentToolAuditHookProjectionV1["outcome"] = "failed";
|
|
744
|
+
const timeoutMs = registration.timeoutMs ?? 5_000;
|
|
745
|
+
const controller = new AbortController();
|
|
746
|
+
const detach = attachAbortForwarding(options.signal, controller);
|
|
747
|
+
try {
|
|
748
|
+
const context: AgentToolHookContextV1 = Object.freeze({ signal: controller.signal, deadlineUnixMs: Date.now() + timeoutMs });
|
|
749
|
+
const activated = await awaitHook(record.activation.activateAsync(scope), controller.signal, timeoutMs);
|
|
750
|
+
if (activated.kind !== "value") {
|
|
751
|
+
if (activated.kind === "timeout") controller.abort();
|
|
752
|
+
outcome = activated.kind === "timeout" ? "timed-out" : activated.kind === "cancelled" ? "cancelled" : "failed";
|
|
753
|
+
} else {
|
|
754
|
+
const handler = activated.value as { observe?: unknown };
|
|
755
|
+
if (typeof handler.observe !== "function") {
|
|
756
|
+
outcome = "failed";
|
|
757
|
+
} else {
|
|
758
|
+
const observed = await awaitHook(
|
|
759
|
+
Promise.resolve((handler.observe as (event: { readonly type: "agent-tool.observer/v1"; readonly settlement: AgentToolSettlementEventV1 }, context: AgentToolHookContextV1) => unknown)({ type: "agent-tool.observer/v1", settlement }, context)),
|
|
760
|
+
controller.signal,
|
|
761
|
+
Math.max(1, context.deadlineUnixMs - Date.now()),
|
|
762
|
+
);
|
|
763
|
+
if (observed.kind === "timeout" || observed.kind === "value") controller.abort();
|
|
764
|
+
outcome = observed.kind === "value" ? "recorded" : observed.kind === "timeout" ? "timed-out" : observed.kind === "cancelled" ? "cancelled" : "failed";
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
} catch {
|
|
768
|
+
outcome = controller.signal.aborted && options.signal?.aborted ? "cancelled" : "failed";
|
|
769
|
+
} finally {
|
|
770
|
+
detach?.();
|
|
771
|
+
if (await this.disposeScope(scope, this.scopeDisposeTimeoutMs) !== undefined) outcome = "failed";
|
|
772
|
+
}
|
|
773
|
+
await this.recordObserverHook(settlement, registration, record, outcome, timeoutMs);
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
private async recordObserverHook(
|
|
778
|
+
settlement: AgentToolSettlementEventV1,
|
|
779
|
+
registration: Extract<AgentOwnedHookRegistrationV1["payload"]["registration"], { readonly kind: "observer" }>,
|
|
780
|
+
record: AgentOwnedHookRegistrationV1,
|
|
781
|
+
outcome: AgentToolAuditHookProjectionV1["outcome"],
|
|
782
|
+
timeoutMs: number,
|
|
783
|
+
): Promise<void> {
|
|
784
|
+
if (!this.auditSink) return;
|
|
785
|
+
const projection: AgentToolAuditHookProjectionV1 = Object.freeze({
|
|
786
|
+
kind: registration.kind, id: registration.id, version: registration.version,
|
|
787
|
+
tier: this.isPlatformHook(record) ? "platform" : "application", order: registration.order ?? 0, outcome,
|
|
788
|
+
});
|
|
789
|
+
const entry: AgentToolAuditEntry = Object.freeze({
|
|
790
|
+
kind: "agent-tool", phase: "observer", callId: settlement.callId, toolName: settlement.toolName,
|
|
791
|
+
input: Object.freeze({}), metadata: Object.freeze({}), hook: projection,
|
|
792
|
+
startedAtUnixMs: Date.now(), finishedAtUnixMs: Date.now(), durationMs: 0,
|
|
793
|
+
});
|
|
794
|
+
// Observer evidence is deliberately best effort and uses an independent
|
|
795
|
+
// bounded write: it cannot alter the already-terminal Tool result.
|
|
796
|
+
await awaitHook(Promise.resolve().then(() => auditSinkFunction(this.auditSink!)(entry)), undefined, timeoutMs);
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
private async executePipeline(
|
|
800
|
+
call: AgentToolCall,
|
|
801
|
+
options: AgentToolExecutionOptions,
|
|
802
|
+
executionState: ToolExecutionState,
|
|
803
|
+
startedAt: number,
|
|
804
|
+
): Promise<AgentToolResult> {
|
|
805
|
+
let metadata: JsonObject;
|
|
806
|
+
let audit: AuditSettings;
|
|
807
|
+
let retry: RetrySettings;
|
|
808
|
+
let idempotencyKey: string | undefined;
|
|
809
|
+
try {
|
|
810
|
+
metadata = normalizeMetadata(options.metadata);
|
|
811
|
+
assertNonNegativeInteger(options.timeoutMs, "toolExecution.timeoutMs");
|
|
812
|
+
assertPositiveInteger(options.scopeDisposeTimeoutMs, "toolExecution.scopeDisposeTimeoutMs");
|
|
813
|
+
retry = this.retrySettings(options);
|
|
814
|
+
idempotencyKey = normalizeOptionalText(options.idempotencyKey, "toolExecution.idempotencyKey");
|
|
815
|
+
audit = this.auditSettings(options);
|
|
816
|
+
} catch (error) {
|
|
817
|
+
return this.errorResult(call, "TOOL_EXECUTION_OPTIONS_INVALID", errorMessageOf(error), {}, startedAt);
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
const tool = this.registry.getTool(call.name);
|
|
821
|
+
if (!tool) {
|
|
822
|
+
const original = this.errorResult(
|
|
823
|
+
call,
|
|
824
|
+
"TOOL_NOT_REGISTERED",
|
|
825
|
+
`Tool "${call.name}" is not registered.`,
|
|
826
|
+
metadata,
|
|
827
|
+
startedAt,
|
|
828
|
+
);
|
|
829
|
+
const frameworkAuditFailed = await this.recordFrameworkTerminalAudit(original, startedAt, options.signal, executionState.deadlineUnixMs === undefined ? undefined : Math.max(0, executionState.deadlineUnixMs - Date.now()));
|
|
830
|
+
const result = frameworkAuditFailed
|
|
831
|
+
? this.errorResult(call, "TOOL_HOOK_AUDIT_FAILED", "Agent Tool terminal audit could not be recorded.", metadata, startedAt)
|
|
832
|
+
: original;
|
|
833
|
+
if (this.hasMandatoryHooks()) return result;
|
|
834
|
+
return this.finish(call, result, startedAt, metadata, options, audit, undefined, undefined, undefined, executionState);
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
if (
|
|
838
|
+
(tool.metadata.sideEffect === "write" || tool.metadata.sideEffect === "external")
|
|
839
|
+
&& audit.sink === undefined
|
|
840
|
+
&& !(options.allowUnauditedSideEffects ?? this.allowUnauditedSideEffects)
|
|
841
|
+
) {
|
|
842
|
+
const result = this.errorResult(
|
|
843
|
+
call,
|
|
844
|
+
"TOOL_AUDIT_REQUIRED",
|
|
845
|
+
`Tool "${tool.metadata.name}" has side effects and requires an audit sink.`,
|
|
846
|
+
metadata,
|
|
847
|
+
startedAt,
|
|
848
|
+
);
|
|
849
|
+
return this.finish(call, result, startedAt, metadata, options, audit, tool, options.agentName, undefined, executionState);
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
const visibility = this.authorizeAgent(tool, options);
|
|
853
|
+
if (!visibility.allowed) {
|
|
854
|
+
const code = visibility.code ?? "TOOL_NOT_VISIBLE_TO_AGENT";
|
|
855
|
+
const message = visibility.message ?? `Tool "${tool.metadata.name}" is not visible to this agent.`;
|
|
856
|
+
if (visibility.status === "denied") {
|
|
857
|
+
const result = this.deniedResult(
|
|
858
|
+
call,
|
|
859
|
+
code,
|
|
860
|
+
message,
|
|
861
|
+
{ toolName: tool.metadata.name, agentName: options.agentName ?? "" },
|
|
862
|
+
metadata,
|
|
863
|
+
startedAt,
|
|
864
|
+
);
|
|
865
|
+
return this.finish(call, result, startedAt, metadata, options, audit, tool, options.agentName, undefined, executionState);
|
|
866
|
+
}
|
|
867
|
+
const result = this.errorResult(call, code, message, metadata, startedAt);
|
|
868
|
+
return this.finish(call, result, startedAt, metadata, options, audit, tool, options.agentName, undefined, executionState);
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
const schema = this.schemaSettings(options);
|
|
872
|
+
const schemaGate = this.ensureSchemaValidator(call, tool, schema, metadata, startedAt);
|
|
873
|
+
if (schemaGate) {
|
|
874
|
+
return this.finish(call, schemaGate, startedAt, metadata, options, audit, tool, visibility.agentName, undefined, executionState);
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
// Validation belongs to the same admitted attempt deadline. Its promise
|
|
878
|
+
// is observed before racing so a late validator rejection stays handled.
|
|
879
|
+
const validationRemainingMs = executionState.deadlineUnixMs === undefined
|
|
880
|
+
? undefined
|
|
881
|
+
: Math.max(0, executionState.deadlineUnixMs - Date.now());
|
|
882
|
+
const inputOutcome = await awaitHook(
|
|
883
|
+
this.prepareInput(call, tool, schema, metadata, startedAt),
|
|
884
|
+
options.signal,
|
|
885
|
+
validationRemainingMs,
|
|
886
|
+
);
|
|
887
|
+
if (inputOutcome.kind === "timeout") {
|
|
888
|
+
return this.finish(call, this.errorResult(call, "TOOL_TIMEOUT", "Tool execution timed out during input validation.", metadata, startedAt, { phase: "pre-execute" }), startedAt, metadata, options, audit, tool, visibility.agentName, undefined, executionState);
|
|
889
|
+
}
|
|
890
|
+
if (inputOutcome.kind === "cancelled" || options.signal?.aborted) {
|
|
891
|
+
const cancelled = executionState.callerSignal?.aborted === true;
|
|
892
|
+
return this.finish(call, this.errorResult(call, cancelled ? "TOOL_ABORTED" : "TOOL_TIMEOUT", cancelled ? "Tool execution was aborted during input validation." : "Tool execution timed out during input validation.", metadata, startedAt, { phase: "pre-execute" }), startedAt, metadata, options, audit, tool, visibility.agentName, undefined, executionState);
|
|
893
|
+
}
|
|
894
|
+
if (inputOutcome.kind === "failed") {
|
|
895
|
+
return this.finish(call, this.errorResult(call, "TOOL_SCHEMA_VALIDATION_FAILED", "Tool input schema validation failed.", metadata, startedAt), startedAt, metadata, options, audit, tool, visibility.agentName, undefined, executionState);
|
|
896
|
+
}
|
|
897
|
+
const input = inputOutcome.value;
|
|
898
|
+
if (!input.ok) {
|
|
899
|
+
return this.finish(call, input.result, startedAt, metadata, options, audit, tool, visibility.agentName, undefined, executionState);
|
|
900
|
+
}
|
|
901
|
+
const effectiveCall = this.effectiveCall(call, tool, input.value);
|
|
902
|
+
const effectiveInput = this.freezeToolInput(input.value);
|
|
903
|
+
|
|
904
|
+
const approvalRemainingMs = executionState.deadlineUnixMs === undefined
|
|
905
|
+
? undefined : Math.max(0, executionState.deadlineUnixMs - Date.now());
|
|
906
|
+
const approvalOutcome = await awaitHook(
|
|
907
|
+
this.authorize(effectiveCall, tool, metadata, options, idempotencyKey),
|
|
908
|
+
options.signal,
|
|
909
|
+
approvalRemainingMs,
|
|
910
|
+
);
|
|
911
|
+
if (approvalOutcome.kind !== "value") {
|
|
912
|
+
const code = approvalOutcome.kind === "cancelled" && executionState.callerSignal?.aborted === true ? "TOOL_ABORTED" : "TOOL_TIMEOUT";
|
|
913
|
+
return this.finish(effectiveCall, this.errorResult(effectiveCall, code, code === "TOOL_ABORTED" ? "Tool execution was aborted during approval." : "Tool execution timed out during approval.", metadata, startedAt, { phase: "pre-execute" }), startedAt, metadata, options, audit, tool, visibility.agentName, undefined, executionState);
|
|
914
|
+
}
|
|
915
|
+
const approval = approvalOutcome.value;
|
|
916
|
+
if (options.signal?.aborted) {
|
|
917
|
+
const cancelled = executionState.callerSignal?.aborted === true;
|
|
918
|
+
return this.finish(effectiveCall, this.errorResult(effectiveCall, cancelled ? "TOOL_ABORTED" : "TOOL_TIMEOUT", cancelled ? "Tool execution was aborted during approval." : "Tool execution timed out during approval.", metadata, startedAt, { phase: "pre-execute" }), startedAt, metadata, options, audit, tool, visibility.agentName, undefined, executionState);
|
|
919
|
+
}
|
|
920
|
+
if (!approval.allowed) {
|
|
921
|
+
const result = this.deniedResult(
|
|
922
|
+
effectiveCall,
|
|
923
|
+
approval.code ?? "TOOL_APPROVAL_DENIED",
|
|
924
|
+
approval.message ?? "Tool execution was denied by approval policy.",
|
|
925
|
+
{
|
|
926
|
+
sideEffect: tool.metadata.sideEffect,
|
|
927
|
+
approval: tool.metadata.approval,
|
|
928
|
+
reason: approval.reason ?? tool.metadata.approval,
|
|
929
|
+
},
|
|
930
|
+
metadata,
|
|
931
|
+
startedAt,
|
|
932
|
+
);
|
|
933
|
+
return this.finish(effectiveCall, result, startedAt, metadata, options, audit, tool, visibility.agentName, undefined, executionState);
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
const retryGate = this.ensureRetryPolicy(effectiveCall, tool, retry, idempotencyKey, metadata, startedAt);
|
|
937
|
+
if (retryGate) {
|
|
938
|
+
return this.finish(effectiveCall, retryGate, startedAt, metadata, options, audit, tool, visibility.agentName, undefined, executionState);
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
let result: AgentToolResult | undefined;
|
|
942
|
+
let finalAttempt: AttemptContext | undefined;
|
|
943
|
+
for (let attempt = 1; attempt <= retry.maxAttempts; attempt += 1) {
|
|
944
|
+
executionState.retryableCandidate = false;
|
|
945
|
+
const attemptContext: AttemptContext = Object.freeze({
|
|
946
|
+
attempt,
|
|
947
|
+
maxAttempts: retry.maxAttempts,
|
|
948
|
+
...(idempotencyKey !== undefined ? { idempotencyKey } : {}),
|
|
949
|
+
});
|
|
950
|
+
const auditRemainingMs = executionState.deadlineUnixMs === undefined
|
|
951
|
+
? undefined : Math.max(0, executionState.deadlineUnixMs - Date.now());
|
|
952
|
+
const auditAttemptOutcome = await this.audit(
|
|
953
|
+
this.auditProjector.create("attempt", effectiveCall, startedAt, metadata, options, tool, visibility.agentName, undefined, attemptContext),
|
|
954
|
+
audit,
|
|
955
|
+
effectiveCall,
|
|
956
|
+
metadata,
|
|
957
|
+
startedAt,
|
|
958
|
+
undefined,
|
|
959
|
+
options.signal,
|
|
960
|
+
auditRemainingMs,
|
|
961
|
+
);
|
|
962
|
+
if (auditAttemptOutcome.kind !== "value") {
|
|
963
|
+
const code = auditAttemptOutcome.kind === "cancelled" && executionState.callerSignal?.aborted === true ? "TOOL_ABORTED" : "TOOL_TIMEOUT";
|
|
964
|
+
return this.finish(effectiveCall, this.errorResult(effectiveCall, code, code === "TOOL_ABORTED" ? "Tool execution was aborted during attempt audit." : "Tool execution timed out during attempt audit.", this.attemptMetadata(metadata, attemptContext), startedAt, { phase: "pre-execute" }), startedAt, metadata, options, audit, tool, visibility.agentName, attemptContext, executionState);
|
|
965
|
+
}
|
|
966
|
+
const auditAttemptError = auditAttemptOutcome.value;
|
|
967
|
+
if (options.signal?.aborted) {
|
|
968
|
+
const cancelled = executionState.callerSignal?.aborted === true;
|
|
969
|
+
return this.finish(effectiveCall, this.errorResult(
|
|
970
|
+
effectiveCall,
|
|
971
|
+
cancelled ? "TOOL_ABORTED" : "TOOL_TIMEOUT",
|
|
972
|
+
cancelled ? "Tool execution was aborted during attempt audit." : "Tool execution timed out during attempt audit.",
|
|
973
|
+
this.attemptMetadata(metadata, attemptContext),
|
|
974
|
+
startedAt,
|
|
975
|
+
{ phase: "pre-execute" },
|
|
976
|
+
), startedAt, metadata, options, audit, tool, visibility.agentName, attemptContext, executionState);
|
|
977
|
+
}
|
|
978
|
+
if (auditAttemptError) {
|
|
979
|
+
return this.finish(effectiveCall, auditAttemptError, startedAt, metadata, options, audit, tool, visibility.agentName, attemptContext, executionState);
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
finalAttempt = attemptContext;
|
|
983
|
+
const enforcement = await this.enforceHooks(
|
|
984
|
+
effectiveCall,
|
|
985
|
+
tool,
|
|
986
|
+
approval.executionApproval,
|
|
987
|
+
visibility.agentName,
|
|
988
|
+
metadata,
|
|
989
|
+
options,
|
|
990
|
+
startedAt,
|
|
991
|
+
attemptContext,
|
|
992
|
+
executionState,
|
|
993
|
+
);
|
|
994
|
+
if (enforcement !== undefined) {
|
|
995
|
+
result = await this.finish(effectiveCall, enforcement, startedAt, metadata, options, audit, tool, visibility.agentName, attemptContext, executionState);
|
|
996
|
+
break;
|
|
997
|
+
}
|
|
998
|
+
result = await this.executeAttempt(
|
|
999
|
+
effectiveCall,
|
|
1000
|
+
tool,
|
|
1001
|
+
effectiveInput,
|
|
1002
|
+
approval.executionApproval,
|
|
1003
|
+
visibility.agentName,
|
|
1004
|
+
metadata,
|
|
1005
|
+
schema,
|
|
1006
|
+
options,
|
|
1007
|
+
audit,
|
|
1008
|
+
startedAt,
|
|
1009
|
+
attemptContext,
|
|
1010
|
+
executionState,
|
|
1011
|
+
);
|
|
1012
|
+
executionState.retryableCandidate = this.shouldRetry(result, retry, attempt);
|
|
1013
|
+
|
|
1014
|
+
result = await this.finish(effectiveCall, result, startedAt, metadata, options, audit, tool, visibility.agentName, attemptContext, executionState);
|
|
1015
|
+
|
|
1016
|
+
if (options.signal?.aborted) {
|
|
1017
|
+
return result;
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
if (!this.shouldRetry(result, retry, attempt)) {
|
|
1021
|
+
break;
|
|
1022
|
+
}
|
|
1023
|
+
if (await this.delayBeforeRetry(retry, attempt, options.signal)) {
|
|
1024
|
+
result = this.errorResult(
|
|
1025
|
+
effectiveCall,
|
|
1026
|
+
"TOOL_ABORTED",
|
|
1027
|
+
"Tool retry was aborted.",
|
|
1028
|
+
this.attemptMetadata(metadata, attemptContext),
|
|
1029
|
+
startedAt,
|
|
1030
|
+
{ phase: executionState.phase },
|
|
1031
|
+
);
|
|
1032
|
+
result = await this.finish(effectiveCall, result, startedAt, metadata, options, audit, tool, visibility.agentName, attemptContext, executionState);
|
|
1033
|
+
break;
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
if (!result || !finalAttempt) {
|
|
1038
|
+
result = this.errorResult(effectiveCall, "TOOL_RETRY_POLICY_INVALID", "Tool retry policy produced no execution attempts.", metadata, startedAt);
|
|
1039
|
+
}
|
|
1040
|
+
if (!finalAttempt) return this.finish(effectiveCall, result, startedAt, metadata, options, audit, tool, visibility.agentName, undefined, executionState);
|
|
1041
|
+
return result;
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
private async enforceHooks(
|
|
1045
|
+
call: AgentToolCall,
|
|
1046
|
+
tool: ToolDefinition,
|
|
1047
|
+
approval: AgentToolExecutionApproval,
|
|
1048
|
+
agentName: string | undefined,
|
|
1049
|
+
metadata: JsonObject,
|
|
1050
|
+
options: AgentToolExecutionOptions,
|
|
1051
|
+
startedAt: number,
|
|
1052
|
+
attempt: AttemptContext,
|
|
1053
|
+
executionState: ToolExecutionState,
|
|
1054
|
+
): Promise<AgentToolResult | undefined> {
|
|
1055
|
+
const snapshot = this.hookSnapshot;
|
|
1056
|
+
if (!snapshot || snapshot.hooks.length === 0) return undefined;
|
|
1057
|
+
let event: AgentToolBeforeEffectEventV1;
|
|
1058
|
+
try {
|
|
1059
|
+
event = Object.freeze({
|
|
1060
|
+
type: "agent-tool.before-effect/v1",
|
|
1061
|
+
...(options.invocationId === undefined ? {} : { invocationId: options.invocationId }),
|
|
1062
|
+
...(agentName === undefined ? {} : { agentName }),
|
|
1063
|
+
attempt: attempt.attempt,
|
|
1064
|
+
maxAttempts: attempt.maxAttempts,
|
|
1065
|
+
...(attempt.idempotencyKey === undefined ? {} : { idempotencyKey: attempt.idempotencyKey }),
|
|
1066
|
+
call: Object.freeze({ id: call.id, name: call.name, input: redactSensitive(normalizeJsonValue(call.input, "hook.call.input"), this.hookRedaction), metadata: redactSensitive(normalizeMetadata(metadata), this.hookRedaction) as JsonObject }),
|
|
1067
|
+
tool: Object.freeze({ name: tool.metadata.name, description: tool.metadata.description, sideEffect: tool.metadata.sideEffect, approval: tool.metadata.approval, tags: Object.freeze([...(tool.metadata.tags ?? [])]) }),
|
|
1068
|
+
executionApproval: approval,
|
|
1069
|
+
});
|
|
1070
|
+
} catch {
|
|
1071
|
+
return this.errorResult(call, "TOOL_HOOK_REDACTION_FAILED", "Agent Tool hook projection could not be safely prepared.", metadata, startedAt);
|
|
1072
|
+
}
|
|
1073
|
+
executionState.beforeEffect = event;
|
|
1074
|
+
const isPlatform = (record: AgentOwnedHookRegistrationV1): boolean => this.isPlatformHook(record);
|
|
1075
|
+
const hooks = this.orderedHooks(snapshot.hooks);
|
|
1076
|
+
for (const record of hooks) {
|
|
1077
|
+
const registration = record.payload.registration;
|
|
1078
|
+
if (registration.kind !== "enforcement") continue;
|
|
1079
|
+
const scope = this.services.createScope();
|
|
1080
|
+
let terminal: AgentToolResult | undefined;
|
|
1081
|
+
let projection: AgentToolAuditHookProjectionV1 = {
|
|
1082
|
+
kind: registration.kind, id: registration.id, version: registration.version,
|
|
1083
|
+
tier: isPlatform(record) ? "platform" : "application", order: registration.order ?? 0,
|
|
1084
|
+
outcome: "failed",
|
|
1085
|
+
};
|
|
1086
|
+
const configuredTimeoutMs = registration.timeoutMs ?? 5_000;
|
|
1087
|
+
const remainingMs = executionState.deadlineUnixMs === undefined
|
|
1088
|
+
? undefined : Math.max(0, executionState.deadlineUnixMs - Date.now());
|
|
1089
|
+
const timeoutMs = remainingMs === undefined ? configuredTimeoutMs : Math.min(configuredTimeoutMs, remainingMs);
|
|
1090
|
+
const controller = new AbortController();
|
|
1091
|
+
const detach = attachAbortForwarding(options.signal, controller);
|
|
1092
|
+
try {
|
|
1093
|
+
if (timeoutMs <= 0) {
|
|
1094
|
+
controller.abort();
|
|
1095
|
+
projection = { ...projection, outcome: "timed-out" };
|
|
1096
|
+
terminal = this.errorResult(call, "TOOL_HOOK_TIMEOUT", "Agent Tool enforcement deadline elapsed.", metadata, startedAt);
|
|
1097
|
+
} else {
|
|
1098
|
+
const context: AgentToolHookContextV1 = Object.freeze({ signal: controller.signal, deadlineUnixMs: Date.now() + timeoutMs });
|
|
1099
|
+
const activated = await awaitHook(record.activation.activateAsync(scope), controller.signal, timeoutMs);
|
|
1100
|
+
if (activated.kind !== "value") {
|
|
1101
|
+
if (activated.kind === "timeout") controller.abort();
|
|
1102
|
+
projection = { ...projection, outcome: activated.kind === "timeout" ? "timed-out" : activated.kind === "cancelled" ? "cancelled" : "failed" };
|
|
1103
|
+
terminal = this.errorResult(call, activated.kind === "timeout" ? "TOOL_HOOK_TIMEOUT" : activated.kind === "cancelled" ? "TOOL_HOOK_CANCELLED" : "TOOL_HOOK_UNAVAILABLE", "Agent Tool enforcement handler was unavailable.", metadata, startedAt);
|
|
1104
|
+
} else {
|
|
1105
|
+
const handler = activated.value as { enforce?: unknown };
|
|
1106
|
+
if (typeof handler.enforce !== "function") {
|
|
1107
|
+
terminal = this.errorResult(call, "TOOL_HOOK_UNAVAILABLE", "Agent Tool enforcement handler was unavailable.", metadata, startedAt);
|
|
1108
|
+
} else {
|
|
1109
|
+
const outcome = await awaitHook(Promise.resolve((handler.enforce as (value: AgentToolBeforeEffectEventV1, context: AgentToolHookContextV1) => AgentToolEnforcementDecisionV1 | Promise<AgentToolEnforcementDecisionV1>)(event, context)), controller.signal, Math.max(0, context.deadlineUnixMs - Date.now()));
|
|
1110
|
+
if (outcome.kind === "timeout") controller.abort();
|
|
1111
|
+
if (outcome.kind === "timeout") { projection = { ...projection, outcome: "timed-out" }; terminal = this.errorResult(call, "TOOL_HOOK_TIMEOUT", "Agent Tool enforcement hook timed out.", metadata, startedAt); }
|
|
1112
|
+
else if (outcome.kind === "cancelled") { projection = { ...projection, outcome: "cancelled" }; terminal = this.errorResult(call, "TOOL_HOOK_CANCELLED", "Agent Tool enforcement hook was cancelled.", metadata, startedAt); }
|
|
1113
|
+
else if (outcome.kind === "failed") { terminal = this.errorResult(call, "TOOL_HOOK_FAILED", "Agent Tool enforcement hook failed.", metadata, startedAt); }
|
|
1114
|
+
else if (!isHookDecision(outcome.value)) { projection = { ...projection, outcome: "invalid-result" }; terminal = this.errorResult(call, "TOOL_HOOK_DECISION_INVALID", "Agent Tool enforcement hook returned an invalid decision.", metadata, startedAt); }
|
|
1115
|
+
else {
|
|
1116
|
+
const decision = outcome.value;
|
|
1117
|
+
projection = { ...projection, outcome: decision.decision === "allow" ? "allowed" : "denied", ...(decision.decision === "deny" && decision.reasonCode ? { reasonCode: decision.reasonCode } : {}), ...(decision.evidence ? { evidence: redactSensitive(normalizeHookEvidence(decision.evidence), this.hookRedaction) as JsonObject } : {}) };
|
|
1118
|
+
if (decision.decision === "deny") terminal = this.deniedResult(call, "TOOL_HOOK_DENIED", "Tool execution was denied by an enforcement hook.", {}, metadata, startedAt);
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
} catch {
|
|
1124
|
+
projection = { ...projection, outcome: "failed" };
|
|
1125
|
+
terminal = this.errorResult(call, options.signal?.aborted ? "TOOL_HOOK_CANCELLED" : "TOOL_HOOK_FAILED", "Agent Tool enforcement hook failed.", metadata, startedAt);
|
|
1126
|
+
} finally {
|
|
1127
|
+
detach?.();
|
|
1128
|
+
const disposeError = await this.disposeScope(scope, this.scopeDisposeTimeoutMs);
|
|
1129
|
+
if (disposeError !== undefined) {
|
|
1130
|
+
projection = { ...projection, outcome: "failed" };
|
|
1131
|
+
terminal = this.errorResult(call, "TOOL_HOOK_FAILED", "Agent Tool enforcement hook disposal failed.", metadata, startedAt);
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
const evidenceRemainingMs = executionState.deadlineUnixMs === undefined
|
|
1135
|
+
? registration.timeoutMs ?? 5_000
|
|
1136
|
+
: Math.max(0, Math.min(registration.timeoutMs ?? 5_000, executionState.deadlineUnixMs - Date.now()));
|
|
1137
|
+
// Mandatory evidence records the cancellation terminal itself. It keeps
|
|
1138
|
+
// the remaining absolute deadline, but does not inherit the already
|
|
1139
|
+
// aborted caller signal and thereby erase that terminal classification.
|
|
1140
|
+
if (await this.recordMandatoryHook("enforcement", call, tool, metadata, options, startedAt, projection, evidenceRemainingMs)) {
|
|
1141
|
+
return this.errorResult(call, "TOOL_HOOK_AUDIT_FAILED", "Agent Tool enforcement evidence could not be recorded.", metadata, startedAt);
|
|
1142
|
+
}
|
|
1143
|
+
if (terminal !== undefined) return terminal;
|
|
1144
|
+
}
|
|
1145
|
+
return undefined;
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
private isPlatformHook(record: AgentOwnedHookRegistrationV1): boolean {
|
|
1149
|
+
return this.requiredPlatformHooks.some((reference) => reference.owner === record.payload.owner && reference.kind === record.payload.registration.kind && reference.id === record.payload.registration.id && reference.version === record.payload.registration.version);
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
private orderedHooks(records: readonly AgentOwnedHookRegistrationV1[]): readonly AgentOwnedHookRegistrationV1[] {
|
|
1153
|
+
return [...records].sort((left, right) => {
|
|
1154
|
+
const leftRegistration = left.payload.registration;
|
|
1155
|
+
const rightRegistration = right.payload.registration;
|
|
1156
|
+
return Number(this.isPlatformHook(right)) - Number(this.isPlatformHook(left))
|
|
1157
|
+
|| (leftRegistration.order ?? 0) - (rightRegistration.order ?? 0)
|
|
1158
|
+
|| leftRegistration.id.localeCompare(rightRegistration.id)
|
|
1159
|
+
|| leftRegistration.version - rightRegistration.version
|
|
1160
|
+
|| left.ownerName.localeCompare(right.ownerName);
|
|
1161
|
+
});
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
private async recordMandatoryHook(
|
|
1165
|
+
phase: "enforcement" | "settlement",
|
|
1166
|
+
call: AgentToolCall, tool: ToolDefinition, metadata: JsonObject, options: AgentToolExecutionOptions, startedAt: number,
|
|
1167
|
+
hook: AgentToolAuditHookProjectionV1,
|
|
1168
|
+
timeoutMs: number,
|
|
1169
|
+
signal?: AbortSignal,
|
|
1170
|
+
): Promise<boolean> {
|
|
1171
|
+
if (!this.auditSink) return true;
|
|
1172
|
+
try {
|
|
1173
|
+
const outcome = await awaitHook(Promise.resolve().then(() => auditSinkFunction(this.auditSink!)({ kind: "agent-tool", phase, callId: call.id, toolName: tool.metadata.name, input: redactSensitive(normalizeJsonValue(call.input, "hook.audit.input"), this.hookRedaction), tool: { name: tool.metadata.name, sideEffect: tool.metadata.sideEffect, approval: tool.metadata.approval, tags: tool.metadata.tags ?? [] }, startedAtUnixMs: startedAt, finishedAtUnixMs: Date.now(), durationMs: Date.now() - startedAt, metadata: redactSensitive(metadata, this.hookRedaction) as JsonObject, hook })), signal, timeoutMs);
|
|
1174
|
+
return outcome.kind !== "value";
|
|
1175
|
+
} catch { return true; }
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
private frameworkTerminalAuditTimeout(remainingMs: number | undefined): number {
|
|
1179
|
+
const hookBound = this.hookSnapshot!.hooks
|
|
1180
|
+
.filter(({ payload }) => payload.registration.kind === "enforcement" || payload.registration.kind === "settlement")
|
|
1181
|
+
.reduce((bound, { payload }) => Math.min(bound, payload.registration.timeoutMs ?? 5_000), Number.POSITIVE_INFINITY);
|
|
1182
|
+
return remainingMs === undefined ? hookBound : Math.min(hookBound, Math.max(0, remainingMs));
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
private async recordFrameworkTerminalAudit(result: AgentToolResult, startedAt: number, signal: AbortSignal | undefined, remainingMs: number | undefined): Promise<boolean> {
|
|
1186
|
+
if (!this.hasMandatoryHooks() || !this.auditSink) return false;
|
|
1187
|
+
try {
|
|
1188
|
+
const timeoutMs = this.frameworkTerminalAuditTimeout(remainingMs);
|
|
1189
|
+
const outcome = await awaitHook(Promise.resolve().then(() => auditSinkFunction(this.auditSink!)({
|
|
1190
|
+
kind: "agent-tool", phase: "result", callId: result.callId, toolName: result.name,
|
|
1191
|
+
input: Object.freeze({}), metadata: Object.freeze({}),
|
|
1192
|
+
result: redactSensitive(result, this.hookRedaction) as AgentToolResult,
|
|
1193
|
+
startedAtUnixMs: startedAt, finishedAtUnixMs: Date.now(), durationMs: Date.now() - startedAt,
|
|
1194
|
+
})), signal, timeoutMs);
|
|
1195
|
+
return outcome.kind !== "value";
|
|
1196
|
+
} catch { return true; }
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
private hasMandatoryHooks(): boolean {
|
|
1200
|
+
return this.hookSnapshot?.hooks.some(({ payload }) =>
|
|
1201
|
+
payload.registration.kind === "enforcement" || payload.registration.kind === "settlement",
|
|
1202
|
+
) === true;
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
private authorizeAgent(
|
|
1206
|
+
tool: ToolDefinition,
|
|
1207
|
+
options: AgentToolExecutionOptions,
|
|
1208
|
+
): AgentVisibilityDecision {
|
|
1209
|
+
const allowUnboundTools = options.allowUnboundTools ?? this.allowUnboundTools;
|
|
1210
|
+
if (!options.agentName) {
|
|
1211
|
+
if (allowUnboundTools) {
|
|
1212
|
+
return { allowed: true };
|
|
1213
|
+
}
|
|
1214
|
+
return {
|
|
1215
|
+
allowed: false,
|
|
1216
|
+
code: "TOOL_AGENT_REQUIRED",
|
|
1217
|
+
message: `Tool "${tool.metadata.name}" execution requires agentName.`,
|
|
1218
|
+
status: "error",
|
|
1219
|
+
};
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
const agent = this.registry.getAgent(options.agentName);
|
|
1223
|
+
if (!agent) {
|
|
1224
|
+
return {
|
|
1225
|
+
allowed: false,
|
|
1226
|
+
code: "AGENT_NOT_REGISTERED",
|
|
1227
|
+
message: `Agent "${options.agentName}" is not registered.`,
|
|
1228
|
+
status: "error",
|
|
1229
|
+
};
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
for (let index = 0; index < agent.tools.length; index += 1) {
|
|
1233
|
+
const visible = agent.tools[index] as ToolDefinition;
|
|
1234
|
+
if (visible.target === tool.target || visible.metadata.name === tool.metadata.name) {
|
|
1235
|
+
return { allowed: true, agentName: agent.metadata.name };
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
return {
|
|
1240
|
+
allowed: false,
|
|
1241
|
+
agentName: agent.metadata.name,
|
|
1242
|
+
code: "TOOL_NOT_VISIBLE_TO_AGENT",
|
|
1243
|
+
message: `Tool "${tool.metadata.name}" is not visible to agent "${agent.metadata.name}".`,
|
|
1244
|
+
status: "denied",
|
|
1245
|
+
};
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
private async authorize(
|
|
1249
|
+
call: AgentToolCall,
|
|
1250
|
+
tool: ToolDefinition,
|
|
1251
|
+
metadata: JsonObject,
|
|
1252
|
+
options: AgentToolExecutionOptions,
|
|
1253
|
+
idempotencyKey: string | undefined,
|
|
1254
|
+
): Promise<ApprovalDecision> {
|
|
1255
|
+
const approval = tool.metadata.approval;
|
|
1256
|
+
if (approval === "never") {
|
|
1257
|
+
return { allowed: true, executionApproval: "not-required" };
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
const policy = options.approvalPolicy ?? this.approvalPolicy;
|
|
1261
|
+
if (!policy) {
|
|
1262
|
+
if (approval === "policy" && (tool.metadata.sideEffect === "none" || tool.metadata.sideEffect === "read")) {
|
|
1263
|
+
return { allowed: true, executionApproval: "not-required" };
|
|
1264
|
+
}
|
|
1265
|
+
return {
|
|
1266
|
+
allowed: false,
|
|
1267
|
+
executionApproval: "not-required",
|
|
1268
|
+
code: approval === "required" ? "TOOL_APPROVAL_REQUIRED" : "TOOL_APPROVAL_POLICY_MISSING",
|
|
1269
|
+
message:
|
|
1270
|
+
approval === "required"
|
|
1271
|
+
? `Tool "${tool.metadata.name}" requires explicit approval.`
|
|
1272
|
+
: `Tool "${tool.metadata.name}" requires an approval policy.`,
|
|
1273
|
+
reason: approval,
|
|
1274
|
+
};
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
const reason: AgentToolApprovalReason = approval === "required" ? "required" : "policy";
|
|
1278
|
+
try {
|
|
1279
|
+
const approved = await approvalPolicyFunction(policy)({
|
|
1280
|
+
call,
|
|
1281
|
+
tool,
|
|
1282
|
+
reason,
|
|
1283
|
+
agentName: options.agentName,
|
|
1284
|
+
invocationId: options.invocationId,
|
|
1285
|
+
idempotencyKey,
|
|
1286
|
+
metadata,
|
|
1287
|
+
signal: options.signal,
|
|
1288
|
+
});
|
|
1289
|
+
if (approved === true) {
|
|
1290
|
+
return { allowed: true, executionApproval: "approved" };
|
|
1291
|
+
}
|
|
1292
|
+
return {
|
|
1293
|
+
allowed: false,
|
|
1294
|
+
executionApproval: "not-required",
|
|
1295
|
+
code: "TOOL_APPROVAL_DENIED",
|
|
1296
|
+
message: `Tool "${tool.metadata.name}" was denied by approval policy.`,
|
|
1297
|
+
reason,
|
|
1298
|
+
};
|
|
1299
|
+
} catch (error) {
|
|
1300
|
+
return {
|
|
1301
|
+
allowed: false,
|
|
1302
|
+
executionApproval: "not-required",
|
|
1303
|
+
code: "TOOL_APPROVAL_FAILED",
|
|
1304
|
+
message: `Tool approval policy failed: ${errorMessageOf(error)}`,
|
|
1305
|
+
reason,
|
|
1306
|
+
};
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
private schemaSettings(options: AgentToolExecutionOptions): SchemaValidationSettings {
|
|
1311
|
+
return {
|
|
1312
|
+
validator: options.schemaValidator ?? this.schemaValidator,
|
|
1313
|
+
allowUnvalidatedSchemas: options.allowUnvalidatedSchemas ?? this.allowUnvalidatedSchemas,
|
|
1314
|
+
};
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
private auditSettings(options: AgentToolExecutionOptions): AuditSettings {
|
|
1318
|
+
return {
|
|
1319
|
+
sink: options.auditSink ?? this.auditSink,
|
|
1320
|
+
failureMode: auditFailureModeOf(options.auditFailureMode, "toolExecution.auditFailureMode") ?? this.auditFailureMode,
|
|
1321
|
+
};
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
private retrySettings(options: AgentToolExecutionOptions): RetrySettings {
|
|
1325
|
+
if (options.retryPolicy === undefined) {
|
|
1326
|
+
return this.retryPolicy;
|
|
1327
|
+
}
|
|
1328
|
+
return this.normalizeRetryPolicy(options.retryPolicy, "toolExecution.retryPolicy");
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
private normalizeRetryPolicy(policy: AgentToolRetryPolicy | undefined, field: string): RetrySettings {
|
|
1332
|
+
if (policy === undefined) {
|
|
1333
|
+
return DEFAULT_RETRY_SETTINGS;
|
|
1334
|
+
}
|
|
1335
|
+
if (!isObjectRecord(policy)) {
|
|
1336
|
+
throw new AgentToolExecutionError(`${field} must be an object.`);
|
|
1337
|
+
}
|
|
1338
|
+
const typedPolicy = policy as AgentToolRetryPolicy;
|
|
1339
|
+
|
|
1340
|
+
assertPositiveInteger(typedPolicy.maxAttempts, `${field}.maxAttempts`);
|
|
1341
|
+
assertNonNegativeInteger(typedPolicy.delayMs, `${field}.delayMs`);
|
|
1342
|
+
assertNonNegativeInteger(typedPolicy.maxDelayMs, `${field}.maxDelayMs`);
|
|
1343
|
+
|
|
1344
|
+
const retryOnErrorCodes = this.retryOnErrorCodes(typedPolicy.retryOnErrorCodes, `${field}.retryOnErrorCodes`);
|
|
1345
|
+
const delayMs = typedPolicy.delayMs ?? 0;
|
|
1346
|
+
return Object.freeze({
|
|
1347
|
+
maxAttempts: typedPolicy.maxAttempts ?? DEFAULT_RETRY_SETTINGS.maxAttempts,
|
|
1348
|
+
delayMs,
|
|
1349
|
+
...(typedPolicy.maxDelayMs !== undefined ? { maxDelayMs: typedPolicy.maxDelayMs } : {}),
|
|
1350
|
+
backoff: retryBackoffOf(typedPolicy.backoff, `${field}.backoff`) ?? (delayMs > 0 ? "fixed" : "none"),
|
|
1351
|
+
...(retryOnErrorCodes !== undefined ? { retryOnErrorCodes } : {}),
|
|
1352
|
+
});
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
private retryOnErrorCodes(values: readonly string[] | undefined, field: string): ReadonlySet<string> | undefined {
|
|
1356
|
+
if (values === undefined) {
|
|
1357
|
+
return undefined;
|
|
1358
|
+
}
|
|
1359
|
+
if (!Array.isArray(values)) {
|
|
1360
|
+
throw new AgentToolExecutionError(`${field} must be an array of strings.`);
|
|
1361
|
+
}
|
|
1362
|
+
const normalized = new Set<string>();
|
|
1363
|
+
for (let index = 0; index < values.length; index += 1) {
|
|
1364
|
+
const value = values[index] as unknown;
|
|
1365
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
1366
|
+
throw new AgentToolExecutionError(`${field}[${index}] must be a non-empty string.`);
|
|
1367
|
+
}
|
|
1368
|
+
normalized.add(value.trim());
|
|
1369
|
+
}
|
|
1370
|
+
return normalized;
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
private ensureRetryPolicy(
|
|
1374
|
+
call: AgentToolCall,
|
|
1375
|
+
tool: ToolDefinition,
|
|
1376
|
+
retry: RetrySettings,
|
|
1377
|
+
idempotencyKey: string | undefined,
|
|
1378
|
+
metadata: JsonObject,
|
|
1379
|
+
startedAt: number,
|
|
1380
|
+
): AgentToolResult | undefined {
|
|
1381
|
+
if (retry.maxAttempts <= 1) {
|
|
1382
|
+
return undefined;
|
|
1383
|
+
}
|
|
1384
|
+
if (tool.metadata.sideEffect !== "write" && tool.metadata.sideEffect !== "external") {
|
|
1385
|
+
return undefined;
|
|
1386
|
+
}
|
|
1387
|
+
if (idempotencyKey !== undefined) {
|
|
1388
|
+
return undefined;
|
|
1389
|
+
}
|
|
1390
|
+
// A policy that selects only terminal non-retryable codes cannot ever
|
|
1391
|
+
// schedule a second write/external attempt, so it does not create the
|
|
1392
|
+
// idempotency hazard this gate protects against.
|
|
1393
|
+
const retryableCodes = retry.retryOnErrorCodes ?? DEFAULT_RETRYABLE_ERROR_CODES;
|
|
1394
|
+
if ([...retryableCodes].every((code) => NEVER_RETRY_ERROR_CODES.has(code))) {
|
|
1395
|
+
return undefined;
|
|
1396
|
+
}
|
|
1397
|
+
return this.errorResult(
|
|
1398
|
+
call,
|
|
1399
|
+
"TOOL_RETRY_IDEMPOTENCY_REQUIRED",
|
|
1400
|
+
`Tool "${tool.metadata.name}" has "${tool.metadata.sideEffect}" side effects and requires idempotencyKey when retryPolicy.maxAttempts is greater than 1.`,
|
|
1401
|
+
metadata,
|
|
1402
|
+
startedAt,
|
|
1403
|
+
{
|
|
1404
|
+
toolName: tool.metadata.name,
|
|
1405
|
+
sideEffect: tool.metadata.sideEffect,
|
|
1406
|
+
maxAttempts: retry.maxAttempts,
|
|
1407
|
+
},
|
|
1408
|
+
);
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
private ensureSchemaValidator(
|
|
1412
|
+
call: AgentToolCall,
|
|
1413
|
+
tool: ToolDefinition,
|
|
1414
|
+
schema: SchemaValidationSettings,
|
|
1415
|
+
metadata: JsonObject,
|
|
1416
|
+
startedAt: number,
|
|
1417
|
+
): AgentToolResult | undefined {
|
|
1418
|
+
if (schema.validator || schema.allowUnvalidatedSchemas) {
|
|
1419
|
+
return undefined;
|
|
1420
|
+
}
|
|
1421
|
+
if (!tool.metadata.input && !tool.metadata.output) {
|
|
1422
|
+
return undefined;
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
return this.errorResult(
|
|
1426
|
+
call,
|
|
1427
|
+
"TOOL_SCHEMA_VALIDATOR_MISSING",
|
|
1428
|
+
`Tool "${tool.metadata.name}" declares schema classes but no schema validator is configured.`,
|
|
1429
|
+
metadata,
|
|
1430
|
+
startedAt,
|
|
1431
|
+
{
|
|
1432
|
+
input: tool.metadata.input ? toolContractName(tool.metadata.input) : null,
|
|
1433
|
+
output: tool.metadata.output ? toolContractName(tool.metadata.output) : null,
|
|
1434
|
+
},
|
|
1435
|
+
);
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
private async prepareInput(
|
|
1439
|
+
call: AgentToolCall,
|
|
1440
|
+
tool: ToolDefinition,
|
|
1441
|
+
schema: SchemaValidationSettings,
|
|
1442
|
+
metadata: JsonObject,
|
|
1443
|
+
startedAt: number,
|
|
1444
|
+
): Promise<PreparedToolValue> {
|
|
1445
|
+
if (!tool.metadata.input) {
|
|
1446
|
+
return { ok: true, value: call.input };
|
|
1447
|
+
}
|
|
1448
|
+
return this.bindAndValidateClassContract(
|
|
1449
|
+
call,
|
|
1450
|
+
tool,
|
|
1451
|
+
tool.metadata.input,
|
|
1452
|
+
call.input,
|
|
1453
|
+
"input",
|
|
1454
|
+
schema,
|
|
1455
|
+
metadata,
|
|
1456
|
+
startedAt,
|
|
1457
|
+
);
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
private async executeAttempt(
|
|
1461
|
+
call: AgentToolCall,
|
|
1462
|
+
tool: ToolDefinition,
|
|
1463
|
+
input: unknown,
|
|
1464
|
+
approval: AgentToolExecutionApproval,
|
|
1465
|
+
agentName: string | undefined,
|
|
1466
|
+
metadata: JsonObject,
|
|
1467
|
+
schema: SchemaValidationSettings,
|
|
1468
|
+
options: AgentToolExecutionOptions,
|
|
1469
|
+
audit: AuditSettings,
|
|
1470
|
+
startedAt: number,
|
|
1471
|
+
attempt: AttemptContext,
|
|
1472
|
+
executionState: ToolExecutionState,
|
|
1473
|
+
): Promise<AgentToolResult> {
|
|
1474
|
+
const scope = this.services.createScope();
|
|
1475
|
+
const abortController = new AbortController();
|
|
1476
|
+
const detach = attachAbortForwarding(options.signal, abortController);
|
|
1477
|
+
const scopeDisposeTimeoutMs = options.scopeDisposeTimeoutMs ?? this.scopeDisposeTimeoutMs;
|
|
1478
|
+
if (abortController.signal.aborted) {
|
|
1479
|
+
detach?.();
|
|
1480
|
+
await this.disposeScope(scope, scopeDisposeTimeoutMs);
|
|
1481
|
+
return this.errorResult(
|
|
1482
|
+
call,
|
|
1483
|
+
"TOOL_ABORTED",
|
|
1484
|
+
"Tool execution was aborted before it started.",
|
|
1485
|
+
this.attemptMetadata(metadata, attempt),
|
|
1486
|
+
startedAt,
|
|
1487
|
+
{ phase: "pre-execute" },
|
|
1488
|
+
);
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1491
|
+
const operation = this.executeInScope(
|
|
1492
|
+
call,
|
|
1493
|
+
tool,
|
|
1494
|
+
input,
|
|
1495
|
+
approval,
|
|
1496
|
+
agentName,
|
|
1497
|
+
scope,
|
|
1498
|
+
metadata,
|
|
1499
|
+
schema,
|
|
1500
|
+
options,
|
|
1501
|
+
startedAt,
|
|
1502
|
+
attempt,
|
|
1503
|
+
abortController.signal,
|
|
1504
|
+
executionState,
|
|
1505
|
+
);
|
|
1506
|
+
let result: AgentToolResult;
|
|
1507
|
+
try {
|
|
1508
|
+
result = await timed(
|
|
1509
|
+
operation,
|
|
1510
|
+
undefined,
|
|
1511
|
+
() => abortController.abort(),
|
|
1512
|
+
abortController.signal,
|
|
1513
|
+
);
|
|
1514
|
+
} catch (error) {
|
|
1515
|
+
if (error instanceof AgentToolTimeoutError || error instanceof AgentToolAbortError) {
|
|
1516
|
+
this.deferScopeDisposal(scope, operation, call, scopeDisposeTimeoutMs, async (settledResult) => {
|
|
1517
|
+
await this.recordDeferredSettlement(
|
|
1518
|
+
this.auditProjector.create(
|
|
1519
|
+
"settlement",
|
|
1520
|
+
call,
|
|
1521
|
+
startedAt,
|
|
1522
|
+
metadata,
|
|
1523
|
+
options,
|
|
1524
|
+
tool,
|
|
1525
|
+
agentName,
|
|
1526
|
+
settledResult,
|
|
1527
|
+
attempt,
|
|
1528
|
+
),
|
|
1529
|
+
audit,
|
|
1530
|
+
);
|
|
1531
|
+
});
|
|
1532
|
+
const code = this.interruptionCode(error, tool, executionState);
|
|
1533
|
+
return this.errorResult(call, code, executionState.timedOut ? "Tool execution timed out." : errorMessageOf(error), this.attemptMetadata(metadata, attempt), startedAt, { phase: executionState.phase });
|
|
1534
|
+
}
|
|
1535
|
+
result = this.errorResult(
|
|
1536
|
+
call,
|
|
1537
|
+
"TOOL_EXECUTION_FAILED",
|
|
1538
|
+
errorMessageOf(error),
|
|
1539
|
+
this.attemptMetadata(metadata, attempt),
|
|
1540
|
+
startedAt,
|
|
1541
|
+
);
|
|
1542
|
+
} finally {
|
|
1543
|
+
detach?.();
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1546
|
+
const disposeError = await this.disposeScope(scope, scopeDisposeTimeoutMs);
|
|
1547
|
+
if (disposeError && result.status === "success") {
|
|
1548
|
+
const outcomeUnknown = tool.metadata.sideEffect === "write" || tool.metadata.sideEffect === "external";
|
|
1549
|
+
return this.errorResult(
|
|
1550
|
+
call,
|
|
1551
|
+
outcomeUnknown ? "TOOL_SCOPE_DISPOSE_FAILED_OUTCOME_UNKNOWN" : "TOOL_SCOPE_DISPOSE_FAILED",
|
|
1552
|
+
outcomeUnknown
|
|
1553
|
+
? `Tool "${tool.metadata.name}" completed its side effect but scope disposal failed; retry safety is unknown: ${disposeError}`
|
|
1554
|
+
: `Tool scope disposal failed: ${disposeError}`,
|
|
1555
|
+
this.attemptMetadata(metadata, attempt),
|
|
1556
|
+
startedAt,
|
|
1557
|
+
outcomeUnknown ? { sideEffect: tool.metadata.sideEffect } : undefined,
|
|
1558
|
+
);
|
|
1559
|
+
}
|
|
1560
|
+
return result;
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1563
|
+
private async executeInScope(
|
|
1564
|
+
call: AgentToolCall,
|
|
1565
|
+
tool: ToolDefinition,
|
|
1566
|
+
input: unknown,
|
|
1567
|
+
approval: AgentToolExecutionApproval,
|
|
1568
|
+
agentName: string | undefined,
|
|
1569
|
+
scope: ServiceScope,
|
|
1570
|
+
metadata: JsonObject,
|
|
1571
|
+
schema: SchemaValidationSettings,
|
|
1572
|
+
options: AgentToolExecutionOptions,
|
|
1573
|
+
startedAt: number,
|
|
1574
|
+
attempt: AttemptContext,
|
|
1575
|
+
signal: AbortSignal,
|
|
1576
|
+
executionState: ToolExecutionState,
|
|
1577
|
+
): Promise<AgentToolResult> {
|
|
1578
|
+
let instance: unknown;
|
|
1579
|
+
try {
|
|
1580
|
+
const contributed = this.hookSnapshot?.tools.filter((entry) => entry.payload.target === tool.target) ?? [];
|
|
1581
|
+
instance = contributed.length === 1
|
|
1582
|
+
? await contributed[0]!.activation.activateAsync(scope)
|
|
1583
|
+
: await scope.resolveAsync(tool.target);
|
|
1584
|
+
} catch (error) {
|
|
1585
|
+
return this.errorResult(
|
|
1586
|
+
call,
|
|
1587
|
+
"TOOL_RESOLVE_FAILED",
|
|
1588
|
+
`Tool resolve failed: ${errorMessageOf(error)}`,
|
|
1589
|
+
this.attemptMetadata(metadata, attempt),
|
|
1590
|
+
startedAt,
|
|
1591
|
+
);
|
|
1592
|
+
}
|
|
1593
|
+
|
|
1594
|
+
if (signal.aborted) {
|
|
1595
|
+
return this.errorResult(
|
|
1596
|
+
call,
|
|
1597
|
+
"TOOL_ABORTED",
|
|
1598
|
+
"Tool execution was aborted during dependency resolution.",
|
|
1599
|
+
this.attemptMetadata(metadata, attempt),
|
|
1600
|
+
startedAt,
|
|
1601
|
+
{ phase: "pre-execute" },
|
|
1602
|
+
);
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1605
|
+
if (!hasExecute(instance)) {
|
|
1606
|
+
return this.errorResult(
|
|
1607
|
+
call,
|
|
1608
|
+
"TOOL_METHOD_MISSING",
|
|
1609
|
+
`Tool "${tool.metadata.name}" must expose execute(input, context).`,
|
|
1610
|
+
this.attemptMetadata(metadata, attempt),
|
|
1611
|
+
startedAt,
|
|
1612
|
+
);
|
|
1613
|
+
}
|
|
1614
|
+
|
|
1615
|
+
const context: AgentToolExecutionContext = Object.freeze({
|
|
1616
|
+
call,
|
|
1617
|
+
tool,
|
|
1618
|
+
agentName,
|
|
1619
|
+
invocationId: options.invocationId,
|
|
1620
|
+
attempt: attempt.attempt,
|
|
1621
|
+
maxAttempts: attempt.maxAttempts,
|
|
1622
|
+
...(attempt.idempotencyKey !== undefined ? { idempotencyKey: attempt.idempotencyKey } : {}),
|
|
1623
|
+
approval,
|
|
1624
|
+
metadata,
|
|
1625
|
+
signal,
|
|
1626
|
+
});
|
|
1627
|
+
|
|
1628
|
+
let output: unknown;
|
|
1629
|
+
try {
|
|
1630
|
+
executionState.executeCompleted = false;
|
|
1631
|
+
executionState.executeStarted = true;
|
|
1632
|
+
executionState.phase = "execute";
|
|
1633
|
+
output = await instance.execute(input, context);
|
|
1634
|
+
executionState.executeCompleted = true;
|
|
1635
|
+
executionState.phase = "post-execute";
|
|
1636
|
+
} catch (error) {
|
|
1637
|
+
executionState.executeCompleted = true;
|
|
1638
|
+
const explicitlyPreCommit = error instanceof AgentToolPreCommitError;
|
|
1639
|
+
const outcomeUnknown = !explicitlyPreCommit
|
|
1640
|
+
&& (tool.metadata.sideEffect === "write" || tool.metadata.sideEffect === "external");
|
|
1641
|
+
return this.errorResult(
|
|
1642
|
+
call,
|
|
1643
|
+
outcomeUnknown ? "TOOL_EXECUTION_FAILED_OUTCOME_UNKNOWN" : "TOOL_EXECUTION_FAILED",
|
|
1644
|
+
outcomeUnknown
|
|
1645
|
+
? `Tool "${tool.metadata.name}" threw after side-effect execution started; retry safety is unknown: ${errorMessageOf(error)}`
|
|
1646
|
+
: errorMessageOf(error),
|
|
1647
|
+
this.attemptMetadata(metadata, attempt),
|
|
1648
|
+
startedAt,
|
|
1649
|
+
outcomeUnknown ? { sideEffect: tool.metadata.sideEffect, phase: "execute" } : undefined,
|
|
1650
|
+
);
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1653
|
+
let preparedOutput: PreparedToolValue;
|
|
1654
|
+
try {
|
|
1655
|
+
preparedOutput = await this.prepareOutput(
|
|
1656
|
+
call,
|
|
1657
|
+
tool,
|
|
1658
|
+
output,
|
|
1659
|
+
schema,
|
|
1660
|
+
this.attemptMetadata(metadata, attempt),
|
|
1661
|
+
startedAt,
|
|
1662
|
+
);
|
|
1663
|
+
} catch (error) {
|
|
1664
|
+
return this.postExecutionOutcome(
|
|
1665
|
+
call,
|
|
1666
|
+
tool,
|
|
1667
|
+
this.errorResult(
|
|
1668
|
+
call,
|
|
1669
|
+
"TOOL_OUTPUT_INVALID",
|
|
1670
|
+
`Tool output processing failed: ${errorMessageOf(error)}`,
|
|
1671
|
+
this.attemptMetadata(metadata, attempt),
|
|
1672
|
+
startedAt,
|
|
1673
|
+
),
|
|
1674
|
+
metadata,
|
|
1675
|
+
startedAt,
|
|
1676
|
+
attempt,
|
|
1677
|
+
);
|
|
1678
|
+
}
|
|
1679
|
+
if (!preparedOutput.ok) {
|
|
1680
|
+
return this.postExecutionOutcome(call, tool, preparedOutput.result, metadata, startedAt, attempt);
|
|
1681
|
+
}
|
|
1682
|
+
|
|
1683
|
+
try {
|
|
1684
|
+
return agentToolResult({
|
|
1685
|
+
callId: call.id,
|
|
1686
|
+
name: call.name,
|
|
1687
|
+
output: preparedOutput.value,
|
|
1688
|
+
durationMs: this.durationMs(startedAt),
|
|
1689
|
+
metadata: this.resultMetadata(tool, metadata, approval, agentName, attempt),
|
|
1690
|
+
});
|
|
1691
|
+
} catch (error) {
|
|
1692
|
+
return this.postExecutionOutcome(
|
|
1693
|
+
call,
|
|
1694
|
+
tool,
|
|
1695
|
+
this.errorResult(
|
|
1696
|
+
call,
|
|
1697
|
+
"TOOL_RESULT_INVALID",
|
|
1698
|
+
errorMessageOf(error),
|
|
1699
|
+
this.attemptMetadata(metadata, attempt),
|
|
1700
|
+
startedAt,
|
|
1701
|
+
),
|
|
1702
|
+
metadata,
|
|
1703
|
+
startedAt,
|
|
1704
|
+
attempt,
|
|
1705
|
+
);
|
|
1706
|
+
}
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1709
|
+
private postExecutionOutcome(
|
|
1710
|
+
call: AgentToolCall,
|
|
1711
|
+
tool: ToolDefinition,
|
|
1712
|
+
result: AgentToolResult,
|
|
1713
|
+
metadata: JsonObject,
|
|
1714
|
+
startedAt: number,
|
|
1715
|
+
attempt: AttemptContext,
|
|
1716
|
+
): AgentToolResult {
|
|
1717
|
+
if (tool.metadata.sideEffect !== "write" && tool.metadata.sideEffect !== "external") return result;
|
|
1718
|
+
return this.errorResult(
|
|
1719
|
+
call,
|
|
1720
|
+
"TOOL_POST_EXECUTION_FAILED_OUTCOME_UNKNOWN",
|
|
1721
|
+
`Tool "${tool.metadata.name}" completed its side effect but post-execution processing failed; retry safety is unknown.`,
|
|
1722
|
+
this.attemptMetadata(metadata, attempt),
|
|
1723
|
+
startedAt,
|
|
1724
|
+
{
|
|
1725
|
+
originalCode: result.error?.code ?? null,
|
|
1726
|
+
originalStatus: result.status,
|
|
1727
|
+
sideEffect: tool.metadata.sideEffect,
|
|
1728
|
+
},
|
|
1729
|
+
);
|
|
1730
|
+
}
|
|
1731
|
+
|
|
1732
|
+
private async prepareOutput(
|
|
1733
|
+
call: AgentToolCall,
|
|
1734
|
+
tool: ToolDefinition,
|
|
1735
|
+
output: unknown,
|
|
1736
|
+
schema: SchemaValidationSettings,
|
|
1737
|
+
metadata: JsonObject,
|
|
1738
|
+
startedAt: number,
|
|
1739
|
+
): Promise<PreparedToolValue> {
|
|
1740
|
+
if (!tool.metadata.output) {
|
|
1741
|
+
return { ok: true, value: output };
|
|
1742
|
+
}
|
|
1743
|
+
|
|
1744
|
+
const prepared = await this.bindAndValidateClassContract(
|
|
1745
|
+
call,
|
|
1746
|
+
tool,
|
|
1747
|
+
tool.metadata.output,
|
|
1748
|
+
output,
|
|
1749
|
+
"output",
|
|
1750
|
+
schema,
|
|
1751
|
+
metadata,
|
|
1752
|
+
startedAt,
|
|
1753
|
+
);
|
|
1754
|
+
if (!prepared.ok) {
|
|
1755
|
+
return prepared;
|
|
1756
|
+
}
|
|
1757
|
+
return { ok: true, value: this.plainObjectFromModel(prepared.value as object) };
|
|
1758
|
+
}
|
|
1759
|
+
|
|
1760
|
+
private async bindAndValidateClassContract(
|
|
1761
|
+
call: AgentToolCall,
|
|
1762
|
+
tool: ToolDefinition,
|
|
1763
|
+
contract: Class<object>,
|
|
1764
|
+
value: unknown,
|
|
1765
|
+
direction: "input" | "output",
|
|
1766
|
+
schema: SchemaValidationSettings,
|
|
1767
|
+
metadata: JsonObject,
|
|
1768
|
+
startedAt: number,
|
|
1769
|
+
): Promise<PreparedToolValue> {
|
|
1770
|
+
const prepared = await this.contractValidator.validate(contract, value, direction, tool.metadata.name, schema.validator);
|
|
1771
|
+
return prepared.ok ? prepared : {
|
|
1772
|
+
ok: false,
|
|
1773
|
+
result: this.errorResult(call, prepared.error.code, prepared.error.message, metadata, startedAt, prepared.error.details),
|
|
1774
|
+
};
|
|
1775
|
+
}
|
|
1776
|
+
|
|
1777
|
+
private plainObjectFromModel(instance: object): JsonObject {
|
|
1778
|
+
return agentModelJson(instance, "toolResult.output");
|
|
1779
|
+
}
|
|
1780
|
+
|
|
1781
|
+
private effectiveCall(call: AgentToolCall, tool: ToolDefinition, input: unknown): AgentToolCall {
|
|
1782
|
+
if (tool.metadata.input === undefined) return call;
|
|
1783
|
+
return agentToolCall({
|
|
1784
|
+
id: call.id,
|
|
1785
|
+
name: call.name,
|
|
1786
|
+
input: this.plainObjectFromModel(input as object),
|
|
1787
|
+
metadata: call.metadata,
|
|
1788
|
+
});
|
|
1789
|
+
}
|
|
1790
|
+
|
|
1791
|
+
private freezeToolInput<T>(value: T): T {
|
|
1792
|
+
const visit = (item: unknown, seen: WeakSet<object>): void => {
|
|
1793
|
+
if (item === null || typeof item !== "object" || seen.has(item)) return;
|
|
1794
|
+
seen.add(item);
|
|
1795
|
+
for (const child of Object.values(item as Record<string, unknown>)) visit(child, seen);
|
|
1796
|
+
Object.freeze(item);
|
|
1797
|
+
};
|
|
1798
|
+
visit(value, new WeakSet<object>());
|
|
1799
|
+
return value;
|
|
1800
|
+
}
|
|
1801
|
+
|
|
1802
|
+
private shouldRetry(result: AgentToolResult, retry: RetrySettings, attempt: number): boolean {
|
|
1803
|
+
if (attempt >= retry.maxAttempts) {
|
|
1804
|
+
return false;
|
|
1805
|
+
}
|
|
1806
|
+
if (result.status !== "error" || !result.error) {
|
|
1807
|
+
return false;
|
|
1808
|
+
}
|
|
1809
|
+
const code = result.error.code;
|
|
1810
|
+
if (!code || NEVER_RETRY_ERROR_CODES.has(code)) {
|
|
1811
|
+
return false;
|
|
1812
|
+
}
|
|
1813
|
+
const retryableCodes = retry.retryOnErrorCodes ?? DEFAULT_RETRYABLE_ERROR_CODES;
|
|
1814
|
+
return retryableCodes.has(code);
|
|
1815
|
+
}
|
|
1816
|
+
|
|
1817
|
+
private async delayBeforeRetry(
|
|
1818
|
+
retry: RetrySettings,
|
|
1819
|
+
completedAttempt: number,
|
|
1820
|
+
signal: AbortSignal | undefined,
|
|
1821
|
+
): Promise<boolean> {
|
|
1822
|
+
const delayMs = this.retryDelayMs(retry, completedAttempt);
|
|
1823
|
+
if (delayMs <= 0) {
|
|
1824
|
+
return signal?.aborted === true;
|
|
1825
|
+
}
|
|
1826
|
+
if (signal?.aborted) {
|
|
1827
|
+
return true;
|
|
1828
|
+
}
|
|
1829
|
+
return new Promise<boolean>((resolve) => {
|
|
1830
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
1831
|
+
const finish = (aborted: boolean): void => {
|
|
1832
|
+
if (timer === undefined) return;
|
|
1833
|
+
clearTimeout(timer);
|
|
1834
|
+
timer = undefined;
|
|
1835
|
+
signal?.removeEventListener("abort", onAbort);
|
|
1836
|
+
resolve(aborted);
|
|
1837
|
+
};
|
|
1838
|
+
const onAbort = () => finish(true);
|
|
1839
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
1840
|
+
timer = setTimeout(() => finish(false), delayMs);
|
|
1841
|
+
});
|
|
1842
|
+
}
|
|
1843
|
+
|
|
1844
|
+
private retryDelayMs(retry: RetrySettings, completedAttempt: number): number {
|
|
1845
|
+
if (retry.delayMs <= 0 || retry.backoff === "none") {
|
|
1846
|
+
return 0;
|
|
1847
|
+
}
|
|
1848
|
+
const rawDelay =
|
|
1849
|
+
retry.backoff === "exponential"
|
|
1850
|
+
? retry.delayMs * 2 ** Math.max(0, completedAttempt - 1)
|
|
1851
|
+
: retry.delayMs;
|
|
1852
|
+
return retry.maxDelayMs === undefined ? rawDelay : Math.min(rawDelay, retry.maxDelayMs);
|
|
1853
|
+
}
|
|
1854
|
+
|
|
1855
|
+
private async finish(
|
|
1856
|
+
call: AgentToolCall,
|
|
1857
|
+
result: AgentToolResult,
|
|
1858
|
+
startedAt: number,
|
|
1859
|
+
metadata: JsonObject,
|
|
1860
|
+
options: AgentToolExecutionOptions,
|
|
1861
|
+
audit: AuditSettings,
|
|
1862
|
+
tool?: ToolDefinition,
|
|
1863
|
+
agentName?: string,
|
|
1864
|
+
attempt?: AttemptContext,
|
|
1865
|
+
executionState?: ToolExecutionState,
|
|
1866
|
+
alreadySettled = false,
|
|
1867
|
+
): Promise<AgentToolResult> {
|
|
1868
|
+
let terminal = result;
|
|
1869
|
+
const state = executionState ?? { executeStarted: false, executeCompleted: false, phase: "pre-execute" as const };
|
|
1870
|
+
const settlement = tool === undefined
|
|
1871
|
+
? undefined
|
|
1872
|
+
: this.settlementEvent(call, tool, terminal, options, agentName, state, attempt);
|
|
1873
|
+
if (!alreadySettled && tool !== undefined) {
|
|
1874
|
+
const settled = await this.settleHooks(call, terminal, metadata, options, startedAt, state, settlement!);
|
|
1875
|
+
terminal = result.error?.code === "TOOL_HOOK_AUDIT_FAILED" ? result : settled;
|
|
1876
|
+
}
|
|
1877
|
+
if (executionState !== undefined) executionState.phase = "result-audit";
|
|
1878
|
+
const auditRemainingMs = executionState?.deadlineUnixMs === undefined
|
|
1879
|
+
? undefined : Math.max(0, executionState.deadlineUnixMs - Date.now());
|
|
1880
|
+
const auditOutcome = await this.audit(
|
|
1881
|
+
this.auditProjector.create("result", call, startedAt, metadata, options, tool, agentName, terminal, attempt),
|
|
1882
|
+
audit,
|
|
1883
|
+
call,
|
|
1884
|
+
metadata,
|
|
1885
|
+
startedAt,
|
|
1886
|
+
terminal,
|
|
1887
|
+
options.signal,
|
|
1888
|
+
auditRemainingMs,
|
|
1889
|
+
);
|
|
1890
|
+
const auditError = auditOutcome.kind === "value"
|
|
1891
|
+
? auditOutcome.value
|
|
1892
|
+
: (() => {
|
|
1893
|
+
if (terminal.status !== "success") return terminal;
|
|
1894
|
+
const outcomeUnknown = (terminal.status === "success" && (tool?.metadata.sideEffect === "write" || tool?.metadata.sideEffect === "external"))
|
|
1895
|
+
|| terminal.error?.code?.endsWith("_OUTCOME_UNKNOWN") === true;
|
|
1896
|
+
const cancelled = state.callerSignal?.aborted === true;
|
|
1897
|
+
return this.errorResult(call,
|
|
1898
|
+
cancelled ? (outcomeUnknown ? "TOOL_ABORTED_OUTCOME_UNKNOWN" : "TOOL_ABORTED") : (outcomeUnknown ? "TOOL_TIMEOUT_OUTCOME_UNKNOWN" : "TOOL_TIMEOUT"),
|
|
1899
|
+
"Tool result audit did not complete before the execution boundary.", metadata, startedAt,
|
|
1900
|
+
{ phase: "result-audit", sideEffect: tool?.metadata.sideEffect ?? null });
|
|
1901
|
+
})();
|
|
1902
|
+
const deadlineExpired = state.deadlineUnixMs !== undefined
|
|
1903
|
+
&& state.deadlineUnixMs <= Date.now()
|
|
1904
|
+
&& state.callerSignal?.aborted !== true;
|
|
1905
|
+
const deadlineError = auditError === undefined && terminal.status === "success" && deadlineExpired
|
|
1906
|
+
? this.errorResult(call,
|
|
1907
|
+
tool?.metadata.sideEffect === "write" || tool?.metadata.sideEffect === "external"
|
|
1908
|
+
? "TOOL_TIMEOUT_OUTCOME_UNKNOWN" : "TOOL_TIMEOUT",
|
|
1909
|
+
"Tool execution timed out before result audit completed.", metadata, startedAt,
|
|
1910
|
+
{ phase: "result-audit", sideEffect: tool?.metadata.sideEffect ?? null })
|
|
1911
|
+
: undefined;
|
|
1912
|
+
if (settlement !== undefined) {
|
|
1913
|
+
// Observers receive the exact immutable redacted object that required
|
|
1914
|
+
// settlement received, never a separately reconstructed raw result.
|
|
1915
|
+
await this.observeHooks(settlement, options);
|
|
1916
|
+
}
|
|
1917
|
+
return auditError ?? deadlineError ?? terminal;
|
|
1918
|
+
}
|
|
1919
|
+
|
|
1920
|
+
private async audit(
|
|
1921
|
+
entry: AgentToolAuditEntry,
|
|
1922
|
+
audit: AuditSettings,
|
|
1923
|
+
call: AgentToolCall,
|
|
1924
|
+
metadata: JsonObject,
|
|
1925
|
+
startedAt: number,
|
|
1926
|
+
originalResult?: AgentToolResult,
|
|
1927
|
+
signal?: AbortSignal,
|
|
1928
|
+
timeoutMs?: number,
|
|
1929
|
+
): Promise<HookOutcome<AgentToolResult | undefined>> {
|
|
1930
|
+
if (!audit.sink) {
|
|
1931
|
+
return { kind: "value", value: undefined };
|
|
1932
|
+
}
|
|
1933
|
+
|
|
1934
|
+
let recorded: void | Promise<void>;
|
|
1935
|
+
try {
|
|
1936
|
+
// Invoke the sink synchronously. A void return means the audit is
|
|
1937
|
+
// already recorded, including when the caller has just aborted.
|
|
1938
|
+
recorded = auditSinkFunction(audit.sink)(entry);
|
|
1939
|
+
} catch (error) {
|
|
1940
|
+
return { kind: "value", value: this.auditFailure(entry, audit, call, metadata, startedAt, originalResult, error) };
|
|
1941
|
+
}
|
|
1942
|
+
let then: unknown;
|
|
1943
|
+
try {
|
|
1944
|
+
then = recorded && (recorded as unknown as { then?: unknown }).then;
|
|
1945
|
+
} catch (error) {
|
|
1946
|
+
return { kind: "value", value: this.auditFailure(entry, audit, call, metadata, startedAt, originalResult, error) };
|
|
1947
|
+
}
|
|
1948
|
+
if (typeof then !== "function") {
|
|
1949
|
+
return { kind: "value", value: undefined };
|
|
1950
|
+
}
|
|
1951
|
+
const pending = new Promise<void>((resolve, reject) => {
|
|
1952
|
+
try {
|
|
1953
|
+
(then as (this: unknown, resolve: () => void, reject: (error: unknown) => void) => unknown).call(recorded, resolve, reject);
|
|
1954
|
+
} catch (error) {
|
|
1955
|
+
reject(error);
|
|
1956
|
+
}
|
|
1957
|
+
});
|
|
1958
|
+
const settled = pending.then(
|
|
1959
|
+
() => ({ ok: true } as const),
|
|
1960
|
+
(error: unknown) => ({ ok: false, error } as const),
|
|
1961
|
+
);
|
|
1962
|
+
const outcome = await awaitHook(settled, signal, timeoutMs);
|
|
1963
|
+
if (outcome.kind !== "value") return outcome;
|
|
1964
|
+
return outcome.value.ok
|
|
1965
|
+
? { kind: "value", value: undefined }
|
|
1966
|
+
: { kind: "value", value: this.auditFailure(entry, audit, call, metadata, startedAt, originalResult, outcome.value.error) };
|
|
1967
|
+
}
|
|
1968
|
+
|
|
1969
|
+
private auditFailure(
|
|
1970
|
+
entry: AgentToolAuditEntry,
|
|
1971
|
+
audit: AuditSettings,
|
|
1972
|
+
call: AgentToolCall,
|
|
1973
|
+
metadata: JsonObject,
|
|
1974
|
+
startedAt: number,
|
|
1975
|
+
originalResult: AgentToolResult | undefined,
|
|
1976
|
+
error: unknown,
|
|
1977
|
+
): AgentToolResult | undefined {
|
|
1978
|
+
if (audit.failureMode === "best-effort") return undefined;
|
|
1979
|
+
const sideEffectingSuccess = entry.phase === "result"
|
|
1980
|
+
&& originalResult?.status === "success"
|
|
1981
|
+
&& (entry.tool?.sideEffect === "write" || entry.tool?.sideEffect === "external");
|
|
1982
|
+
const originalOutcomeUnknown = entry.phase === "result"
|
|
1983
|
+
&& originalResult?.error?.code?.endsWith("_OUTCOME_UNKNOWN") === true;
|
|
1984
|
+
const outcomeUnknown = sideEffectingSuccess || originalOutcomeUnknown;
|
|
1985
|
+
return this.errorResult(
|
|
1986
|
+
call,
|
|
1987
|
+
outcomeUnknown ? "TOOL_AUDIT_FAILED_OUTCOME_UNKNOWN" : "TOOL_AUDIT_FAILED",
|
|
1988
|
+
outcomeUnknown
|
|
1989
|
+
? `Tool result audit failed after an outcome-unknown side-effecting operation; retry safety is unknown: ${errorMessageOf(error)}`
|
|
1990
|
+
: `Tool audit failed: ${errorMessageOf(error)}`,
|
|
1991
|
+
metadata,
|
|
1992
|
+
startedAt,
|
|
1993
|
+
{
|
|
1994
|
+
phase: entry.phase,
|
|
1995
|
+
originalStatus: originalResult?.status ?? null,
|
|
1996
|
+
originalCode: originalResult?.error?.code ?? null,
|
|
1997
|
+
sideEffect: entry.tool?.sideEffect ?? null,
|
|
1998
|
+
},
|
|
1999
|
+
);
|
|
2000
|
+
}
|
|
2001
|
+
|
|
2002
|
+
private async disposeScope(scope: ServiceScope, timeoutMs: number): Promise<string | undefined> {
|
|
2003
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
2004
|
+
const disposal = Promise.resolve()
|
|
2005
|
+
.then(() => scope.dispose())
|
|
2006
|
+
.then(
|
|
2007
|
+
() => undefined,
|
|
2008
|
+
(error: unknown) => errorMessageOf(error),
|
|
2009
|
+
);
|
|
2010
|
+
const timeout = new Promise<string>((resolve) => {
|
|
2011
|
+
timer = setTimeout(() => resolve(`Tool scope disposal timed out after ${timeoutMs} ms.`), timeoutMs);
|
|
2012
|
+
});
|
|
2013
|
+
try {
|
|
2014
|
+
return await Promise.race([disposal, timeout]);
|
|
2015
|
+
} finally {
|
|
2016
|
+
if (timer !== undefined) clearTimeout(timer);
|
|
2017
|
+
}
|
|
2018
|
+
}
|
|
2019
|
+
|
|
2020
|
+
private deferScopeDisposal(
|
|
2021
|
+
scope: ServiceScope,
|
|
2022
|
+
operation: Promise<AgentToolResult>,
|
|
2023
|
+
call: AgentToolCall,
|
|
2024
|
+
scopeDisposeTimeoutMs: number,
|
|
2025
|
+
onSettled: (result: AgentToolResult) => void | Promise<void>,
|
|
2026
|
+
): void {
|
|
2027
|
+
void operation
|
|
2028
|
+
.then(
|
|
2029
|
+
(result) => result,
|
|
2030
|
+
(error: unknown) => agentToolResult({
|
|
2031
|
+
callId: call.id,
|
|
2032
|
+
name: call.name,
|
|
2033
|
+
status: "error",
|
|
2034
|
+
error: { code: "TOOL_SETTLEMENT_FAILED", message: redactSensitive(errorMessageOf(error)) as string },
|
|
2035
|
+
}),
|
|
2036
|
+
)
|
|
2037
|
+
.then(async (result) => {
|
|
2038
|
+
const error = await this.disposeScope(scope, scopeDisposeTimeoutMs);
|
|
2039
|
+
if (error !== undefined) {
|
|
2040
|
+
console.error("[agent-tool] deferred scope disposal failed:", redactSensitive(error));
|
|
2041
|
+
}
|
|
2042
|
+
await onSettled(result);
|
|
2043
|
+
})
|
|
2044
|
+
.catch((error: unknown) => {
|
|
2045
|
+
console.error("[agent-tool] deferred settlement handling failed:", redactSensitive(errorMessageOf(error)));
|
|
2046
|
+
});
|
|
2047
|
+
}
|
|
2048
|
+
|
|
2049
|
+
private async recordDeferredSettlement(entry: AgentToolAuditEntry, audit: AuditSettings): Promise<void> {
|
|
2050
|
+
if (audit.sink === undefined) return;
|
|
2051
|
+
try {
|
|
2052
|
+
await auditSinkFunction(audit.sink)(entry);
|
|
2053
|
+
} catch (error) {
|
|
2054
|
+
console.error("[agent-tool] deferred settlement audit failed:", redactSensitive(errorMessageOf(error)));
|
|
2055
|
+
}
|
|
2056
|
+
}
|
|
2057
|
+
|
|
2058
|
+
|
|
2059
|
+
private errorResult(
|
|
2060
|
+
call: AgentToolCall,
|
|
2061
|
+
code: string,
|
|
2062
|
+
message: string,
|
|
2063
|
+
metadata: JsonObject,
|
|
2064
|
+
startedAt: number,
|
|
2065
|
+
details?: JsonObject,
|
|
2066
|
+
): AgentToolResult {
|
|
2067
|
+
const safeMessage = redactSensitive(message) as string;
|
|
2068
|
+
const safeDetails = details === undefined ? undefined : redactSensitive(details) as JsonObject;
|
|
2069
|
+
return agentToolResult({
|
|
2070
|
+
callId: call.id,
|
|
2071
|
+
name: call.name,
|
|
2072
|
+
status: "error",
|
|
2073
|
+
error: safeDetails ? { code, message: safeMessage, details: safeDetails } : { code, message: safeMessage },
|
|
2074
|
+
durationMs: this.durationMs(startedAt),
|
|
2075
|
+
metadata,
|
|
2076
|
+
});
|
|
2077
|
+
}
|
|
2078
|
+
|
|
2079
|
+
private deniedResult(
|
|
2080
|
+
call: AgentToolCall,
|
|
2081
|
+
code: string,
|
|
2082
|
+
message: string,
|
|
2083
|
+
details: JsonObject,
|
|
2084
|
+
metadata: JsonObject,
|
|
2085
|
+
startedAt: number,
|
|
2086
|
+
): AgentToolResult {
|
|
2087
|
+
const safeMessage = redactSensitive(message) as string;
|
|
2088
|
+
const safeDetails = redactSensitive(details) as JsonObject;
|
|
2089
|
+
return agentToolResult({
|
|
2090
|
+
callId: call.id,
|
|
2091
|
+
name: call.name,
|
|
2092
|
+
status: "denied",
|
|
2093
|
+
error: { code, message: safeMessage, details: safeDetails },
|
|
2094
|
+
durationMs: this.durationMs(startedAt),
|
|
2095
|
+
metadata,
|
|
2096
|
+
});
|
|
2097
|
+
}
|
|
2098
|
+
|
|
2099
|
+
private resultMetadata(
|
|
2100
|
+
tool: ToolDefinition,
|
|
2101
|
+
metadata: JsonObject,
|
|
2102
|
+
approval: AgentToolExecutionApproval,
|
|
2103
|
+
agentName: string | undefined,
|
|
2104
|
+
attempt: AttemptContext,
|
|
2105
|
+
): JsonObject {
|
|
2106
|
+
return Object.freeze({
|
|
2107
|
+
...metadata,
|
|
2108
|
+
agentName: agentName ?? null,
|
|
2109
|
+
toolSideEffect: tool.metadata.sideEffect,
|
|
2110
|
+
toolApproval: tool.metadata.approval,
|
|
2111
|
+
executionApproval: approval,
|
|
2112
|
+
toolAttempt: attempt.attempt,
|
|
2113
|
+
toolMaxAttempts: attempt.maxAttempts,
|
|
2114
|
+
toolRetried: attempt.attempt > 1,
|
|
2115
|
+
});
|
|
2116
|
+
}
|
|
2117
|
+
|
|
2118
|
+
private attemptMetadata(metadata: JsonObject, attempt: AttemptContext): JsonObject {
|
|
2119
|
+
return Object.freeze({
|
|
2120
|
+
...metadata,
|
|
2121
|
+
toolAttempt: attempt.attempt,
|
|
2122
|
+
toolMaxAttempts: attempt.maxAttempts,
|
|
2123
|
+
toolRetried: attempt.attempt > 1,
|
|
2124
|
+
});
|
|
2125
|
+
}
|
|
2126
|
+
|
|
2127
|
+
private durationMs(startedAt: number): number {
|
|
2128
|
+
return Math.max(0, Date.now() - startedAt);
|
|
2129
|
+
}
|
|
2130
|
+
}
|