@retinue/agentkit 0.1.0
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 +22 -0
- package/README.md +310 -0
- package/dist/adapters/bullmq/consumer.d.ts +33 -0
- package/dist/adapters/bullmq/consumer.js +41 -0
- package/dist/adapters/bullmq/dispatcher.d.ts +74 -0
- package/dist/adapters/bullmq/dispatcher.js +160 -0
- package/dist/adapters/bullmq/export.d.ts +31 -0
- package/dist/adapters/bullmq/export.js +53 -0
- package/dist/adapters/bullmq/extraction.d.ts +42 -0
- package/dist/adapters/bullmq/extraction.js +63 -0
- package/dist/adapters/bullmq/index.d.ts +13 -0
- package/dist/adapters/bullmq/index.js +13 -0
- package/dist/adapters/bullmq/lock.d.ts +77 -0
- package/dist/adapters/bullmq/lock.js +126 -0
- package/dist/adapters/bullmq/queue.d.ts +50 -0
- package/dist/adapters/bullmq/queue.js +81 -0
- package/dist/adapters/memory/artifact-exports.d.ts +11 -0
- package/dist/adapters/memory/artifact-exports.js +102 -0
- package/dist/adapters/memory/artifacts.d.ts +15 -0
- package/dist/adapters/memory/artifacts.js +134 -0
- package/dist/adapters/memory/blobs.d.ts +7 -0
- package/dist/adapters/memory/blobs.js +27 -0
- package/dist/adapters/memory/evaluation.d.ts +18 -0
- package/dist/adapters/memory/evaluation.js +148 -0
- package/dist/adapters/memory/files.d.ts +27 -0
- package/dist/adapters/memory/files.js +0 -0
- package/dist/adapters/memory/flows.d.ts +16 -0
- package/dist/adapters/memory/flows.js +117 -0
- package/dist/adapters/memory/hitl.d.ts +9 -0
- package/dist/adapters/memory/hitl.js +130 -0
- package/dist/adapters/memory/idempotency.d.ts +13 -0
- package/dist/adapters/memory/idempotency.js +32 -0
- package/dist/adapters/memory/index.d.ts +39 -0
- package/dist/adapters/memory/index.js +107 -0
- package/dist/adapters/memory/knowledge.d.ts +43 -0
- package/dist/adapters/memory/knowledge.js +248 -0
- package/dist/adapters/memory/mcp.d.ts +9 -0
- package/dist/adapters/memory/mcp.js +37 -0
- package/dist/adapters/memory/message-store.d.ts +17 -0
- package/dist/adapters/memory/message-store.js +70 -0
- package/dist/adapters/memory/principal-memory.d.ts +7 -0
- package/dist/adapters/memory/principal-memory.js +83 -0
- package/dist/adapters/memory/runtime.d.ts +29 -0
- package/dist/adapters/memory/runtime.js +0 -0
- package/dist/adapters/memory/sessions.d.ts +29 -0
- package/dist/adapters/memory/sessions.js +0 -0
- package/dist/adapters/memory/skills.d.ts +10 -0
- package/dist/adapters/memory/skills.js +41 -0
- package/dist/adapters/memory/thread-summary.d.ts +7 -0
- package/dist/adapters/memory/thread-summary.js +29 -0
- package/dist/adapters/memory/usage-limits.d.ts +13 -0
- package/dist/adapters/memory/usage-limits.js +72 -0
- package/dist/adapters/memory/usage.d.ts +16 -0
- package/dist/adapters/memory/usage.js +279 -0
- package/dist/adapters/otel/index.d.ts +111 -0
- package/dist/adapters/otel/index.js +133 -0
- package/dist/adapters/postgres/artifact-exports.d.ts +12 -0
- package/dist/adapters/postgres/artifact-exports.js +117 -0
- package/dist/adapters/postgres/artifacts.d.ts +16 -0
- package/dist/adapters/postgres/artifacts.js +172 -0
- package/dist/adapters/postgres/checkpoint-store.d.ts +16 -0
- package/dist/adapters/postgres/checkpoint-store.js +34 -0
- package/dist/adapters/postgres/config.d.ts +15 -0
- package/dist/adapters/postgres/config.js +187 -0
- package/dist/adapters/postgres/conversation-store.d.ts +4 -0
- package/dist/adapters/postgres/conversation-store.js +82 -0
- package/dist/adapters/postgres/evaluation.d.ts +17 -0
- package/dist/adapters/postgres/evaluation.js +193 -0
- package/dist/adapters/postgres/file-content.d.ts +30 -0
- package/dist/adapters/postgres/file-content.js +111 -0
- package/dist/adapters/postgres/files.d.ts +19 -0
- package/dist/adapters/postgres/files.js +209 -0
- package/dist/adapters/postgres/flows.d.ts +20 -0
- package/dist/adapters/postgres/flows.js +206 -0
- package/dist/adapters/postgres/hitl.d.ts +5 -0
- package/dist/adapters/postgres/hitl.js +247 -0
- package/dist/adapters/postgres/index.d.ts +35 -0
- package/dist/adapters/postgres/index.js +35 -0
- package/dist/adapters/postgres/knowledge.d.ts +48 -0
- package/dist/adapters/postgres/knowledge.js +255 -0
- package/dist/adapters/postgres/memory.d.ts +14 -0
- package/dist/adapters/postgres/memory.js +194 -0
- package/dist/adapters/postgres/message-store.d.ts +11 -0
- package/dist/adapters/postgres/message-store.js +145 -0
- package/dist/adapters/postgres/migrations.d.ts +69 -0
- package/dist/adapters/postgres/migrations.js +1594 -0
- package/dist/adapters/postgres/pg-executor.d.ts +19 -0
- package/dist/adapters/postgres/pg-executor.js +32 -0
- package/dist/adapters/postgres/retention.d.ts +26 -0
- package/dist/adapters/postgres/retention.js +59 -0
- package/dist/adapters/postgres/rollups.d.ts +17 -0
- package/dist/adapters/postgres/rollups.js +267 -0
- package/dist/adapters/postgres/run-coordinator.d.ts +5 -0
- package/dist/adapters/postgres/run-coordinator.js +98 -0
- package/dist/adapters/postgres/run-event-log.d.ts +26 -0
- package/dist/adapters/postgres/run-event-log.js +30 -0
- package/dist/adapters/postgres/run-store.d.ts +4 -0
- package/dist/adapters/postgres/run-store.js +199 -0
- package/dist/adapters/postgres/schema.d.ts +39 -0
- package/dist/adapters/postgres/schema.js +70 -0
- package/dist/adapters/postgres/session-state.d.ts +7 -0
- package/dist/adapters/postgres/session-state.js +99 -0
- package/dist/adapters/postgres/sql.d.ts +8 -0
- package/dist/adapters/postgres/sql.js +2 -0
- package/dist/adapters/postgres/transaction.d.ts +37 -0
- package/dist/adapters/postgres/transaction.js +93 -0
- package/dist/adapters/postgres/unit-of-work.d.ts +18 -0
- package/dist/adapters/postgres/unit-of-work.js +8 -0
- package/dist/adapters/postgres/usage-limits.d.ts +15 -0
- package/dist/adapters/postgres/usage-limits.js +136 -0
- package/dist/adapters/postgres/usage.d.ts +15 -0
- package/dist/adapters/postgres/usage.js +226 -0
- package/dist/adapters/redis/index.d.ts +9 -0
- package/dist/adapters/redis/index.js +9 -0
- package/dist/adapters/redis/realtime.d.ts +74 -0
- package/dist/adapters/redis/realtime.js +112 -0
- package/dist/adapters/supabase/index.d.ts +88 -0
- package/dist/adapters/supabase/index.js +84 -0
- package/dist/adapters/supabase/realtime.d.ts +30 -0
- package/dist/adapters/supabase/realtime.js +53 -0
- package/dist/adapters/supabase/rls.d.ts +99 -0
- package/dist/adapters/supabase/rls.js +216 -0
- package/dist/adapters/supabase/storage.d.ts +50 -0
- package/dist/adapters/supabase/storage.js +207 -0
- package/dist/agents/agent.d.ts +66 -0
- package/dist/agents/agent.js +209 -0
- package/dist/agents/define.d.ts +21 -0
- package/dist/agents/define.js +63 -0
- package/dist/agents/engine.d.ts +98 -0
- package/dist/agents/engine.js +462 -0
- package/dist/agents/index.d.ts +50 -0
- package/dist/agents/index.js +17 -0
- package/dist/artifacts/index.d.ts +114 -0
- package/dist/artifacts/index.js +219 -0
- package/dist/authorization/index.d.ts +76 -0
- package/dist/authorization/index.js +76 -0
- package/dist/capabilities/index.d.ts +120 -0
- package/dist/capabilities/index.js +167 -0
- package/dist/capabilities/runtime.d.ts +89 -0
- package/dist/capabilities/runtime.js +84 -0
- package/dist/citations/index.d.ts +161 -0
- package/dist/citations/index.js +182 -0
- package/dist/context/assembler.d.ts +82 -0
- package/dist/context/assembler.js +129 -0
- package/dist/context/compaction.d.ts +45 -0
- package/dist/context/compaction.js +55 -0
- package/dist/context/index.d.ts +75 -0
- package/dist/context/index.js +17 -0
- package/dist/core/content-parts.d.ts +194 -0
- package/dist/core/content-parts.js +23 -0
- package/dist/core/context.d.ts +51 -0
- package/dist/core/context.js +9 -0
- package/dist/core/env.d.ts +25 -0
- package/dist/core/env.js +41 -0
- package/dist/core/errors.d.ts +30 -0
- package/dist/core/errors.js +65 -0
- package/dist/core/events.d.ts +139 -0
- package/dist/core/events.js +99 -0
- package/dist/core/ids.d.ts +52 -0
- package/dist/core/ids.js +13 -0
- package/dist/core/index.d.ts +9 -0
- package/dist/core/index.js +9 -0
- package/dist/core/tokens.d.ts +22 -0
- package/dist/core/tokens.js +22 -0
- package/dist/core/validation.d.ts +34 -0
- package/dist/core/validation.js +176 -0
- package/dist/documents/extraction.d.ts +121 -0
- package/dist/documents/extraction.js +293 -0
- package/dist/documents/index.d.ts +199 -0
- package/dist/documents/index.js +65 -0
- package/dist/documents/parsers/pdf.d.ts +47 -0
- package/dist/documents/parsers/pdf.js +508 -0
- package/dist/documents/parsers/text.d.ts +59 -0
- package/dist/documents/parsers/text.js +325 -0
- package/dist/documents/read-tool.d.ts +52 -0
- package/dist/documents/read-tool.js +109 -0
- package/dist/documents/render.d.ts +29 -0
- package/dist/documents/render.js +59 -0
- package/dist/documents/vision.d.ts +159 -0
- package/dist/documents/vision.js +214 -0
- package/dist/entries/adapters-bullmq.d.ts +8 -0
- package/dist/entries/adapters-bullmq.js +8 -0
- package/dist/entries/adapters-otel.d.ts +13 -0
- package/dist/entries/adapters-otel.js +13 -0
- package/dist/entries/adapters-postgres.d.ts +10 -0
- package/dist/entries/adapters-postgres.js +10 -0
- package/dist/entries/adapters-redis.d.ts +3 -0
- package/dist/entries/adapters-redis.js +3 -0
- package/dist/entries/context.d.ts +20 -0
- package/dist/entries/context.js +20 -0
- package/dist/entries/flows.d.ts +15 -0
- package/dist/entries/flows.js +15 -0
- package/dist/entries/hitl.d.ts +10 -0
- package/dist/entries/hitl.js +10 -0
- package/dist/entries/knowledge.d.ts +18 -0
- package/dist/entries/knowledge.js +19 -0
- package/dist/entries/mcp.d.ts +10 -0
- package/dist/entries/mcp.js +10 -0
- package/dist/entries/observability.d.ts +14 -0
- package/dist/entries/observability.js +16 -0
- package/dist/entries/persistence.d.ts +11 -0
- package/dist/entries/persistence.js +11 -0
- package/dist/entries/providers.d.ts +14 -0
- package/dist/entries/providers.js +14 -0
- package/dist/entries/runtime.d.ts +13 -0
- package/dist/entries/runtime.js +13 -0
- package/dist/entries/server.d.ts +24 -0
- package/dist/entries/server.js +24 -0
- package/dist/entries/tools.d.ts +21 -0
- package/dist/entries/tools.js +21 -0
- package/dist/entries/usage.d.ts +10 -0
- package/dist/entries/usage.js +10 -0
- package/dist/evaluation/gate.d.ts +168 -0
- package/dist/evaluation/gate.js +180 -0
- package/dist/evaluation/graders.d.ts +125 -0
- package/dist/evaluation/graders.js +203 -0
- package/dist/evaluation/index.d.ts +120 -0
- package/dist/evaluation/index.js +183 -0
- package/dist/evaluation/judge.d.ts +75 -0
- package/dist/evaluation/judge.js +111 -0
- package/dist/export/index.d.ts +162 -0
- package/dist/export/index.js +363 -0
- package/dist/export/markdown.d.ts +19 -0
- package/dist/export/markdown.js +29 -0
- package/dist/export/pdf.d.ts +73 -0
- package/dist/export/pdf.js +407 -0
- package/dist/files/context.d.ts +97 -0
- package/dist/files/context.js +185 -0
- package/dist/files/index.d.ts +210 -0
- package/dist/files/index.js +338 -0
- package/dist/files/read-tool.d.ts +81 -0
- package/dist/files/read-tool.js +163 -0
- package/dist/files/turn-parts.d.ts +96 -0
- package/dist/files/turn-parts.js +171 -0
- package/dist/flows/index.d.ts +270 -0
- package/dist/flows/index.js +62 -0
- package/dist/flows/interpreter.d.ts +146 -0
- package/dist/flows/interpreter.js +426 -0
- package/dist/flows/runner.d.ts +145 -0
- package/dist/flows/runner.js +270 -0
- package/dist/graphql/index.d.ts +8 -0
- package/dist/graphql/index.js +8 -0
- package/dist/graphql/resolvers.d.ts +237 -0
- package/dist/graphql/resolvers.js +253 -0
- package/dist/graphql/schema.d.ts +11 -0
- package/dist/graphql/schema.js +258 -0
- package/dist/graphql/sse.d.ts +77 -0
- package/dist/graphql/sse.js +100 -0
- package/dist/hitl/approved-execution.d.ts +127 -0
- package/dist/hitl/approved-execution.js +177 -0
- package/dist/hitl/index.d.ts +79 -0
- package/dist/hitl/index.js +12 -0
- package/dist/hitl/service.d.ts +221 -0
- package/dist/hitl/service.js +268 -0
- package/dist/idempotency/index.d.ts +70 -0
- package/dist/idempotency/index.js +59 -0
- package/dist/index.d.ts +103 -0
- package/dist/index.js +60 -0
- package/dist/knowledge/chunking.d.ts +57 -0
- package/dist/knowledge/chunking.js +158 -0
- package/dist/knowledge/index.d.ts +119 -0
- package/dist/knowledge/index.js +166 -0
- package/dist/knowledge/retrieval.d.ts +146 -0
- package/dist/knowledge/retrieval.js +170 -0
- package/dist/loadtest/harness.d.ts +168 -0
- package/dist/loadtest/harness.js +507 -0
- package/dist/loadtest/index.d.ts +13 -0
- package/dist/loadtest/index.js +13 -0
- package/dist/loadtest/injection.d.ts +89 -0
- package/dist/loadtest/injection.js +147 -0
- package/dist/loadtest/metrics.d.ts +197 -0
- package/dist/loadtest/metrics.js +160 -0
- package/dist/loadtest/runbooks.d.ts +28 -0
- package/dist/loadtest/runbooks.js +159 -0
- package/dist/loadtest/scenario.d.ts +104 -0
- package/dist/loadtest/scenario.js +208 -0
- package/dist/mcp/egress.d.ts +53 -0
- package/dist/mcp/egress.js +115 -0
- package/dist/mcp/index.d.ts +93 -0
- package/dist/mcp/index.js +33 -0
- package/dist/mcp/provider.d.ts +62 -0
- package/dist/mcp/provider.js +0 -0
- package/dist/models/index.d.ts +98 -0
- package/dist/models/index.js +74 -0
- package/dist/models/pricing.d.ts +24 -0
- package/dist/models/pricing.js +37 -0
- package/dist/models/provider-factory.d.ts +31 -0
- package/dist/models/provider-factory.js +67 -0
- package/dist/models/streaming.d.ts +145 -0
- package/dist/models/streaming.js +272 -0
- package/dist/models/vision.d.ts +38 -0
- package/dist/models/vision.js +62 -0
- package/dist/persistence/index.d.ts +1654 -0
- package/dist/persistence/index.js +226 -0
- package/dist/principal-memory/index.d.ts +106 -0
- package/dist/principal-memory/index.js +89 -0
- package/dist/retention/index.d.ts +89 -0
- package/dist/retention/index.js +70 -0
- package/dist/runtime/checkpoint.d.ts +37 -0
- package/dist/runtime/checkpoint.js +22 -0
- package/dist/runtime/index.d.ts +118 -0
- package/dist/runtime/index.js +69 -0
- package/dist/runtime/retry.d.ts +95 -0
- package/dist/runtime/retry.js +126 -0
- package/dist/runtime/serialization.d.ts +85 -0
- package/dist/runtime/serialization.js +95 -0
- package/dist/runtime/streaming.d.ts +54 -0
- package/dist/runtime/streaming.js +115 -0
- package/dist/runtime/worker.d.ts +130 -0
- package/dist/runtime/worker.js +405 -0
- package/dist/security/checklist.d.ts +53 -0
- package/dist/security/checklist.js +204 -0
- package/dist/security/findings.d.ts +56 -0
- package/dist/security/findings.js +168 -0
- package/dist/security/index.d.ts +14 -0
- package/dist/security/index.js +14 -0
- package/dist/security/prompt-safety.d.ts +100 -0
- package/dist/security/prompt-safety.js +133 -0
- package/dist/server/boot.d.ts +32 -0
- package/dist/server/boot.js +36 -0
- package/dist/server/cli-worker.d.ts +37 -0
- package/dist/server/cli-worker.js +151 -0
- package/dist/server/cli.d.ts +27 -0
- package/dist/server/cli.js +74 -0
- package/dist/server/config.d.ts +42 -0
- package/dist/server/config.js +127 -0
- package/dist/server/health.d.ts +59 -0
- package/dist/server/health.js +90 -0
- package/dist/server/host.d.ts +39 -0
- package/dist/server/host.js +124 -0
- package/dist/server/index.d.ts +15 -0
- package/dist/server/index.js +15 -0
- package/dist/server/main.d.ts +16 -0
- package/dist/server/main.js +31 -0
- package/dist/server/sse-route.d.ts +21 -0
- package/dist/server/sse-route.js +282 -0
- package/dist/skills/index.d.ts +67 -0
- package/dist/skills/index.js +31 -0
- package/dist/skills/resolver.d.ts +54 -0
- package/dist/skills/resolver.js +121 -0
- package/dist/teams/index.d.ts +93 -0
- package/dist/teams/index.js +207 -0
- package/dist/telemetry/index.d.ts +157 -0
- package/dist/telemetry/index.js +71 -0
- package/dist/telemetry/instrument.d.ts +108 -0
- package/dist/telemetry/instrument.js +232 -0
- package/dist/telemetry/log-events.d.ts +17 -0
- package/dist/telemetry/log-events.js +58 -0
- package/dist/telemetry/metrics.d.ts +123 -0
- package/dist/telemetry/metrics.js +135 -0
- package/dist/telemetry/noop.d.ts +39 -0
- package/dist/telemetry/noop.js +143 -0
- package/dist/telemetry/redaction.d.ts +64 -0
- package/dist/telemetry/redaction.js +153 -0
- package/dist/telemetry/spans.d.ts +56 -0
- package/dist/telemetry/spans.js +78 -0
- package/dist/telemetry/trace-context.d.ts +55 -0
- package/dist/telemetry/trace-context.js +60 -0
- package/dist/toolkit/compute.d.ts +53 -0
- package/dist/toolkit/compute.js +152 -0
- package/dist/toolkit/data.d.ts +98 -0
- package/dist/toolkit/data.js +235 -0
- package/dist/toolkit/http.d.ts +113 -0
- package/dist/toolkit/http.js +205 -0
- package/dist/toolkit/index.d.ts +21 -0
- package/dist/toolkit/index.js +17 -0
- package/dist/toolkit/web.d.ts +107 -0
- package/dist/toolkit/web.js +147 -0
- package/dist/tools/define.d.ts +25 -0
- package/dist/tools/define.js +45 -0
- package/dist/tools/delegating.d.ts +132 -0
- package/dist/tools/delegating.js +211 -0
- package/dist/tools/index.d.ts +129 -0
- package/dist/tools/index.js +33 -0
- package/dist/tools/library/compute.d.ts +11 -0
- package/dist/tools/library/compute.js +46 -0
- package/dist/tools/library/data.d.ts +16 -0
- package/dist/tools/library/data.js +92 -0
- package/dist/tools/library/http.d.ts +28 -0
- package/dist/tools/library/http.js +71 -0
- package/dist/tools/library/index.d.ts +97 -0
- package/dist/tools/library/index.js +134 -0
- package/dist/tools/library/knowledge.d.ts +39 -0
- package/dist/tools/library/knowledge.js +58 -0
- package/dist/tools/library/web.d.ts +19 -0
- package/dist/tools/library/web.js +65 -0
- package/dist/tools/meta-tools.d.ts +19 -0
- package/dist/tools/meta-tools.js +36 -0
- package/dist/tools/registry.d.ts +146 -0
- package/dist/tools/registry.js +291 -0
- package/dist/usage/index.d.ts +105 -0
- package/dist/usage/index.js +20 -0
- package/dist/usage/quota.d.ts +258 -0
- package/dist/usage/quota.js +510 -0
- package/dist/usage/recorder.d.ts +29 -0
- package/dist/usage/recorder.js +96 -0
- package/dist/usage/rollups.d.ts +121 -0
- package/dist/usage/rollups.js +157 -0
- package/dist/worker/export.d.ts +57 -0
- package/dist/worker/export.js +81 -0
- package/dist/worker/extraction.d.ts +57 -0
- package/dist/worker/extraction.js +84 -0
- package/dist/worker/main.d.ts +103 -0
- package/dist/worker/main.js +159 -0
- package/package.json +187 -0
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Human-in-the-loop services — `docs/04-durable-runtime-and-hitl.md` → Questions & Approvals.
|
|
3
|
+
*
|
|
4
|
+
* `ask`/`request` persist a durable interaction (surviving restart/deploy) and pause the run into
|
|
5
|
+
* `waiting-for-question` / `waiting-for-approval`. `answer`/`decide` record the outcome idempotently
|
|
6
|
+
* and queue the continuation exactly once — a duplicate call is a safe no-op, so a run never resumes
|
|
7
|
+
* twice. An approval stores the exact normalized tool name + input. The `ApprovalGate` makes the
|
|
8
|
+
* approval unbypassable: a policy-classified tool cannot execute directly without either a standing
|
|
9
|
+
* grant or a one-time approval a human has decided and the runtime has claimed.
|
|
10
|
+
*
|
|
11
|
+
* **This file is the *what*, not the *when*.** Requesting, deciding and gating live here; the run path
|
|
12
|
+
* that raises an approval on a refusal and executes the stored input on resumption is
|
|
13
|
+
* `./approved-execution.ts`, and the engine calls it (`../agents/engine.ts`). That path did not
|
|
14
|
+
* exist until the loop was wired, and this docstring described a resumption nothing performed.
|
|
15
|
+
*/
|
|
16
|
+
import type { ExecutionContext } from "../core/context.js";
|
|
17
|
+
import type { QuestionAnswer } from "../core/content-parts.js";
|
|
18
|
+
import type { ConversationId, InteractionId, RunId, TenantId } from "../core/ids.js";
|
|
19
|
+
import { AgentPlatformError } from "../core/errors.js";
|
|
20
|
+
import type { ApprovalGrantStore, InteractionStore, RunStore } from "../persistence/index.js";
|
|
21
|
+
import type { JobDispatcher } from "../runtime/index.js";
|
|
22
|
+
import type { ApprovalDecision, ApprovalGrant, PendingApproval, PendingQuestion } from "./index.js";
|
|
23
|
+
type Clock = () => string;
|
|
24
|
+
type IdFactory = () => string;
|
|
25
|
+
/**
|
|
26
|
+
* One question the agent asks a person.
|
|
27
|
+
*
|
|
28
|
+
* `options` + `multiple` + `allowOther` cover the three shapes a UI actually needs: pick one, pick several, or
|
|
29
|
+
* write your own — and any combination. Previously only `options` existed, so a client could not tell a choice
|
|
30
|
+
* from a hint and had to guess.
|
|
31
|
+
*/
|
|
32
|
+
export type QuestionSpec = {
|
|
33
|
+
readonly key: string;
|
|
34
|
+
readonly prompt: string;
|
|
35
|
+
/** Suggested answers. Absent means free text only. */
|
|
36
|
+
readonly options?: readonly string[];
|
|
37
|
+
/** More than one may be chosen. A multi-select answer is an array. */
|
|
38
|
+
readonly multiple?: boolean;
|
|
39
|
+
/** An answer not on the list is accepted. Defaults to true when there are no options, false when there are. */
|
|
40
|
+
readonly allowOther?: boolean;
|
|
41
|
+
};
|
|
42
|
+
/** Re-exported from `core`, where it lives so the port and this service can share it without a cycle. */
|
|
43
|
+
export type { QuestionAnswer } from "../core/content-parts.js";
|
|
44
|
+
export declare const createQuestionService: (deps: {
|
|
45
|
+
readonly interactions: InteractionStore;
|
|
46
|
+
readonly dispatcher: JobDispatcher;
|
|
47
|
+
/**
|
|
48
|
+
* The run store, so answering can put the run back to `queued` (#144).
|
|
49
|
+
*
|
|
50
|
+
* Optional only so an existing caller keeps compiling; without it the resume enqueues a run the worker cannot
|
|
51
|
+
* claim, and the run waits forever. See `resumeRun` below.
|
|
52
|
+
*/
|
|
53
|
+
readonly runs?: RunStore;
|
|
54
|
+
readonly clock?: Clock;
|
|
55
|
+
readonly idFactory?: IdFactory;
|
|
56
|
+
}) => {
|
|
57
|
+
/**
|
|
58
|
+
* Persist a pending question. The run is paused into `waiting-for-question` by the worker.
|
|
59
|
+
*
|
|
60
|
+
* That last sentence was aspirational until #163: the worker does pause on a `question.requested` event,
|
|
61
|
+
* but nothing emitted one, so a tool that called `ask` had its question stored and the run ran on to
|
|
62
|
+
* completion. See `questionPending` below for the signal that closes the loop.
|
|
63
|
+
*/
|
|
64
|
+
ask(context: ExecutionContext, runId: RunId, questions: readonly QuestionSpec[]): Promise<PendingQuestion>;
|
|
65
|
+
/**
|
|
66
|
+
* The question a run is parked on, or null — the read side of `answer` (#163).
|
|
67
|
+
*
|
|
68
|
+
* The service had `ask` and `answer` and no way to *look at* what was asked, so the only client that could
|
|
69
|
+
* render a picker was one that had also raised the question and kept it in memory. A worker raises it and a
|
|
70
|
+
* browser renders it, which are different processes; the browser had the interaction id from the event and
|
|
71
|
+
* nothing else, and drew an empty text box.
|
|
72
|
+
*/
|
|
73
|
+
pending(input: {
|
|
74
|
+
tenantId: TenantId;
|
|
75
|
+
runId: RunId;
|
|
76
|
+
}): Promise<PendingQuestion | null>;
|
|
77
|
+
/**
|
|
78
|
+
* The answer a resumed run has to tell the model about — #163.
|
|
79
|
+
*
|
|
80
|
+
* Returns null when nothing was asked, or when the answer was already part of an earlier turn's history:
|
|
81
|
+
* both mean "say nothing new". The engine calls this on resume, the way it calls `approvals.resume`.
|
|
82
|
+
*/
|
|
83
|
+
answered(input: {
|
|
84
|
+
tenantId: TenantId;
|
|
85
|
+
runId: RunId;
|
|
86
|
+
}): Promise<PendingQuestion | null>;
|
|
87
|
+
/**
|
|
88
|
+
* Record answers and queue the continuation — exactly once. A second answer to the same
|
|
89
|
+
* interaction is a no-op and does NOT re-enqueue, so the run never resumes twice.
|
|
90
|
+
*/
|
|
91
|
+
answer(input: TenantScopeInput & {
|
|
92
|
+
interactionId: InteractionId;
|
|
93
|
+
runId: RunId;
|
|
94
|
+
answers: Readonly<Record<string, QuestionAnswer>>;
|
|
95
|
+
}): Promise<{
|
|
96
|
+
resumed: boolean;
|
|
97
|
+
}>;
|
|
98
|
+
};
|
|
99
|
+
type TenantScopeInput = {
|
|
100
|
+
tenantId: TenantId;
|
|
101
|
+
};
|
|
102
|
+
export type ApprovalRequest = {
|
|
103
|
+
readonly toolName: string;
|
|
104
|
+
readonly normalizedInput: unknown;
|
|
105
|
+
readonly riskCategory: string;
|
|
106
|
+
readonly summary: string;
|
|
107
|
+
readonly estimatedCostMinorUnits?: number;
|
|
108
|
+
readonly expiresAt: string;
|
|
109
|
+
readonly idempotencyKey: string;
|
|
110
|
+
};
|
|
111
|
+
/**
|
|
112
|
+
* Decisions that authorize the approved call to run.
|
|
113
|
+
*
|
|
114
|
+
* A whitelist rather than `!== "deny"`: a decision added later — a deferral, an escalation — would
|
|
115
|
+
* read as permission under the negative form, which is the wrong direction to be wrong in.
|
|
116
|
+
*/
|
|
117
|
+
export declare const ALLOW_DECISIONS: readonly ["allow-once", "allow-conversation", "allow-always"];
|
|
118
|
+
export declare const isAllowDecision: (decision: ApprovalDecision | undefined) => boolean;
|
|
119
|
+
export declare const createApprovalService: (deps: {
|
|
120
|
+
readonly interactions: InteractionStore;
|
|
121
|
+
readonly grants: ApprovalGrantStore;
|
|
122
|
+
readonly dispatcher: JobDispatcher;
|
|
123
|
+
/** As on `createQuestionService`: without it the resumed run is never claimable. See `resumeRun`. */
|
|
124
|
+
readonly runs?: RunStore;
|
|
125
|
+
readonly clock?: Clock;
|
|
126
|
+
readonly idFactory?: IdFactory;
|
|
127
|
+
}) => {
|
|
128
|
+
/** Persist a pending approval storing the exact normalized tool + input. Run pauses to waiting. */
|
|
129
|
+
request(context: ExecutionContext, runId: RunId, req: ApprovalRequest): Promise<PendingApproval>;
|
|
130
|
+
/**
|
|
131
|
+
* The approval a run is parked on, or null — the read side of `decide` (#163).
|
|
132
|
+
*
|
|
133
|
+
* Same reasoning as `pending` on the question service: the worker raises it, a browser renders it, and the
|
|
134
|
+
* browser had only the interaction id from the event. Without this the card could not name the tool it was
|
|
135
|
+
* asking about, and an approval prompt that says "run a tool?" is one people learn to click through.
|
|
136
|
+
*/
|
|
137
|
+
pending(input: {
|
|
138
|
+
tenantId: TenantId;
|
|
139
|
+
runId: RunId;
|
|
140
|
+
}): Promise<PendingApproval | null>;
|
|
141
|
+
/**
|
|
142
|
+
* Record a decision (once), issue a standing grant for allow-conversation/allow-always, and queue
|
|
143
|
+
* the continuation exactly once. The resumed run executes the *stored* normalized input from the
|
|
144
|
+
* pending approval — never a regenerated one; see `./approved-execution.ts`.
|
|
145
|
+
*
|
|
146
|
+
* `allow-once` deliberately issues **no grant**. A grant is standing by definition, so minting one
|
|
147
|
+
* for a one-time decision would hand over authority the human did not give. Its single execution is
|
|
148
|
+
* claimed off the interaction instead (`InteractionStore.claimApproval`).
|
|
149
|
+
*/
|
|
150
|
+
decide(input: TenantScopeInput & {
|
|
151
|
+
interactionId: InteractionId;
|
|
152
|
+
runId: RunId;
|
|
153
|
+
conversationId?: ConversationId;
|
|
154
|
+
decision: ApprovalDecision;
|
|
155
|
+
}): Promise<{
|
|
156
|
+
resumed: boolean;
|
|
157
|
+
grant?: ApprovalGrant;
|
|
158
|
+
}>;
|
|
159
|
+
};
|
|
160
|
+
/**
|
|
161
|
+
* A single approved execution, presented at the moment of the call.
|
|
162
|
+
*
|
|
163
|
+
* The ticket is just the interaction id, and it is **not** a credential — the gate below verifies it
|
|
164
|
+
* against the stored interaction rather than believing it. That is deliberate: the alternative to a
|
|
165
|
+
* ticket was issuing a grant for `allow-once`, and a grant is standing by definition, so a one-time
|
|
166
|
+
* decision would have silently become a permanent one.
|
|
167
|
+
*
|
|
168
|
+
* Per-call rather than carried on the `ExecutionContext`: a context is reused for every call in a
|
|
169
|
+
* turn, so an approval living on it would authorise all of them.
|
|
170
|
+
*/
|
|
171
|
+
export type OneTimeApproval = {
|
|
172
|
+
readonly interactionId: InteractionId | string;
|
|
173
|
+
};
|
|
174
|
+
/**
|
|
175
|
+
* The gate that makes approval unbypassable. A policy-classified tool (`approvalPolicy` other than
|
|
176
|
+
* `never`, or an external/destructive effect under `policy`) may only execute directly when a
|
|
177
|
+
* standing grant covers it, or when the call presents a one-time approval a human has decided and the
|
|
178
|
+
* runtime has claimed.
|
|
179
|
+
*
|
|
180
|
+
* Both paths fail closed. A tool with no grant and no ticket is refused; a ticket presented with no
|
|
181
|
+
* `interactions` store to check it against is refused too, because an unwired dependency must never
|
|
182
|
+
* be the reason something was allowed.
|
|
183
|
+
*/
|
|
184
|
+
export declare const createApprovalGate: (deps: {
|
|
185
|
+
readonly grants: ApprovalGrantStore;
|
|
186
|
+
/**
|
|
187
|
+
* Where one-time approvals are verified. Optional so a caller that only uses standing grants need
|
|
188
|
+
* not wire it — with it absent, every ticket is refused rather than trusted.
|
|
189
|
+
*/
|
|
190
|
+
readonly interactions?: InteractionStore;
|
|
191
|
+
readonly clock?: Clock;
|
|
192
|
+
}) => {
|
|
193
|
+
isAllowed(context: ExecutionContext, tool: {
|
|
194
|
+
readonly name: string;
|
|
195
|
+
readonly category: string;
|
|
196
|
+
readonly approvalPolicy: "never" | "policy" | "always";
|
|
197
|
+
}, oneTime?: OneTimeApproval): Promise<boolean>;
|
|
198
|
+
};
|
|
199
|
+
export type ApprovalGate = ReturnType<typeof createApprovalGate>;
|
|
200
|
+
/**
|
|
201
|
+
* The signal a tool raises to park its run on a question — #163.
|
|
202
|
+
*
|
|
203
|
+
* A tool cannot pause a run by itself, and it must not block waiting for a person: a delegate that awaited a
|
|
204
|
+
* human reply would hold a worker slot for as long as someone takes to read, which is the entire thing the
|
|
205
|
+
* durable runtime exists to avoid. So the tool stores the question, throws this, and returns control.
|
|
206
|
+
*
|
|
207
|
+
* The engine recognises the `question_pending` code, tells the model the run is parked, and emits
|
|
208
|
+
* `question.requested` — the event the worker turns into `waiting-for-question`. Exactly the path an approval
|
|
209
|
+
* takes, which is the point: the two halves of "ask a human something" should not work differently.
|
|
210
|
+
*
|
|
211
|
+
* Thrown rather than returned because it has to survive the tool registry, which flattens a delegate's throw
|
|
212
|
+
* into a `ToolResult` error. A distinguishing code is what makes that flattening lossless.
|
|
213
|
+
*/
|
|
214
|
+
export declare const questionPending: (question: {
|
|
215
|
+
readonly id: InteractionId;
|
|
216
|
+
}) => AgentPlatformError;
|
|
217
|
+
/** True when a refusal is a parked question rather than a failure. Used by the engine; exported for hosts. */
|
|
218
|
+
export declare const isQuestionPending: (error: {
|
|
219
|
+
readonly code: string;
|
|
220
|
+
}) => boolean;
|
|
221
|
+
//# sourceMappingURL=service.d.ts.map
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Human-in-the-loop services — `docs/04-durable-runtime-and-hitl.md` → Questions & Approvals.
|
|
3
|
+
*
|
|
4
|
+
* `ask`/`request` persist a durable interaction (surviving restart/deploy) and pause the run into
|
|
5
|
+
* `waiting-for-question` / `waiting-for-approval`. `answer`/`decide` record the outcome idempotently
|
|
6
|
+
* and queue the continuation exactly once — a duplicate call is a safe no-op, so a run never resumes
|
|
7
|
+
* twice. An approval stores the exact normalized tool name + input. The `ApprovalGate` makes the
|
|
8
|
+
* approval unbypassable: a policy-classified tool cannot execute directly without either a standing
|
|
9
|
+
* grant or a one-time approval a human has decided and the runtime has claimed.
|
|
10
|
+
*
|
|
11
|
+
* **This file is the *what*, not the *when*.** Requesting, deciding and gating live here; the run path
|
|
12
|
+
* that raises an approval on a refusal and executes the stored input on resumption is
|
|
13
|
+
* `./approved-execution.ts`, and the engine calls it (`../agents/engine.ts`). That path did not
|
|
14
|
+
* exist until the loop was wired, and this docstring described a resumption nothing performed.
|
|
15
|
+
*/
|
|
16
|
+
import { asId } from "../core/ids.js";
|
|
17
|
+
import { AgentPlatformError } from "../core/errors.js";
|
|
18
|
+
const defaults = (clock, idFactory) => ({
|
|
19
|
+
clock: clock ?? (() => new Date().toISOString()),
|
|
20
|
+
newId: idFactory ?? (() => `int-${Math.round(Math.random() * 1e9)}`),
|
|
21
|
+
});
|
|
22
|
+
export const createQuestionService = (deps) => {
|
|
23
|
+
const { clock, newId } = defaults(deps.clock, deps.idFactory);
|
|
24
|
+
return {
|
|
25
|
+
/**
|
|
26
|
+
* Persist a pending question. The run is paused into `waiting-for-question` by the worker.
|
|
27
|
+
*
|
|
28
|
+
* That last sentence was aspirational until #163: the worker does pause on a `question.requested` event,
|
|
29
|
+
* but nothing emitted one, so a tool that called `ask` had its question stored and the run ran on to
|
|
30
|
+
* completion. See `questionPending` below for the signal that closes the loop.
|
|
31
|
+
*/
|
|
32
|
+
async ask(context, runId, questions) {
|
|
33
|
+
const question = {
|
|
34
|
+
id: asId(newId()),
|
|
35
|
+
tenantId: context.tenantId,
|
|
36
|
+
runId,
|
|
37
|
+
questions,
|
|
38
|
+
createdAt: clock(),
|
|
39
|
+
};
|
|
40
|
+
await deps.interactions.createQuestion({ tenantId: context.tenantId, question });
|
|
41
|
+
return question;
|
|
42
|
+
},
|
|
43
|
+
/**
|
|
44
|
+
* The question a run is parked on, or null — the read side of `answer` (#163).
|
|
45
|
+
*
|
|
46
|
+
* The service had `ask` and `answer` and no way to *look at* what was asked, so the only client that could
|
|
47
|
+
* render a picker was one that had also raised the question and kept it in memory. A worker raises it and a
|
|
48
|
+
* browser renders it, which are different processes; the browser had the interaction id from the event and
|
|
49
|
+
* nothing else, and drew an empty text box.
|
|
50
|
+
*/
|
|
51
|
+
async pending(input) {
|
|
52
|
+
return deps.interactions.findPendingQuestion({ tenantId: input.tenantId, runId: input.runId });
|
|
53
|
+
},
|
|
54
|
+
/**
|
|
55
|
+
* The answer a resumed run has to tell the model about — #163.
|
|
56
|
+
*
|
|
57
|
+
* Returns null when nothing was asked, or when the answer was already part of an earlier turn's history:
|
|
58
|
+
* both mean "say nothing new". The engine calls this on resume, the way it calls `approvals.resume`.
|
|
59
|
+
*/
|
|
60
|
+
async answered(input) {
|
|
61
|
+
return deps.interactions.findAnsweredQuestion({ tenantId: input.tenantId, runId: input.runId });
|
|
62
|
+
},
|
|
63
|
+
/**
|
|
64
|
+
* Record answers and queue the continuation — exactly once. A second answer to the same
|
|
65
|
+
* interaction is a no-op and does NOT re-enqueue, so the run never resumes twice.
|
|
66
|
+
*/
|
|
67
|
+
async answer(input) {
|
|
68
|
+
const { alreadyResolved } = await deps.interactions.answerQuestion({
|
|
69
|
+
tenantId: input.tenantId,
|
|
70
|
+
interactionId: input.interactionId,
|
|
71
|
+
answers: input.answers,
|
|
72
|
+
at: clock(),
|
|
73
|
+
});
|
|
74
|
+
if (alreadyResolved)
|
|
75
|
+
return { resumed: false };
|
|
76
|
+
await resumeRun(deps, { tenantId: input.tenantId, runId: input.runId, at: clock() });
|
|
77
|
+
return { resumed: true };
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
};
|
|
81
|
+
/**
|
|
82
|
+
* Put a paused run back on the queue — status *then* job.
|
|
83
|
+
*
|
|
84
|
+
* **A bug the load harness found (#144).** `decide` and `answer` used to enqueue and nothing else. But
|
|
85
|
+
* `RunStore.claim` accepts only `queued` or a `running` run with an expired lease, and pausing a run for a human
|
|
86
|
+
* leaves it in `waiting-for-approval` — so the enqueued job was handed to a worker, the claim matched no row, the
|
|
87
|
+
* run was skipped, and it waited forever. Sixty-five of a hundred and sixty runs in one load step, silently.
|
|
88
|
+
*
|
|
89
|
+
* It survived because the unit tests assert `resumed: true` and that a job was enqueued, which was exactly true
|
|
90
|
+
* and not the same as the run resuming. Only driving a real worker against a real store showed it.
|
|
91
|
+
*
|
|
92
|
+
* The transition comes **first**: enqueueing before it lets a worker pick the job up while the run is still
|
|
93
|
+
* paused, fail the claim, and drop the only job that would have resumed it.
|
|
94
|
+
*
|
|
95
|
+
* `workerId` is the actor's name rather than a worker's. `transition` guards on `claimedBy`, and pausing a run
|
|
96
|
+
* releases the claim — so the field is unconstrained here and the honest value is who is doing this.
|
|
97
|
+
*/
|
|
98
|
+
const resumeRun = async (deps, input) => {
|
|
99
|
+
await deps.runs?.transition({
|
|
100
|
+
tenantId: input.tenantId,
|
|
101
|
+
id: input.runId,
|
|
102
|
+
workerId: "hitl",
|
|
103
|
+
to: "queued",
|
|
104
|
+
now: input.at,
|
|
105
|
+
});
|
|
106
|
+
await deps.dispatcher.enqueueRun({ tenantId: input.tenantId, runId: input.runId });
|
|
107
|
+
};
|
|
108
|
+
/**
|
|
109
|
+
* Decisions that authorize the approved call to run.
|
|
110
|
+
*
|
|
111
|
+
* A whitelist rather than `!== "deny"`: a decision added later — a deferral, an escalation — would
|
|
112
|
+
* read as permission under the negative form, which is the wrong direction to be wrong in.
|
|
113
|
+
*/
|
|
114
|
+
export const ALLOW_DECISIONS = ["allow-once", "allow-conversation", "allow-always"];
|
|
115
|
+
export const isAllowDecision = (decision) => decision !== undefined && ALLOW_DECISIONS.includes(decision);
|
|
116
|
+
const grantScopeFor = (decision) => decision === "allow-conversation" ? "conversation" : decision === "allow-always" ? "tenant" : null;
|
|
117
|
+
export const createApprovalService = (deps) => {
|
|
118
|
+
const { clock, newId } = defaults(deps.clock, deps.idFactory);
|
|
119
|
+
return {
|
|
120
|
+
/** Persist a pending approval storing the exact normalized tool + input. Run pauses to waiting. */
|
|
121
|
+
async request(context, runId, req) {
|
|
122
|
+
const approval = {
|
|
123
|
+
id: asId(newId()),
|
|
124
|
+
tenantId: context.tenantId,
|
|
125
|
+
runId,
|
|
126
|
+
toolName: req.toolName,
|
|
127
|
+
normalizedInput: req.normalizedInput,
|
|
128
|
+
riskCategory: req.riskCategory,
|
|
129
|
+
summary: req.summary,
|
|
130
|
+
...(req.estimatedCostMinorUnits === undefined ? {} : { estimatedCostMinorUnits: req.estimatedCostMinorUnits }),
|
|
131
|
+
expiresAt: req.expiresAt,
|
|
132
|
+
idempotencyKey: req.idempotencyKey,
|
|
133
|
+
};
|
|
134
|
+
await deps.interactions.createApproval({ tenantId: context.tenantId, approval });
|
|
135
|
+
return approval;
|
|
136
|
+
},
|
|
137
|
+
/**
|
|
138
|
+
* The approval a run is parked on, or null — the read side of `decide` (#163).
|
|
139
|
+
*
|
|
140
|
+
* Same reasoning as `pending` on the question service: the worker raises it, a browser renders it, and the
|
|
141
|
+
* browser had only the interaction id from the event. Without this the card could not name the tool it was
|
|
142
|
+
* asking about, and an approval prompt that says "run a tool?" is one people learn to click through.
|
|
143
|
+
*/
|
|
144
|
+
async pending(input) {
|
|
145
|
+
return deps.interactions.findPendingApproval({ tenantId: input.tenantId, runId: input.runId });
|
|
146
|
+
},
|
|
147
|
+
/**
|
|
148
|
+
* Record a decision (once), issue a standing grant for allow-conversation/allow-always, and queue
|
|
149
|
+
* the continuation exactly once. The resumed run executes the *stored* normalized input from the
|
|
150
|
+
* pending approval — never a regenerated one; see `./approved-execution.ts`.
|
|
151
|
+
*
|
|
152
|
+
* `allow-once` deliberately issues **no grant**. A grant is standing by definition, so minting one
|
|
153
|
+
* for a one-time decision would hand over authority the human did not give. Its single execution is
|
|
154
|
+
* claimed off the interaction instead (`InteractionStore.claimApproval`).
|
|
155
|
+
*/
|
|
156
|
+
async decide(input) {
|
|
157
|
+
const { approval, alreadyResolved } = await deps.interactions.decideApproval({
|
|
158
|
+
tenantId: input.tenantId,
|
|
159
|
+
interactionId: input.interactionId,
|
|
160
|
+
decision: input.decision,
|
|
161
|
+
at: clock(),
|
|
162
|
+
});
|
|
163
|
+
if (alreadyResolved)
|
|
164
|
+
return { resumed: false };
|
|
165
|
+
let grant;
|
|
166
|
+
const scope = grantScopeFor(input.decision);
|
|
167
|
+
// A conversation-scoped grant needs a conversationId; without one, skip the standing grant
|
|
168
|
+
// (the run still resumes once) rather than silently widening it to the whole tenant.
|
|
169
|
+
if (scope && !(scope === "conversation" && input.conversationId === undefined)) {
|
|
170
|
+
grant = {
|
|
171
|
+
id: asId(newId()),
|
|
172
|
+
tenantId: input.tenantId,
|
|
173
|
+
scope,
|
|
174
|
+
toolNameOrCategory: approval.toolName,
|
|
175
|
+
...(scope === "conversation" && input.conversationId !== undefined ? { conversationId: input.conversationId } : {}),
|
|
176
|
+
grantedAt: clock(),
|
|
177
|
+
};
|
|
178
|
+
await deps.grants.grant({ tenantId: input.tenantId, grant });
|
|
179
|
+
}
|
|
180
|
+
// Both allow and deny queue a continuation; the resumed engine reads the decision and acts.
|
|
181
|
+
await resumeRun(deps, { tenantId: input.tenantId, runId: input.runId, at: clock() });
|
|
182
|
+
return grant ? { resumed: true, grant } : { resumed: true };
|
|
183
|
+
},
|
|
184
|
+
};
|
|
185
|
+
};
|
|
186
|
+
/**
|
|
187
|
+
* The gate that makes approval unbypassable. A policy-classified tool (`approvalPolicy` other than
|
|
188
|
+
* `never`, or an external/destructive effect under `policy`) may only execute directly when a
|
|
189
|
+
* standing grant covers it, or when the call presents a one-time approval a human has decided and the
|
|
190
|
+
* runtime has claimed.
|
|
191
|
+
*
|
|
192
|
+
* Both paths fail closed. A tool with no grant and no ticket is refused; a ticket presented with no
|
|
193
|
+
* `interactions` store to check it against is refused too, because an unwired dependency must never
|
|
194
|
+
* be the reason something was allowed.
|
|
195
|
+
*/
|
|
196
|
+
export const createApprovalGate = (deps) => {
|
|
197
|
+
const clock = deps.clock ?? (() => new Date().toISOString());
|
|
198
|
+
/**
|
|
199
|
+
* Whether this ticket really authorises *this* call.
|
|
200
|
+
*
|
|
201
|
+
* Every clause is a way the loop could otherwise be widened, and each is checked against what was
|
|
202
|
+
* stored at request time rather than against anything the caller supplied:
|
|
203
|
+
*
|
|
204
|
+
* - the interaction exists in this tenant — a forged or foreign id authorises nothing;
|
|
205
|
+
* - it belongs to this run, so a ticket cannot be carried into another run;
|
|
206
|
+
* - the decision is an allow — a denial can never read as permission;
|
|
207
|
+
* - the tool is the one the human saw, so an approval for `publish` cannot run `delete`;
|
|
208
|
+
* - it has been claimed. The claim is the at-most-once counter (`InteractionStore.claimApproval`),
|
|
209
|
+
* and requiring it here is what keeps a merely *decided* approval from being executable by
|
|
210
|
+
* anything that has not first taken the single execution it grants.
|
|
211
|
+
*/
|
|
212
|
+
const oneTimeAllows = async (context, tool, oneTime) => {
|
|
213
|
+
if (!deps.interactions)
|
|
214
|
+
return false;
|
|
215
|
+
const approval = await deps.interactions.findApproval({
|
|
216
|
+
tenantId: context.tenantId,
|
|
217
|
+
interactionId: asId(String(oneTime.interactionId)),
|
|
218
|
+
});
|
|
219
|
+
if (!approval)
|
|
220
|
+
return false;
|
|
221
|
+
if (approval.runId !== context.runId)
|
|
222
|
+
return false;
|
|
223
|
+
if (approval.toolName !== tool.name)
|
|
224
|
+
return false;
|
|
225
|
+
if (!isAllowDecision(approval.decision))
|
|
226
|
+
return false;
|
|
227
|
+
return approval.consumedAt !== undefined;
|
|
228
|
+
};
|
|
229
|
+
return {
|
|
230
|
+
async isAllowed(context, tool, oneTime) {
|
|
231
|
+
if (tool.approvalPolicy === "never")
|
|
232
|
+
return true;
|
|
233
|
+
const now = clock();
|
|
234
|
+
const conversationId = context.conversationId;
|
|
235
|
+
const scope = { tenantId: context.tenantId, now, ...(conversationId ? { conversationId } : {}) };
|
|
236
|
+
const byName = await deps.grants.findActive({ ...scope, toolNameOrCategory: tool.name });
|
|
237
|
+
if (byName)
|
|
238
|
+
return true;
|
|
239
|
+
const byCategory = await deps.grants.findActive({ ...scope, toolNameOrCategory: tool.category });
|
|
240
|
+
if (byCategory)
|
|
241
|
+
return true;
|
|
242
|
+
return oneTime === undefined ? false : oneTimeAllows(context, tool, oneTime);
|
|
243
|
+
},
|
|
244
|
+
};
|
|
245
|
+
};
|
|
246
|
+
/**
|
|
247
|
+
* The signal a tool raises to park its run on a question — #163.
|
|
248
|
+
*
|
|
249
|
+
* A tool cannot pause a run by itself, and it must not block waiting for a person: a delegate that awaited a
|
|
250
|
+
* human reply would hold a worker slot for as long as someone takes to read, which is the entire thing the
|
|
251
|
+
* durable runtime exists to avoid. So the tool stores the question, throws this, and returns control.
|
|
252
|
+
*
|
|
253
|
+
* The engine recognises the `question_pending` code, tells the model the run is parked, and emits
|
|
254
|
+
* `question.requested` — the event the worker turns into `waiting-for-question`. Exactly the path an approval
|
|
255
|
+
* takes, which is the point: the two halves of "ask a human something" should not work differently.
|
|
256
|
+
*
|
|
257
|
+
* Thrown rather than returned because it has to survive the tool registry, which flattens a delegate's throw
|
|
258
|
+
* into a `ToolResult` error. A distinguishing code is what makes that flattening lossless.
|
|
259
|
+
*/
|
|
260
|
+
export const questionPending = (question) => new AgentPlatformError({
|
|
261
|
+
code: "question_pending",
|
|
262
|
+
message: "A question has been put to the person. The run is paused; do not retry or guess an answer.",
|
|
263
|
+
retryable: false,
|
|
264
|
+
details: { interactionId: question.id },
|
|
265
|
+
});
|
|
266
|
+
/** True when a refusal is a parked question rather than a failure. Used by the engine; exported for hosts. */
|
|
267
|
+
export const isQuestionPending = (error) => error.code === "question_pending";
|
|
268
|
+
//# sourceMappingURL=service.js.map
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Idempotency contract — `docs/04-durable-runtime-and-hitl.md`. **Frozen v1.**
|
|
3
|
+
*
|
|
4
|
+
* Every external/destructive tool call carries a key derived from tenant + run + tool-call
|
|
5
|
+
* identity. A resumed or retried call returns the original result instead of repeating the side
|
|
6
|
+
* effect — the pairing that makes Claude-style retries safe.
|
|
7
|
+
*/
|
|
8
|
+
import type { RunId, TenantId, ToolCallId } from "../core/ids.js";
|
|
9
|
+
declare const brand: unique symbol;
|
|
10
|
+
export type IdempotencyKey = string & {
|
|
11
|
+
readonly [brand]: "IdempotencyKey";
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Pure and deterministic: the same (tenant, run, tool-call) always yields the same key, so a
|
|
15
|
+
* retry of the same logical call collides with its first attempt.
|
|
16
|
+
*/
|
|
17
|
+
export declare const deriveIdempotencyKey: (input: {
|
|
18
|
+
readonly tenantId: TenantId;
|
|
19
|
+
readonly runId: RunId;
|
|
20
|
+
readonly toolCallId: ToolCallId;
|
|
21
|
+
}) => IdempotencyKey;
|
|
22
|
+
/**
|
|
23
|
+
* A stable string for a value.
|
|
24
|
+
*
|
|
25
|
+
* Object keys are sorted, because `{a, b}` and `{b, a}` are the same arguments and must not produce
|
|
26
|
+
* two keys — the whole point of a key is that an identical call collides with itself.
|
|
27
|
+
*/
|
|
28
|
+
export declare const canonicalizeArgs: (value: unknown) => string;
|
|
29
|
+
/**
|
|
30
|
+
* The key for a call the runtime identifies by its **arguments** rather than by a provider tool-call
|
|
31
|
+
* id (`docs/04` → Approvals, Idempotency).
|
|
32
|
+
*
|
|
33
|
+
* A resumed approval has no tool-call id to derive from: the call it executes came off a stored
|
|
34
|
+
* interaction, not out of a model stream. Two properties matter, and they pull in opposite directions.
|
|
35
|
+
* It is **run-scoped**, so "publish this" today and the same call next week do not collide — a key
|
|
36
|
+
* shared across runs would return the first result and never publish the second. And it is
|
|
37
|
+
* **argument-derived**, so within one run the same logical call collides with itself: the model
|
|
38
|
+
* re-asking for a publish it already asked for finds the same approval and the same stored result,
|
|
39
|
+
* rather than a second approval and a second publish.
|
|
40
|
+
*
|
|
41
|
+
* Derive it from *normalised* arguments. A schema that lowercases a channel would otherwise give one
|
|
42
|
+
* logical call two keys, and the second would not see the first's result.
|
|
43
|
+
*/
|
|
44
|
+
export declare const deriveCallIdempotencyKey: (input: {
|
|
45
|
+
readonly tenantId: TenantId;
|
|
46
|
+
readonly runId: RunId;
|
|
47
|
+
readonly toolName: string;
|
|
48
|
+
readonly args: unknown;
|
|
49
|
+
}) => IdempotencyKey;
|
|
50
|
+
/** Wraps a stored result so a repeated call can return it without re-executing. */
|
|
51
|
+
export type IdempotentResult<T> = {
|
|
52
|
+
readonly key: IdempotencyKey;
|
|
53
|
+
/** True on the first execution; false when the stored result is returned. */
|
|
54
|
+
readonly firstSeen: boolean;
|
|
55
|
+
readonly result: T;
|
|
56
|
+
};
|
|
57
|
+
/** Persists and returns prior results by key. Adapter lands with persistence (#17+). */
|
|
58
|
+
export interface IdempotencyStore {
|
|
59
|
+
get<T>(input: {
|
|
60
|
+
tenantId: TenantId;
|
|
61
|
+
key: IdempotencyKey;
|
|
62
|
+
}): Promise<IdempotentResult<T> | null>;
|
|
63
|
+
put<T>(input: {
|
|
64
|
+
tenantId: TenantId;
|
|
65
|
+
key: IdempotencyKey;
|
|
66
|
+
result: T;
|
|
67
|
+
}): Promise<void>;
|
|
68
|
+
}
|
|
69
|
+
export {};
|
|
70
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Idempotency contract — `docs/04-durable-runtime-and-hitl.md`. **Frozen v1.**
|
|
3
|
+
*
|
|
4
|
+
* Every external/destructive tool call carries a key derived from tenant + run + tool-call
|
|
5
|
+
* identity. A resumed or retried call returns the original result instead of repeating the side
|
|
6
|
+
* effect — the pairing that makes Claude-style retries safe.
|
|
7
|
+
*/
|
|
8
|
+
const brand = Symbol("IdempotencyKey");
|
|
9
|
+
/**
|
|
10
|
+
* Pure and deterministic: the same (tenant, run, tool-call) always yields the same key, so a
|
|
11
|
+
* retry of the same logical call collides with its first attempt.
|
|
12
|
+
*/
|
|
13
|
+
export const deriveIdempotencyKey = (input) => `${input.tenantId}:${input.runId}:${input.toolCallId}`;
|
|
14
|
+
/**
|
|
15
|
+
* A stable string for a value.
|
|
16
|
+
*
|
|
17
|
+
* Object keys are sorted, because `{a, b}` and `{b, a}` are the same arguments and must not produce
|
|
18
|
+
* two keys — the whole point of a key is that an identical call collides with itself.
|
|
19
|
+
*/
|
|
20
|
+
export const canonicalizeArgs = (value) => {
|
|
21
|
+
if (value === null || typeof value !== "object")
|
|
22
|
+
return JSON.stringify(value) ?? "null";
|
|
23
|
+
if (Array.isArray(value))
|
|
24
|
+
return `[${value.map(canonicalizeArgs).join(",")}]`;
|
|
25
|
+
const entries = Object.entries(value)
|
|
26
|
+
.filter(([, v]) => v !== undefined)
|
|
27
|
+
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
|
|
28
|
+
return `{${entries.map(([k, v]) => `${JSON.stringify(k)}:${canonicalizeArgs(v)}`).join(",")}}`;
|
|
29
|
+
};
|
|
30
|
+
/** FNV-1a over the canonical form. Bounds the key's length without needing a crypto dependency. */
|
|
31
|
+
const fnv1a = (text) => {
|
|
32
|
+
let hash = 0x811c9dc5;
|
|
33
|
+
for (let i = 0; i < text.length; i += 1) {
|
|
34
|
+
hash ^= text.charCodeAt(i);
|
|
35
|
+
hash = Math.imul(hash, 0x01000193) >>> 0;
|
|
36
|
+
}
|
|
37
|
+
return hash.toString(16).padStart(8, "0");
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* The key for a call the runtime identifies by its **arguments** rather than by a provider tool-call
|
|
41
|
+
* id (`docs/04` → Approvals, Idempotency).
|
|
42
|
+
*
|
|
43
|
+
* A resumed approval has no tool-call id to derive from: the call it executes came off a stored
|
|
44
|
+
* interaction, not out of a model stream. Two properties matter, and they pull in opposite directions.
|
|
45
|
+
* It is **run-scoped**, so "publish this" today and the same call next week do not collide — a key
|
|
46
|
+
* shared across runs would return the first result and never publish the second. And it is
|
|
47
|
+
* **argument-derived**, so within one run the same logical call collides with itself: the model
|
|
48
|
+
* re-asking for a publish it already asked for finds the same approval and the same stored result,
|
|
49
|
+
* rather than a second approval and a second publish.
|
|
50
|
+
*
|
|
51
|
+
* Derive it from *normalised* arguments. A schema that lowercases a channel would otherwise give one
|
|
52
|
+
* logical call two keys, and the second would not see the first's result.
|
|
53
|
+
*/
|
|
54
|
+
export const deriveCallIdempotencyKey = (input) => deriveIdempotencyKey({
|
|
55
|
+
tenantId: input.tenantId,
|
|
56
|
+
runId: input.runId,
|
|
57
|
+
toolCallId: `${input.toolName}:${fnv1a(canonicalizeArgs(input.args))}`,
|
|
58
|
+
});
|
|
59
|
+
//# sourceMappingURL=index.js.map
|