@springbrand/agent-runtime 0.2.0-alpha.72 → 0.3.0-alpha.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/package.json +3 -3
- package/src/{agent-tool-runtime.ts → adapter/cloudflare/agent-tool.ts} +1 -1
- package/src/adapter/cloudflare/agent.ts +1539 -0
- package/src/{db/index.ts → adapter/cloudflare/database.ts} +18 -55
- package/src/{runtime-agent.ts → adapter/cloudflare/define-agent.ts} +25 -35
- package/src/adapter/cloudflare/dynamic-worker-observability.ts +1 -1
- package/src/adapter/cloudflare/resources/runtime-resources.ts +4 -4
- package/src/adapter/cloudflare/sandbox/adapter.ts +1 -1
- package/src/{db → adapter/cloudflare}/schema.ts +16 -83
- package/src/adapter/cloudflare/subagent/definition.ts +63 -33
- package/src/adapter/cloudflare/subagent/runner.ts +174 -120
- package/src/adapter/cloudflare/subagent/tools.ts +13 -14
- package/src/adapter/cloudflare/universal-agent/preparation.ts +7 -7
- package/src/adapter/cloudflare/universal-agent/tools.ts +10 -10
- package/src/adapter/cloudflare/workspace/git-fs.ts +1 -1
- package/src/adapter/cloudflare/workspace/scoped-workspace.ts +1 -1
- package/src/adapter/cloudflare/workspace/version-control.ts +2 -2
- package/src/config/activation.ts +140 -0
- package/src/{runtime-agent-context.ts → config/agent-context.ts} +3 -3
- package/src/{runtime-assembler.ts → config/assembler.ts} +20 -19
- package/src/{kernel/runtime-assembly.ts → config/assembly-projection.ts} +4 -4
- package/src/{pi/assembly/snapshot.ts → config/assembly-snapshot.ts} +6 -6
- package/src/{runtime-definition.ts → config/definition.ts} +13 -13
- package/src/config/harness-runtime.ts +182 -0
- package/src/config/index.ts +14 -0
- package/src/{pi/runtime-adapter/assembly.ts → config/pinning.ts} +38 -16
- package/src/{kernel/bindings.ts → config/ports.ts} +9 -13
- package/src/{kernel → config}/profile.ts +7 -0
- package/src/{kernel → config}/public-contracts.ts +6 -6
- package/src/{kernel → config}/runtime-load.ts +1 -1
- package/src/{pi/assembly/context.ts → context/assembly.ts} +4 -206
- package/src/{pi/assembly/extensions.ts → context/extension/assembly.ts} +5 -5
- package/src/context/extension/index.ts +3 -0
- package/src/context/index.ts +6 -0
- package/src/{layers/context → context/memory}/budget/gate.ts +2 -2
- package/src/conversation/harness-resume.ts +288 -0
- package/src/conversation/harness-stream.ts +98 -0
- package/src/{pi/runtime-adapter/transcript.ts → conversation/history.ts} +206 -233
- package/src/conversation/index.ts +6 -0
- package/src/{pi → conversation}/message/contract.ts +3 -0
- package/src/{pi → conversation}/message/conversion.ts +75 -0
- package/src/{pi → conversation}/message/index.ts +1 -0
- package/src/{pi → conversation}/message/model-input.ts +1 -1
- package/src/{pi → conversation}/message/projection.ts +76 -10
- package/src/{db/message-ui.repo.ts → conversation/repository.ts} +4 -0
- package/src/conversation/state.ts +175 -0
- package/src/harness/index.ts +7 -0
- package/src/harness/session-harness.ts +481 -0
- package/src/harness/submission-operation.ts +49 -0
- package/src/index.ts +38 -43
- package/src/lib/mcp.ts +1 -1
- package/src/{pi/runtime-adapter/models.ts → models/index.ts} +64 -18
- package/src/{pi/turn/tool-recovery.ts → session/cloudflare/legacy-tool-recovery.ts} +33 -525
- package/src/{pi/session → session/cloudflare}/migration.ts +334 -8
- package/src/{pi/session → session/cloudflare}/storage.ts +8 -36
- package/src/session/index.ts +39 -0
- package/src/submission/admission.ts +275 -0
- package/src/{kernel/receipts.ts → submission/contracts.ts} +14 -0
- package/src/submission/delivery.ts +212 -0
- package/src/submission/finalization.ts +111 -0
- package/src/submission/index.ts +9 -0
- package/src/{kernel/submission-lifecycle.ts → submission/lifecycle.ts} +41 -28
- package/src/{db/submission.repo.ts → submission/repository.ts} +61 -118
- package/src/submission/steer-delivery.ts +167 -0
- package/src/{db/steer.repo.ts → submission/steer-repository.ts} +27 -5
- package/src/telemetry/contract.ts +12 -0
- package/src/telemetry/coordinator.ts +65 -0
- package/src/telemetry/delivery.ts +2 -2
- package/src/telemetry/index.ts +5 -0
- package/src/{model-usage.ts → telemetry/model-usage.ts} +1 -1
- package/src/telemetry/recorder.ts +1 -1
- package/src/telemetry/runtime-telemetry.ts +12 -0
- package/src/{kernel/durable-lifecycle.ts → telemetry/subagent-lifecycle.ts} +1 -1
- package/src/telemetry/turn-events.ts +279 -0
- package/src/{pi/tool → tools}/ai-adapter.ts +2 -2
- package/src/tools/approval/index.ts +1 -0
- package/src/{db/approval.repo.ts → tools/approval/repository.ts} +10 -30
- package/src/{pi/tool → tools}/base.ts +14 -30
- package/src/tools/compiler.ts +327 -0
- package/src/{pi/tool → tools}/core-host.ts +3 -3
- package/src/{pi/tool → tools}/core.ts +3 -3
- package/src/tools/decision-gate.ts +227 -0
- package/src/{pi/tool → tools}/gateway.ts +1 -1
- package/src/tools/human-decisions.ts +127 -0
- package/src/tools/index.ts +30 -0
- package/src/tools/interaction/index.ts +1 -0
- package/src/{pi/tool → tools}/mcp.ts +1 -1
- package/src/{pi/tool → tools}/schedule.ts +3 -3
- package/src/{pi/tool → tools}/skill.ts +4 -4
- package/src/{kernel → tools}/subagent-runtime.ts +1 -1
- package/src/{pi/tool → tools}/subagent.ts +4 -4
- package/src/tools/temporary-agent/approvals.ts +126 -0
- package/src/{layers/orchestration/temporary-agent/core.ts → tools/temporary-agent/coordinator.ts} +41 -49
- package/src/tools/temporary-agent/core.ts +53 -0
- package/src/{layers/orchestration → tools}/temporary-agent/runner.ts +1 -1
- package/src/{layers/orchestration → tools}/temporary-agent/workspace.ts +1 -1
- package/src/{pi/tool → tools}/web-search/api.ts +1 -1
- package/src/{pi/tool → tools}/workspace-revision.ts +2 -2
- package/src/{pi/tool → tools}/workspace-sandbox.ts +2 -2
- package/src/{pi/tool → tools}/workspace-state-connector.ts +1 -1
- package/src/db/milestone.repo.ts +0 -104
- package/src/db/settlement.repo.ts +0 -104
- package/src/kernel/approval-lifecycle.ts +0 -638
- package/src/kernel/interaction-lifecycle.ts +0 -433
- package/src/kernel/recoverable-chat-agent.ts +0 -1020
- package/src/kernel/state.ts +0 -91
- package/src/pi/assembly/index.ts +0 -19
- package/src/pi/runtime-adapter/execution.ts +0 -1163
- package/src/pi/runtime-adapter/index.ts +0 -293
- package/src/pi/runtime-adapter/recovery.ts +0 -982
- package/src/pi/session/index.ts +0 -24
- package/src/pi/tool/compiler.ts +0 -483
- package/src/pi/tool/index.ts +0 -38
- package/src/pi/turn/approval.ts +0 -181
- package/src/pi/turn/index.ts +0 -82
- package/src/pi/turn/interaction.ts +0 -181
- package/src/runtime.ts +0 -4760
- /package/src/{kernel/runtime-assembly-view.ts → config/assembly-view.ts} +0 -0
- /package/src/{kernel → config}/degradation.ts +0 -0
- /package/src/{kernel → config}/runtime-config.ts +0 -0
- /package/src/{kernel/extensions.ts → context/extension/contracts.ts} +0 -0
- /package/src/{db/ext-context.repo.ts → context/extension/repository.ts} +0 -0
- /package/src/{pi/runtime-adapter/openrouter-messages.ts → models/openrouter/messages.ts} +0 -0
- /package/src/{pi/runtime-adapter → models/openrouter}/test-fixtures/openrouter-tool-search.sse +0 -0
- /package/src/{db/submission-admission.repo.ts → submission/admission-repository.ts} +0 -0
- /package/src/{db/telemetry-outbox.repo.ts → telemetry/repository.ts} +0 -0
- /package/src/{db/runtime-event-outbox.repo.ts → telemetry/runtime-event-repository.ts} +0 -0
- /package/src/{pi/tool → tools}/declared.ts +0 -0
- /package/src/{db/interaction.repo.ts → tools/interaction/repository.ts} +0 -0
- /package/src/{pi/tool → tools}/nested-tools.ts +0 -0
- /package/src/{kernel/submission-authority.ts → tools/subagent-authority.ts} +0 -0
- /package/src/{db/agent-tool.repo.ts → tools/subagent-run-repository.ts} +0 -0
- /package/src/{layers/orchestration/subagents/agent-types → tools/subagent-types}/contract.ts +0 -0
- /package/src/{layers/orchestration/subagents/agent-types → tools/subagent-types}/extract/index.ts +0 -0
- /package/src/{layers/orchestration/subagents/agent-types → tools/subagent-types}/fanout/index.ts +0 -0
- /package/src/{layers/orchestration/subagents/agent-types → tools/subagent-types}/registry.ts +0 -0
- /package/src/{pi/tool → tools}/time.ts +0 -0
- /package/src/{pi/tool → tools}/web-fetch.ts +0 -0
- /package/src/{pi/tool → tools}/web-search/index.ts +0 -0
- /package/src/{pi/tool → tools}/web-search/web-search.ts +0 -0
- /package/src/{workspace-versioning.ts → tools/workspace-versioning.ts} +0 -0
|
@@ -9,10 +9,10 @@ import {
|
|
|
9
9
|
type TSchema,
|
|
10
10
|
} from "@earendil-works/pi-ai";
|
|
11
11
|
import { estimateStringTokens } from "agents/experimental/memory/utils";
|
|
12
|
-
import type { RuntimeMemoryPort } from "
|
|
13
|
-
import type { RuntimeMemoryProfile } from "
|
|
14
|
-
import { serializeOutput } from "
|
|
15
|
-
import { modelAttachmentText } from "../message/conversion";
|
|
12
|
+
import type { RuntimeMemoryPort } from "../config/ports";
|
|
13
|
+
import type { RuntimeMemoryProfile } from "../config/profile";
|
|
14
|
+
import { serializeOutput } from "../lib/artifacts";
|
|
15
|
+
import { modelAttachmentText } from "../conversation/message/conversion";
|
|
16
16
|
import type { PiToolCandidate } from "./compiler";
|
|
17
17
|
import { webSearchPiToolCandidate } from "./web-search";
|
|
18
18
|
import type { WebSearch } from "./web-search/api";
|
|
@@ -88,10 +88,7 @@ function recordOf(value: unknown): Record<string, unknown> | null {
|
|
|
88
88
|
: null;
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
-
function questionsOf(
|
|
92
|
-
value: unknown,
|
|
93
|
-
allowLegacy = false,
|
|
94
|
-
): AskUserInput["questions"] | null {
|
|
91
|
+
function questionsOf(value: unknown): AskUserInput["questions"] | null {
|
|
95
92
|
const questions = recordOf(value)?.questions;
|
|
96
93
|
if (!Array.isArray(questions) || questions.length < 1 || questions.length > 10) {
|
|
97
94
|
return null;
|
|
@@ -103,6 +100,10 @@ function questionsOf(
|
|
|
103
100
|
if (!question || typeof question.question !== "string" || !question.question) {
|
|
104
101
|
return null;
|
|
105
102
|
}
|
|
103
|
+
if (Object.keys(question).some((key) =>
|
|
104
|
+
key !== "question" && key !== "responseType" && key !== "options" &&
|
|
105
|
+
key !== "allowCustom" && key !== "required"
|
|
106
|
+
)) return null;
|
|
106
107
|
if (
|
|
107
108
|
question.required !== undefined && typeof question.required !== "boolean"
|
|
108
109
|
) return null;
|
|
@@ -114,18 +115,7 @@ function questionsOf(
|
|
|
114
115
|
responseType as typeof askUserResponseTypes[number],
|
|
115
116
|
)
|
|
116
117
|
) {
|
|
117
|
-
|
|
118
|
-
if (question.kind === "attachment") {
|
|
119
|
-
responseType = "attachment";
|
|
120
|
-
} else if (question.kind === undefined) {
|
|
121
|
-
responseType = Array.isArray(question.options)
|
|
122
|
-
? question.multiSelect === true
|
|
123
|
-
? "multi_select"
|
|
124
|
-
: "single_select"
|
|
125
|
-
: "text";
|
|
126
|
-
} else {
|
|
127
|
-
return null;
|
|
128
|
-
}
|
|
118
|
+
return null;
|
|
129
119
|
}
|
|
130
120
|
const normalizedQuestion = {
|
|
131
121
|
question: question.question,
|
|
@@ -137,8 +127,7 @@ function questionsOf(
|
|
|
137
127
|
if (
|
|
138
128
|
attachmentQuestions > 1 ||
|
|
139
129
|
question.options !== undefined ||
|
|
140
|
-
question.allowCustom !== undefined
|
|
141
|
-
(!allowLegacy && question.multiSelect !== undefined)
|
|
130
|
+
question.allowCustom !== undefined
|
|
142
131
|
) return null;
|
|
143
132
|
normalized.push(normalizedQuestion);
|
|
144
133
|
continue;
|
|
@@ -146,8 +135,7 @@ function questionsOf(
|
|
|
146
135
|
if (responseType === "text") {
|
|
147
136
|
if (
|
|
148
137
|
question.options !== undefined ||
|
|
149
|
-
|
|
150
|
-
(question.allowCustom !== undefined || question.multiSelect !== undefined))
|
|
138
|
+
question.allowCustom !== undefined
|
|
151
139
|
) {
|
|
152
140
|
return null;
|
|
153
141
|
}
|
|
@@ -165,10 +153,6 @@ function questionsOf(
|
|
|
165
153
|
(question.allowCustom !== undefined &&
|
|
166
154
|
typeof question.allowCustom !== "boolean")
|
|
167
155
|
) return null;
|
|
168
|
-
if (
|
|
169
|
-
!allowLegacy &&
|
|
170
|
-
(question.kind !== undefined || question.multiSelect !== undefined)
|
|
171
|
-
) return null;
|
|
172
156
|
normalized.push({
|
|
173
157
|
...normalizedQuestion,
|
|
174
158
|
options: question.options,
|
|
@@ -199,7 +183,7 @@ function prepareAskUserArguments(input: unknown): AskUserInput {
|
|
|
199
183
|
// 手写校验而不是拉 TypeBox compiler:这里只需要判真假,
|
|
200
184
|
// 且校验位于 Worker 响应信任边界,不应为每次回答编译 schema。
|
|
201
185
|
function isAskUserResponse(input: unknown, value: unknown): boolean {
|
|
202
|
-
const questions = questionsOf(input
|
|
186
|
+
const questions = questionsOf(input);
|
|
203
187
|
const answers = recordOf(value)?.answers;
|
|
204
188
|
if (!questions || !Array.isArray(answers) || answers.length !== questions.length) {
|
|
205
189
|
return false;
|
|
@@ -400,7 +384,7 @@ export function basePiToolCandidates(
|
|
|
400
384
|
interaction: {
|
|
401
385
|
validateResponse: isAskUserResponse,
|
|
402
386
|
settle: (input, response) => {
|
|
403
|
-
const questions = questionsOf(input
|
|
387
|
+
const questions = questionsOf(input)!;
|
|
404
388
|
const responseAnswers = (response as AskUserResponse).answers;
|
|
405
389
|
const answers = questions.map((question, index) => {
|
|
406
390
|
const answer = responseAnswers[index]!;
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AgentHarnessTool,
|
|
3
|
+
AgentTool,
|
|
4
|
+
AgentToolResult,
|
|
5
|
+
} from "@earendil-works/pi-agent-core";
|
|
6
|
+
import type { ApprovalReceipt } from "../submission/contracts";
|
|
7
|
+
import {
|
|
8
|
+
boundDurableToolOutput,
|
|
9
|
+
type BudgetPolicy,
|
|
10
|
+
spillDurableToolOutput,
|
|
11
|
+
} from "../context/memory/budget/gate";
|
|
12
|
+
import {
|
|
13
|
+
type ArtifactRef,
|
|
14
|
+
serializeOutput,
|
|
15
|
+
type SpillWorkspace,
|
|
16
|
+
} from "../lib/artifacts";
|
|
17
|
+
import {
|
|
18
|
+
type ExecutionLevel,
|
|
19
|
+
} from "../lib/execution-level";
|
|
20
|
+
|
|
21
|
+
// 本文件沿用 `../../index.ts` 入口定义的 Tool Candidate 和 Tool Settlement 术语。
|
|
22
|
+
|
|
23
|
+
// #region Public contracts
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* 声明一个 Tool 的结果由客户端投递,而不是由 `execute()` 产出。
|
|
27
|
+
*
|
|
28
|
+
* 带这个字段的 Tool 会在执行链里 park 住,等宿主经 `respondToolInteraction` 把响应送进来,
|
|
29
|
+
* 再用 `settle` 把响应映射成这次 `toolCallId` 的权威 ToolResult。它的 `execute` 不会被调用。
|
|
30
|
+
*
|
|
31
|
+
* 这是 AI SDK 里「没有 execute 的 user-interaction tool」的等价物;差别是我们 park 住整个 Turn,
|
|
32
|
+
* 而不是让 tool part 悬空到下一次请求 —— 我们的 transcript 是服务端权威的,悬空的 tool_use
|
|
33
|
+
* 会让下一次模型调用非法。
|
|
34
|
+
*/
|
|
35
|
+
export interface PiToolInteractionSpec {
|
|
36
|
+
/** 结合 Tool 输入校验客户端响应体;不通过则拒绝投递,park 保持不变。 */
|
|
37
|
+
readonly validateResponse: (input: unknown, response: unknown) => boolean;
|
|
38
|
+
/** 把通过校验的响应映射成 ToolResult;缺省是原样回传。 */
|
|
39
|
+
readonly settle?: (
|
|
40
|
+
input: unknown,
|
|
41
|
+
response: unknown,
|
|
42
|
+
) => Omit<AgentToolResult<unknown>, "terminate">;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** 描述一个尚未进入最终 Tool Surface 和结算包装的 Pi 工具。 */
|
|
46
|
+
export type ToolExposureMode = "direct" | "codemode" | "both";
|
|
47
|
+
|
|
48
|
+
export interface PiToolCandidate {
|
|
49
|
+
readonly owner: string;
|
|
50
|
+
readonly tool: AgentTool<any, any>;
|
|
51
|
+
/** @internal Send the complete schema but hide it until provider Tool Search finds it. */
|
|
52
|
+
readonly deferLoading?: true;
|
|
53
|
+
/** Omitted means visible both directly and through Code Mode. */
|
|
54
|
+
readonly exposureMode?: ToolExposureMode;
|
|
55
|
+
/** Conservative maximum used in the stable Runtime descriptor. */
|
|
56
|
+
readonly requiredExecutionLevel: ExecutionLevel;
|
|
57
|
+
/** Trusted parameter-level policy, evaluated before approval or dispatch. */
|
|
58
|
+
readonly requiredExecutionLevelForInput?: (
|
|
59
|
+
input: unknown,
|
|
60
|
+
) => ExecutionLevel | Promise<ExecutionLevel>;
|
|
61
|
+
/**
|
|
62
|
+
* 无条件需要人来决定:不看执行档位矩阵,每次调用都 park 出审批。
|
|
63
|
+
*
|
|
64
|
+
* 档位差是「够不够格自己跑」,不是「该不该问人」。花用户钱、改变账户
|
|
65
|
+
* 状态这类工具,一旦 Agent 档位被 `allow_level` 永久拉高就会静默执行,
|
|
66
|
+
* 那是失效不是降级。`deny` 仍然优先,被 deny 的工具根本不会注入。
|
|
67
|
+
*/
|
|
68
|
+
readonly alwaysRequiresApproval?: boolean;
|
|
69
|
+
/** Recovery may replay this Tool Call only when the candidate guarantees convergence. Omitted means non-idempotent. */
|
|
70
|
+
readonly retry?: "idempotent" | "non-idempotent";
|
|
71
|
+
readonly summary?: string;
|
|
72
|
+
readonly source?: ApprovalReceipt["source"];
|
|
73
|
+
readonly interaction?: PiToolInteractionSpec;
|
|
74
|
+
/** Large payloads spill by default; protocol-shaped results opt into structure. */
|
|
75
|
+
readonly outputBudget?: BudgetPolicy;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface CompileHarnessToolsOptions {
|
|
79
|
+
readonly workspace?: SpillWorkspace;
|
|
80
|
+
/** Recheck live authorization at the effect boundary, including safe replay. */
|
|
81
|
+
readonly authorize?: (
|
|
82
|
+
candidate: PiToolCandidate,
|
|
83
|
+
args: unknown,
|
|
84
|
+
invocation: { readonly toolCallId: string; readonly signal: AbortSignal },
|
|
85
|
+
) => void | Promise<void>;
|
|
86
|
+
/** Return an already decided interaction result without invoking the handler. */
|
|
87
|
+
readonly resolveReplay?: (
|
|
88
|
+
candidate: PiToolCandidate,
|
|
89
|
+
toolCallId: string,
|
|
90
|
+
args: unknown,
|
|
91
|
+
) => AgentToolResult<unknown> | undefined | Promise<AgentToolResult<unknown> | undefined>;
|
|
92
|
+
/** Optional business projection; Pi remains the authoritative Tool result. */
|
|
93
|
+
readonly onSettlement?: (
|
|
94
|
+
call: SettledPiToolCall,
|
|
95
|
+
) => void | Promise<void>;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** 描述一次已进入持久化结算边界的 Pi 工具调用。 */
|
|
99
|
+
export interface SettledPiToolCall {
|
|
100
|
+
readonly toolCallId: string;
|
|
101
|
+
readonly toolName: string;
|
|
102
|
+
readonly args: unknown;
|
|
103
|
+
readonly result: AgentToolResult<unknown>;
|
|
104
|
+
readonly isError: boolean;
|
|
105
|
+
/** Diagnostic-only original error; callers must not persist or expose it. */
|
|
106
|
+
readonly cause?: unknown;
|
|
107
|
+
readonly summary?: string;
|
|
108
|
+
readonly source?: ApprovalReceipt["source"];
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// DO SQLite rows also contain arguments and transcript/recovery envelopes.
|
|
112
|
+
// Keep each serialized result below half the 2 MiB row limit.
|
|
113
|
+
const DURABLE_TOOL_RESULT_MAX_BYTES = 1024 * 1024;
|
|
114
|
+
const MEDIA_CAPTION_MAX_CHARS = 4 * 1024;
|
|
115
|
+
|
|
116
|
+
// #endregion
|
|
117
|
+
|
|
118
|
+
// #region Governance helpers
|
|
119
|
+
|
|
120
|
+
// 把任意失败值收敛为可安全显示的简短消息。
|
|
121
|
+
// 受治理工具在工具执行或结算抛错时调用它。
|
|
122
|
+
// 读取错误自身也可能抛错,因此最外层必须保留固定回退文本。
|
|
123
|
+
function errorMessage(error: unknown): string {
|
|
124
|
+
try {
|
|
125
|
+
if (error instanceof Error) {
|
|
126
|
+
return error.message || error.name;
|
|
127
|
+
}
|
|
128
|
+
return String(error);
|
|
129
|
+
} catch {
|
|
130
|
+
return "Tool execution failed";
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Most Pi adapters keep the original Tool value in details and its serialized
|
|
135
|
+
// model form in one text block. Spill that original once instead of duplicating
|
|
136
|
+
// it inside an AgentToolResult JSON envelope.
|
|
137
|
+
function spillPayload(result: AgentToolResult<unknown>): unknown {
|
|
138
|
+
const text = result.content.length === 1 && result.content[0]?.type === "text"
|
|
139
|
+
? result.content[0].text
|
|
140
|
+
: undefined;
|
|
141
|
+
if (text !== undefined && serializeOutput(result.details).text === text) {
|
|
142
|
+
return result.details;
|
|
143
|
+
}
|
|
144
|
+
return result;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function artifactResult(
|
|
148
|
+
result: AgentToolResult<unknown>,
|
|
149
|
+
ref: ArtifactRef,
|
|
150
|
+
): AgentToolResult<unknown> {
|
|
151
|
+
const presentation = result.details !== null && typeof result.details === "object"
|
|
152
|
+
? (result.details as Record<string, unknown>).presentation
|
|
153
|
+
: undefined;
|
|
154
|
+
let captionRemaining = MEDIA_CAPTION_MAX_CHARS;
|
|
155
|
+
const mediaContent = result.content.some((part) => part.type !== "text")
|
|
156
|
+
? result.content.flatMap((part): AgentToolResult<unknown>["content"] => {
|
|
157
|
+
if (part.type !== "text") return [part];
|
|
158
|
+
const text = part.text.slice(0, captionRemaining);
|
|
159
|
+
captionRemaining -= text.length;
|
|
160
|
+
return text ? [{ ...part, text }] : [];
|
|
161
|
+
})
|
|
162
|
+
: [];
|
|
163
|
+
return {
|
|
164
|
+
...result,
|
|
165
|
+
content: [{
|
|
166
|
+
type: "text",
|
|
167
|
+
text: [
|
|
168
|
+
ref.note,
|
|
169
|
+
`Path: ${ref.path}`,
|
|
170
|
+
`Bytes: ${ref.bytes}`,
|
|
171
|
+
`Preview:\n${ref.preview}`,
|
|
172
|
+
].join("\n"),
|
|
173
|
+
}, ...mediaContent],
|
|
174
|
+
details: presentation === undefined ? ref : { ...ref, presentation },
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async function durableResult(
|
|
179
|
+
result: AgentToolResult<unknown>,
|
|
180
|
+
candidate: PiToolCandidate,
|
|
181
|
+
workspace: SpillWorkspace | undefined,
|
|
182
|
+
): Promise<AgentToolResult<unknown>> {
|
|
183
|
+
const spill = (candidate.outputBudget ?? { kind: "spill" }).kind === "spill";
|
|
184
|
+
let ref = spill
|
|
185
|
+
? await spillDurableToolOutput(spillPayload(result), { workspace })
|
|
186
|
+
: null;
|
|
187
|
+
const bounded = ref
|
|
188
|
+
? artifactResult(result, ref)
|
|
189
|
+
: boundDurableToolOutput(result) as AgentToolResult<unknown>;
|
|
190
|
+
const bytes = new TextEncoder().encode(JSON.stringify(bounded)).byteLength;
|
|
191
|
+
if (bytes <= DURABLE_TOOL_RESULT_MAX_BYTES) return bounded;
|
|
192
|
+
|
|
193
|
+
// Binary blocks cannot be truncated, and this boundary has no image resizer.
|
|
194
|
+
// Preserve the original where possible and fail before any SQLite write.
|
|
195
|
+
if (!spill) ref = await spillDurableToolOutput(spillPayload(result), { workspace });
|
|
196
|
+
throw new Error(
|
|
197
|
+
`Tool output exceeds the 1 MiB durable result limit (${bytes} bytes). ` +
|
|
198
|
+
"Return a smaller result, such as lower-resolution images or fewer media items; the original cannot be included inline. " +
|
|
199
|
+
(ref ? `Original output saved to Workspace: ${ref.path}` : "The original output could not be saved to Workspace."),
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
async function untilAborted<T>(
|
|
204
|
+
signal: AbortSignal | undefined,
|
|
205
|
+
run: () => T | Promise<T>,
|
|
206
|
+
): Promise<T> {
|
|
207
|
+
// ponytail: 这会立即释放 Turn,但无法杀死不配合的进程内副作用;需要物理终止的 Tool 必须放进可销毁的隔离执行器。
|
|
208
|
+
signal?.throwIfAborted();
|
|
209
|
+
if (!signal) return run();
|
|
210
|
+
let onAbort!: () => void;
|
|
211
|
+
const aborted = new Promise<never>((_resolve, reject) => {
|
|
212
|
+
onAbort = () => reject(
|
|
213
|
+
signal.reason ?? new DOMException("Aborted", "AbortError"),
|
|
214
|
+
);
|
|
215
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
216
|
+
});
|
|
217
|
+
try {
|
|
218
|
+
return await Promise.race([run(), aborted]);
|
|
219
|
+
} finally {
|
|
220
|
+
signal.removeEventListener("abort", onAbort);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// #endregion
|
|
225
|
+
|
|
226
|
+
// #region Governance and compilation
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Convert the configured candidate surface once at the AgentHarness boundary.
|
|
230
|
+
* Schema, argument preparation, progress, cancellation, replay classification,
|
|
231
|
+
* and output bounds stay attached to the original Tool declaration.
|
|
232
|
+
*/
|
|
233
|
+
export function compileHarnessTools(
|
|
234
|
+
candidates: readonly PiToolCandidate[],
|
|
235
|
+
options: CompileHarnessToolsOptions = {},
|
|
236
|
+
): AgentHarnessTool<object | undefined>[] {
|
|
237
|
+
return candidates.map((candidate) => {
|
|
238
|
+
const source = candidate.tool;
|
|
239
|
+
const tool = source.constrainedSampling === false
|
|
240
|
+
? source
|
|
241
|
+
: {
|
|
242
|
+
...source,
|
|
243
|
+
constrainedSampling: source.constrainedSampling ?? {
|
|
244
|
+
type: "json_schema" as const,
|
|
245
|
+
strict: "prefer" as const,
|
|
246
|
+
},
|
|
247
|
+
};
|
|
248
|
+
return {
|
|
249
|
+
...tool,
|
|
250
|
+
replay: candidate.retry === "idempotent" &&
|
|
251
|
+
candidate.requiredExecutionLevel === "safe" &&
|
|
252
|
+
!candidate.alwaysRequiresApproval &&
|
|
253
|
+
!candidate.requiredExecutionLevelForInput &&
|
|
254
|
+
!candidate.interaction
|
|
255
|
+
? "safe" as const
|
|
256
|
+
: "never" as const,
|
|
257
|
+
...(candidate.deferLoading ? { deferLoading: true as const } : {}),
|
|
258
|
+
async execute(
|
|
259
|
+
toolCallId,
|
|
260
|
+
args,
|
|
261
|
+
onUpdate,
|
|
262
|
+
_toolContext,
|
|
263
|
+
_invocation,
|
|
264
|
+
context,
|
|
265
|
+
) {
|
|
266
|
+
const signal = context.abortSignal ?? new AbortController().signal;
|
|
267
|
+
await options.authorize?.(candidate, args, {
|
|
268
|
+
toolCallId,
|
|
269
|
+
signal,
|
|
270
|
+
});
|
|
271
|
+
const replay = await options.resolveReplay?.(
|
|
272
|
+
candidate,
|
|
273
|
+
toolCallId,
|
|
274
|
+
args,
|
|
275
|
+
);
|
|
276
|
+
if (replay) return durableResult(replay, candidate, options.workspace);
|
|
277
|
+
let result: AgentToolResult<unknown>;
|
|
278
|
+
try {
|
|
279
|
+
result = await untilAborted(
|
|
280
|
+
signal,
|
|
281
|
+
async () => durableResult(
|
|
282
|
+
await source.execute(
|
|
283
|
+
toolCallId,
|
|
284
|
+
args,
|
|
285
|
+
signal,
|
|
286
|
+
(update) => onUpdate(update),
|
|
287
|
+
),
|
|
288
|
+
candidate,
|
|
289
|
+
options.workspace,
|
|
290
|
+
),
|
|
291
|
+
);
|
|
292
|
+
} catch (cause) {
|
|
293
|
+
const message = errorMessage(cause);
|
|
294
|
+
const failed = boundDurableToolOutput({
|
|
295
|
+
content: [{ type: "text", text: message }],
|
|
296
|
+
details: {},
|
|
297
|
+
}) as AgentToolResult<unknown>;
|
|
298
|
+
await options.onSettlement?.({
|
|
299
|
+
toolCallId,
|
|
300
|
+
toolName: source.name,
|
|
301
|
+
args,
|
|
302
|
+
result: failed,
|
|
303
|
+
isError: true,
|
|
304
|
+
cause,
|
|
305
|
+
summary: candidate.summary,
|
|
306
|
+
source: candidate.source,
|
|
307
|
+
});
|
|
308
|
+
throw cause instanceof Error && cause.message === message
|
|
309
|
+
? cause
|
|
310
|
+
: new Error(message, { cause });
|
|
311
|
+
}
|
|
312
|
+
await options.onSettlement?.({
|
|
313
|
+
toolCallId,
|
|
314
|
+
toolName: source.name,
|
|
315
|
+
args,
|
|
316
|
+
result,
|
|
317
|
+
isError: false,
|
|
318
|
+
summary: candidate.summary,
|
|
319
|
+
source: candidate.source,
|
|
320
|
+
});
|
|
321
|
+
return result;
|
|
322
|
+
},
|
|
323
|
+
} satisfies AgentHarnessTool<object | undefined>;
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// #endregion
|
|
@@ -21,14 +21,14 @@ import type {
|
|
|
21
21
|
RuntimeBrowserPort,
|
|
22
22
|
RuntimeCodeExecutionPort,
|
|
23
23
|
WorkspacePort,
|
|
24
|
-
} from "
|
|
24
|
+
} from "../config/ports";
|
|
25
25
|
import {
|
|
26
26
|
WorkspaceStateConnector,
|
|
27
27
|
WORKSPACE_PAGING_INSTRUCTIONS,
|
|
28
28
|
WORKSPACE_STATE_TYPES,
|
|
29
29
|
} from "./workspace-state-connector";
|
|
30
|
-
import type { RuntimeCodeExecutionFactory } from "
|
|
31
|
-
import { serializeOutput } from "
|
|
30
|
+
import type { RuntimeCodeExecutionFactory } from "../config/ports";
|
|
31
|
+
import { serializeOutput } from "../lib/artifacts";
|
|
32
32
|
import type { PiToolCandidate } from "./compiler";
|
|
33
33
|
import { piCandidatesToAiTools } from "./nested-tools";
|
|
34
34
|
|
|
@@ -5,9 +5,9 @@ import type {
|
|
|
5
5
|
import { Type } from "@earendil-works/pi-ai";
|
|
6
6
|
import type {
|
|
7
7
|
RuntimeCodeExecutionPort,
|
|
8
|
-
} from "
|
|
9
|
-
import { serializeOutput } from "
|
|
10
|
-
import type { PiLoadedExtension } from "../assembly
|
|
8
|
+
} from "../config/ports";
|
|
9
|
+
import { serializeOutput } from "../lib/artifacts";
|
|
10
|
+
import type { PiLoadedExtension } from "../context/extension/assembly";
|
|
11
11
|
import type { PiToolCandidate } from "./compiler";
|
|
12
12
|
|
|
13
13
|
// 本文件沿用 `../../index.ts` 入口定义的 Extension、Port 和 Tool Candidate 术语。
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AgentToolResult,
|
|
3
|
+
HookHandler,
|
|
4
|
+
} from "@earendil-works/pi-agent-core";
|
|
5
|
+
import type { ExecutionLevel } from "../lib/execution-level";
|
|
6
|
+
import {
|
|
7
|
+
EXECUTION_LEVELS,
|
|
8
|
+
requiresExecutionApproval,
|
|
9
|
+
} from "../lib/execution-level";
|
|
10
|
+
import type { PiToolCandidate } from "./compiler";
|
|
11
|
+
import type { ApprovalRepository } from "./approval";
|
|
12
|
+
import type { ToolInteractionRepository } from "./interaction";
|
|
13
|
+
import type { RuntimeTelemetryCoordinator } from "../telemetry/coordinator";
|
|
14
|
+
|
|
15
|
+
export interface HarnessDecisionSubmission {
|
|
16
|
+
readonly submissionId: string;
|
|
17
|
+
readonly requestId: string;
|
|
18
|
+
readonly executionLevel: ExecutionLevel;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface HarnessDecisionGateOptions {
|
|
22
|
+
readonly submission: HarnessDecisionSubmission;
|
|
23
|
+
readonly candidates: readonly PiToolCandidate[];
|
|
24
|
+
readonly approvals: ApprovalRepository;
|
|
25
|
+
readonly interactions: ToolInteractionRepository;
|
|
26
|
+
readonly transaction: <T>(run: () => T) => T;
|
|
27
|
+
readonly release: (reason: "approval" | "interaction") => void;
|
|
28
|
+
readonly telemetry?: Pick<RuntimeTelemetryCoordinator, "capture">;
|
|
29
|
+
readonly onChanged?: (
|
|
30
|
+
kind: "approval" | "interaction",
|
|
31
|
+
id: string,
|
|
32
|
+
) => void | Promise<void>;
|
|
33
|
+
readonly gateTool?: (input: {
|
|
34
|
+
readonly toolCallId: string;
|
|
35
|
+
readonly toolName: string;
|
|
36
|
+
readonly input: unknown;
|
|
37
|
+
readonly requiredExecutionLevel: ExecutionLevel;
|
|
38
|
+
readonly signal: AbortSignal;
|
|
39
|
+
}) => void | Promise<void>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function waitForRelease(signal: AbortSignal): Promise<never> {
|
|
43
|
+
return new Promise((_resolve, reject) => {
|
|
44
|
+
const abort = () => {
|
|
45
|
+
signal.removeEventListener("abort", abort);
|
|
46
|
+
reject(signal.reason ?? new DOMException("Aborted", "AbortError"));
|
|
47
|
+
};
|
|
48
|
+
if (signal.aborted) return abort();
|
|
49
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function operationSignal(signal: AbortSignal | undefined): AbortSignal {
|
|
54
|
+
if (!signal) throw new Error("Harness Tool hook has no operation signal");
|
|
55
|
+
return signal;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function stableInput(input: unknown): string {
|
|
59
|
+
return JSON.stringify(input);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function interactionResult(
|
|
63
|
+
response: unknown,
|
|
64
|
+
): AgentToolResult<unknown> {
|
|
65
|
+
return {
|
|
66
|
+
content: [{ type: "text", text: JSON.stringify(response) }],
|
|
67
|
+
details: response,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Durable policy facts around Pi's before_tool hook; Pi owns execution state. */
|
|
72
|
+
export class HarnessDecisionGate {
|
|
73
|
+
private readonly candidates: ReadonlyMap<string, PiToolCandidate>;
|
|
74
|
+
|
|
75
|
+
constructor(private readonly options: HarnessDecisionGateOptions) {
|
|
76
|
+
this.candidates = new Map(
|
|
77
|
+
options.candidates.map((candidate) => [candidate.tool.name, candidate]),
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
readonly beforeTool: HookHandler<"before_tool"> = async (event, context) => {
|
|
82
|
+
const candidate = this.candidates.get(event.toolName);
|
|
83
|
+
if (!candidate) return undefined;
|
|
84
|
+
const requiredExecutionLevel = candidate.requiredExecutionLevelForInput
|
|
85
|
+
? await candidate.requiredExecutionLevelForInput(event.args)
|
|
86
|
+
: candidate.requiredExecutionLevel;
|
|
87
|
+
if (!EXECUTION_LEVELS.includes(requiredExecutionLevel)) {
|
|
88
|
+
throw new Error(
|
|
89
|
+
`SpringBrand Tool "${event.toolName}" resolved an invalid execution level`,
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
const executionId = `${this.options.submission.submissionId}:${event.toolCallId}`;
|
|
93
|
+
const existing = this.options.approvals.find(executionId);
|
|
94
|
+
if (existing?.status === "rejected") {
|
|
95
|
+
return { block: { reason: existing.reason ?? "Tool execution denied by user" } };
|
|
96
|
+
}
|
|
97
|
+
const needsApproval = candidate.alwaysRequiresApproval ||
|
|
98
|
+
requiresExecutionApproval(
|
|
99
|
+
this.options.submission.executionLevel,
|
|
100
|
+
requiredExecutionLevel,
|
|
101
|
+
);
|
|
102
|
+
if (needsApproval && existing?.status !== "approved") {
|
|
103
|
+
if (existing && (
|
|
104
|
+
existing.requestId !== this.options.submission.requestId ||
|
|
105
|
+
existing.toolName !== event.toolName ||
|
|
106
|
+
existing.inputJson !== stableInput(event.args)
|
|
107
|
+
)) {
|
|
108
|
+
throw new Error(`Conflicting durable Tool approval: ${event.toolCallId}`);
|
|
109
|
+
}
|
|
110
|
+
if (!existing) {
|
|
111
|
+
const createdAt = Date.now();
|
|
112
|
+
this.options.transaction(() => this.options.approvals.insert({
|
|
113
|
+
executionId,
|
|
114
|
+
submissionId: this.options.submission.submissionId,
|
|
115
|
+
requestId: this.options.submission.requestId,
|
|
116
|
+
source: candidate.source ?? "action",
|
|
117
|
+
toolCallId: event.toolCallId,
|
|
118
|
+
toolName: event.toolName,
|
|
119
|
+
summary: candidate.summary ?? candidate.tool.label ?? event.toolName,
|
|
120
|
+
executionLevel: this.options.submission.executionLevel,
|
|
121
|
+
requiredExecutionLevel,
|
|
122
|
+
inputJson: stableInput(event.args),
|
|
123
|
+
status: "pending",
|
|
124
|
+
createdAt,
|
|
125
|
+
}));
|
|
126
|
+
this.options.telemetry?.capture("approvalRequested", {
|
|
127
|
+
submissionId: this.options.submission.submissionId,
|
|
128
|
+
toolCallId: event.toolCallId,
|
|
129
|
+
executionId,
|
|
130
|
+
toolName: event.toolName,
|
|
131
|
+
summary: candidate.summary ?? candidate.tool.label ?? event.toolName,
|
|
132
|
+
input: event.args,
|
|
133
|
+
occurredAt: createdAt,
|
|
134
|
+
});
|
|
135
|
+
await this.options.onChanged?.("approval", executionId);
|
|
136
|
+
}
|
|
137
|
+
this.options.release("approval");
|
|
138
|
+
return waitForRelease(operationSignal(context.abortSignal));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (!candidate.interaction) return undefined;
|
|
142
|
+
const interaction = this.options.interactions.find(executionId);
|
|
143
|
+
if (interaction?.status === "responded" || interaction?.status === "cancelled") {
|
|
144
|
+
return undefined;
|
|
145
|
+
}
|
|
146
|
+
if (interaction && (
|
|
147
|
+
interaction.requestId !== this.options.submission.requestId ||
|
|
148
|
+
interaction.toolName !== event.toolName ||
|
|
149
|
+
interaction.inputJson !== stableInput(event.args)
|
|
150
|
+
)) {
|
|
151
|
+
throw new Error(`Conflicting durable Tool interaction: ${event.toolCallId}`);
|
|
152
|
+
}
|
|
153
|
+
if (!interaction) {
|
|
154
|
+
const createdAt = Date.now();
|
|
155
|
+
this.options.transaction(() => this.options.interactions.insert({
|
|
156
|
+
interactionId: executionId,
|
|
157
|
+
submissionId: this.options.submission.submissionId,
|
|
158
|
+
requestId: this.options.submission.requestId,
|
|
159
|
+
toolCallId: event.toolCallId,
|
|
160
|
+
toolName: event.toolName,
|
|
161
|
+
inputJson: stableInput(event.args),
|
|
162
|
+
status: "pending",
|
|
163
|
+
createdAt,
|
|
164
|
+
}));
|
|
165
|
+
this.options.telemetry?.capture("interactionRequested", {
|
|
166
|
+
submissionId: this.options.submission.submissionId,
|
|
167
|
+
toolCallId: event.toolCallId,
|
|
168
|
+
interactionId: executionId,
|
|
169
|
+
toolName: event.toolName,
|
|
170
|
+
input: event.args,
|
|
171
|
+
occurredAt: createdAt,
|
|
172
|
+
});
|
|
173
|
+
await this.options.onChanged?.("interaction", executionId);
|
|
174
|
+
}
|
|
175
|
+
this.options.release("interaction");
|
|
176
|
+
return waitForRelease(operationSignal(context.abortSignal));
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
async authorize(
|
|
180
|
+
candidate: PiToolCandidate,
|
|
181
|
+
args: unknown,
|
|
182
|
+
invocation: { readonly toolCallId: string; readonly signal: AbortSignal },
|
|
183
|
+
): Promise<void> {
|
|
184
|
+
const requiredExecutionLevel = candidate.requiredExecutionLevelForInput
|
|
185
|
+
? await candidate.requiredExecutionLevelForInput(args)
|
|
186
|
+
: candidate.requiredExecutionLevel;
|
|
187
|
+
await this.options.gateTool?.({
|
|
188
|
+
toolCallId: invocation.toolCallId,
|
|
189
|
+
toolName: candidate.tool.name,
|
|
190
|
+
input: args,
|
|
191
|
+
requiredExecutionLevel,
|
|
192
|
+
signal: invocation.signal,
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
resolveReplay(
|
|
197
|
+
candidate: PiToolCandidate,
|
|
198
|
+
toolCallId: string,
|
|
199
|
+
args: unknown,
|
|
200
|
+
): AgentToolResult<unknown> | undefined {
|
|
201
|
+
if (!candidate.interaction) return undefined;
|
|
202
|
+
const interaction = this.options.interactions.find(
|
|
203
|
+
`${this.options.submission.submissionId}:${toolCallId}`,
|
|
204
|
+
);
|
|
205
|
+
if (!interaction || interaction.status === "pending") return undefined;
|
|
206
|
+
if (interaction.inputJson !== stableInput(args)) {
|
|
207
|
+
throw new Error(`Conflicting durable Tool interaction: ${toolCallId}`);
|
|
208
|
+
}
|
|
209
|
+
if (interaction.status === "cancelled") {
|
|
210
|
+
return {
|
|
211
|
+
content: [{
|
|
212
|
+
type: "text",
|
|
213
|
+
text: "The interaction was cancelled before the user answered.",
|
|
214
|
+
}],
|
|
215
|
+
details: { cancelled: true },
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
const response = JSON.parse(interaction.responseJson ?? "null");
|
|
219
|
+
if (!candidate.interaction.validateResponse(args, response)) {
|
|
220
|
+
throw new Error(`Stored Tool interaction response is invalid: ${toolCallId}`);
|
|
221
|
+
}
|
|
222
|
+
const settled = candidate.interaction.settle?.(args, response);
|
|
223
|
+
return settled
|
|
224
|
+
? { ...settled, terminate: false }
|
|
225
|
+
: interactionResult(response);
|
|
226
|
+
}
|
|
227
|
+
}
|